diff options
author | Gonçalo Silva <goncalossilva@gmail.com> | 2010-07-10 17:36:10 +0100 |
---|---|---|
committer | Gonçalo Silva <goncalossilva@gmail.com> | 2010-07-10 17:36:10 +0100 |
commit | cd2bbed9846d84a1230a1b9e52843eedca17b28d (patch) | |
tree | 5214b7855f3d102e4c22239b9d62bc5717cb3547 | |
parent | d2c633ba0bfb7baacdee89a46d7d036d24c68817 (diff) | |
parent | 80e47d7b88dcc732ebeb5290faab6e529829dac6 (diff) | |
download | rails-cd2bbed9846d84a1230a1b9e52843eedca17b28d.tar.gz rails-cd2bbed9846d84a1230a1b9e52843eedca17b28d.tar.bz2 rails-cd2bbed9846d84a1230a1b9e52843eedca17b28d.zip |
Merge branch 'master' of http://github.com/rails/rails
143 files changed, 1978 insertions, 1080 deletions
diff --git a/.gitignore b/.gitignore index 16bffc157b..2dfdf96e7f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ *.gem pkg .bundle +Gemfile.lock debug.log doc/rdoc activemodel/doc diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index 11fa978b9c..ed4bea0c77 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -737,13 +737,13 @@ module ActionMailer #:nodoc: raise "You can no longer call ActionMailer::Base.default_url_options " \ "directly. You need to set config.action_mailer.default_url_options. " \ "If you are using ActionMailer standalone, you need to include the " \ - "url_helpers of a router directly." + "routing url_helpers directly." end end # This module will complain if the user tries to set default_url_options # directly instead of through the config object. In Action Mailer's Railtie, - # we include the url_helpers of the router, which will override this module + # we include the router's url_helpers, which will override this module. extend DeprecatedUrlOptions ActiveSupport.run_load_hooks(:action_mailer, self) diff --git a/actionmailer/test/base_test.rb b/actionmailer/test/base_test.rb index 88e38a583b..5bc0491cff 100644 --- a/actionmailer/test/base_test.rb +++ b/actionmailer/test/base_test.rb @@ -1,5 +1,6 @@ # encoding: utf-8 require 'abstract_unit' +require 'active_support/time' class BaseTest < ActiveSupport::TestCase # TODO Add some tests for implicity layout render and url helpers diff --git a/actionmailer/test/old_base/url_test.rb b/actionmailer/test/old_base/url_test.rb index 6895fb230e..b6496bfe1b 100644 --- a/actionmailer/test/old_base/url_test.rb +++ b/actionmailer/test/old_base/url_test.rb @@ -28,7 +28,7 @@ class UrlTestMailer < ActionMailer::Base end end -class ActionMailerUrlTest < Test::Unit::TestCase +class ActionMailerUrlTest < ActionMailer::TestCase def encode( text, charset="UTF-8" ) quoted_printable( text, charset ) @@ -57,10 +57,12 @@ class ActionMailerUrlTest < Test::Unit::TestCase def test_signed_up_with_url UrlTestMailer.delivery_method = :test - - AppRoutes.draw do |map| - map.connect ':controller/:action/:id' - map.welcome 'welcome', :controller=>"foo", :action=>"bar" + + assert_deprecated do + AppRoutes.draw do |map| + map.connect ':controller/:action/:id' + map.welcome 'welcome', :controller=>"foo", :action=>"bar" + end end expected = new_mail diff --git a/actionpack/actionpack.gemspec b/actionpack/actionpack.gemspec index b4311599a9..df0b5ac9a2 100644 --- a/actionpack/actionpack.gemspec +++ b/actionpack/actionpack.gemspec @@ -27,5 +27,5 @@ Gem::Specification.new do |s| s.add_dependency('rack-test', '~> 0.5.4') #s.add_dependency('rack-mount', '~> 0.6.6') s.add_dependency('tzinfo', '~> 0.3.16') - s.add_dependency('erubis', '~> 2.6.5') + s.add_dependency('erubis', '~> 2.6.6') end diff --git a/actionpack/lib/abstract_controller/rendering.rb b/actionpack/lib/abstract_controller/rendering.rb index 6e3998aa21..b81d5954eb 100644 --- a/actionpack/lib/abstract_controller/rendering.rb +++ b/actionpack/lib/abstract_controller/rendering.rb @@ -50,8 +50,8 @@ module AbstractController if controller.respond_to?(:_helpers) include controller._helpers - if controller.respond_to?(:_router) - include controller._router.url_helpers + if controller.respond_to?(:_routes) + include controller._routes.url_helpers end # TODO: Fix RJS to not require this diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index a70ba0d2e3..1a2cbaab65 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -60,17 +60,11 @@ module ActionController include ActionController::Compatibility def self.inherited(klass) - ::ActionController::Base.subclasses << klass.to_s super klass.helper :all end - def self.subclasses - @subclasses ||= [] - end - config_accessor :asset_host, :asset_path - ActiveSupport.run_load_hooks(:action_controller, self) end end diff --git a/actionpack/lib/action_controller/metal/helpers.rb b/actionpack/lib/action_controller/metal/helpers.rb index 89201fb5ee..e0bc47318a 100644 --- a/actionpack/lib/action_controller/metal/helpers.rb +++ b/actionpack/lib/action_controller/metal/helpers.rb @@ -1,3 +1,4 @@ +require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/class/attribute' module ActionController @@ -64,7 +65,7 @@ module ActionController def helpers_dir=(value) ActiveSupport::Deprecation.warn "helpers_dir= is deprecated, use helpers_path= instead", caller - self.helpers_path = Array(value) + self.helpers_path = Array.wrap(value) end # Declares helper accessors for controller attributes. For example, the @@ -103,7 +104,7 @@ module ActionController # Extract helper names from files in app/helpers/**/*_helper.rb def all_application_helpers helpers = [] - helpers_path.each do |path| + Array.wrap(helpers_path).each do |path| extract = /^#{Regexp.quote(path.to_s)}\/?(.*)_helper.rb$/ helpers += Dir["#{path}/**/*_helper.rb"].map { |file| file.sub(extract, '\1') } end diff --git a/actionpack/lib/action_controller/metal/url_for.rb b/actionpack/lib/action_controller/metal/url_for.rb index c465035ca1..a51fc5b8e4 100644 --- a/actionpack/lib/action_controller/metal/url_for.rb +++ b/actionpack/lib/action_controller/metal/url_for.rb @@ -12,17 +12,17 @@ module ActionController ).merge(:script_name => request.script_name) end - def _router - raise "In order to use #url_for, you must include the helpers of a particular " \ - "router. For instance, `include Rails.application.routes.url_helpers" + def _routes + raise "In order to use #url_for, you must include routing helpers explicitly. " \ + "For instance, `include Rails.application.routes.url_helpers" end module ClassMethods def action_methods @action_methods ||= begin - super - _router.named_routes.helper_names + super - _routes.named_routes.helper_names end end end end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_controller/railtie.rb b/actionpack/lib/action_controller/railtie.rb index 86395c4d93..9261422f0b 100644 --- a/actionpack/lib/action_controller/railtie.rb +++ b/actionpack/lib/action_controller/railtie.rb @@ -2,7 +2,6 @@ require "rails" require "action_controller" require "action_dispatch/railtie" require "action_view/railtie" -require "active_support/core_ext/class/subclasses" require "active_support/deprecation/proxy_wrappers" require "active_support/deprecation" diff --git a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb index 08bc80dbc2..64f4d1d532 100644 --- a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb @@ -64,6 +64,11 @@ module ActionDispatch super(key.to_s, value) end + def clear + load_for_write! + super + end + def to_hash load_for_read! h = {}.replace(self) diff --git a/actionpack/lib/action_dispatch/routing.rb b/actionpack/lib/action_dispatch/routing.rb index 401d98b663..c664fb0bc2 100644 --- a/actionpack/lib/action_dispatch/routing.rb +++ b/actionpack/lib/action_dispatch/routing.rb @@ -31,7 +31,7 @@ module ActionDispatch # Think of creating routes as drawing a map for your requests. The map tells # them where to go based on some predefined pattern: # - # AppName::Applications.routes.draw do |map| + # AppName::Application.routes.draw do |map| # Pattern 1 tells some request to go to one place # Pattern 2 tell them to go to another # ... @@ -62,7 +62,7 @@ module ActionDispatch # # redirect_to show_item_path(:id => 25) # - # Use <tt>root</tt> as a shorthand to name a route for the root path "". + # Use <tt>root</tt> as a shorthand to name a route for the root path "/". # # # In routes.rb # root :to => 'blogs#index' @@ -72,7 +72,7 @@ module ActionDispatch # # # and provide these named routes # root_url # => 'http://www.example.com/' - # root_path # => '' + # root_path # => '/' # # Note: when using +controller+, the route is simply named after the # method you call on the block parameter rather than map. @@ -91,9 +91,7 @@ module ActionDispatch # # Routes can generate pretty URLs. For example: # - # match '/articles/:year/:month/:day', :constraints => { - # :controller => 'articles', - # :action => 'find_by_date', + # match '/articles/:year/:month/:day' => 'articles#find_by_id', :constraints => { # :year => /\d{4}/, # :month => /\d{1,2}/, # :day => /\d{1,2}/ @@ -167,7 +165,7 @@ module ActionDispatch # # You can reload routes if you feel you must: # - # Rails::Application.reload_routes! + # Rails.application.reload_routes! # # This will clear all named routes and reload routes.rb if the file has been modified from # last load. To absolutely force reloading, use <tt>reload!</tt>. diff --git a/actionpack/lib/action_dispatch/routing/deprecated_mapper.rb b/actionpack/lib/action_dispatch/routing/deprecated_mapper.rb index bc3f62fb48..b3146a1c60 100644 --- a/actionpack/lib/action_dispatch/routing/deprecated_mapper.rb +++ b/actionpack/lib/action_dispatch/routing/deprecated_mapper.rb @@ -19,9 +19,9 @@ module ActionDispatch def in_memory_controller_namespaces namespaces = Set.new - ActionController::Base.subclasses.each do |klass| - controller_name = klass.underscore - namespaces << controller_name.split('/')[0...-1].join('/') + ActionController::Base.descendants.each do |klass| + next if klass.anonymous? + namespaces << klass.name.underscore.split('/')[0...-1].join('/') end namespaces.delete('') namespaces @@ -31,7 +31,7 @@ module ActionDispatch class DeprecatedMapper #:nodoc: def initialize(set) #:nodoc: ActiveSupport::Deprecation.warn "You are using the old router DSL which will be removed in Rails 3.1. " << - "Please check how to update your router file at: http://www.engineyard.com/blog/2010/the-lowdown-on-routes-in-rails-3/" + "Please check how to update your routes file at: http://www.engineyard.com/blog/2010/the-lowdown-on-routes-in-rails-3/" @set = set end diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 0b4ba8c9f5..430f6fc5a3 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -13,6 +13,8 @@ module ActionDispatch end end + attr_reader :app + def initialize(app, constraints, request) @app, @constraints, @request = app, constraints, request end @@ -63,6 +65,16 @@ module ActionDispatch end end + if path.match(':controller') + raise ArgumentError, ":controller segment is not allowed within a namespace block" if @scope[:module] + + # Add a default constraint for :controller path segments that matches namespaced + # controllers with default routes like :controller/:action/:id(.:format), e.g: + # GET /admin/products/show/1 + # => { :controller => 'admin/products', :action => 'show', :id => '1' } + options.reverse_merge!(:controller => /.+?/) + end + path = normalize_path(path) path_without_format = path.sub(/\(\.:format\)$/, '') @@ -133,8 +145,11 @@ module ActionDispatch defaults[:controller] ||= default_controller defaults[:action] ||= default_action - defaults.delete(:controller) if defaults[:controller].blank? - defaults.delete(:action) if defaults[:action].blank? + defaults.delete(:controller) if defaults[:controller].blank? || defaults[:controller].is_a?(Regexp) + defaults.delete(:action) if defaults[:action].blank? || defaults[:action].is_a?(Regexp) + + defaults[:controller] = defaults[:controller].to_s if defaults.key?(:controller) + defaults[:action] = defaults[:action].to_s if defaults.key?(:action) if defaults[:controller].blank? && segment_keys.exclude?("controller") raise ArgumentError, "missing :controller" @@ -183,15 +198,15 @@ module ActionDispatch def default_controller if @options[:controller] - @options[:controller].to_s + @options[:controller] elsif @scope[:controller] - @scope[:controller].to_s + @scope[:controller] end end def default_action if @options[:action] - @options[:action].to_s + @options[:action] end end end @@ -431,12 +446,16 @@ module ActionDispatch end def merge_options_scope(parent, child) - (parent || {}).merge(child) + (parent || {}).except(*override_keys(child)).merge(child) end def merge_shallow_scope(parent, child) child ? true : false end + + def override_keys(child) + child.key?(:only) || child.key?(:except) ? [:only, :except] : [] + end end module Resources @@ -444,7 +463,7 @@ module ActionDispatch # a path appended since they fit properly in their scope level. VALID_ON_OPTIONS = [:new, :collection, :member] CANONICAL_ACTIONS = [:index, :create, :new, :show, :update, :destroy] - MERGE_FROM_SCOPE_OPTIONS = [:shallow, :constraints] + RESOURCE_OPTIONS = [:as, :controller, :path, :only, :except] class Resource #:nodoc: DEFAULT_ACTIONS = [:index, :create, :new, :show, :update, :destroy, :edit] @@ -464,9 +483,9 @@ module ActionDispatch end def actions - if only = options[:only] + if only = @options[:only] Array(only).map(&:to_sym) - elsif except = options[:except] + elsif except = @options[:except] default_actions - Array(except).map(&:to_sym) else default_actions @@ -489,68 +508,31 @@ module ActionDispatch singular end - alias_method :nested_name, :member_name - # Checks for uncountable plurals, and appends "_index" if they're. def collection_name singular == plural ? "#{plural}_index" : plural end - def shallow? - options[:shallow] ? true : false - end - - def constraints - options[:constraints] || {} - end - - def id_constraint? - options[:id] && options[:id].is_a?(Regexp) || constraints[:id] && constraints[:id].is_a?(Regexp) - end - - def id_constraint - options[:id] || constraints[:id] - end - - def collection_options - (options || {}).dup.tap do |opts| - opts.delete(:id) - opts[:constraints] = options[:constraints].dup if options[:constraints] - opts[:constraints].delete(:id) if options[:constraints].is_a?(Hash) - end - end - - def nested_path - "#{path}/:#{singular}_id" - end - - def nested_options - {}.tap do |opts| - opts[:as] = member_name - opts["#{singular}_id".to_sym] = id_constraint if id_constraint? - opts[:options] = { :shallow => shallow? } unless options[:shallow].nil? - end - end - def resource_scope - [{ :controller => controller }] + { :controller => controller } end def collection_scope - [path, collection_options] + path end def member_scope - ["#{path}/:id", options] + "#{path}/:id" end def new_scope(new_path) - ["#{path}/#{new_path}"] + "#{path}/#{new_path}" end def nested_scope - [nested_path, nested_options] + "#{path}/:#{singular}_id" end + end class SingletonResource < Resource #:nodoc: @@ -559,7 +541,7 @@ module ActionDispatch def initialize(entities, options) @name = entities.to_s @path = options.delete(:path) || @name - @controller = (options.delete(:controller) || @name.to_s.pluralize).to_s + @controller = (options.delete(:controller) || plural).to_s @as = options.delete(:as) @options = options end @@ -569,24 +551,10 @@ module ActionDispatch end alias :collection_name :member_name - def nested_path - path - end - - def nested_options - {}.tap do |opts| - opts[:as] = member_name - opts[:options] = { :shallow => shallow? } unless @options[:shallow].nil? - end - end - - def shallow? - false - end - def member_scope - [path, options] + path end + alias :nested_scope :member_scope end def initialize(*args) #:nodoc: @@ -693,18 +661,18 @@ module ActionDispatch end with_scope_level(:nested) do - if parent_resource.shallow? + if shallow? with_exclusive_scope do if @scope[:shallow_path].blank? - scope(*parent_resource.nested_scope) { yield } + scope(parent_resource.nested_scope, nested_options) { yield } else scope(@scope[:shallow_path], :as => @scope[:shallow_prefix]) do - scope(*parent_resource.nested_scope) { yield } + scope(parent_resource.nested_scope, nested_options) { yield } end end end else - scope(*parent_resource.nested_scope) { yield } + scope(parent_resource.nested_scope, nested_options) { yield } end end end @@ -723,6 +691,10 @@ module ActionDispatch end end + def shallow? + parent_resource.instance_of?(Resource) && @scope[:shallow] + end + def match(*args) options = args.extract_options!.dup options[:anchor] = true unless options.key?(:anchor) @@ -800,16 +772,17 @@ module ActionDispatch return true end - if path_names = options.delete(:path_names) - scope(:path_names => path_names) do + scope_options = options.slice!(*RESOURCE_OPTIONS) + unless scope_options.empty? + scope(scope_options) do send(method, resources.pop, options, &block) end return true end - scope_options = @scope.slice(*MERGE_FROM_SCOPE_OPTIONS).delete_if{ |k,v| v.blank? } - options.reverse_merge!(scope_options) unless scope_options.empty? - options.reverse_merge!(@scope[:options]) unless @scope[:options].blank? + unless action_options?(options) + options.merge!(scope_action_options) if scope_action_options? + end if resource_scope? nested do @@ -821,6 +794,18 @@ module ActionDispatch false end + def action_options?(options) + options[:only] || options[:except] + end + + def scope_action_options? + @scope[:options].is_a?(Hash) && (@scope[:options][:only] || @scope[:options][:except]) + end + + def scope_action_options + @scope[:options].slice(:only, :except) + end + def resource_scope? [:resource, :resources].include?(@scope[:scope_level]) end @@ -853,7 +838,7 @@ module ActionDispatch def resource_scope(resource) with_scope_level(resource.is_a?(SingletonResource) ? :resource : :resources, resource) do - scope(*parent_resource.resource_scope) do + scope(parent_resource.resource_scope) do yield end end @@ -861,7 +846,7 @@ module ActionDispatch def new_scope with_scope_level(:new) do - scope(*parent_resource.new_scope(action_path(:new))) do + scope(parent_resource.new_scope(action_path(:new))) do yield end end @@ -869,7 +854,7 @@ module ActionDispatch def collection_scope with_scope_level(:collection) do - scope(*parent_resource.collection_scope) do + scope(parent_resource.collection_scope) do yield end end @@ -877,18 +862,33 @@ module ActionDispatch def member_scope with_scope_level(:member) do - scope(*parent_resource.member_scope) do + scope(parent_resource.member_scope) do yield end end end + def nested_options + {}.tap do |options| + options[:as] = parent_resource.member_name + options[:constraints] = { "#{parent_resource.singular}_id".to_sym => id_constraint } if id_constraint? + end + end + + def id_constraint? + @scope[:id].is_a?(Regexp) || (@scope[:constraints] && @scope[:constraints][:id].is_a?(Regexp)) + end + + def id_constraint + @scope[:id] || @scope[:constraints][:id] + end + def canonical_action?(action, flag) flag && CANONICAL_ACTIONS.include?(action) end def shallow_scoping? - parent_resource && parent_resource.shallow? && @scope[:scope_level] == :member + shallow? && @scope[:scope_level] == :member end def path_for_action(action, path) @@ -932,7 +932,7 @@ module ActionDispatch collection_name = parent_resource.collection_name member_name = parent_resource.member_name name_prefix = "#{name_prefix}_" if name_prefix.present? - end + end case @scope[:scope_level] when :collection diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 5ecad6bc04..1b1a221c60 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -26,7 +26,7 @@ module ActionDispatch return [404, {'X-Cascade' => 'pass'}, []] end - controller.action(params[:action]).call(env) + dispatch(controller, params[:action], env) end def prepare_params!(params) @@ -34,29 +34,43 @@ module ActionDispatch split_glob_param!(params) if @glob_param end - def controller(params, raise_error=true) + # If this is a default_controller (i.e. a controller specified by the user) + # we should raise an error in case it's not found, because it usually means + # an user error. However, if the controller was retrieved through a dynamic + # segment, as in :controller(/:action), we should simply return nil and + # delegate the control back to Rack cascade. Besides, if this is not a default + # controller, it means we should respect the @scope[:module] parameter. + def controller(params, default_controller=true) if params && params.key?(:controller) controller_param = params[:controller] - unless controller = @controllers[controller_param] - controller_name = "#{controller_param.camelize}Controller" - controller = @controllers[controller_param] = - ActiveSupport::Dependencies.ref(controller_name) - end - - controller.get + controller_reference(controller_param) end rescue NameError => e - raise ActionController::RoutingError, e.message, e.backtrace if raise_error + raise ActionController::RoutingError, e.message, e.backtrace if default_controller end - private - def merge_default_action!(params) - params[:action] ||= 'index' - end + private - def split_glob_param!(params) - params[@glob_param] = params[@glob_param].split('/').map { |v| URI.unescape(v) } + def controller_reference(controller_param) + unless controller = @controllers[controller_param] + controller_name = "#{controller_param.camelize}Controller" + controller = @controllers[controller_param] = + ActiveSupport::Dependencies.ref(controller_name) end + controller.get + end + + def dispatch(controller, action, env) + controller.action(action).call(env) + end + + def merge_default_action!(params) + params[:action] ||= 'index' + end + + def split_glob_param!(params) + params[@glob_param] = params[@glob_param].split('/').map { |v| URI.unescape(v) } + end end # A NamedRouteCollection instance is a collection of named routes, and also @@ -268,10 +282,10 @@ module ActionDispatch # Yes plz - JP included do routes.install_helpers(self) - singleton_class.send(:define_method, :_router) { routes } + singleton_class.send(:define_method, :_routes) { routes } end - define_method(:_router) { routes } + define_method(:_routes) { routes } end helpers @@ -302,7 +316,6 @@ module ActionDispatch @extras = extras normalize_options! - normalize_recall! normalize_controller_action_id! use_relative_controller! controller.sub!(%r{^/}, '') if controller @@ -319,7 +332,11 @@ module ActionDispatch def use_recall_for(key) if @recall[key] && (!@options.key?(key) || @options[key] == @recall[key]) - @options[key] = @recall.delete(key) + if named_route_exists? + @options[key] = @recall.delete(key) if segment_keys.include?(key) + else + @options[key] = @recall.delete(key) + end end end @@ -343,15 +360,6 @@ module ActionDispatch end end - def normalize_recall! - # If the target route is not a standard route then remove controller and action - # from the options otherwise they will appear in the url parameters - if block_or_proc_route_target? - recall.delete(:controller) unless segment_keys.include?(:controller) - recall.delete(:action) unless segment_keys.include?(:action) - end - end - # This pulls :controller, :action, and :id out of the recall. # The recall key is only used if there is no key in the options # or if the key in the options is identical. If any of @@ -424,12 +432,8 @@ module ActionDispatch named_route && set.named_routes[named_route] end - def block_or_proc_route_target? - named_route_exists? && !set.named_routes[named_route].app.is_a?(Dispatcher) - end - def segment_keys - named_route_exists? ? set.named_routes[named_route].segment_keys : [] + set.named_routes[named_route].segment_keys end end @@ -474,7 +478,7 @@ module ActionDispatch path_options = yield(path_options) if block_given? path = generate(path_options, path_segments || {}) - # ROUTES TODO: This can be called directly, so script_name should probably be set in the router + # ROUTES TODO: This can be called directly, so script_name should probably be set in the routes rewritten_url << (options[:trailing_slash] ? path.sub(/\?|\z/) { "/" + $& } : path) rewritten_url << "##{Rack::Mount::Utils.escape_uri(options[:anchor].to_param.to_s)}" if options[:anchor] @@ -506,6 +510,8 @@ module ActionDispatch end dispatcher = route.app + dispatcher = dispatcher.app while dispatcher.is_a?(Mapper::Constraints) + if dispatcher.is_a?(Dispatcher) && dispatcher.controller(params, false) dispatcher.prepare_params!(params) return params diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb index db17615d92..980abd44df 100644 --- a/actionpack/lib/action_dispatch/routing/url_for.rb +++ b/actionpack/lib/action_dispatch/routing/url_for.rb @@ -128,7 +128,7 @@ module ActionDispatch when String options when nil, Hash - _router.url_for(url_options.merge((options || {}).symbolize_keys)) + _routes.url_for(url_options.merge((options || {}).symbolize_keys)) else polymorphic_url(options) end diff --git a/actionpack/lib/action_view/base.rb b/actionpack/lib/action_view/base.rb index 956c88a553..20a2e7c1f0 100644 --- a/actionpack/lib/action_view/base.rb +++ b/actionpack/lib/action_view/base.rb @@ -173,7 +173,7 @@ module ActionView #:nodoc: @@field_error_proc = Proc.new{ |html_tag, instance| "<div class=\"field_with_errors\">#{html_tag}</div>".html_safe } class_attribute :helpers - class_attribute :_router + class_attribute :_routes class << self delegate :erb_trim_mode=, :to => 'ActionView::Template::Handlers::ERB' diff --git a/actionpack/lib/action_view/helpers/form_helper.rb b/actionpack/lib/action_view/helpers/form_helper.rb index d1b10a9281..6302491c2a 100644 --- a/actionpack/lib/action_view/helpers/form_helper.rb +++ b/actionpack/lib/action_view/helpers/form_helper.rb @@ -213,12 +213,22 @@ module ActionView # ... # <% end %> # - # And for namespaced routes, like +admin_post_url+: + # For namespaced routes, like +admin_post_url+: # # <%= form_for([:admin, @post]) do |f| %> # ... # <% end %> # + # If your resource has associations defined, for example, you want to add comments + # to the post given that the routes are set correctly: + # + # <%= form_for([@document, @comment]) do |f| %> + # ... + # <% end %> + # + # Where +@document = Document.find(params[:id])+ and + # +@comment = Comment.new+. + # # === Unobtrusive JavaScript # # Specifying: diff --git a/actionpack/lib/action_view/helpers/url_helper.rb b/actionpack/lib/action_view/helpers/url_helper.rb index b8d6dc22f2..0e3cc54fd1 100644 --- a/actionpack/lib/action_view/helpers/url_helper.rb +++ b/actionpack/lib/action_view/helpers/url_helper.rb @@ -12,7 +12,7 @@ module ActionView # and controllers. module UrlHelper # This helper may be included in any class that includes the - # URL helpers of a router (router.url_helpers). Some methods + # URL helpers of a routes (routes.url_helpers). Some methods # provided here will only work in the4 context of a request # (link_to_unless_current, for instance), which must be provided # as a method called #request on the context. diff --git a/actionpack/lib/action_view/test_case.rb b/actionpack/lib/action_view/test_case.rb index e5614c9e3b..56aebca957 100644 --- a/actionpack/lib/action_view/test_case.rb +++ b/actionpack/lib/action_view/test_case.rb @@ -151,7 +151,7 @@ module ActionView @view ||= begin view = ActionView::Base.new(ActionController::Base.view_paths, {}, @controller) view.singleton_class.send :include, _helpers - view.singleton_class.send :include, @controller._router.url_helpers + view.singleton_class.send :include, @controller._routes.url_helpers view.singleton_class.send :delegate, :alert, :notice, :to => "request.flash" view.extend(Locals) view.locals = self.locals @@ -192,13 +192,13 @@ module ActionView end end - def _router - @controller._router if @controller.respond_to?(:_router) + def _routes + @controller._routes if @controller.respond_to?(:_routes) end def method_missing(selector, *args) - if @controller.respond_to?(:_router) && - @controller._router.named_routes.helpers.include?(selector) + if @controller.respond_to?(:_routes) && + @controller._routes.named_routes.helpers.include?(selector) @controller.__send__(selector, *args) else super diff --git a/actionpack/test/abstract/helper_test.rb b/actionpack/test/abstract/helper_test.rb index 0cdf5c2298..15c27501f2 100644 --- a/actionpack/test/abstract/helper_test.rb +++ b/actionpack/test/abstract/helper_test.rb @@ -1,6 +1,6 @@ require 'abstract_unit' -ActionController::Base.helpers_path = [File.dirname(__FILE__) + '/../fixtures/helpers'] +ActionController::Base.helpers_path = File.expand_path('../../fixtures/helpers', __FILE__) module AbstractController module Testing diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb index c8477fb83f..5be47f7c96 100644 --- a/actionpack/test/abstract_unit.rb +++ b/actionpack/test/abstract_unit.rb @@ -41,7 +41,7 @@ require 'pp' # require 'pp' early to prevent hidden_methods from not picking up module Rails end -# Monkey patch the old router initialization to be silenced. +# Monkey patch the old routes initialization to be silenced. class ActionDispatch::Routing::DeprecatedMapper def initialize_with_silencer(*args) ActiveSupport::Deprecation.silence { initialize_without_silencer(*args) } @@ -182,13 +182,16 @@ class ActionController::IntegrationTest < ActiveSupport::TestCase self.app = build_app - class StubDispatcher - def self.new(*args) - lambda { |env| - params = env['action_dispatch.request.path_parameters'] - controller, action = params[:controller], params[:action] - [200, {'Content-Type' => 'text/html'}, ["#{controller}##{action}"]] - } + # Stub Rails dispatcher so it does not get controller references and + # simply return the controller#action as Rack::Body. + class StubDispatcher < ::ActionDispatch::Routing::RouteSet::Dispatcher + protected + def controller_reference(controller_param) + controller_param + end + + def dispatch(controller, action, env) + [200, {'Content-Type' => 'text/html'}, ["#{controller}##{action}"]] end end @@ -275,9 +278,9 @@ end class ActionController::Base def self.test_routes(&block) - router = ActionDispatch::Routing::RouteSet.new - router.draw(&block) - include router.url_helpers + routes = ActionDispatch::Routing::RouteSet.new + routes.draw(&block) + include routes.url_helpers end end diff --git a/actionpack/test/controller/helper_test.rb b/actionpack/test/controller/helper_test.rb index c9a6e5dd45..9b9657929f 100644 --- a/actionpack/test/controller/helper_test.rb +++ b/actionpack/test/controller/helper_test.rb @@ -1,7 +1,7 @@ require 'abstract_unit' require 'active_support/core_ext/kernel/reporting' -ActionController::Base.helpers_path = [File.dirname(__FILE__) + '/../fixtures/helpers'] +ActionController::Base.helpers_path = File.expand_path('../../fixtures/helpers', __FILE__) module Fun class GamesController < ActionController::Base @@ -106,7 +106,7 @@ class HelperTest < ActiveSupport::TestCase end def test_all_helpers_with_alternate_helper_dir - @controller_class.helpers_path = [File.dirname(__FILE__) + '/../fixtures/alternate_helpers'] + @controller_class.helpers_path = File.expand_path('../../fixtures/alternate_helpers', __FILE__) # Reload helpers @controller_class._helpers = Module.new @@ -143,7 +143,7 @@ class HelperTest < ActiveSupport::TestCase assert_equal ["some/foo/bar"], ActionController::Base.helpers_dir end ensure - ActionController::Base.helpers_path = [File.dirname(__FILE__) + '/../fixtures/helpers'] + ActionController::Base.helpers_path = File.expand_path('../../fixtures/helpers', __FILE__) end private diff --git a/actionpack/test/controller/mime_responds_test.rb b/actionpack/test/controller/mime_responds_test.rb index d654338dba..b5ce391b61 100644 --- a/actionpack/test/controller/mime_responds_test.rb +++ b/actionpack/test/controller/mime_responds_test.rb @@ -36,6 +36,15 @@ class RespondToController < ActionController::Base type.all { render :text => "Nothing" } end end + + def json_xml_or_html + respond_to do |type| + type.json { render :text => 'JSON' } + type.xml { render :xml => 'XML' } + type.html { render :text => 'HTML' } + end + end + def forced_xml request.format = :xml @@ -364,6 +373,17 @@ class RespondToControllerTest < ActionController::TestCase get :handle_any_any assert_equal 'Whatever you ask for, I got it', @response.body end + + def test_browser_check_with_any_any + @request.accept = "application/json, application/xml" + get :json_xml_or_html + assert_equal 'JSON', @response.body + + @request.accept = "application/json, application/xml, */*" + get :json_xml_or_html + assert_equal 'HTML', @response.body + end + def test_rjs_type_skips_layout @request.accept = "text/javascript" diff --git a/actionpack/test/controller/url_for_test.rb b/actionpack/test/controller/url_for_test.rb index 4b30b0af36..71a4a43477 100644 --- a/actionpack/test/controller/url_for_test.rb +++ b/actionpack/test/controller/url_for_test.rb @@ -120,7 +120,7 @@ module AbstractController def test_relative_url_root_is_respected # ROUTES TODO: Tests should not have to pass :relative_url_root directly. This - # should probably come from the router. + # should probably come from routes. add_host! assert_equal('https://www.basecamphq.com/subdir/c/a/i', diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index 26bd641cd6..2a014bf976 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -45,7 +45,10 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest match 'account/logout' => redirect("/logout"), :as => :logout_redirect match 'account/login', :to => redirect("/login") - match 'account/overview' + constraints(lambda { |req| true }) do + match 'account/overview' + end + match '/account/nested/overview' match 'sign_in' => "sessions#new" @@ -298,8 +301,8 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest resource :dashboard, :constraints => { :ip => /192\.168\.1\.\d{1,3}/ } + resource :token, :module => :api scope :module => :api do - resource :token resources :errors, :shallow => true do resources :notices end @@ -310,11 +313,6 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest match '/' => 'mes#index' end - namespace :private do - root :to => redirect('/private/index') - match "index", :to => 'private#index' - end - get "(/:username)/followers" => "followers#index" get "/groups(/user/:username)" => "groups#index" get "(/user/:username)/photos" => "photos#index" @@ -325,7 +323,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end end - match "whatever/:controller(/:action(/:id))" + match "whatever/:controller(/:action(/:id))", :id => /\d+/ resource :profile do get :settings @@ -348,6 +346,63 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end end + namespace :private do + root :to => redirect('/private/index') + match "index", :to => 'private#index' + end + + scope :only => [:index, :show] do + namespace :only do + resources :clubs do + resources :players + resource :chairman + end + end + end + + scope :except => [:new, :create, :edit, :update, :destroy] do + namespace :except do + resources :clubs do + resources :players + resource :chairman + end + end + end + + scope :only => :show do + namespace :only do + resources :sectors, :only => :index do + resources :companies do + scope :only => :index do + resources :divisions + end + scope :except => [:show, :update, :destroy] do + resources :departments + end + end + resource :leader + resources :managers, :except => [:show, :update, :destroy] + end + end + end + + scope :except => :index do + namespace :except do + resources :sectors, :except => [:show, :update, :destroy] do + resources :companies do + scope :except => [:show, :update, :destroy] do + resources :divisions + end + scope :only => :index do + resources :departments + end + end + resource :leader + resources :managers, :only => :index + end + end + end + match '/:locale/*file.:format', :to => 'files#show', :file => /path\/to\/existing\/file/ end end @@ -470,6 +525,18 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end end + def test_namespace_with_controller_segment + assert_raise(ArgumentError) do + self.class.stub_controllers do |routes| + routes.draw do + namespace :admin do + match '/:controller(/:action(/:id(.:format)))' + end + end + end + end + end + def test_session_singleton_resource with_test_routes do get '/session' @@ -1240,7 +1307,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest assert_equal 'pass', @response.headers['X-Cascade'] get '/products/0001/images' assert_equal 'images#index', @response.body - get '/products/0001/images/1' + get '/products/0001/images/0001' assert_equal 'images#show', @response.body get '/dashboard', {}, {'REMOTE_ADDR' => '10.0.0.100'} @@ -1285,6 +1352,22 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end end + def test_url_generator_for_namespaced_generic_route + with_test_routes do + get 'whatever/foo/bar/show' + assert_equal 'foo/bar#show', @response.body + + get 'whatever/foo/bar/show/1' + assert_equal 'foo/bar#show', @response.body + + assert_equal 'http://www.example.com/whatever/foo/bar/show', + url_for(:controller => "foo/bar", :action => "show") + + assert_equal 'http://www.example.com/whatever/foo/bar/show/1', + url_for(:controller => "foo/bar", :action => "show", :id => '1') + end + end + def test_assert_recognizes_account_overview with_test_routes do assert_recognizes({:controller => "account", :action => "overview"}, "/account/overview") @@ -1628,6 +1711,182 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end end + def test_only_should_be_read_from_scope + with_test_routes do + get '/only/clubs' + assert_equal 'only/clubs#index', @response.body + assert_equal '/only/clubs', only_clubs_path + + get '/only/clubs/1/edit' + assert_equal 'Not Found', @response.body + assert_raise(NoMethodError) { edit_only_club_path(:id => '1') } + + get '/only/clubs/1/players' + assert_equal 'only/players#index', @response.body + assert_equal '/only/clubs/1/players', only_club_players_path(:club_id => '1') + + get '/only/clubs/1/players/2/edit' + assert_equal 'Not Found', @response.body + assert_raise(NoMethodError) { edit_only_club_player_path(:club_id => '1', :id => '2') } + + get '/only/clubs/1/chairman' + assert_equal 'only/chairmen#show', @response.body + assert_equal '/only/clubs/1/chairman', only_club_chairman_path(:club_id => '1') + + get '/only/clubs/1/chairman/edit' + assert_equal 'Not Found', @response.body + assert_raise(NoMethodError) { edit_only_club_chairman_path(:club_id => '1') } + end + end + + def test_except_should_be_read_from_scope + with_test_routes do + get '/except/clubs' + assert_equal 'except/clubs#index', @response.body + assert_equal '/except/clubs', except_clubs_path + + get '/except/clubs/1/edit' + assert_equal 'Not Found', @response.body + assert_raise(NoMethodError) { edit_except_club_path(:id => '1') } + + get '/except/clubs/1/players' + assert_equal 'except/players#index', @response.body + assert_equal '/except/clubs/1/players', except_club_players_path(:club_id => '1') + + get '/except/clubs/1/players/2/edit' + assert_equal 'Not Found', @response.body + assert_raise(NoMethodError) { edit_except_club_player_path(:club_id => '1', :id => '2') } + + get '/except/clubs/1/chairman' + assert_equal 'except/chairmen#show', @response.body + assert_equal '/except/clubs/1/chairman', except_club_chairman_path(:club_id => '1') + + get '/except/clubs/1/chairman/edit' + assert_equal 'Not Found', @response.body + assert_raise(NoMethodError) { edit_except_club_chairman_path(:club_id => '1') } + end + end + + def test_only_option_should_override_scope + with_test_routes do + get '/only/sectors' + assert_equal 'only/sectors#index', @response.body + assert_equal '/only/sectors', only_sectors_path + + get '/only/sectors/1' + assert_equal 'Not Found', @response.body + assert_raise(NoMethodError) { only_sector_path(:id => '1') } + end + end + + def test_only_option_should_not_inherit + with_test_routes do + get '/only/sectors/1/companies/2' + assert_equal 'only/companies#show', @response.body + assert_equal '/only/sectors/1/companies/2', only_sector_company_path(:sector_id => '1', :id => '2') + + get '/only/sectors/1/leader' + assert_equal 'only/leaders#show', @response.body + assert_equal '/only/sectors/1/leader', only_sector_leader_path(:sector_id => '1') + end + end + + def test_except_option_should_override_scope + with_test_routes do + get '/except/sectors' + assert_equal 'except/sectors#index', @response.body + assert_equal '/except/sectors', except_sectors_path + + get '/except/sectors/1' + assert_equal 'Not Found', @response.body + assert_raise(NoMethodError) { except_sector_path(:id => '1') } + end + end + + def test_except_option_should_not_inherit + with_test_routes do + get '/except/sectors/1/companies/2' + assert_equal 'except/companies#show', @response.body + assert_equal '/except/sectors/1/companies/2', except_sector_company_path(:sector_id => '1', :id => '2') + + get '/except/sectors/1/leader' + assert_equal 'except/leaders#show', @response.body + assert_equal '/except/sectors/1/leader', except_sector_leader_path(:sector_id => '1') + end + end + + def test_except_option_should_override_scoped_only + with_test_routes do + get '/only/sectors/1/managers' + assert_equal 'only/managers#index', @response.body + assert_equal '/only/sectors/1/managers', only_sector_managers_path(:sector_id => '1') + + get '/only/sectors/1/managers/2' + assert_equal 'Not Found', @response.body + assert_raise(NoMethodError) { only_sector_manager_path(:sector_id => '1', :id => '2') } + end + end + + def test_only_option_should_override_scoped_except + with_test_routes do + get '/except/sectors/1/managers' + assert_equal 'except/managers#index', @response.body + assert_equal '/except/sectors/1/managers', except_sector_managers_path(:sector_id => '1') + + get '/except/sectors/1/managers/2' + assert_equal 'Not Found', @response.body + assert_raise(NoMethodError) { except_sector_manager_path(:sector_id => '1', :id => '2') } + end + end + + def test_only_scope_should_override_parent_scope + with_test_routes do + get '/only/sectors/1/companies/2/divisions' + assert_equal 'only/divisions#index', @response.body + assert_equal '/only/sectors/1/companies/2/divisions', only_sector_company_divisions_path(:sector_id => '1', :company_id => '2') + + get '/only/sectors/1/companies/2/divisions/3' + assert_equal 'Not Found', @response.body + assert_raise(NoMethodError) { only_sector_company_division_path(:sector_id => '1', :company_id => '2', :id => '3') } + end + end + + def test_except_scope_should_override_parent_scope + with_test_routes do + get '/except/sectors/1/companies/2/divisions' + assert_equal 'except/divisions#index', @response.body + assert_equal '/except/sectors/1/companies/2/divisions', except_sector_company_divisions_path(:sector_id => '1', :company_id => '2') + + get '/except/sectors/1/companies/2/divisions/3' + assert_equal 'Not Found', @response.body + assert_raise(NoMethodError) { except_sector_company_division_path(:sector_id => '1', :company_id => '2', :id => '3') } + end + end + + def test_except_scope_should_override_parent_only_scope + with_test_routes do + get '/only/sectors/1/companies/2/departments' + assert_equal 'only/departments#index', @response.body + assert_equal '/only/sectors/1/companies/2/departments', only_sector_company_departments_path(:sector_id => '1', :company_id => '2') + + get '/only/sectors/1/companies/2/departments/3' + assert_equal 'Not Found', @response.body + assert_raise(NoMethodError) { only_sector_company_department_path(:sector_id => '1', :company_id => '2', :id => '3') } + end + end + + def test_only_scope_should_override_parent_except_scope + with_test_routes do + get '/except/sectors/1/companies/2/departments' + assert_equal 'except/departments#index', @response.body + assert_equal '/except/sectors/1/companies/2/departments', except_sector_company_departments_path(:sector_id => '1', :company_id => '2') + + get '/except/sectors/1/companies/2/departments/3' + assert_equal 'Not Found', @response.body + assert_raise(NoMethodError) { except_sector_company_department_path(:sector_id => '1', :company_id => '2', :id => '3') } + end + end + private def with_test_routes yield diff --git a/actionpack/test/dispatch/session/cookie_store_test.rb b/actionpack/test/dispatch/session/cookie_store_test.rb index fd63f5ad5e..f0e01bfff0 100644 --- a/actionpack/test/dispatch/session/cookie_store_test.rb +++ b/actionpack/test/dispatch/session/cookie_store_test.rb @@ -30,6 +30,11 @@ class CookieStoreTest < ActionController::IntegrationTest render :text => "id: #{request.session_options[:id]}" end + def call_session_clear + session.clear + head :ok + end + def call_reset_session reset_session head :ok @@ -175,6 +180,23 @@ class CookieStoreTest < ActionController::IntegrationTest end end + def test_setting_session_value_after_session_clear + with_test_route_set do + get '/set_session_value' + assert_response :success + session_payload = response.body + assert_equal "_myapp_session=#{response.body}; path=/; HttpOnly", + headers['Set-Cookie'] + + get '/call_session_clear' + assert_response :success + + get '/get_session_value' + assert_response :success + assert_equal 'foo: nil', response.body + end + end + def test_persistent_session_id with_test_route_set do cookies[SessionKey] = SignedBar diff --git a/actionpack/test/dispatch/url_generation_test.rb b/actionpack/test/dispatch/url_generation_test.rb index 326b336a1a..f83651d583 100644 --- a/actionpack/test/dispatch/url_generation_test.rb +++ b/actionpack/test/dispatch/url_generation_test.rb @@ -2,24 +2,24 @@ require 'abstract_unit' module TestUrlGeneration class WithMountPoint < ActionDispatch::IntegrationTest - Router = ActionDispatch::Routing::RouteSet.new - Router.draw { match "/foo", :to => "my_route_generating#index", :as => :foo } + Routes = ActionDispatch::Routing::RouteSet.new + Routes.draw { match "/foo", :to => "my_route_generating#index", :as => :foo } class ::MyRouteGeneratingController < ActionController::Base - include Router.url_helpers + include Routes.url_helpers def index render :text => foo_path end end - include Router.url_helpers + include Routes.url_helpers - def _router - Router + def _routes + Routes end def app - Router + Routes end test "generating URLS normally" do @@ -30,7 +30,7 @@ module TestUrlGeneration assert_equal "/bar/foo", foo_path(:script_name => "/bar") end - test "the request's SCRIPT_NAME takes precedence over the router's" do + test "the request's SCRIPT_NAME takes precedence over the routes'" do get "/foo", {}, 'SCRIPT_NAME' => "/new" assert_equal "/new/foo", response.body end diff --git a/actionpack/test/template/url_helper_test.rb b/actionpack/test/template/url_helper_test.rb index 765beebfa3..048f96c9a9 100644 --- a/actionpack/test/template/url_helper_test.rb +++ b/actionpack/test/template/url_helper_test.rb @@ -406,6 +406,14 @@ end class UrlHelperControllerTest < ActionController::TestCase class UrlHelperController < ActionController::Base test_routes do |map| + match 'url_helper_controller_test/url_helper/show/:id', + :to => 'url_helper_controller_test/url_helper#show', + :as => :show + + match 'url_helper_controller_test/url_helper/profile/:name', + :to => 'url_helper_controller_test/url_helper#show', + :as => :profile + match 'url_helper_controller_test/url_helper/show_named_route', :to => 'url_helper_controller_test/url_helper#show_named_route', :as => :show_named_route @@ -418,6 +426,14 @@ class UrlHelperControllerTest < ActionController::TestCase :as => :normalize_recall_params end + def show + if params[:name] + render :inline => 'ok' + else + redirect_to profile_path(params[:id]) + end + end + def show_url_for render :inline => "<%= url_for :controller => 'url_helper_controller_test/url_helper', :action => 'show_url_for' %>" end @@ -484,15 +500,24 @@ class UrlHelperControllerTest < ActionController::TestCase assert_equal 'http://testtwo.host/url_helper_controller_test/url_helper/show_named_route', @response.body end - def test_recall_params_should_be_normalized_when_using_block_route + def test_recall_params_should_be_normalized get :normalize_recall_params assert_equal '/url_helper_controller_test/url_helper/normalize_recall_params', @response.body end - def test_recall_params_should_not_be_changed_when_using_normal_route + def test_recall_params_should_not_be_changed get :recall_params_not_changed assert_equal '/url_helper_controller_test/url_helper/show_url_for', @response.body end + + def test_recall_params_should_normalize_id + get :show, :id => '123' + assert_equal 302, @response.status + assert_equal 'http://test.host/url_helper_controller_test/url_helper/profile/123', @response.location + + get :show, :name => '123' + assert_equal 'ok', @response.body + end end class TasksController < ActionController::Base diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb index 026430fee3..5ed21a39c2 100644 --- a/activemodel/lib/active_model.rb +++ b/activemodel/lib/active_model.rb @@ -38,6 +38,7 @@ module ActiveModel autoload :EachValidator, 'active_model/validator' autoload :Errors autoload :Lint + autoload :MassAssignmentSecurity autoload :Name, 'active_model/naming' autoload :Naming autoload :Observer, 'active_model/observing' diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index d42fc5291d..482b3dac47 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- require 'active_support/core_ext/array/wrap' +require 'active_support/core_ext/array/conversions' require 'active_support/core_ext/string/inflections' require 'active_support/core_ext/object/blank' require 'active_support/core_ext/hash/reverse_merge' diff --git a/activemodel/lib/active_model/mass_assignment_security.rb b/activemodel/lib/active_model/mass_assignment_security.rb new file mode 100644 index 0000000000..66cd9fdde6 --- /dev/null +++ b/activemodel/lib/active_model/mass_assignment_security.rb @@ -0,0 +1,160 @@ +require 'active_support/core_ext/class/attribute.rb' +require 'active_model/mass_assignment_security/permission_set' + +module ActiveModel + # = Active Model Mass-Assignment Security + module MassAssignmentSecurity + extend ActiveSupport::Concern + + included do + class_attribute :_accessible_attributes + class_attribute :_protected_attributes + class_attribute :_active_authorizer + end + + # Mass assignment security provides an interface for protecting attributes + # from end-user assignment. For more complex permissions, mass assignment security + # may be handled outside the model by extending a non-ActiveRecord class, + # such as a controller, with this behavior. + # + # For example, a logged in user may need to assign additional attributes depending + # on their role: + # + # class AccountsController < ApplicationController + # include ActiveModel::MassAssignmentSecurity + # + # attr_accessible :first_name, :last_name + # + # def self.admin_accessible_attributes + # accessible_attributes + [ :plan_id ] + # end + # + # def update + # ... + # @account.update_attributes(account_params) + # ... + # end + # + # protected + # + # def account_params + # sanitize_for_mass_assignment(params[:account]) + # end + # + # def mass_assignment_authorizer + # admin ? admin_accessible_attributes : super + # end + # + # end + # + module ClassMethods + # Attributes named in this macro are protected from mass-assignment + # whenever attributes are sanitized before assignment. + # + # Mass-assignment to these attributes will simply be ignored, to assign + # to them you can use direct writer methods. This is meant to protect + # sensitive attributes from being overwritten by malicious users + # tampering with URLs or forms. + # + # == Example + # + # class Customer + # include ActiveModel::MassAssignmentSecurity + # + # attr_accessor :name, :credit_rating + # attr_protected :credit_rating + # + # def attributes=(values) + # sanitize_for_mass_assignment(values).each do |k, v| + # send("#{k}=", v) + # end + # end + # end + # + # customer = Customer.new + # customer.attributes = { "name" => "David", "credit_rating" => "Excellent" } + # customer.name # => "David" + # customer.credit_rating # => nil + # + # customer.credit_rating = "Average" + # customer.credit_rating # => "Average" + # + # To start from an all-closed default and enable attributes as needed, + # have a look at +attr_accessible+. + # + # Note that using <tt>Hash#except</tt> or <tt>Hash#slice</tt> in place of +attr_protected+ + # to sanitize attributes won't provide sufficient protection. + def attr_protected(*names) + self._protected_attributes = self.protected_attributes + names + self._active_authorizer = self._protected_attributes + end + + # Specifies a white list of model attributes that can be set via + # mass-assignment. + # + # This is the opposite of the +attr_protected+ macro: Mass-assignment + # will only set attributes in this list, to assign to the rest of + # attributes you can use direct writer methods. This is meant to protect + # sensitive attributes from being overwritten by malicious users + # tampering with URLs or forms. If you'd rather start from an all-open + # default and restrict attributes as needed, have a look at + # +attr_protected+. + # + # class Customer + # include ActiveModel::MassAssignmentSecurity + # + # attr_accessor :name, :credit_rating + # attr_accessible :name + # + # def attributes=(values) + # sanitize_for_mass_assignment(values).each do |k, v| + # send("#{k}=", v) + # end + # end + # end + # + # customer = Customer.new + # customer.attributes = { :name => "David", :credit_rating => "Excellent" } + # customer.name # => "David" + # customer.credit_rating # => nil + # + # customer.credit_rating = "Average" + # customer.credit_rating # => "Average" + # + # Note that using <tt>Hash#except</tt> or <tt>Hash#slice</tt> in place of +attr_accessible+ + # to sanitize attributes won't provide sufficient protection. + def attr_accessible(*names) + self._accessible_attributes = self.accessible_attributes + names + self._active_authorizer = self._accessible_attributes + end + + def protected_attributes + self._protected_attributes ||= BlackList.new(attributes_protected_by_default).tap do |w| + w.logger = self.logger if self.respond_to?(:logger) + end + end + + def accessible_attributes + self._accessible_attributes ||= WhiteList.new.tap { |w| w.logger = self.logger if self.respond_to?(:logger) } + end + + def active_authorizer + self._active_authorizer ||= protected_attributes + end + + def attributes_protected_by_default + [] + end + end + + protected + + def sanitize_for_mass_assignment(attributes) + mass_assignment_authorizer.sanitize(attributes) + end + + def mass_assignment_authorizer + self.class.active_authorizer + end + end +end diff --git a/activemodel/lib/active_model/mass_assignment_security/permission_set.rb b/activemodel/lib/active_model/mass_assignment_security/permission_set.rb new file mode 100644 index 0000000000..7c48472799 --- /dev/null +++ b/activemodel/lib/active_model/mass_assignment_security/permission_set.rb @@ -0,0 +1,39 @@ +require 'active_model/mass_assignment_security/sanitizer' + +module ActiveModel + module MassAssignmentSecurity + class PermissionSet < Set + attr_accessor :logger + + def +(values) + super(values.map(&:to_s)) + end + + def include?(key) + super(remove_multiparameter_id(key)) + end + + protected + + def remove_multiparameter_id(key) + key.to_s.gsub(/\(.+/, '') + end + end + + class WhiteList < PermissionSet + include Sanitizer + + def deny?(key) + !include?(key) + end + end + + class BlackList < PermissionSet + include Sanitizer + + def deny?(key) + include?(key) + end + end + end +end
\ No newline at end of file diff --git a/activemodel/lib/active_model/mass_assignment_security/sanitizer.rb b/activemodel/lib/active_model/mass_assignment_security/sanitizer.rb new file mode 100644 index 0000000000..150beb1ff2 --- /dev/null +++ b/activemodel/lib/active_model/mass_assignment_security/sanitizer.rb @@ -0,0 +1,23 @@ +module ActiveModel + module MassAssignmentSecurity + module Sanitizer + # Returns all attributes not denied by the authorizer. + def sanitize(attributes) + sanitized_attributes = attributes.reject { |key, value| deny?(key) } + debug_protected_attribute_removal(attributes, sanitized_attributes) + sanitized_attributes + end + + protected + + def debug_protected_attribute_removal(attributes, sanitized_attributes) + removed_keys = attributes.keys - sanitized_attributes.keys + warn!(removed_keys) if removed_keys.any? + end + + def warn!(attrs) + self.logger.debug "WARNING: Can't mass-assign protected attributes: #{attrs.join(', ')}" if self.logger + end + end + end +end diff --git a/activemodel/lib/active_model/observing.rb b/activemodel/lib/active_model/observing.rb index d0f36ce3b1..c6a79acf81 100644 --- a/activemodel/lib/active_model/observing.rb +++ b/activemodel/lib/active_model/observing.rb @@ -1,6 +1,7 @@ require 'singleton' require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/module/aliasing' +require 'active_support/core_ext/module/remove_method' require 'active_support/core_ext/string/inflections' module ActiveModel @@ -157,6 +158,7 @@ module ActiveModel def observe(*models) models.flatten! models.collect! { |model| model.respond_to?(:to_sym) ? model.to_s.camelize.constantize : model } + remove_possible_method(:observed_classes) define_method(:observed_classes) { models } end diff --git a/activemodel/test/cases/mass_assignment_security/black_list_test.rb b/activemodel/test/cases/mass_assignment_security/black_list_test.rb new file mode 100644 index 0000000000..ed168bc016 --- /dev/null +++ b/activemodel/test/cases/mass_assignment_security/black_list_test.rb @@ -0,0 +1,28 @@ +require "cases/helper" + +class BlackListTest < ActiveModel::TestCase + + def setup + @black_list = ActiveModel::MassAssignmentSecurity::BlackList.new + @included_key = 'admin' + @black_list += [ @included_key ] + end + + test "deny? is true for included items" do + assert_equal true, @black_list.deny?(@included_key) + end + + test "deny? is false for non-included items" do + assert_equal false, @black_list.deny?('first_name') + end + + test "sanitize attributes" do + original_attributes = { 'first_name' => 'allowed', 'admin' => 'denied', 'admin(1)' => 'denied' } + attributes = @black_list.sanitize(original_attributes) + + assert attributes.key?('first_name'), "Allowed key shouldn't be rejected" + assert !attributes.key?('admin'), "Denied key should be rejected" + assert !attributes.key?('admin(1)'), "Multi-parameter key should be detected" + end + +end diff --git a/activemodel/test/cases/mass_assignment_security/permission_set_test.rb b/activemodel/test/cases/mass_assignment_security/permission_set_test.rb new file mode 100644 index 0000000000..d005b638e4 --- /dev/null +++ b/activemodel/test/cases/mass_assignment_security/permission_set_test.rb @@ -0,0 +1,30 @@ +require "cases/helper" + +class PermissionSetTest < ActiveModel::TestCase + + def setup + @permission_list = ActiveModel::MassAssignmentSecurity::PermissionSet.new + end + + test "+ stringifies added collection values" do + symbol_collection = [ :admin ] + new_list = @permission_list += symbol_collection + + assert new_list.include?('admin'), "did not add collection to #{@permission_list.inspect}}" + end + + test "include? normalizes multi-parameter keys" do + multi_param_key = 'admin(1)' + new_list = @permission_list += [ 'admin' ] + + assert new_list.include?(multi_param_key), "#{multi_param_key} not found in #{@permission_list.inspect}" + end + + test "include? normal keys" do + normal_key = 'admin' + new_list = @permission_list += [ normal_key ] + + assert new_list.include?(normal_key), "#{normal_key} not found in #{@permission_list.inspect}" + end + +end diff --git a/activemodel/test/cases/mass_assignment_security/sanitizer_test.rb b/activemodel/test/cases/mass_assignment_security/sanitizer_test.rb new file mode 100644 index 0000000000..367207aab3 --- /dev/null +++ b/activemodel/test/cases/mass_assignment_security/sanitizer_test.rb @@ -0,0 +1,37 @@ +require "cases/helper" +require 'logger' + +class SanitizerTest < ActiveModel::TestCase + + class SanitizingAuthorizer + include ActiveModel::MassAssignmentSecurity::Sanitizer + + attr_accessor :logger + + def deny?(key) + [ 'admin' ].include?(key) + end + + end + + def setup + @sanitizer = SanitizingAuthorizer.new + end + + test "sanitize attributes" do + original_attributes = { 'first_name' => 'allowed', 'admin' => 'denied' } + attributes = @sanitizer.sanitize(original_attributes) + + assert attributes.key?('first_name'), "Allowed key shouldn't be rejected" + assert !attributes.key?('admin'), "Denied key should be rejected" + end + + test "debug mass assignment removal" do + original_attributes = { 'first_name' => 'allowed', 'admin' => 'denied' } + log = StringIO.new + @sanitizer.logger = Logger.new(log) + @sanitizer.sanitize(original_attributes) + assert (log.string =~ /admin/), "Should log removed attributes: #{log.string}" + end + +end diff --git a/activemodel/test/cases/mass_assignment_security/white_list_test.rb b/activemodel/test/cases/mass_assignment_security/white_list_test.rb new file mode 100644 index 0000000000..aa3596ad2a --- /dev/null +++ b/activemodel/test/cases/mass_assignment_security/white_list_test.rb @@ -0,0 +1,28 @@ +require "cases/helper" + +class WhiteListTest < ActiveModel::TestCase + + def setup + @white_list = ActiveModel::MassAssignmentSecurity::WhiteList.new + @included_key = 'first_name' + @white_list += [ @included_key ] + end + + test "deny? is false for included items" do + assert_equal false, @white_list.deny?(@included_key) + end + + test "deny? is true for non-included items" do + assert_equal true, @white_list.deny?('admin') + end + + test "sanitize attributes" do + original_attributes = { 'first_name' => 'allowed', 'admin' => 'denied', 'admin(1)' => 'denied' } + attributes = @white_list.sanitize(original_attributes) + + assert attributes.key?('first_name'), "Allowed key shouldn't be rejected" + assert !attributes.key?('admin'), "Denied key should be rejected" + assert !attributes.key?('admin(1)'), "Multi-parameter key should be detected" + end + +end diff --git a/activemodel/test/cases/mass_assignment_security_test.rb b/activemodel/test/cases/mass_assignment_security_test.rb new file mode 100644 index 0000000000..0f7a38b0bc --- /dev/null +++ b/activemodel/test/cases/mass_assignment_security_test.rb @@ -0,0 +1,52 @@ +require "cases/helper" +require 'models/mass_assignment_specific' + +class MassAssignmentSecurityTest < ActiveModel::TestCase + + def test_attribute_protection + user = User.new + expected = { "name" => "John Smith", "email" => "john@smith.com" } + sanitized = user.sanitize_for_mass_assignment(expected.merge("admin" => true)) + assert_equal expected, sanitized + end + + def test_attributes_accessible + user = Person.new + expected = { "name" => "John Smith", "email" => "john@smith.com" } + sanitized = user.sanitize_for_mass_assignment(expected.merge("super_powers" => true)) + assert_equal expected, sanitized + end + + def test_attributes_protected_by_default + firm = Firm.new + expected = { } + sanitized = firm.sanitize_for_mass_assignment({ "type" => "Client" }) + assert_equal expected, sanitized + end + + def test_mass_assignment_protection_inheritance + assert LoosePerson.accessible_attributes.blank? + assert_equal Set.new([ 'credit_rating', 'administrator']), LoosePerson.protected_attributes + + assert LooseDescendant.accessible_attributes.blank? + assert_equal Set.new([ 'credit_rating', 'administrator', 'phone_number']), LooseDescendant.protected_attributes + + assert LooseDescendantSecond.accessible_attributes.blank? + assert_equal Set.new([ 'credit_rating', 'administrator', 'phone_number', 'name']), LooseDescendantSecond.protected_attributes, + 'Running attr_protected twice in one class should merge the protections' + + assert (TightPerson.protected_attributes - TightPerson.attributes_protected_by_default).blank? + assert_equal Set.new([ 'name', 'address' ]), TightPerson.accessible_attributes + + assert (TightDescendant.protected_attributes - TightDescendant.attributes_protected_by_default).blank? + assert_equal Set.new([ 'name', 'address', 'phone_number' ]), TightDescendant.accessible_attributes + end + + def test_mass_assignment_multiparameter_protector + task = Task.new + attributes = { "starting(1i)" => "2004", "starting(2i)" => "6", "starting(3i)" => "24" } + sanitized = task.sanitize_for_mass_assignment(attributes) + assert_equal sanitized, { } + end + +end
\ No newline at end of file diff --git a/activemodel/test/models/mass_assignment_specific.rb b/activemodel/test/models/mass_assignment_specific.rb new file mode 100644 index 0000000000..2a8fe170c2 --- /dev/null +++ b/activemodel/test/models/mass_assignment_specific.rb @@ -0,0 +1,57 @@ +class User + include ActiveModel::MassAssignmentSecurity + attr_protected :admin + + public :sanitize_for_mass_assignment +end + +class Person + include ActiveModel::MassAssignmentSecurity + attr_accessible :name, :email + + public :sanitize_for_mass_assignment +end + +class Firm + include ActiveModel::MassAssignmentSecurity + + public :sanitize_for_mass_assignment + + def self.attributes_protected_by_default + ["type"] + end +end + +class Task + include ActiveModel::MassAssignmentSecurity + attr_protected :starting + + public :sanitize_for_mass_assignment +end + +class LoosePerson + include ActiveModel::MassAssignmentSecurity + attr_protected :credit_rating, :administrator +end + +class LooseDescendant < LoosePerson + attr_protected :phone_number +end + +class LooseDescendantSecond< LoosePerson + attr_protected :phone_number + attr_protected :name +end + +class TightPerson + include ActiveModel::MassAssignmentSecurity + attr_accessible :name, :address + + def self.attributes_protected_by_default + ["mobile_number"] + end +end + +class TightDescendant < TightPerson + attr_accessible :phone_number +end
\ No newline at end of file diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb index f13c250ca4..cbec5789fd 100644 --- a/activerecord/lib/active_record/association_preload.rb +++ b/activerecord/lib/active_record/association_preload.rb @@ -282,7 +282,12 @@ module ActiveRecord end end else - records.first.class.preload_associations(records, through_association) + options = {} + options[:include] = reflection.options[:include] || reflection.options[:source] if reflection.options[:conditions] + options[:order] = reflection.options[:order] + options[:conditions] = reflection.options[:conditions] + records.first.class.preload_associations(records, through_association, options) + records.each do |record| through_records.concat Array.wrap(record.send(through_association)) end diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 4caa434fc0..65daa8ffbe 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -763,20 +763,27 @@ module ActiveRecord # An empty array is returned if none are found. # [collection<<(object, ...)] # Adds one or more objects to the collection by setting their foreign keys to the collection's primary key. + # Note that this operation instantly fires update sql without waiting for the save or update call on the + # parent object. # [collection.delete(object, ...)] # Removes one or more objects from the collection by setting their foreign keys to +NULL+. # Objects will be in addition destroyed if they're associated with <tt>:dependent => :destroy</tt>, # and deleted if they're associated with <tt>:dependent => :delete_all</tt>. # [collection=objects] - # Replaces the collections content by deleting and adding objects as appropriate. + # Replaces the collections content by deleting and adding objects as appropriate. If the <tt>:through</tt> + # option is true callbacks in the join models are triggered except destroy callbacks, since deletion is + # direct. # [collection_singular_ids] # Returns an array of the associated objects' ids # [collection_singular_ids=ids] - # Replace the collection with the objects identified by the primary keys in +ids+ + # Replace the collection with the objects identified by the primary keys in +ids+. This + # method loads the models and calls <tt>collection=</tt>. See above. # [collection.clear] # Removes every object from the collection. This destroys the associated objects if they # are associated with <tt>:dependent => :destroy</tt>, deletes them directly from the # database if <tt>:dependent => :delete_all</tt>, otherwise sets their foreign keys to +NULL+. + # If the <tt>:through</tt> option is true no destroy callbacks are invoked on the join models. + # Join models are directly deleted. # [collection.empty?] # Returns +true+ if there are no associated objects. # [collection.size] @@ -869,9 +876,11 @@ module ActiveRecord # [:as] # Specifies a polymorphic interface (See <tt>belongs_to</tt>). # [:through] - # Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt> and <tt>:foreign_key</tt> + # Specifies a join model through which to perform the query. Options for <tt>:class_name</tt> and <tt>:foreign_key</tt> # are ignored, as the association uses the source reflection. You can only use a <tt>:through</tt> query through a <tt>belongs_to</tt> - # <tt>has_one</tt> or <tt>has_many</tt> association on the join model. + # <tt>has_one</tt> or <tt>has_many</tt> association on the join model. The collection of join models can be managed via the collection + # API. For example, new join models are created for newly associated objects, and if some are gone their rows are deleted (directly, + # no destroy callbacks are triggered). # [:source] # Specifies the source association name used by <tt>has_many :through</tt> queries. Only use it if the name cannot be # inferred from the association. <tt>has_many :subscribers, :through => :subscriptions</tt> will look for either <tt>:subscribers</tt> or @@ -1186,6 +1195,8 @@ module ActiveRecord # [collection<<(object, ...)] # Adds one or more objects to the collection by creating associations in the join table # (<tt>collection.push</tt> and <tt>collection.concat</tt> are aliases to this method). + # Note that this operation instantly fires update sql without waiting for the save or update call on the + # parent object. # [collection.delete(object, ...)] # Removes one or more objects from the collection by removing their associations from the join table. # This does not destroy the objects. diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index ddf4ce4058..615b7d2719 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -253,9 +253,10 @@ module ActiveRecord # See destroy for more info. def destroy_all load_target - destroy(@target) - reset_target! - reset_named_scopes_cache! + destroy(@target).tap do + reset_target! + reset_named_scopes_cache! + end end def create(attrs = {}) @@ -393,7 +394,12 @@ module ActiveRecord @target = find_target.map do |f| i = @target.index(f) t = @target.delete_at(i) if i - (t && t.changed?) ? t : f + if t && t.changed? + t + else + f.mark_for_destruction if t && t.marked_for_destruction? + f + end end + @target else @target = find_target diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index c8795e4496..c78060c956 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -24,7 +24,7 @@ require 'active_record/errors' require 'active_record/log_subscriber' module ActiveRecord #:nodoc: - # = Active Record + # = Active Record # # Active Record objects don't specify their attributes directly, but rather infer them from the table definition with # which they're linked. Adding, removing, and changing attributes and their type is done directly in the database. Any change @@ -476,112 +476,16 @@ module ActiveRecord #:nodoc: connection.select_value(sql, "#{name} Count").to_i end - # Attributes named in this macro are protected from mass-assignment, - # such as <tt>new(attributes)</tt>, - # <tt>update_attributes(attributes)</tt>, or - # <tt>attributes=(attributes)</tt>. - # - # Mass-assignment to these attributes will simply be ignored, to assign - # to them you can use direct writer methods. This is meant to protect - # sensitive attributes from being overwritten by malicious users - # tampering with URLs or forms. - # - # class Customer < ActiveRecord::Base - # attr_protected :credit_rating - # end - # - # customer = Customer.new("name" => David, "credit_rating" => "Excellent") - # customer.credit_rating # => nil - # customer.attributes = { "description" => "Jolly fellow", "credit_rating" => "Superb" } - # customer.credit_rating # => nil - # - # customer.credit_rating = "Average" - # customer.credit_rating # => "Average" - # - # To start from an all-closed default and enable attributes as needed, - # have a look at +attr_accessible+. - # - # If the access logic of your application is richer you can use <tt>Hash#except</tt> - # or <tt>Hash#slice</tt> to sanitize the hash of parameters before they are - # passed to Active Record. - # - # For example, it could be the case that the list of protected attributes - # for a given model depends on the role of the user: - # - # # Assumes plan_id is not protected because it depends on the role. - # params[:account] = params[:account].except(:plan_id) unless admin? - # @account.update_attributes(params[:account]) - # - # Note that +attr_protected+ is still applied to the received hash. Thus, - # with this technique you can at most _extend_ the list of protected - # attributes for a particular mass-assignment call. - def attr_protected(*attributes) - write_inheritable_attribute(:attr_protected, Set.new(attributes.map {|a| a.to_s}) + (protected_attributes || [])) + # Attributes listed as readonly can be set for a new record, but will be ignored in database updates afterwards. + def attr_readonly(*attributes) + write_inheritable_attribute(:attr_readonly, Set.new(attributes.map(&:to_s)) + (readonly_attributes || [])) end - # Returns an array of all the attributes that have been protected from mass-assignment. - def protected_attributes # :nodoc: - read_inheritable_attribute(:attr_protected) + # Returns an array of all the attributes that have been specified as readonly. + def readonly_attributes + read_inheritable_attribute(:attr_readonly) || [] end - # Specifies a white list of model attributes that can be set via - # mass-assignment, such as <tt>new(attributes)</tt>, - # <tt>update_attributes(attributes)</tt>, or - # <tt>attributes=(attributes)</tt> - # - # This is the opposite of the +attr_protected+ macro: Mass-assignment - # will only set attributes in this list, to assign to the rest of - # attributes you can use direct writer methods. This is meant to protect - # sensitive attributes from being overwritten by malicious users - # tampering with URLs or forms. If you'd rather start from an all-open - # default and restrict attributes as needed, have a look at - # +attr_protected+. - # - # class Customer < ActiveRecord::Base - # attr_accessible :name, :nickname - # end - # - # customer = Customer.new(:name => "David", :nickname => "Dave", :credit_rating => "Excellent") - # customer.credit_rating # => nil - # customer.attributes = { :name => "Jolly fellow", :credit_rating => "Superb" } - # customer.credit_rating # => nil - # - # customer.credit_rating = "Average" - # customer.credit_rating # => "Average" - # - # If the access logic of your application is richer you can use <tt>Hash#except</tt> - # or <tt>Hash#slice</tt> to sanitize the hash of parameters before they are - # passed to Active Record. - # - # For example, it could be the case that the list of accessible attributes - # for a given model depends on the role of the user: - # - # # Assumes plan_id is accessible because it depends on the role. - # params[:account] = params[:account].except(:plan_id) unless admin? - # @account.update_attributes(params[:account]) - # - # Note that +attr_accessible+ is still applied to the received hash. Thus, - # with this technique you can at most _narrow_ the list of accessible - # attributes for a particular mass-assignment call. - def attr_accessible(*attributes) - write_inheritable_attribute(:attr_accessible, Set.new(attributes.map(&:to_s)) + (accessible_attributes || [])) - end - - # Returns an array of all the attributes that have been made accessible to mass-assignment. - def accessible_attributes # :nodoc: - read_inheritable_attribute(:attr_accessible) - end - - # Attributes listed as readonly can be set for a new record, but will be ignored in database updates afterwards. - def attr_readonly(*attributes) - write_inheritable_attribute(:attr_readonly, Set.new(attributes.map(&:to_s)) + (readonly_attributes || [])) - end - - # Returns an array of all the attributes that have been specified as readonly. - def readonly_attributes - read_inheritable_attribute(:attr_readonly) || [] - end - # If you have an attribute that needs to be saved to the database as an object, and retrieved as the same object, # then specify the name of that attribute using this method and it will be handled automatically. # The serialization is done through YAML. If +class_name+ is specified, the serialized object must be of that @@ -869,6 +773,9 @@ module ActiveRecord #:nodoc: # Returns the base AR subclass that this class descends from. If A # extends AR::Base, A.base_class will return A. If B descends from A # through some arbitrarily deep hierarchy, B.base_class will return A. + # + # If B < A and C < B and if A is an abstract_class then both B.base_class + # and C.base_class would return B as the answer since A is an abstract_class. def base_class class_of_active_record_descendant(self) end @@ -876,8 +783,7 @@ module ActiveRecord #:nodoc: # Set this to true if this is an abstract class (see <tt>abstract_class?</tt>). attr_accessor :abstract_class - # Returns whether this class is a base AR class. If A is a base class and - # B descends from A, then B.base_class will return B. + # Returns whether this class is an abstract class or not. def abstract_class? defined?(@abstract_class) && @abstract_class == true end @@ -896,11 +802,6 @@ module ActiveRecord #:nodoc: store_full_sti_class ? name : name.demodulize end - def relation - @relation ||= Relation.new(self, arel_table) - finder_needs_type_condition? ? @relation.where(type_condition) : @relation - end - def arel_table @arel_table ||= Arel::Table.new(table_name, :engine => arel_engine) end @@ -941,6 +842,12 @@ module ActiveRecord #:nodoc: end private + + def relation #:nodoc: + @relation ||= Relation.new(self, arel_table) + finder_needs_type_condition? ? @relation.where(type_condition) : @relation + end + # Finder methods must instantiate through this method to work with the # single-table inheritance model that makes it possible to create # objects of different types from the same table. @@ -1245,11 +1152,6 @@ MSG end end - # Returns the name of the class descending directly from Active Record in the inheritance hierarchy. - def class_name_of_active_record_descendant(klass) #:nodoc: - klass.base_class.name - end - # Accepts an array, hash, or string of SQL conditions and sanitizes # them into a valid SQL fragment for a WHERE clause. # ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'" @@ -1569,11 +1471,11 @@ MSG # user.send(:attributes=, { :username => 'Phusion', :is_admin => true }, false) # user.is_admin? # => true def attributes=(new_attributes, guard_protected_attributes = true) - return if new_attributes.nil? + return unless new_attributes.is_a? Hash attributes = new_attributes.stringify_keys multi_parameter_attributes = [] - attributes = remove_attributes_protected_from_mass_assignment(attributes) if guard_protected_attributes + attributes = sanitize_for_mass_assignment(attributes) if guard_protected_attributes attributes.each do |k, v| if k.include?("(") @@ -1713,46 +1615,10 @@ MSG end end - def remove_attributes_protected_from_mass_assignment(attributes) - safe_attributes = - if self.class.accessible_attributes.nil? && self.class.protected_attributes.nil? - attributes.reject { |key, value| attributes_protected_by_default.include?(key.gsub(/\(.+/, "")) } - elsif self.class.protected_attributes.nil? - attributes.reject { |key, value| !self.class.accessible_attributes.include?(key.gsub(/\(.+/, "")) || attributes_protected_by_default.include?(key.gsub(/\(.+/, "")) } - elsif self.class.accessible_attributes.nil? - attributes.reject { |key, value| self.class.protected_attributes.include?(key.gsub(/\(.+/,"")) || attributes_protected_by_default.include?(key.gsub(/\(.+/, "")) } - else - raise "Declare either attr_protected or attr_accessible for #{self.class}, but not both." - end - - removed_attributes = attributes.keys - safe_attributes.keys - - if removed_attributes.any? - log_protected_attribute_removal(removed_attributes) - end - - safe_attributes - end - - # Removes attributes which have been marked as readonly. - def remove_readonly_attributes(attributes) - unless self.class.readonly_attributes.nil? - attributes.delete_if { |key, value| self.class.readonly_attributes.include?(key.gsub(/\(.+/,"")) } - else - attributes - end - end - - def log_protected_attribute_removal(*attributes) - if logger - logger.debug "WARNING: Can't mass-assign these protected attributes: #{attributes.join(', ')}" - end - end - # The primary key and inheritance column can never be set by mass-assignment for security reasons. - def attributes_protected_by_default - default = [ self.class.primary_key, self.class.inheritance_column ] - default << 'id' unless self.class.primary_key.eql? 'id' + def self.attributes_protected_by_default + default = [ primary_key, inheritance_column ] + default << 'id' unless primary_key.eql? 'id' default end @@ -1917,6 +1783,7 @@ MSG include AttributeMethods::PrimaryKey include AttributeMethods::TimeZoneConversion include AttributeMethods::Dirty + include ActiveModel::MassAssignmentSecurity include Callbacks, ActiveModel::Observing, Timestamp include Associations, AssociationPreload, NamedScope diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index 76b65bf219..ffc3847a31 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -103,10 +103,15 @@ module ActiveRecord # The +options+ hash can include the following keys: # [<tt>:id</tt>] # Whether to automatically add a primary key column. Defaults to true. - # Join tables for +has_and_belongs_to_many+ should set <tt>:id => false</tt>. + # Join tables for +has_and_belongs_to_many+ should set it to false. # [<tt>:primary_key</tt>] # The name of the primary key, if one is to be added automatically. - # Defaults to +id+. + # Defaults to +id+. If <tt>:id</tt> is false this option is ignored. + # + # Also note that this just sets the primary key in the table. You additionally + # need to configure the primary key in the model via the +set_primary_key+ macro. + # Models do NOT auto-detect the primary key from their table definition. + # # [<tt>:options</tt>] # Any extra options you want appended to the table definition. # [<tt>:temporary</tt>] diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 0d9a86a1ea..e5e92f2b1c 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -4,7 +4,17 @@ module ActiveRecord class Base # sqlite3 adapter reuses sqlite_connection. def self.sqlite3_connection(config) # :nodoc: - parse_sqlite_config!(config) + # Require database. + unless config[:database] + raise ArgumentError, "No database file specified. Missing argument: database" + end + + # Allow database path relative to Rails.root, but only if + # the database path is not the special path that tells + # Sqlite to build a database only in memory. + if defined?(Rails.root) && ':memory:' != config[:database] + config[:database] = File.expand_path(config[:database], Rails.root) + end unless 'sqlite3' == config[:adapter] raise ArgumentError, 'adapter name should be "sqlite3"' @@ -16,8 +26,7 @@ module ActiveRecord db = SQLite3::Database.new( config[:database], - :results_as_hash => true, - :type_translation => false + :results_as_hash => true ) db.busy_timeout(config[:timeout]) unless config[:timeout].nil? diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb index 1927585c49..117cf447df 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb @@ -2,25 +2,6 @@ require 'active_record/connection_adapters/abstract_adapter' require 'active_support/core_ext/kernel/requires' module ActiveRecord - class Base - class << self - private - def parse_sqlite_config!(config) - # Require database. - unless config[:database] - raise ArgumentError, "No database file specified. Missing argument: database" - end - - # Allow database path relative to Rails.root, but only if - # the database path is not the special path that tells - # Sqlite to build a database only in memory. - if defined?(Rails.root) && ':memory:' != config[:database] - config[:database] = File.expand_path(config[:database], Rails.root) - end - end - end - end - module ConnectionAdapters #:nodoc: class SQLiteColumn < Column #:nodoc: class << self diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index 999322129a..237cd56683 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -17,9 +17,18 @@ module ActiveRecord def reset_counters(id, *counters) object = find(id) counters.each do |association| - child_class = reflect_on_association(association.to_sym).klass - belongs_name = self.name.demodulize.underscore.to_sym - counter_name = child_class.reflect_on_association(belongs_name).counter_cache_column + has_many_association = reflect_on_association(association.to_sym) + + expected_name = if has_many_association.options[:as] + has_many_association.options[:as].to_s.classify + else + self.name + end + + child_class = has_many_association.klass + belongs_to = child_class.reflect_on_all_associations(:belongs_to) + reflection = belongs_to.find { |e| e.class_name == expected_name } + counter_name = reflection.counter_cache_column self.unscoped.where(arel_table[self.primary_key].eq(object.id)).arel.update({ arel_table[counter_name] => object.send(association).count @@ -103,4 +112,4 @@ module ActiveRecord update_counters(id, counter_name => -1) end end -end
\ No newline at end of file +end diff --git a/activerecord/lib/active_record/named_scope.rb b/activerecord/lib/active_record/named_scope.rb index c010dac64e..849ec9c884 100644 --- a/activerecord/lib/active_record/named_scope.rb +++ b/activerecord/lib/active_record/named_scope.rb @@ -29,7 +29,7 @@ module ActiveRecord if options.present? scoped.apply_finder_options(options) else - current_scoped_methods ? unscoped.merge(current_scoped_methods) : unscoped.clone + current_scoped_methods ? relation.merge(current_scoped_methods) : relation.clone end end diff --git a/activerecord/lib/active_record/observer.rb b/activerecord/lib/active_record/observer.rb index 5f80bd86df..d2ed643f35 100644 --- a/activerecord/lib/active_record/observer.rb +++ b/activerecord/lib/active_record/observer.rb @@ -94,7 +94,7 @@ module ActiveRecord def initialize super - observed_subclasses.each { |klass| add_observer!(klass) } + observed_descendants.each { |klass| add_observer!(klass) } end def self.method_added(method) @@ -108,8 +108,8 @@ module ActiveRecord protected - def observed_subclasses - observed_classes.sum([]) { |klass| klass.send(:descendants) } + def observed_descendants + observed_classes.sum([]) { |klass| klass.descendants } end def observe_callbacks? diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 50166c4385..828a8b41b6 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -102,12 +102,21 @@ module ActiveRecord became end - # Updates a single attribute and saves the record without going through the normal validation procedure. - # This is especially useful for boolean flags on existing records. The regular +update_attribute+ method - # in Base is replaced with this when the validations module is mixed in, which it is by default. + # Updates a single attribute and saves the record without going through the normal validation procedure + # or callbacks. This is especially useful for boolean flags on existing records. def update_attribute(name, value) send("#{name}=", value) - save(:validate => false) + hash = { name => read_attribute(name) } + + if record_update_timestamps + timestamp_attributes_for_update_in_model.each do |column| + hash[column] = read_attribute(column) + end + end + + @changed_attributes.delete(name.to_s) + primary_key = self.class.primary_key + self.class.update_all(hash, { primary_key => self[primary_key] }) == 1 end # Updates all the attributes from the passed-in Hash and saves the record. @@ -234,4 +243,4 @@ module ActiveRecord end end end -end
\ No newline at end of file +end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 7882f05d07..5024787c3c 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -1,7 +1,7 @@ namespace :db do task :load_config => :rails_env do require 'active_record' - ActiveRecord::Base.configurations = Rails::Application.config.database_configuration + ActiveRecord::Base.configurations = Rails.application.config.database_configuration end namespace :create do diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index bc708b573f..d9fc1b4940 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -213,8 +213,7 @@ module ActiveRecord if conditions where(conditions).destroy_all else - to_a.each {|object| object.destroy} - reset + to_a.each {|object| object.destroy }.tap { reset } end end diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index f39951e16c..3bf4c5bdd1 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -339,7 +339,7 @@ module ActiveRecord end def using_limitable_reflections?(reflections) - reflections.collect(&:collection?).length.zero? + reflections.none?(&:collection?) end end diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index ffd12d2082..1075a60f07 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -35,8 +35,7 @@ module ActiveRecord if attribute write_attribute(attribute, current_time) else - write_attribute('updated_at', current_time) if respond_to?(:updated_at) - write_attribute('updated_on', current_time) if respond_to?(:updated_on) + timestamp_attributes_for_update_in_model.each { |column| write_attribute(column.to_s, current_time) } end save! @@ -50,26 +49,36 @@ module ActiveRecord write_attribute('created_at', current_time) if respond_to?(:created_at) && created_at.nil? write_attribute('created_on', current_time) if respond_to?(:created_on) && created_on.nil? - write_attribute('updated_at', current_time) if respond_to?(:updated_at) && updated_at.nil? - write_attribute('updated_on', current_time) if respond_to?(:updated_on) && updated_on.nil? + timestamp_attributes_for_update_in_model.each do |column| + write_attribute(column.to_s, current_time) if self.send(column).nil? + end end super end def update(*args) #:nodoc: + record_update_timestamps + super + end + + def record_update_timestamps if record_timestamps && (!partial_updates? || changed?) current_time = current_time_from_proper_timezone - - write_attribute('updated_at', current_time) if respond_to?(:updated_at) - write_attribute('updated_on', current_time) if respond_to?(:updated_on) + timestamp_attributes_for_update_in_model.each { |column| write_attribute(column.to_s, current_time) } + true + else + false end + end - super + def timestamp_attributes_for_update_in_model #:nodoc: + [:updated_at, :updated_on].select { |elem| respond_to?(elem) } end - def current_time_from_proper_timezone + def current_time_from_proper_timezone #:nodoc: self.class.default_timezone == :utc ? Time.now.utc : Time.now end end -end
\ No newline at end of file +end + diff --git a/activerecord/test/cases/adapters/sqlite/sqlite_adapter_test.rb b/activerecord/test/cases/adapters/sqlite/sqlite_adapter_test.rb new file mode 100644 index 0000000000..2505372b7e --- /dev/null +++ b/activerecord/test/cases/adapters/sqlite/sqlite_adapter_test.rb @@ -0,0 +1,120 @@ +require "cases/helper" + +module ActiveRecord + module ConnectionAdapters + class SQLiteAdapterTest < ActiveRecord::TestCase + def setup + @ctx = Base.sqlite3_connection :database => ':memory:', + :adapter => 'sqlite3', + :timeout => nil + @ctx.execute <<-eosql + CREATE TABLE items ( + id integer PRIMARY KEY AUTOINCREMENT, + number integer + ) + eosql + end + + def test_execute + @ctx.execute "INSERT INTO items (number) VALUES (10)" + records = @ctx.execute "SELECT * FROM items" + assert_equal 1, records.length + + record = records.first + assert_equal 10, record['number'] + assert_equal 1, record['id'] + end + + def test_quote_string + assert_equal "''", @ctx.quote_string("'") + end + + def test_insert_sql + 2.times do |i| + rv = @ctx.insert_sql "INSERT INTO items (number) VALUES (#{i})" + assert_equal(i + 1, rv) + end + + records = @ctx.execute "SELECT * FROM items" + assert_equal 2, records.length + end + + def test_insert_sql_logged + sql = "INSERT INTO items (number) VALUES (10)" + name = "foo" + + assert_logged([[sql, name]]) do + @ctx.insert_sql sql, name + end + end + + def test_insert_id_value_returned + sql = "INSERT INTO items (number) VALUES (10)" + idval = 'vuvuzela' + id = @ctx.insert_sql sql, nil, nil, idval + assert_equal idval, id + end + + def test_select_rows + 2.times do |i| + @ctx.create "INSERT INTO items (number) VALUES (#{i})" + end + rows = @ctx.select_rows 'select number, id from items' + assert_equal [[0, 1], [1, 2]], rows + end + + def test_select_rows_logged + sql = "select * from items" + name = "foo" + + assert_logged([[sql, name]]) do + @ctx.select_rows sql, name + end + end + + def test_transaction + count_sql = 'select count(*) from items' + + @ctx.begin_db_transaction + @ctx.create "INSERT INTO items (number) VALUES (10)" + + assert_equal 1, @ctx.select_rows(count_sql).first.first + @ctx.rollback_db_transaction + assert_equal 0, @ctx.select_rows(count_sql).first.first + end + + def test_tables + assert_equal %w{ items }, @ctx.tables + + @ctx.execute <<-eosql + CREATE TABLE people ( + id integer PRIMARY KEY AUTOINCREMENT, + number integer + ) + eosql + assert_equal %w{ items people }.sort, @ctx.tables.sort + end + + def test_tables_logs_name + name = "hello" + assert_logged [[name]] do + @ctx.tables(name) + assert_not_nil @ctx.logged.first.shift + end + end + + def assert_logged logs + @ctx.extend(Module.new { + attr_reader :logged + def log sql, name + @logged ||= [] + @logged << [sql, name] + yield + end + }) + yield + assert_equal logs, @ctx.logged + end + end + end +end diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 445e6889c0..40859d425f 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -357,6 +357,12 @@ class EagerAssociationTest < ActiveRecord::TestCase end end + def test_eager_with_has_many_through_with_conditions_join_model_with_include + post_tags = Post.find(posts(:welcome).id).misc_tags + eager_post_tags = Post.find(1, :include => :misc_tags).misc_tags + assert_equal post_tags, eager_post_tags + end + def test_eager_with_has_many_and_limit posts = Post.find(:all, :order => 'posts.id asc', :include => [ :author, :comments ], :limit => 2) assert_equal 2, posts.size diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb index 004d0156e1..b11969a841 100644 --- a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb @@ -684,8 +684,8 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase end def test_find_scoped_grouped - assert_equal 4, categories(:general).posts_gruoped_by_title.size - assert_equal 1, categories(:technology).posts_gruoped_by_title.size + assert_equal 4, categories(:general).posts_grouped_by_title.size + assert_equal 1, categories(:technology).posts_grouped_by_title.size end def test_find_scoped_grouped_having diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 5e3ba778f3..a52cedd8c2 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -817,8 +817,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_destroy_all force_signal37_to_load_all_clients_of_firm - assert !companies(:first_firm).clients_of_firm.empty?, "37signals has clients after load" - companies(:first_firm).clients_of_firm.destroy_all + clients = companies(:first_firm).clients_of_firm.to_a + assert !clients.empty?, "37signals has clients after load" + destroyed = companies(:first_firm).clients_of_firm.destroy_all + assert_equal clients.sort_by(&:id), destroyed.sort_by(&:id) + assert destroyed.all? { |client| client.frozen? }, "destroyed clients should be frozen" assert companies(:first_firm).clients_of_firm.empty?, "37signals has no clients after destroy all" assert companies(:first_firm).clients_of_firm(true).empty?, "37signals has no clients after destroy all and refresh" end diff --git a/activerecord/test/cases/associations/has_one_through_associations_test.rb b/activerecord/test/cases/associations/has_one_through_associations_test.rb index 9aef3eb374..178c57435b 100644 --- a/activerecord/test/cases/associations/has_one_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb @@ -65,10 +65,6 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase assert_equal clubs(:moustache_club), @member.sponsor_club end - def has_one_through_to_has_many - assert_equal 2, @member.fellow_members.size - end - def test_has_one_through_eager_loading members = assert_queries(3) do #base table, through table, clubs table Member.find(:all, :include => :club, :conditions => ["name = ?", "Groucho Marx"]) diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb index d99fb44f01..4ae776c35a 100644 --- a/activerecord/test/cases/associations_test.rb +++ b/activerecord/test/cases/associations_test.rb @@ -65,6 +65,16 @@ class AssociationsTest < ActiveRecord::TestCase assert_equal 1, firm.clients(true).size, "New firm should have reloaded clients count" end + def test_using_limitable_reflections_helper + using_limitable_reflections = lambda { |reflections| Tagging.scoped.send :using_limitable_reflections?, reflections } + belongs_to_reflections = [Tagging.reflect_on_association(:tag), Tagging.reflect_on_association(:super_tag)] + has_many_reflections = [Tag.reflect_on_association(:taggings), Developer.reflect_on_association(:projects)] + mixed_reflections = (belongs_to_reflections + has_many_reflections).uniq + assert using_limitable_reflections.call(belongs_to_reflections), "Belong to associations are limitable" + assert !using_limitable_reflections.call(has_many_reflections), "All has many style associations are not limitable" + assert !using_limitable_reflections.call(mixed_reflections), "No collection associations (has many style) should pass" + end + def test_force_reload_is_uncached firm = Firm.create!("name" => "A New Firm, Inc") client = Client.create!("name" => "TheClient.com", :firm => firm) diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index ba7db838ca..a4cf5120e1 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -17,6 +17,7 @@ require 'models/comment' require 'models/minimalistic' require 'models/warehouse_thing' require 'models/parrot' +require 'models/loose_person' require 'rexml/document' require 'active_support/core_ext/exception' @@ -37,46 +38,12 @@ class Computer < ActiveRecord::Base; end class NonExistentTable < ActiveRecord::Base; end class TestOracleDefault < ActiveRecord::Base; end -class LoosePerson < ActiveRecord::Base - self.table_name = 'people' - self.abstract_class = true - attr_protected :credit_rating, :administrator -end - -class LooseDescendant < LoosePerson - attr_protected :phone_number -end - -class LooseDescendantSecond< LoosePerson - attr_protected :phone_number - attr_protected :name -end - -class TightPerson < ActiveRecord::Base - self.table_name = 'people' - attr_accessible :name, :address -end - -class TightDescendant < TightPerson - attr_accessible :phone_number -end - class ReadonlyTitlePost < Post attr_readonly :title end class Booleantest < ActiveRecord::Base; end -class Task < ActiveRecord::Base - attr_protected :starting -end - -class TopicWithProtectedContentAndAccessibleAuthorName < ActiveRecord::Base - self.table_name = 'topics' - attr_accessible :author_name - attr_protected :content -end - class BasicsTest < ActiveRecord::TestCase fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :categorizations, :categories, :posts @@ -94,6 +61,13 @@ class BasicsTest < ActiveRecord::TestCase assert_equal(topics(:first).author_email_address, Topic.find(1).author_email_address) end + def test_set_attributes_without_hash + topic = Topic.new + assert_nothing_raised do + topic.attributes = '' + end + end + def test_integers_as_nil test = AutoId.create('value' => '') assert_nil AutoId.find(test.id).value @@ -684,16 +658,24 @@ class BasicsTest < ActiveRecord::TestCase end def test_destroy_all - original_count = Topic.count - topics_by_mary = Topic.count(:conditions => mary = "author_name = 'Mary'") - - Topic.destroy_all mary - assert_equal original_count - topics_by_mary, Topic.count + conditions = "author_name = 'Mary'" + topics_by_mary = Topic.all(:conditions => conditions, :order => 'id') + assert ! topics_by_mary.empty? + + assert_difference('Topic.count', -topics_by_mary.size) do + destroyed = Topic.destroy_all(conditions).sort_by(&:id) + assert_equal topics_by_mary, destroyed + assert destroyed.all? { |topic| topic.frozen? }, "destroyed topics should be frozen" + end end def test_destroy_many + clients = Client.find([2, 3], :order => 'id') + assert_difference('Client.count', -2) do - Client.destroy([2, 3]) + destroyed = Client.destroy([2, 3]).sort_by(&:id) + assert_equal clients, destroyed + assert destroyed.all? { |client| client.frozen? }, "destroyed clients should be frozen" end end @@ -918,6 +900,46 @@ class BasicsTest < ActiveRecord::TestCase assert !Topic.find(1).approved? end + def test_update_attribute_with_one_changed_and_one_updated + t = Topic.order('id').limit(1).first + title, author_name = t.title, t.author_name + t.author_name = 'John' + t.update_attribute(:title, 'super_title') + assert_equal 'John', t.author_name + assert_equal 'super_title', t.title + assert t.changed?, "topic should have changed" + assert t.author_name_changed?, "author_name should have changed" + assert !t.title_changed?, "title should not have changed" + assert_nil t.title_change, 'title change should be nil' + assert_equal ['author_name'], t.changed + + t.reload + assert_equal 'David', t.author_name + assert_equal 'super_title', t.title + end + + def test_update_attribute_with_one_updated + t = Topic.first + title = t.title + t.update_attribute(:title, 'super_title') + assert_equal 'super_title', t.title + assert !t.changed?, "topic should not have changed" + assert !t.title_changed?, "title should not have changed" + assert_nil t.title_change, 'title change should be nil' + + t.reload + assert_equal 'super_title', t.title + end + + def test_update_attribute_for_udpated_at_on + developer = Developer.find(1) + updated_at = developer.updated_at + developer.update_attribute(:salary, 80001) + assert_not_equal updated_at, developer.updated_at + developer.reload + assert_not_equal updated_at, developer.updated_at + end + def test_update_attributes topic = Topic.find(1) assert !topic.approved? @@ -955,88 +977,6 @@ class BasicsTest < ActiveRecord::TestCase Reply.reset_callbacks(:validate) end - def test_mass_assignment_should_raise_exception_if_accessible_and_protected_attribute_writers_are_both_used - topic = TopicWithProtectedContentAndAccessibleAuthorName.new - assert_raise(RuntimeError) { topic.attributes = { "author_name" => "me" } } - assert_raise(RuntimeError) { topic.attributes = { "content" => "stuff" } } - end - - def test_mass_assignment_protection - firm = Firm.new - firm.attributes = { "name" => "Next Angle", "rating" => 5 } - assert_equal 1, firm.rating - end - - def test_mass_assignment_protection_against_class_attribute_writers - [:logger, :configurations, :primary_key_prefix_type, :table_name_prefix, :table_name_suffix, :pluralize_table_names, - :default_timezone, :schema_format, :lock_optimistically, :record_timestamps].each do |method| - assert_respond_to Task, method - assert_respond_to Task, "#{method}=" - assert_respond_to Task.new, method - assert !Task.new.respond_to?("#{method}=") - end - end - - def test_customized_primary_key_remains_protected - subscriber = Subscriber.new(:nick => 'webster123', :name => 'nice try') - assert_nil subscriber.id - - keyboard = Keyboard.new(:key_number => 9, :name => 'nice try') - assert_nil keyboard.id - end - - def test_customized_primary_key_remains_protected_when_referred_to_as_id - subscriber = Subscriber.new(:id => 'webster123', :name => 'nice try') - assert_nil subscriber.id - - keyboard = Keyboard.new(:id => 9, :name => 'nice try') - assert_nil keyboard.id - end - - def test_mass_assigning_invalid_attribute - firm = Firm.new - - assert_raise(ActiveRecord::UnknownAttributeError) do - firm.attributes = { "id" => 5, "type" => "Client", "i_dont_even_exist" => 20 } - end - end - - def test_mass_assignment_protection_on_defaults - firm = Firm.new - firm.attributes = { "id" => 5, "type" => "Client" } - assert_nil firm.id - assert_equal "Firm", firm[:type] - end - - def test_mass_assignment_accessible - reply = Reply.new("title" => "hello", "content" => "world", "approved" => true) - reply.save - - assert reply.approved? - - reply.approved = false - reply.save - - assert !reply.approved? - end - - def test_mass_assignment_protection_inheritance - assert_nil LoosePerson.accessible_attributes - assert_equal Set.new([ 'credit_rating', 'administrator' ]), LoosePerson.protected_attributes - - assert_nil LooseDescendant.accessible_attributes - assert_equal Set.new([ 'credit_rating', 'administrator', 'phone_number' ]), LooseDescendant.protected_attributes - - assert_nil LooseDescendantSecond.accessible_attributes - assert_equal Set.new([ 'credit_rating', 'administrator', 'phone_number', 'name' ]), LooseDescendantSecond.protected_attributes, 'Running attr_protected twice in one class should merge the protections' - - assert_nil TightPerson.protected_attributes - assert_equal Set.new([ 'name', 'address' ]), TightPerson.accessible_attributes - - assert_nil TightDescendant.protected_attributes - assert_equal Set.new([ 'name', 'address', 'phone_number' ]), TightDescendant.accessible_attributes - end - def test_readonly_attributes assert_equal Set.new([ 'title' , 'comments_count' ]), ReadonlyTitlePost.readonly_attributes @@ -1239,15 +1179,6 @@ class BasicsTest < ActiveRecord::TestCase assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on end - def test_multiparameter_mass_assignment_protector - task = Task.new - time = Time.mktime(2000, 1, 1, 1) - task.starting = time - attributes = { "starting(1i)" => "2004", "starting(2i)" => "6", "starting(3i)" => "24" } - task.attributes = attributes - assert_equal time, task.starting - end - def test_multiparameter_assignment_of_aggregation customer = Customer.new address = Address.new("The Street", "The City", "The Country") diff --git a/activerecord/test/cases/counter_cache_test.rb b/activerecord/test/cases/counter_cache_test.rb index 377de168b9..137236255d 100644 --- a/activerecord/test/cases/counter_cache_test.rb +++ b/activerecord/test/cases/counter_cache_test.rb @@ -1,17 +1,20 @@ require 'cases/helper' require 'models/topic' +require 'models/car' +require 'models/wheel' +require 'models/engine' require 'models/reply' require 'models/category' require 'models/categorization' class CounterCacheTest < ActiveRecord::TestCase - fixtures :topics, :categories, :categorizations + fixtures :topics, :categories, :categorizations, :cars - class SpecialTopic < ::Topic + class ::SpecialTopic < ::Topic has_many :special_replies, :foreign_key => 'parent_id' end - class SpecialReply < ::Reply + class ::SpecialReply < ::Reply belongs_to :special_topic, :foreign_key => 'parent_id', :counter_cache => 'replies_count' end @@ -58,6 +61,16 @@ class CounterCacheTest < ActiveRecord::TestCase end end + test "reset counter should with belongs_to which has class_name" do + car = cars(:honda) + assert_nothing_raised do + Car.reset_counters(car.id, :engines) + end + assert_nothing_raised do + Car.reset_counters(car.id, :wheels) + end + end + test "update counter with initial null value" do category = categories(:general) assert_equal 2, category.categorizations.count diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index 75f7453aa9..837386ed24 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -475,10 +475,9 @@ class DirtyTest < ActiveRecord::TestCase pirate = Pirate.find_by_catchphrase("Ahoy!") pirate.update_attribute(:catchphrase, "Ninjas suck!") - assert_equal 2, pirate.previous_changes.size - assert_equal ["Ahoy!", "Ninjas suck!"], pirate.previous_changes['catchphrase'] - assert_not_nil pirate.previous_changes['updated_on'][0] - assert_not_nil pirate.previous_changes['updated_on'][1] + assert_equal 0, pirate.previous_changes.size + assert_nil pirate.previous_changes['catchphrase'] + assert_nil pirate.previous_changes['updated_on'] assert !pirate.previous_changes.key?('parrot_id') assert !pirate.previous_changes.key?('created_on') end diff --git a/activerecord/test/cases/mass_assignment_security_test.rb b/activerecord/test/cases/mass_assignment_security_test.rb new file mode 100644 index 0000000000..025ec1d3fa --- /dev/null +++ b/activerecord/test/cases/mass_assignment_security_test.rb @@ -0,0 +1,43 @@ +require "cases/helper" +require 'models/company' +require 'models/subscriber' +require 'models/keyboard' +require 'models/task' + +class MassAssignmentSecurityTest < ActiveRecord::TestCase + + def test_customized_primary_key_remains_protected + subscriber = Subscriber.new(:nick => 'webster123', :name => 'nice try') + assert_nil subscriber.id + + keyboard = Keyboard.new(:key_number => 9, :name => 'nice try') + assert_nil keyboard.id + end + + def test_customized_primary_key_remains_protected_when_referred_to_as_id + subscriber = Subscriber.new(:id => 'webster123', :name => 'nice try') + assert_nil subscriber.id + + keyboard = Keyboard.new(:id => 9, :name => 'nice try') + assert_nil keyboard.id + end + + def test_mass_assigning_invalid_attribute + firm = Firm.new + + assert_raise(ActiveRecord::UnknownAttributeError) do + firm.attributes = { "id" => 5, "type" => "Client", "i_dont_even_exist" => 20 } + end + end + + def test_protection_against_class_attribute_writers + [:logger, :configurations, :primary_key_prefix_type, :table_name_prefix, :table_name_suffix, :pluralize_table_names, + :default_timezone, :schema_format, :lock_optimistically, :record_timestamps].each do |method| + assert_respond_to Task, method + assert_respond_to Task, "#{method}=" + assert_respond_to Task.new, method + assert !Task.new.respond_to?("#{method}=") + end + end + +end
\ No newline at end of file diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 4ce9bdb46d..2c3fc46831 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -1358,6 +1358,20 @@ if ActiveRecord::Base.connection.supports_migrations? ActiveRecord::Migrator.forward(MIGRATIONS_ROOT + "/valid") assert_equal(3, ActiveRecord::Migrator.current_version) end + + def test_get_all_versions + ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/valid") + assert_equal([1,2,3], ActiveRecord::Migrator.get_all_versions) + + ActiveRecord::Migrator.rollback(MIGRATIONS_ROOT + "/valid") + assert_equal([1,2], ActiveRecord::Migrator.get_all_versions) + + ActiveRecord::Migrator.rollback(MIGRATIONS_ROOT + "/valid") + assert_equal([1], ActiveRecord::Migrator.get_all_versions) + + ActiveRecord::Migrator.rollback(MIGRATIONS_ROOT + "/valid") + assert_equal([], ActiveRecord::Migrator.get_all_versions) + end def test_schema_migrations_table_name ActiveRecord::Base.table_name_prefix = "prefix_" diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index 3c797076e0..c9ea0d8c40 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -195,7 +195,7 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase [1, '1', true, 'true'].each do |truth| @pirate.reload.create_ship(:name => 'Mister Pablo') assert_difference('Ship.count', -1) do - @pirate.update_attribute(:ship_attributes, { :id => @pirate.ship.id, :_destroy => truth }) + @pirate.update_attributes(:ship_attributes => { :id => @pirate.ship.id, :_destroy => truth }) end end end @@ -203,7 +203,7 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase def test_should_not_destroy_an_existing_record_if_destroy_is_not_truthy [nil, '0', 0, 'false', false].each do |not_truth| assert_no_difference('Ship.count') do - @pirate.update_attribute(:ship_attributes, { :id => @pirate.ship.id, :_destroy => not_truth }) + @pirate.update_attributes(:ship_attributes => { :id => @pirate.ship.id, :_destroy => not_truth }) end end end @@ -212,7 +212,7 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase Pirate.accepts_nested_attributes_for :ship, :allow_destroy => false, :reject_if => proc { |attributes| attributes.empty? } assert_no_difference('Ship.count') do - @pirate.update_attribute(:ship_attributes, { :id => @pirate.ship.id, :_destroy => '1' }) + @pirate.update_attributes(:ship_attributes => { :id => @pirate.ship.id, :_destroy => '1' }) end Pirate.accepts_nested_attributes_for :ship, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? } @@ -247,13 +247,13 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase end def test_should_accept_update_only_option - @pirate.update_attribute(:update_only_ship_attributes, { :id => @pirate.ship.id, :name => 'Mayflower' }) + @pirate.update_attributes(:update_only_ship_attributes => { :id => @pirate.ship.id, :name => 'Mayflower' }) end def test_should_create_new_model_when_nothing_is_there_and_update_only_is_true @ship.delete assert_difference('Ship.count', 1) do - @pirate.reload.update_attribute(:update_only_ship_attributes, { :name => 'Mayflower' }) + @pirate.reload.update_attributes(:update_only_ship_attributes => { :name => 'Mayflower' }) end end @@ -353,7 +353,7 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase [1, '1', true, 'true'].each do |truth| @ship.reload.create_pirate(:catchphrase => 'Arr') assert_difference('Pirate.count', -1) do - @ship.update_attribute(:pirate_attributes, { :id => @ship.pirate.id, :_destroy => truth }) + @ship.update_attributes(:pirate_attributes => { :id => @ship.pirate.id, :_destroy => truth }) end end end @@ -361,7 +361,7 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase def test_should_not_destroy_an_existing_record_if_destroy_is_not_truthy [nil, '0', 0, 'false', false].each do |not_truth| assert_no_difference('Pirate.count') do - @ship.update_attribute(:pirate_attributes, { :id => @ship.pirate.id, :_destroy => not_truth }) + @ship.update_attributes(:pirate_attributes => { :id => @ship.pirate.id, :_destroy => not_truth }) end end end @@ -370,7 +370,7 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase Ship.accepts_nested_attributes_for :pirate, :allow_destroy => false, :reject_if => proc { |attributes| attributes.empty? } assert_no_difference('Pirate.count') do - @ship.update_attribute(:pirate_attributes, { :id => @ship.pirate.id, :_destroy => '1' }) + @ship.update_attributes(:pirate_attributes => { :id => @ship.pirate.id, :_destroy => '1' }) end Ship.accepts_nested_attributes_for :pirate, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? } @@ -398,7 +398,7 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase def test_should_create_new_model_when_nothing_is_there_and_update_only_is_true @pirate.delete assert_difference('Pirate.count', 1) do - @ship.reload.update_attribute(:update_only_pirate_attributes, { :catchphrase => 'Arr' }) + @ship.reload.update_attributes(:update_only_pirate_attributes => { :catchphrase => 'Arr' }) end end @@ -489,6 +489,12 @@ module NestedAttributesOnACollectionAssociationTests assert_equal 'Polly', @pirate.send(@association_name).send(:load_target).last.name end + def test_should_not_remove_scheduled_destroys_when_loading_association + @pirate.reload + @pirate.send(association_setter, [{ :id => @child_1.id, :_destroy => '1' }]) + assert @pirate.send(@association_name).send(:load_target).find { |r| r.id == @child_1.id }.marked_for_destruction? + end + def test_should_take_a_hash_with_composite_id_keys_and_assign_the_attributes_to_the_associated_models @child_1.stubs(:id).returns('ABC1X') @child_2.stubs(:id).returns('ABC2X') diff --git a/activerecord/test/fixtures/cars.yml b/activerecord/test/fixtures/cars.yml new file mode 100644 index 0000000000..23c98e8144 --- /dev/null +++ b/activerecord/test/fixtures/cars.yml @@ -0,0 +1,4 @@ +honda: + id: 1 + name: honda + engines_count: 0 diff --git a/activerecord/test/models/car.rb b/activerecord/test/models/car.rb new file mode 100644 index 0000000000..1101180a67 --- /dev/null +++ b/activerecord/test/models/car.rb @@ -0,0 +1,4 @@ +class Car < ActiveRecord::Base + has_many :engines + has_many :wheels, :as => :wheelable +end diff --git a/activerecord/test/models/category.rb b/activerecord/test/models/category.rb index 5efce6aaa6..48415846dd 100644 --- a/activerecord/test/models/category.rb +++ b/activerecord/test/models/category.rb @@ -15,7 +15,7 @@ class Category < ActiveRecord::Base :conditions => { :title => 'Yet Another Testing Title' } has_and_belongs_to_many :popular_grouped_posts, :class_name => "Post", :group => "posts.type", :having => "sum(comments.post_id) > 2", :include => :comments - has_and_belongs_to_many :posts_gruoped_by_title, :class_name => "Post", :group => "title", :select => "title" + has_and_belongs_to_many :posts_grouped_by_title, :class_name => "Post", :group => "title", :select => "title" def self.what_are_you 'a category...' diff --git a/activerecord/test/models/engine.rb b/activerecord/test/models/engine.rb new file mode 100644 index 0000000000..751c3f02d1 --- /dev/null +++ b/activerecord/test/models/engine.rb @@ -0,0 +1,3 @@ +class Engine < ActiveRecord::Base + belongs_to :my_car, :class_name => 'Car', :foreign_key => 'car_id', :counter_cache => :engines_count +end diff --git a/activerecord/test/models/loose_person.rb b/activerecord/test/models/loose_person.rb new file mode 100644 index 0000000000..256c281d0d --- /dev/null +++ b/activerecord/test/models/loose_person.rb @@ -0,0 +1,24 @@ +class LoosePerson < ActiveRecord::Base + self.table_name = 'people' + self.abstract_class = true + + attr_protected :credit_rating, :administrator +end + +class LooseDescendant < LoosePerson + attr_protected :phone_number +end + +class LooseDescendantSecond< LoosePerson + attr_protected :phone_number + attr_protected :name +end + +class TightPerson < ActiveRecord::Base + self.table_name = 'people' + attr_accessible :name, :address +end + +class TightDescendant < TightPerson + attr_accessible :phone_number +end
\ No newline at end of file diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index dd06822cfd..6c7b93be87 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -58,6 +58,7 @@ class Post < ActiveRecord::Base end end + has_many :misc_tags, :through => :taggings, :source => :tag, :conditions => "tags.name = 'Misc'" has_many :funky_tags, :through => :taggings, :source => :tag has_many :super_tags, :through => :taggings has_one :tagging, :as => :taggable diff --git a/activerecord/test/models/wheel.rb b/activerecord/test/models/wheel.rb new file mode 100644 index 0000000000..26868bce5e --- /dev/null +++ b/activerecord/test/models/wheel.rb @@ -0,0 +1,3 @@ +class Wheel < ActiveRecord::Base + belongs_to :wheelable, :polymorphic => true, :counter_cache => true +end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index b212e7cff2..bea351b95a 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -82,6 +82,12 @@ ActiveRecord::Schema.define do t.string :name end + create_table :cars, :force => true do |t| + t.string :name + t.integer :engines_count + t.integer :wheels_count + end + create_table :categories, :force => true do |t| t.string :name, :null => false t.string :type @@ -179,6 +185,9 @@ ActiveRecord::Schema.define do end add_index :edges, [:source_id, :sink_id], :unique => true, :name => 'unique_edge_index' + create_table :engines, :force => true do |t| + t.integer :car_id + end create_table :entrants, :force => true do |t| t.string :name, :null => false @@ -566,6 +575,10 @@ ActiveRecord::Schema.define do t.integer :zine_id end + create_table :wheels, :force => true do |t| + t.references :wheelable, :polymorphic => true + end + create_table :zines, :force => true do |t| t.string :title end diff --git a/activesupport/lib/active_support/all.rb b/activesupport/lib/active_support/all.rb index def8eca89f..f537818300 100644 --- a/activesupport/lib/active_support/all.rb +++ b/activesupport/lib/active_support/all.rb @@ -1,4 +1,3 @@ require 'active_support' require 'active_support/time' require 'active_support/core_ext' -require 'active_support/json' diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb index c4e1eb2c04..1c7802f7de 100644 --- a/activesupport/lib/active_support/callbacks.rb +++ b/activesupport/lib/active_support/callbacks.rb @@ -432,7 +432,7 @@ module ActiveSupport options = filters.last.is_a?(Hash) ? filters.pop : {} filters.unshift(block) if block - ([self] + self.descendants).each do |target| + ([self] + ActiveSupport::DescendantsTracker.descendants(self)).each do |target| chain = target.send("_#{name}_callbacks") yield chain, type, filters, options target.__define_runner(name) @@ -506,7 +506,7 @@ module ActiveSupport def reset_callbacks(symbol) callbacks = send("_#{symbol}_callbacks") - self.descendants.each do |target| + ActiveSupport::DescendantsTracker.descendants(self).each do |target| chain = target.send("_#{symbol}_callbacks") callbacks.each { |c| chain.delete(c) } target.__define_runner(symbol) diff --git a/activesupport/lib/active_support/core_ext/class/subclasses.rb b/activesupport/lib/active_support/core_ext/class/subclasses.rb index 7d58a8b56a..3e5d1a2a42 100644 --- a/activesupport/lib/active_support/core_ext/class/subclasses.rb +++ b/activesupport/lib/active_support/core_ext/class/subclasses.rb @@ -2,54 +2,49 @@ require 'active_support/core_ext/module/anonymous' require 'active_support/core_ext/module/reachable' class Class #:nodoc: - # Returns an array with the names of the subclasses of +self+ as strings. - # - # Integer.subclasses # => ["Bignum", "Fixnum"] - def subclasses - Class.subclasses_of(self).map { |o| o.to_s } - end - # Rubinius if defined?(Class.__subclasses__) + alias :subclasses :__subclasses__ + def descendants - subclasses = [] - __subclasses__.each {|k| subclasses << k; subclasses.concat k.descendants } - subclasses + descendants = [] + __subclasses__.each do |k| + descendants << k + descendants.concat k.descendants + end + descendants end - else - # MRI + else # MRI begin ObjectSpace.each_object(Class.new) {} def descendants - subclasses = [] + descendants = [] ObjectSpace.each_object(class << self; self; end) do |k| - subclasses << k unless k == self + descendants.unshift k unless k == self end - subclasses + descendants end - # JRuby - rescue StandardError + rescue StandardError # JRuby def descendants - subclasses = [] + descendants = [] ObjectSpace.each_object(Class) do |k| - subclasses << k if k < self + descendants.unshift k if k < self end - subclasses.uniq! - subclasses + descendants.uniq! + descendants end end - end - # Exclude this class unless it's a subclass of our supers and is defined. - # We check defined? in case we find a removed class that has yet to be - # garbage collected. This also fails for anonymous classes -- please - # submit a patch if you have a workaround. - def self.subclasses_of(*superclasses) #:nodoc: - subclasses = [] - superclasses.each do |klass| - subclasses.concat klass.descendants.select {|k| k.anonymous? || k.reachable?} + # Returns an array with the direct children of +self+. + # + # Integer.subclasses # => [Bignum, Fixnum] + def subclasses + subclasses, chain = [], descendants + chain.each do |k| + subclasses << k unless chain.any? { |c| c > k } + end + subclasses end - subclasses end end diff --git a/activesupport/lib/active_support/core_ext/date/calculations.rb b/activesupport/lib/active_support/core_ext/date/calculations.rb index 28fec5394f..e6a213625c 100644 --- a/activesupport/lib/active_support/core_ext/date/calculations.rb +++ b/activesupport/lib/active_support/core_ext/date/calculations.rb @@ -1,7 +1,8 @@ require 'date' require 'active_support/duration' -require 'active_support/core_ext/time/zones' require 'active_support/core_ext/object/acts_like' +require 'active_support/core_ext/date/zones' +require 'active_support/core_ext/time/zones' class Date if RUBY_VERSION < '1.9' diff --git a/activesupport/lib/active_support/core_ext/date/conversions.rb b/activesupport/lib/active_support/core_ext/date/conversions.rb index ba17b0a06b..092f936961 100644 --- a/activesupport/lib/active_support/core_ext/date/conversions.rb +++ b/activesupport/lib/active_support/core_ext/date/conversions.rb @@ -1,6 +1,6 @@ require 'date' require 'active_support/inflector' -require 'active_support/core_ext/time/calculations' +require 'active_support/core_ext/date/zones' class Date DATE_FORMATS = { @@ -13,10 +13,10 @@ class Date } # Ruby 1.9 has Date#to_time which converts to localtime only. - remove_method :to_time if instance_methods.include?(:to_time) + remove_method :to_time if method_defined?(:to_time) # Ruby 1.9 has Date#xmlschema which converts to a string without the time component. - remove_method :xmlschema if instance_methods.include?(:xmlschema) + remove_method :xmlschema if method_defined?(:xmlschema) # Convert to a formatted string. See DATE_FORMATS for predefined formats. # @@ -81,16 +81,6 @@ class Date def to_time(form = :local) ::Time.send("#{form}_time", year, month, day) end - - # Converts Date to a TimeWithZone in the current zone if Time.zone_default is set, - # otherwise converts Date to a Time via Date#to_time - def to_time_in_current_zone - if ::Time.zone_default - ::Time.zone.local(year, month, day) - else - to_time - end - end # Converts a Date instance to a DateTime, where the time is set to the beginning of the day # and UTC offset is set to 0. diff --git a/activesupport/lib/active_support/core_ext/date/zones.rb b/activesupport/lib/active_support/core_ext/date/zones.rb new file mode 100644 index 0000000000..3a83af6be2 --- /dev/null +++ b/activesupport/lib/active_support/core_ext/date/zones.rb @@ -0,0 +1,14 @@ +require 'date' +require 'active_support/core_ext/time/zones' + +class Date + # Converts Date to a TimeWithZone in the current zone if Time.zone_default is set, + # otherwise converts Date to a Time via Date#to_time + def to_time_in_current_zone + if ::Time.zone_default + ::Time.zone.local(year, month, day) + else + to_time + end + end +end diff --git a/activesupport/lib/active_support/core_ext/date_time/calculations.rb b/activesupport/lib/active_support/core_ext/date_time/calculations.rb index 26979aa906..1dc3933e12 100644 --- a/activesupport/lib/active_support/core_ext/date_time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/date_time/calculations.rb @@ -1,5 +1,6 @@ require 'rational' unless RUBY_VERSION >= '1.9.2' require 'active_support/core_ext/object/acts_like' +require 'active_support/core_ext/time/zones' class DateTime class << self diff --git a/activesupport/lib/active_support/core_ext/date_time/conversions.rb b/activesupport/lib/active_support/core_ext/date_time/conversions.rb index c3f0acce25..47b8aa59fb 100644 --- a/activesupport/lib/active_support/core_ext/date_time/conversions.rb +++ b/activesupport/lib/active_support/core_ext/date_time/conversions.rb @@ -1,6 +1,7 @@ require 'active_support/inflector' require 'active_support/core_ext/time/conversions' require 'active_support/core_ext/date_time/calculations' +require 'active_support/values/time_zone' class DateTime # Ruby 1.9 has DateTime#to_time which internally relies on Time. We define our own #to_time which allows diff --git a/activesupport/lib/active_support/core_ext/date_time/zones.rb b/activesupport/lib/active_support/core_ext/date_time/zones.rb index 6002d4ad2a..a7411d54ae 100644 --- a/activesupport/lib/active_support/core_ext/date_time/zones.rb +++ b/activesupport/lib/active_support/core_ext/date_time/zones.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/time/zones' + class DateTime # Returns the simultaneous time in <tt>Time.zone</tt>. # diff --git a/activesupport/lib/active_support/core_ext/logger.rb b/activesupport/lib/active_support/core_ext/logger.rb index d023b4bcff..a1c351bfd9 100644 --- a/activesupport/lib/active_support/core_ext/logger.rb +++ b/activesupport/lib/active_support/core_ext/logger.rb @@ -18,7 +18,7 @@ end require 'logger' -# Extensions to the built in Ruby logger. +# Extensions to the built-in Ruby logger. # # If you want to use the default log formatter as defined in the Ruby core, then you # will need to set the formatter for the logger as in: diff --git a/activesupport/lib/active_support/core_ext/object.rb b/activesupport/lib/active_support/core_ext/object.rb index 3a6100f776..27618b55c6 100644 --- a/activesupport/lib/active_support/core_ext/object.rb +++ b/activesupport/lib/active_support/core_ext/object.rb @@ -6,9 +6,9 @@ require 'active_support/core_ext/object/try' require 'active_support/core_ext/object/conversions' require 'active_support/core_ext/object/instance_variables' require 'active_support/core_ext/object/misc' -require 'active_support/core_ext/object/extending' require 'active_support/core_ext/object/returning' +require 'active_support/core_ext/object/to_json' require 'active_support/core_ext/object/to_param' require 'active_support/core_ext/object/to_query' require 'active_support/core_ext/object/with_options' diff --git a/activesupport/lib/active_support/core_ext/object/extending.rb b/activesupport/lib/active_support/core_ext/object/extending.rb deleted file mode 100644 index c4c37b6a2a..0000000000 --- a/activesupport/lib/active_support/core_ext/object/extending.rb +++ /dev/null @@ -1,11 +0,0 @@ -require 'active_support/core_ext/class/subclasses' - -class Object - # Exclude this class unless it's a subclass of our supers and is defined. - # We check defined? in case we find a removed class that has yet to be - # garbage collected. This also fails for anonymous classes -- please - # submit a patch if you have a workaround. - def subclasses_of(*superclasses) #:nodoc: - Class.subclasses_of(*superclasses) - end -end diff --git a/activesupport/lib/active_support/core_ext/object/to_json.rb b/activesupport/lib/active_support/core_ext/object/to_json.rb new file mode 100644 index 0000000000..14ef27340e --- /dev/null +++ b/activesupport/lib/active_support/core_ext/object/to_json.rb @@ -0,0 +1,19 @@ +# Hack to load json gem first so we can overwrite its to_json. +begin + require 'json' +rescue LoadError +end + +# The JSON gem adds a few modules to Ruby core classes containing :to_json definition, overwriting +# their default behavior. That said, we need to define the basic to_json method in all of them, +# otherwise they will always use to_json gem implementation, which is backwards incompatible in +# several cases (for instance, the JSON implementation for Hash does not work) with inheritance +# and consequently classes as ActiveSupport::OrderedHash cannot be serialized to json. +[Object, Array, FalseClass, Float, Hash, Integer, NilClass, String, TrueClass].each do |klass| + klass.class_eval <<-RUBY, __FILE__, __LINE__ + # Dumps object in JSON (JavaScript Object Notation). See www.json.org for more info. + def to_json(options = nil) + ActiveSupport::JSON.encode(self, options) + end + RUBY +end diff --git a/activesupport/lib/active_support/core_ext/string/multibyte.rb b/activesupport/lib/active_support/core_ext/string/multibyte.rb index 3dfe996d06..16ccd36458 100644 --- a/activesupport/lib/active_support/core_ext/string/multibyte.rb +++ b/activesupport/lib/active_support/core_ext/string/multibyte.rb @@ -2,7 +2,7 @@ require 'active_support/multibyte' class String - if '1.9'.respond_to?(:force_encoding) + if RUBY_VERSION >= "1.9" # == Multibyte proxy # # +mb_chars+ is a multibyte safe proxy for string methods. diff --git a/activesupport/lib/active_support/core_ext/time/calculations.rb b/activesupport/lib/active_support/core_ext/time/calculations.rb index 3c218d88e5..90b6cd3685 100644 --- a/activesupport/lib/active_support/core_ext/time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/time/calculations.rb @@ -40,6 +40,11 @@ class Time def local_time(*args) time_with_datetime_fallback(:local, *args) end + + # Returns <tt>Time.zone.now</tt> when <tt>config.time_zone</tt> is set, otherwise just returns <tt>Time.now</tt>. + def current + ::Time.zone_default ? ::Time.zone.now : ::Time.now + end end # Tells whether the Time object's time lies in the past diff --git a/activesupport/lib/active_support/core_ext/time/zones.rb b/activesupport/lib/active_support/core_ext/time/zones.rb index a02402aa3f..6b9ee84d5c 100644 --- a/activesupport/lib/active_support/core_ext/time/zones.rb +++ b/activesupport/lib/active_support/core_ext/time/zones.rb @@ -41,11 +41,6 @@ class Time ::Time.zone = old_zone end - # Returns <tt>Time.zone.now</tt> when <tt>config.time_zone</tt> is set, otherwise just returns <tt>Time.now</tt>. - def current - ::Time.zone_default ? ::Time.zone.now : ::Time.now - end - private def get_zone(time_zone) return time_zone if time_zone.nil? || time_zone.is_a?(ActiveSupport::TimeZone) diff --git a/activesupport/lib/active_support/deprecation/behaviors.rb b/activesupport/lib/active_support/deprecation/behaviors.rb index feb1508586..f54f65dcf0 100644 --- a/activesupport/lib/active_support/deprecation/behaviors.rb +++ b/activesupport/lib/active_support/deprecation/behaviors.rb @@ -1,4 +1,5 @@ require "active_support/notifications" +require "active_support/core_ext/array/wrap" module ActiveSupport module Deprecation @@ -7,11 +8,11 @@ module ActiveSupport attr_accessor :debug def behavior - @behavior ||= DEFAULT_BEHAVIORS[:stderr] + @behavior ||= [DEFAULT_BEHAVIORS[:stderr]] end def behavior=(behavior) - @behavior = DEFAULT_BEHAVIORS[behavior] || behavior + @behavior = Array.wrap(behavior).map { |b| DEFAULT_BEHAVIORS[b] || b } end end diff --git a/activesupport/lib/active_support/deprecation/reporting.rb b/activesupport/lib/active_support/deprecation/reporting.rb index 03c445ffbf..49d58cd3a1 100644 --- a/activesupport/lib/active_support/deprecation/reporting.rb +++ b/activesupport/lib/active_support/deprecation/reporting.rb @@ -4,8 +4,9 @@ module ActiveSupport attr_accessor :silenced def warn(message = nil, callstack = caller) - if behavior && !silenced - behavior.call(deprecation_message(callstack, message), callstack) + return if silenced + deprecation_message(callstack, message).tap do |m| + behavior.each { |b| b.call(m, callstack) } end end diff --git a/activesupport/lib/active_support/descendants_tracker.rb b/activesupport/lib/active_support/descendants_tracker.rb index a587d7770c..6cba84d79e 100644 --- a/activesupport/lib/active_support/descendants_tracker.rb +++ b/activesupport/lib/active_support/descendants_tracker.rb @@ -4,16 +4,23 @@ module ActiveSupport # This module provides an internal implementation to track descendants # which is faster than iterating through ObjectSpace. module DescendantsTracker - @@descendants = Hash.new { |h, k| h[k] = [] } + @@direct_descendants = Hash.new { |h, k| h[k] = [] } - def self.descendants - @@descendants + def self.direct_descendants(klass) + @@direct_descendants[klass] + end + + def self.descendants(klass) + @@direct_descendants[klass].inject([]) do |descendants, klass| + descendants << klass + descendants.concat klass.descendants + end end def self.clear - @@descendants.each do |klass, descendants| + @@direct_descendants.each do |klass, descendants| if ActiveSupport::Dependencies.autoloaded?(klass) - @@descendants.delete(klass) + @@direct_descendants.delete(klass) else descendants.reject! { |v| ActiveSupport::Dependencies.autoloaded?(v) } end @@ -26,14 +33,11 @@ module ActiveSupport end def direct_descendants - @@descendants[self] + DescendantsTracker.direct_descendants(self) end def descendants - @@descendants[self].inject([]) do |descendants, klass| - descendants << klass - descendants.concat klass.descendants - end + DescendantsTracker.descendants(self) end end end
\ No newline at end of file diff --git a/activesupport/lib/active_support/file_update_checker.rb b/activesupport/lib/active_support/file_update_checker.rb index 5f5b264eb9..cd658fe173 100644 --- a/activesupport/lib/active_support/file_update_checker.rb +++ b/activesupport/lib/active_support/file_update_checker.rb @@ -1,16 +1,15 @@ module ActiveSupport # This class is responsible to track files and invoke the given block # whenever one of these files are changed. For example, this class - # is used by Rails to reload routes whenever they are changed upon - # a new request. + # is used by Rails to reload the I18n framework whenever they are + # changed upon a new request. # - # routes_reloader = ActiveSupport::FileUpdateChecker.new(paths) do - # paths.each { |p| load(p) } - # Rails::Application.routes.reload! + # i18n_reloader = ActiveSupport::FileUpdateChecker.new(paths) do + # I18n.reload! # end # # ActionDispatch::Callbacks.to_prepare do - # routes_reloader.execute_if_updated + # i18n_reloader.execute_if_updated # end # class FileUpdateChecker diff --git a/activesupport/lib/active_support/i18n_railtie.rb b/activesupport/lib/active_support/i18n_railtie.rb index d82e54f1d4..db09919fd3 100644 --- a/activesupport/lib/active_support/i18n_railtie.rb +++ b/activesupport/lib/active_support/i18n_railtie.rb @@ -1,6 +1,7 @@ require "active_support" require "rails" require "active_support/file_update_checker" +require "active_support/core_ext/array/wrap" module I18n class Railtie < Rails::Railtie diff --git a/activesupport/lib/active_support/json/encoding.rb b/activesupport/lib/active_support/json/encoding.rb index dd94315111..2f9588e0f4 100644 --- a/activesupport/lib/active_support/json/encoding.rb +++ b/activesupport/lib/active_support/json/encoding.rb @@ -1,20 +1,18 @@ -# encoding: utf-8 +require 'active_support/core_ext/object/to_json' +require 'active_support/core_ext/module/delegation' +require 'active_support/deprecation' +require 'active_support/json/variable' + require 'bigdecimal' -require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/big_decimal/conversions' # for #to_s +require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/hash/except' require 'active_support/core_ext/hash/slice' -require 'active_support/core_ext/module/delegation' require 'active_support/core_ext/object/instance_variables' -require 'active_support/deprecation' - -require 'active_support/time' - -# Hack to load json gem first so we can overwrite its to_json. -begin - require 'json' -rescue LoadError -end +require 'time' +require 'active_support/core_ext/time/conversions' +require 'active_support/core_ext/date_time/conversions' +require 'active_support/core_ext/date/conversions' module ActiveSupport class << self @@ -128,20 +126,6 @@ module ActiveSupport end end -# The JSON gem adds a few modules to Ruby core classes containing :to_json definition, overwriting -# their default behavior. That said, we need to define the basic to_json method in all of them, -# otherwise they will always use to_json gem implementation, which is backwards incompatible in -# several cases (for instance, the JSON implementation for Hash does not work) with inheritance -# and consequently classes as ActiveSupport::OrderedHash cannot be serialized to json. -[Object, Array, FalseClass, Float, Hash, Integer, NilClass, String, TrueClass].each do |klass| - klass.class_eval <<-RUBY, __FILE__, __LINE__ - # Dumps object in JSON (JavaScript Object Notation). See www.json.org for more info. - def to_json(options = nil) - ActiveSupport::JSON.encode(self, options) - end - RUBY -end - class Object def as_json(options = nil) #:nodoc: if respond_to?(:to_hash) @@ -152,12 +136,6 @@ class Object end end -# A string that returns itself as its JSON-encoded form. -class ActiveSupport::JSON::Variable < String - def as_json(options = nil) self end #:nodoc: - def encode_json(encoder) self end #:nodoc: -end - class TrueClass AS_JSON = ActiveSupport::JSON::Variable.new('true').freeze def as_json(options = nil) AS_JSON end #:nodoc: diff --git a/activesupport/lib/active_support/json/variable.rb b/activesupport/lib/active_support/json/variable.rb index daa7449b71..5685ed18b7 100644 --- a/activesupport/lib/active_support/json/variable.rb +++ b/activesupport/lib/active_support/json/variable.rb @@ -2,10 +2,8 @@ module ActiveSupport module JSON # A string that returns itself as its JSON-encoded form. class Variable < String - private - def rails_to_json(*) - self - end + def as_json(options = nil) self end #:nodoc: + def encode_json(encoder) self end #:nodoc: end end end diff --git a/activesupport/lib/active_support/multibyte/chars.rb b/activesupport/lib/active_support/multibyte/chars.rb index 8823e4a5ed..51c2a0edac 100644 --- a/activesupport/lib/active_support/multibyte/chars.rb +++ b/activesupport/lib/active_support/multibyte/chars.rb @@ -325,18 +325,6 @@ module ActiveSupport #:nodoc: end alias_method :[], :slice - # Like <tt>String#slice!</tt>, except instead of byte offsets you specify character offsets. - # - # Example: - # s = 'こんにちは' - # s.mb_chars.slice!(2..3).to_s #=> "にち" - # s #=> "こんは" - def slice!(*args) - slice = self[*args] - self[*args] = '' - slice - end - # Limit the byte size of the string to a number of bytes without breaking characters. Usable # when the storage for a string is limited for some reason. # @@ -425,14 +413,14 @@ module ActiveSupport #:nodoc: chars(Unicode.tidy_bytes(@wrapped_string, force)) end - %w(lstrip rstrip strip reverse upcase downcase tidy_bytes capitalize).each do |method| - define_method("#{method}!") do |*args| - unless args.nil? - @wrapped_string = send(method, *args).to_s - else - @wrapped_string = send(method).to_s + %w(capitalize downcase lstrip reverse rstrip slice strip tidy_bytes upcase).each do |method| + # Only define a corresponding bang method for methods defined in the proxy; On 1.9 the proxy will + # exclude lstrip!, rstrip! and strip! because they are already work as expected on multibyte strings. + if public_method_defined?(method) + define_method("#{method}!") do |*args| + @wrapped_string = send(args.nil? ? method : method, *args).to_s + self end - self end end diff --git a/activesupport/lib/active_support/time.rb b/activesupport/lib/active_support/time.rb index 784c7173a9..86f057d676 100644 --- a/activesupport/lib/active_support/time.rb +++ b/activesupport/lib/active_support/time.rb @@ -24,6 +24,7 @@ require 'active_support/core_ext/date/acts_like' require 'active_support/core_ext/date/freeze' require 'active_support/core_ext/date/calculations' require 'active_support/core_ext/date/conversions' +require 'active_support/core_ext/date/zones' require 'active_support/core_ext/date_time/acts_like' require 'active_support/core_ext/date_time/calculations' diff --git a/activesupport/lib/active_support/values/time_zone.rb b/activesupport/lib/active_support/values/time_zone.rb index 05b40298d3..49dd8a1b99 100644 --- a/activesupport/lib/active_support/values/time_zone.rb +++ b/activesupport/lib/active_support/values/time_zone.rb @@ -1,11 +1,5 @@ require 'active_support/core_ext/object/blank' require 'active_support/core_ext/object/try' -begin - require 'tzinfo' -rescue LoadError => e - $stderr.puts "You don't have tzinfo installed in your application. Please add it to your Gemfile and run bundle install" - raise e -end # The TimeZone class serves as a wrapper around TZInfo::Timezone instances. It allows us to do the following: # @@ -201,6 +195,12 @@ module ActiveSupport # (GMT). Seconds were chosen as the offset unit because that is the unit that # Ruby uses to represent time zone offsets (see Time#utc_offset). def initialize(name, utc_offset = nil, tzinfo = nil) + begin + require 'tzinfo' + rescue LoadError => e + $stderr.puts "You don't have tzinfo installed in your application. Please add it to your Gemfile and run bundle install" + raise e + end @name = name @utc_offset = utc_offset @tzinfo = tzinfo || TimeZone.find_tzinfo(name) diff --git a/activesupport/test/core_ext/class_test.rb b/activesupport/test/core_ext/class_test.rb index b7f3dd9930..08bb13dd35 100644 --- a/activesupport/test/core_ext/class_test.rb +++ b/activesupport/test/core_ext/class_test.rb @@ -1,29 +1,27 @@ require 'abstract_unit' require 'active_support/core_ext/class' -class A -end +class ClassTest < Test::Unit::TestCase + class Parent; end + class Foo < Parent; end + class Bar < Foo; end + class Baz < Bar; end -module X - class B - end -end + class A < Parent; end + class B < A; end + class C < B; end -module Y - module Z - class C - end + def test_descendants + assert_equal [Foo, Bar, Baz, A, B, C], Parent.descendants + assert_equal [Bar, Baz], Foo.descendants + assert_equal [Baz], Bar.descendants + assert_equal [], Baz.descendants end -end -class ClassTest < Test::Unit::TestCase - def test_retrieving_subclasses - @parent = eval("class D; end; D") - @sub = eval("class E < D; end; E") - @subofsub = eval("class F < E; end; F") - assert_equal 2, @parent.subclasses.size - assert_equal [@subofsub.to_s], @sub.subclasses - assert_equal [], @subofsub.subclasses - assert_equal [@sub.to_s, @subofsub.to_s].sort, @parent.subclasses.sort + def test_subclasses + assert_equal [Foo, A], Parent.subclasses + assert_equal [Bar], Foo.subclasses + assert_equal [Baz], Bar.subclasses + assert_equal [], Baz.subclasses end -end +end
\ No newline at end of file diff --git a/activesupport/test/core_ext/object_and_class_ext_test.rb b/activesupport/test/core_ext/object_and_class_ext_test.rb index 5e03389dc2..6588f2e345 100644 --- a/activesupport/test/core_ext/object_and_class_ext_test.rb +++ b/activesupport/test/core_ext/object_and_class_ext_test.rb @@ -40,55 +40,6 @@ class Foo include Bar end -class ClassExtTest < Test::Unit::TestCase - def test_subclasses_of_should_find_nested_classes - assert Class.subclasses_of(ClassK).include?(Nested::ClassL) - end - - def test_subclasses_of_should_not_return_removed_classes - # First create the removed class - old_class = Nested.class_eval { remove_const :ClassL } - new_class = Class.new(ClassK) - Nested.const_set :ClassL, new_class - assert_equal "Nested::ClassL", new_class.name # Sanity check - - subclasses = Class.subclasses_of(ClassK) - assert subclasses.include?(new_class) - assert ! subclasses.include?(old_class) - ensure - Nested.const_set :ClassL, old_class unless defined?(Nested::ClassL) - end - - def test_subclasses_of_should_not_trigger_const_missing - const_missing = false - Nested.on_const_missing { const_missing = true } - - subclasses = Class.subclasses_of ClassK - assert !const_missing - assert_equal [ Nested::ClassL ], subclasses - - removed = Nested.class_eval { remove_const :ClassL } # keep it in memory - subclasses = Class.subclasses_of ClassK - assert !const_missing - assert subclasses.empty? - ensure - Nested.const_set :ClassL, removed unless defined?(Nested::ClassL) - end - - def test_subclasses_of_with_multiple_roots - classes = Class.subclasses_of(ClassI, ClassK) - assert_equal %w(ClassJ Nested::ClassL), classes.collect(&:to_s).sort - end - - def test_subclasses_of_doesnt_find_anonymous_classes - assert_equal [], Class.subclasses_of(Foo) - bar = Class.new(Foo) - assert_nothing_raised do - assert_equal [bar], Class.subclasses_of(Foo) - end - end -end - class ObjectTests < ActiveSupport::TestCase class DuckTime def acts_like_time? diff --git a/activesupport/test/deprecation_test.rb b/activesupport/test/deprecation_test.rb index cf27357b32..cad0810241 100644 --- a/activesupport/test/deprecation_test.rb +++ b/activesupport/test/deprecation_test.rb @@ -80,6 +80,19 @@ class DeprecationTest < ActiveSupport::TestCase assert_deprecated(/foo=nil/) { @dtc.partially } end + def test_several_behaviors + @a, @b = nil, nil + + ActiveSupport::Deprecation.behavior = [ + Proc.new { |msg, callstack| @a = msg }, + Proc.new { |msg, callstack| @b = msg } + ] + + @dtc.partially + assert_match(/foo=nil/, @a) + assert_match(/foo=nil/, @b) + end + def test_deprecated_instance_variable_proxy assert_not_deprecated { @dtc.request.size } @@ -123,6 +136,13 @@ class DeprecationTest < ActiveSupport::TestCase assert_equal 123, result end + def test_assert_deprecated_warn_work_with_default_behavior + ActiveSupport::Deprecation.instance_variable_set('@behavior' , nil) + assert_deprecated('abc') do + ActiveSupport::Deprecation.warn 'abc' + end + end + def test_silence ActiveSupport::Deprecation.silence do assert_not_deprecated { @dtc.partially } diff --git a/activesupport/test/descendants_tracker_test.rb b/activesupport/test/descendants_tracker_test.rb index ff24e310de..79fb893592 100644 --- a/activesupport/test/descendants_tracker_test.rb +++ b/activesupport/test/descendants_tracker_test.rb @@ -37,7 +37,9 @@ class DescendantsTrackerTest < Test::Unit::TestCase def test_clear_with_autoloaded_parent_children_and_granchildren mark_as_autoloaded(*ALL) do ActiveSupport::DescendantsTracker.clear - assert ActiveSupport::DescendantsTracker.descendants.slice(*ALL).empty? + ALL.each do |k| + assert ActiveSupport::DescendantsTracker.descendants(k).empty? + end end end @@ -64,12 +66,12 @@ class DescendantsTrackerTest < Test::Unit::TestCase old_autoloaded = ActiveSupport::Dependencies.autoloaded_constants.dup ActiveSupport::Dependencies.autoloaded_constants = klasses.map(&:name) - old_descendants = ActiveSupport::DescendantsTracker.descendants.dup + old_descendants = ActiveSupport::DescendantsTracker.class_eval("@@direct_descendants").dup old_descendants.each { |k, v| old_descendants[k] = v.dup } yield ensure ActiveSupport::Dependencies.autoloaded_constants = old_autoloaded - ActiveSupport::DescendantsTracker.descendants.replace(old_descendants) + ActiveSupport::DescendantsTracker.class_eval("@@direct_descendants").replace(old_descendants) end end
\ No newline at end of file diff --git a/activesupport/test/json/encoding_test.rb b/activesupport/test/json/encoding_test.rb index a8ecf4e4cf..a679efb41e 100644 --- a/activesupport/test/json/encoding_test.rb +++ b/activesupport/test/json/encoding_test.rb @@ -1,7 +1,5 @@ # encoding: utf-8 require 'abstract_unit' -require 'bigdecimal' -require 'active_support/core_ext/big_decimal/conversions' require 'active_support/json' class TestJSONEncoding < Test::Unit::TestCase @@ -138,6 +136,10 @@ class TestJSONEncoding < Test::Unit::TestCase ActiveSupport.use_standard_json_time_format = false end + def test_hash_with_time_to_json + assert_equal '{"time":"2009/01/01 00:00:00 +0000"}', { :time => Time.utc(2009) }.to_json + end + def test_nested_hash_with_float assert_nothing_raised do hash = { diff --git a/activesupport/test/multibyte_chars_test.rb b/activesupport/test/multibyte_chars_test.rb index 610295fa89..78232d8eb5 100644 --- a/activesupport/test/multibyte_chars_test.rb +++ b/activesupport/test/multibyte_chars_test.rb @@ -123,22 +123,30 @@ class MultibyteCharsUTF8BehaviourTest < Test::Unit::TestCase assert_equal 'こに わ', @chars end - def test_overridden_bang_methods_return_self - [:rstrip!, :lstrip!, :strip!, :reverse!, :upcase!, :downcase!, :capitalize!].each do |method| - assert_equal @chars.object_id, @chars.send(method).object_id - end + %w{capitalize downcase lstrip reverse rstrip strip upcase}.each do |method| + class_eval(<<-EOTESTS) + def test_#{method}_bang_should_return_self + assert_equal @chars.object_id, @chars.send("#{method}!").object_id + end + + def test_#{method}_bang_should_change_wrapped_string + original = ' él piDió Un bUen café ' + proxy = chars(original.dup) + proxy.send("#{method}!") + assert_not_equal original, proxy.to_s + end + EOTESTS end - def test_overridden_bang_methods_change_wrapped_string - [:rstrip!, :lstrip!, :strip!, :reverse!, :upcase!, :downcase!].each do |method| - original = ' Café ' - proxy = chars(original.dup) - proxy.send(method) - assert_not_equal original, proxy.to_s - end - proxy = chars('òu') - proxy.capitalize! - assert_equal 'Òu', proxy.to_s + def test_tidy_bytes_bang_should_return_self + assert_equal @chars.object_id, @chars.tidy_bytes!.object_id + end + + def test_tidy_bytes_bang_should_change_wrapped_string + original = " Un bUen café \x92" + proxy = chars(original.dup) + proxy.tidy_bytes! + assert_not_equal original, proxy.to_s end if RUBY_VERSION >= '1.9' @@ -417,8 +425,9 @@ class MultibyteCharsUTF8BehaviourTest < Test::Unit::TestCase end def test_slice_bang_removes_the_slice_from_the_receiver - @chars.slice!(1..2) - assert_equal 'こわ', @chars + chars = 'úüù'.mb_chars + chars.slice!(0,2) + assert_equal 'úü', chars end def test_slice_should_throw_exceptions_on_invalid_arguments diff --git a/activesupport/test/ordered_hash_test.rb b/activesupport/test/ordered_hash_test.rb index 3d1bae163f..d340bed444 100644 --- a/activesupport/test/ordered_hash_test.rb +++ b/activesupport/test/ordered_hash_test.rb @@ -1,5 +1,6 @@ require 'abstract_unit' require 'active_support/json' +require 'active_support/core_ext/object/to_json' class OrderedHashTest < Test::Unit::TestCase def setup diff --git a/rails.gemspec b/rails.gemspec index 236203c626..2d10ce78a0 100644 --- a/rails.gemspec +++ b/rails.gemspec @@ -25,5 +25,5 @@ Gem::Specification.new do |s| s.add_dependency('activeresource', version) s.add_dependency('actionmailer', version) s.add_dependency('railties', version) - s.add_dependency('bundler', '>= 0.9.26') + s.add_dependency('bundler', '>= 1.0.0.beta.3') end diff --git a/railties/README b/railties/README index b8c84dd07d..d8be15e346 100644 --- a/railties/README +++ b/railties/README @@ -29,13 +29,13 @@ link:files/vendor/rails/actionpack/README.html. == Getting Started 1. At the command prompt, create a new Rails application: - <tt>rails myapp</tt> (where <tt>myapp</tt> is the application name) + <tt>rails new myapp</tt> (where <tt>myapp</tt> is the application name) 2. Change directory to <tt>myapp</tt> and start the web server: <tt>cd myapp; rails server</tt> (run with --help for options) 3. Go to http://localhost:3000/ and you'll see: - "Welcome aboard: You're riding the Rails!" + "Welcome aboard: You're riding Ruby on Rails!" 4. Follow the guidelines to start developing your application. You can find the following resources handy: diff --git a/railties/guides/source/3_0_release_notes.textile b/railties/guides/source/3_0_release_notes.textile index b32ca4fce9..7dcaf508c6 100644 --- a/railties/guides/source/3_0_release_notes.textile +++ b/railties/guides/source/3_0_release_notes.textile @@ -339,7 +339,9 @@ h5. Other Changes * You no longer need to place a minus sign at the end of a ruby interpolation inside an ERb template to remove the trailing carriage return in the HTML output. * Added +grouped_collection_select+ helper to Action View. * +content_for?+ has been added allowing you to check for the existence of content in a view before rendering. - +* passing +:value => nil+ to form helpers will set the field's +value+ attribute to nil as opposed to using the default value +* passing +:id => nil+ to form helpers will cause those fields to be rendered with no +id+ attribute +* passing +:alt => nil+ to +image_tag+ will cause the +img+ tag to render with no +alt+ attribute h3. Active Model diff --git a/railties/guides/source/active_record_validations_callbacks.textile b/railties/guides/source/active_record_validations_callbacks.textile index cfd4ae55cc..84c33e34f9 100644 --- a/railties/guides/source/active_record_validations_callbacks.textile +++ b/railties/guides/source/active_record_validations_callbacks.textile @@ -86,9 +86,9 @@ The following methods skip validations, and will save the object to the database * +update_attribute+ * +update_counters+ -Note that +save+ also has the ability to skip validations if passed +false+ as argument. This technique should be used with caution. +Note that +save+ also has the ability to skip validations if passed +:validate => false+ as argument. This technique should be used with caution. -* +save(false)+ +* +save(:validate => false)+ h4. +valid?+ and +invalid?+ diff --git a/railties/guides/source/active_support_core_extensions.textile b/railties/guides/source/active_support_core_extensions.textile index 844b9428bd..58824d7aeb 100644 --- a/railties/guides/source/active_support_core_extensions.textile +++ b/railties/guides/source/active_support_core_extensions.textile @@ -387,40 +387,6 @@ TIP: Since +with_options+ forwards calls to its receiver they can be nested. Eac NOTE: Defined in +active_support/core_ext/object/with_options.rb+. -h5. +subclasses_of+ - -The method +subclasses_of+ receives an arbitrary number of class objects and returns all their anonymous or reachable descendants as a single array: - -<ruby> -class C; end -subclasses_of(C) # => [] - -subclasses_of(Integer) # => [Bignum, Fixnum] - -module M - class A; end - class B1 < A; end - class B2 < A; end -end - -module N - class C < M::B1; end -end - -subclasses_of(M::A) # => [N::C, M::B2, M::B1] -</ruby> - -The order in which these classes are returned is unspecified. The returned collection may have duplicates: - -<ruby> -subclasses_of(Numeric, Integer) -# => [Bignum, Float, Fixnum, Integer, Date::Infinity, Rational, BigDecimal, Bignum, Fixnum] -</ruby> - -See also +Class#subclasses+ in "Extensions to +Class+ FIX THIS LINK":FIXME. - -NOTE: Defined in +active_support/core_ext/object/extending.rb+. - h4. Instance Variables Active Support provides several methods to ease access to instance variables. @@ -1141,36 +1107,51 @@ If for whatever reason an application loads the definition of a mailer class and NOTE: Defined in +active_support/core_ext/class/delegating_attributes.rb+. -h4. Descendants +h4. Subclasses & Descendants h5. +subclasses+ -The +subclasses+ method returns the names of all the anonymous or reachable descendants of its receiver as an array of strings: +The +subclasses+ method returns the subclasses of the receiver: <ruby> class C; end C.subclasses # => [] -Integer.subclasses # => ["Bignum", "Fixnum"] +class B < C; end +C.subclasses # => [B] -module M - class A; end - class B1 < A; end - class B2 < A; end -end +class A < B; end +C.subclasses # => [B] -module N - class C < M::B1; end -end - -M::A.subclasses # => ["N::C", "M::B2", "M::B1"] +class D < C; end +C.subclasses # => [B, D] </ruby> -The order in which these class names are returned is unspecified. +The order in which these classes are returned is unspecified. + +WARNING: This method is redefined in some Rails core classes but should be all compatible in Rails 3.1. + +NOTE: Defined in +active_support/core_ext/class/subclasses.rb+. -See also +Object#subclasses_of+ in "Extensions to All Objects FIX THIS LINK":FIXME. +h5. +descendants+ + +The +descendants+ method returns all classes that are <tt><</tt> than its receiver: + +<ruby> +class C; end +C.descendants # => [] + +class B < C; end +C.descendants # => [B] + +class A < B; end +C.descendants # => [B, A] + +class D < C; end +C.descendants # => [B, A, D] +</ruby> -WARNING: This method is redefined in some Rails core classes. +The order in which these classes are returned is unspecified. NOTE: Defined in +active_support/core_ext/class/subclasses.rb+. diff --git a/railties/guides/source/association_basics.textile b/railties/guides/source/association_basics.textile index 335d17579d..c69f2ae8c9 100644 --- a/railties/guides/source/association_basics.textile +++ b/railties/guides/source/association_basics.textile @@ -137,6 +137,16 @@ end !images/has_many_through.png(has_many :through Association Diagram)! +The collection of join models can be managed via the API. For example, if you assign + +<ruby> +physician.patients = patients +</ruby> + +new join models are created for newly associated objects, and if some are gone their rows are deleted. + +WARNING: Automatic deletion of join models is direct, no destroy callbacks are triggered. + The +has_many :through+ association is also useful for setting up "shortcuts" through nested +has_many+ associations. For example, if a document has many sections, and a section has many paragraphs, you may sometimes want to get a simple collection of all paragraphs in the document. You could set that up this way: <ruby> diff --git a/railties/guides/source/getting_started.textile b/railties/guides/source/getting_started.textile index 6abb3ed9f4..f547f29087 100644 --- a/railties/guides/source/getting_started.textile +++ b/railties/guides/source/getting_started.textile @@ -958,7 +958,7 @@ You'll see a bit more complexity here than you did in the controller for posts. In addition, the code takes advantage of some of the methods available for an association. We use the +create+ method on +@post.comments+ to create and save the comment. This will automatically link the comment so that it belongs to that particular post. -Once we have made the new comment, we send the user back to the original post using the +post_path(@post)+ helper. As we have already seen, this calls the +show+ action of the +PostsController+ which in turn renders the +show.html.erb+ template. This is where we want the comment to show, so let's add that to the +app/view/posts/show.html.erb+. +Once we have made the new comment, we send the user back to the original post using the +post_path(@post)+ helper. As we have already seen, this calls the +show+ action of the +PostsController+ which in turn renders the +show.html.erb+ template. This is where we want the comment to show, so let's add that to the +app/views/posts/show.html.erb+. <erb> <p class="notice"><%= notice %></p> diff --git a/railties/guides/source/index.html.erb b/railties/guides/source/index.html.erb index be077fcd2f..a930db0f1d 100644 --- a/railties/guides/source/index.html.erb +++ b/railties/guides/source/index.html.erb @@ -68,7 +68,7 @@ Ruby on Rails Guides <% end %> <%= guide("Action View Form Helpers", 'form_helpers.html', :ticket => 1) do %> - <p>Guide to using built in Form helpers.</p> + <p>Guide to using built-in Form helpers.</p> <% end %> </dl> diff --git a/railties/guides/source/initialization.textile b/railties/guides/source/initialization.textile index e458413b35..7a44ef7c77 100644 --- a/railties/guides/source/initialization.textile +++ b/railties/guides/source/initialization.textile @@ -11,7 +11,7 @@ This guide first describes the process of +rails server+ then explains the Passe h3. Launch! -As of Rails 3, +script/server+ has become +rails server+. This was done to centralise all rails related commands to one common file. +As of Rails 3, +script/server+ has become +rails server+. This was done to centralize all rails related commands to one common file. The actual +rails+ command is kept in _railties/bin/rails_ and goes like this: @@ -58,11 +58,8 @@ In +script/rails+ we see the following: #!/usr/bin/env ruby # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. - ENV_PATH = File.expand_path('../../config/environment', __FILE__) - BOOT_PATH = File.expand_path('../../config/boot', __FILE__) - APP_PATH = File.expand_path('../../config/application', __FILE__) - - require BOOT_PATH + APP_PATH = File.expand_path('../../config/application', __FILE__) + require File.expand_path('../../config/boot', __FILE__) require 'rails/commands' </ruby> @@ -79,15 +76,19 @@ h3. _config/boot.rb_ _config/boot.rb_ is the first stop for everything for initializing your application. This boot process does quite a bit of work for you and so this section attempts to go in-depth enough to explain what each of the pieces does. <ruby> - # Use Bundler (preferred) + require 'rubygems' + + # Set up gems listed in the Gemfile. + gemfile = File.expand_path('../../Gemfile', __FILE__) begin - require File.expand_path('../../.bundle/environment', __FILE__) - rescue LoadError - require 'rubygems' + ENV['BUNDLE_GEMFILE'] = gemfile require 'bundler' Bundler.setup - end - + rescue Bundler::GemNotFound => e + STDERR.puts e.message + STDERR.puts "Try running `bundle install`." + exit! + end if File.exist?(gemfile) </ruby> h3. Bundled Rails (3.x) @@ -140,8 +141,8 @@ Here the only two gems we need are +rails+ and +sqlite3-ruby+, so it seems. This * activesupport-3.0.0.beta4.gem * arel-0.4.0.gem * builder-2.1.2.gem -* bundler-0.9.26.gem -* erubis-2.6.5.gem +* bundler-1.0.0.beta.2.gem +* erubis-2.6.6.gem * i18n-0.4.1.gem * mail-2.2.4.gem * memcache-client-1.8.3.gem @@ -156,7 +157,7 @@ Here the only two gems we need are +rails+ and +sqlite3-ruby+, so it seems. This * sqlite3-ruby-1.3.0.gem * text-format-1.0.0.gem * text-hyphen-1.0.0.gem -* thor-0.13.6.gem +* thor-0.13.7.gem * treetop-1.4.8.gem * tzinfo-0.3.22.gem @@ -164,33 +165,7 @@ TODO: Prettify when it becomes more stable. I won't go into what each of these gems are, as that is really something that needs covering on a case-by-case basis. We will however just dig a little under the surface of Bundler. -Back in _config/boot.rb_, the first line will try to include _.bundle/environment.rb_, which doesn't exist in a bare-bones Rails application and because this file does not exist Ruby will raise a +LoadError+ which will be rescued and run the following code: - -<ruby> - require 'rubygems' - require 'bundler' - Bundler.setup -</ruby> - -+Bundler.setup+ here will load and parse the +Gemfile+ and add the _lib_ directory of the gems mentioned **and** their dependencies (**and** their dependencies' dependencies, and so on) to the +$LOAD_PATH+. - -Now we will go down the alternate timeline where we generate a _.bundle/environment.rb_ file using the +bundle lock+ command. This command also creates a _Gemfile.lock_ file which is actually a YAML file loaded by this method in Bundler before it moves on to check for _Gemfile_: - -<ruby> - def definition(gemfile = default_gemfile) - configure - root = Pathname.new(gemfile).dirname - lockfile = root.join("Gemfile.lock") - if lockfile.exist? - Definition.from_lock(lockfile) - else - Definition.from_gemfile(gemfile) - end - end -</ruby> - - -The _.bundle/environment.rb_ file adds the _lib_ directory of all the gems specified in +Gemfile.lock+ to +$LOAD_PATH+. +Back in _config/boot.rb_, we call +Bundler.setup+ which will load and parse the +Gemfile+ and add the _lib_ directory of the gems mentioned **and** their dependencies (**and** their dependencies' dependencies, and so on) to the +$LOAD_PATH+. h3. Requiring Rails @@ -326,6 +301,11 @@ As you can see for the duration of the +eager_autoload+ block the class variable module ActiveSupport extend ActiveSupport::Autoload + autoload :DescendantsTracker + autoload :FileUpdateChecker + autoload :LogSubscriber + autoload :Notifications + # TODO: Narrow this list down eager_autoload do autoload :BacktraceCleaner @@ -348,7 +328,6 @@ As you can see for the duration of the +eager_autoload+ block the class variable autoload :OptionMerger autoload :OrderedHash autoload :OrderedOptions - autoload :Notifications autoload :Rescuable autoload :SecureRandom autoload :StringInquirer @@ -589,19 +568,20 @@ This file (_railties/lib/rails.rb_) requires the very, very basics that Rails ne require 'action_dispatch/railtie' </ruby> -+require 'pathname'+ requires the Pathname class which is used for returning a Pathname object for +Rails.root+ so that instead of doing: ++require 'pathname'+ requires the Pathname class which is used for returning a Pathname object for +Rails.root+. Although is coming to use this path name to generate paths as below: <ruby> - File.join(Rails.root, "app/controllers") + Rails.root.join("app/controllers") </ruby> -You may do: +Pathname can also be converted to string, so the following syntax is preferred: <ruby> - Rails.root.join("app/controllers") + "#{Rails.root}/app/controllers" </ruby> -Although this is not new to Rails 3 (it was available in 2.3.5), it is something worthwhile pointing out. + +This works because Ruby automatically handles file path conversions. Although this is not new to Rails 3 (it was available in 2.3.5), it is something worthwhile pointing out. Inside this file there are other helpful helper methods defined, such as +Rails.root+, +Rails.env+, +Rails.logger+ and +Rails.application+. @@ -1833,7 +1813,7 @@ We do not already have a +Rails.application+, so instead this resorts to calling end </ruby> -This +called_from+ setting looks a little overwhelming to begin with, but the short end of it is that it returns the route to your application's config directory, something like: _/home/you/yourapp/config_. After +called_from+ has been set, +super+ is again called and this means the +Rails::Railtie#inherited+ method (in _railties/lib/rails/railtie.rb_): +This +called_from+ setting looks a little overwhelming to begin with, but the short end of it is that it returns your application's root, something like: _/home/you/yourapp_. After +called_from+ has been set, +super+ is again called and this means the +Rails::Railtie#inherited+ method (in _railties/lib/rails/railtie.rb_): <ruby> def inherited(base) @@ -3588,28 +3568,24 @@ h3. Appendix A This file is _activesupport/lib/active_support/inflector/inflections.rb_ and defines the +ActiveSupport::Inflector::Inflections+ class which defines the +singularize+, +pluralize+, +humanize+, +tableize+, +titleize+ and +classify+ methods as well as the code to defining how to work out the irregular, singular, plural and human versions of words. These methods are called +irregular+, +singular+, +plural+ and +human+ respectively, as is the Rails way. -This file is _activesupport/lib/active_support/inflector/transliterate.rb_ and defines two methods, +transliterate+ and +parameterize+. What +transliterate+ does depends on your Ruby version. If you have something greater than 1.9 installed it will just print out a warning message using the +Kernel#warn+ method (simply called using +warn+) reading "Ruby 1.9 doesn't support Unicode normalization yet". If you're running something that's not 1.9 it will attempt to convert +"föö"+ to +foo+ and if that fails then it'll redefine it. +This file is _activesupport/lib/active_support/inflector/transliterate.rb_ and defines two methods, +transliterate+ and +parameterize+. -This file first makes a require to _activesupport/lib/active_support/core_ext/string/multibyte.rb_ which then goes on to require _activesupport/lib/active_support/multibyte.rb_ and that requires _activesupport/core_ext/module/attribute_accessors.rb_. The _attribute_accessors.rb_ file is used to gain access to the +mattr_accessor+ (module attribute accessor) method which is called in _active_suport/multibyte.rb_. Also in _active_support/multibyte.rb_ there's a couple of autoloaded classes: +This file first requires _activesupport/lib/active_support/core_ext/string/multibyte.rb_, which requires _activesupport/lib/active_support/multibyte.rb_, which subsequently requires _activesupport/core_ext/module/attribute_accessors.rb_. The _attribute_accessors.rb_ file is needed to gain access to the +mattr_accessor+ (module attribute accessor) method, which is called in _active_suport/multibyte.rb_. The file _active_support/multibyte.rb_ also autoloads three other classes: <ruby> module ActiveSupport #:nodoc: module Multibyte autoload :EncodingError, 'active_support/multibyte/exceptions' autoload :Chars, 'active_support/multibyte/chars' - autoload :UnicodeDatabase, 'active_support/multibyte/unicode_database' - autoload :Codepoint, 'active_support/multibyte/unicode_database' - autoload :UCD, 'active_support/multibyte/unicode_database' - ... + autoload :Unicode, 'active_support/multibyte/unicode' + ... end end </ruby> -There's also these method definitions: +There are also these method definitions: <ruby> - self.default_normalization_form = :kc - # The proxy class returned when calling mb_chars. You can use this accessor to configure your own proxy # class so you can support other encodings. See the ActiveSupport::Multibyte::Chars implementation for # an example how to do this. @@ -3628,63 +3604,92 @@ There's also these method definitions: These methods are used in _activesupport/lib/active_support/core_ext/string/multibyte.rb_. -If we go back to _activesupport/lib/active_support/core_ext/string/multibyte.rb_, this file makes a couple of extensions to the +String+ class based on if your version of Ruby's +String+ class responds to the +force_encoding+ method. This method was introduced in Ruby 1.9. If you're using 1.9 the methods are defined like this: - -<ruby> - def mb_chars #:nodoc - self - end - - def is_utf8? #:nodoc - case encoding - when Encoding::UTF_8 - valid_encoding? - when Encoding::ASCII_8BIT, Encoding::US_ASCII - dup.force_encoding(Encoding::UTF_8).valid_encoding? - else - false - end - end -</ruby> - -You can see that calling +mb_chars+ on a +String+ instance in Ruby 1.9 will simply return that +String+ object. +String+ objects in Ruby 1.9 are already multibyte strings, so Rails does not need to do any conversion on them. - -The second method, +is_utf8?+ return +true+ if the +String+ object is of the UTF8 encoding or if it's able to be forced into that encoding and +false+ if it can't force its encoding or if the encoding of the string is neither +UTF8+, +ASCII_8BIT+ or +US_ASCII+. - -If you're using a Ruby version less than 1.9 there are 3 methods defined instead of 2, and they are defined like this: +The file _activesupport/lib/active_support/core_ext/string/chars.rb_ defines the default proxy class that will be returned by +mb_chars+. + +Because Ruby 1.9's +String+ class has support for multibyte encodings, some methods are defined only for Ruby 1.8: + +* +self.wants?+ +* +++ +* +=~+ +* +=~+ +* +center+ +* +include?+ +* +index+ +* +insert+ +* +ljust+ +* +lstrip+, +lstrip!+ +* +ord+ +* +rindex+ +* +rjust+ +* +rstrip+, +rstrip!+ +* +size+ +* +strip+, +strip!+ + +However, Ruby 1.9 lacks support for some needed operations, so the following methods are defined for both Ruby 1.8 and Ruby 1.9: + +* +<=>+ +* +[]=+ +* +capitalize+, +capitalize!+ +* +compose+ +* +decompose+ +* +downcase+, +downcase!+ +* +g_length+ +* +limit+ +* +normalize+ +* +reverse+, +reverse+! +* +slice+, +slice!+ +* +split+ +* +tidy_bytes+, +tidy_bytes!+ +* +titleize+ +* +upcase+, +upcase!+ + +<ruby> + class String + if RUBY_VERSION >= "1.9" + def mb_chars + if ActiveSupport::Multibyte.proxy_class.consumes?(self) + ActiveSupport::Multibyte.proxy_class.new(self) + else + self + end + end -<ruby> - def mb_chars - if ActiveSupport::Multibyte.proxy_class.wants?(self) - ActiveSupport::Multibyte.proxy_class.new(self) + def is_utf8? #:nodoc + case encoding + when Encoding::UTF_8 + valid_encoding? + when Encoding::ASCII_8BIT, Encoding::US_ASCII + dup.force_encoding(Encoding::UTF_8).valid_encoding? + else + false + end + end else - self - end - end - - # Returns true if the string has UTF-8 semantics (a String used for purely byte resources is unlikely to have - # them), returns false otherwise. - def is_utf8? - ActiveSupport::Multibyte::Chars.consumes?(self) - end + def mb_chars + if ActiveSupport::Multibyte.proxy_class.wants?(self) + ActiveSupport::Multibyte.proxy_class.new(self) + else + self + end + end - unless '1.8.7 and later'.respond_to?(:chars) - def chars - ActiveSupport::Deprecation.warn('String#chars has been deprecated in favor of String#mb_chars.', caller) - mb_chars + # Returns true if the string has UTF-8 semantics (a String used for purely byte resources is unlikely to have + # them), returns false otherwise. + def is_utf8? + ActiveSupport::Multibyte::Chars.consumes?(self) + end end - end - </ruby> +As you can see, +mb_chars+ is where the +proxy_class+ property comes in handy. This method will create a new instance of the configured proxy class using the instance of +String+ as a constructor argument. By default, the new +String+-like object will be an instance of the proxy class +ActiveSupport::Multibyte::Chars+. You can use +ActiveSupport::Multibyte.proxy_class=+ to set a different proxy class if you wish. -As you can see, +mb_chars+ is where the +proxy_class+ method comes in handy. This will create a new instance of that class and pass in the +String+ object in order to make it multibyte-compatible. In this case the new +String+ object will be an instance of the +ActiveSupport::Multibyte::Chars+ class. You can use +ActiveSupport::Multibyte.proxy_class=+ to set this to be a different class if you're that way inclined. - -Here, +is_utf8?+ calls a +consumes+ method on the not-yet-loaded +ActiveSupport::Multibyte::Chars+ class. The keen-eye would have seen this was specified as an auto-load earlier, so that is what is going to happen if we call this method or +mb_chars+. This means that it'll require the file located at _activesupport/lib/active_support/multibyte/chars.rb_. This file includes _activesupport/lib/active_support/string/access.rb_ which defines methods such as +at+, +from+, +to+, +first+ and +last+. These methods will return parts of the string depending on what is passed to them and they are defined differently depending on if you're using Ruby 1.9 or not. The second file included is _activesupport/lib/active_support/string/behaviour.rb_ which defines a single method +acts_like_string?+ on +String+ which always returns +true+. This method is used through the +acts_like?+ method which is passed a single argument representing the downcased and symbolised version of the class you want to know if it acts like. In this case the code would be +acts_like?(:string)+. +Here, +mb_chars+ invokes +is_utf8?+ to checks if the string can be treated as UTF-8. On 1.9, the string's +encoding+ property is checked. On 1.8, +wants?+ checks to see if +$KCODE+ is "UTF-8" and, and +consumes?+ checks whether the string can be unpacked as UTF-8 without raising an error. -The +Chars+ class defines, along with +consumes?+, other methods such as the "spaceship" method +<=>+. This method is referenced by the methods defined in the included +Comparable+ module and will return either +-1+, +0+ or +1+ depending on if the word is before, identical or after the compared word. For example, +'é'.mb_chars <=> 'ü'.mb_chars+ returns +-1+ as e comes before u in the alphabet. Other methods are the commonly used +split+, +=~+, +insert+ and +include?+. +The keen eye will have seen +ActiveSupport::Multibyte::Chars+ was specified as an +autoload+ earlier: _activesupport/lib/active_support/multibyte/chars.rb_ will be loaded without an explicit +require+ when we call +is_utf8+ on 1.8, or +mb_chars+ on any Ruby version. This file includes _activesupport/lib/active_support/string/access.rb_ which defines methods such as +at+, +from+, +to+, +first+ and +last+. These methods will return parts of the string depending on what is passed to them. +The second file included is _activesupport/lib/active_support/string/behavior.rb_ which only defines +acts_like_string?+ on +String+, a method which always returns +true+. This method is used by +Object#acts_like?+, which accepts a single argument representing the downcased and symbolised version of a class, and returns true if the object's behavior is like that class. In this case the code would be +acts_like?(:string)+. +The +Chars+ class also defines other important methods such as the "spaceship" method +<=>+, which is needed by the +Comparable+ module, in order to allow UTF-8-aware sorting. h3. Common Includes diff --git a/railties/guides/source/layouts_and_rendering.textile b/railties/guides/source/layouts_and_rendering.textile index a874fa0ca7..f4ba6dd53b 100644 --- a/railties/guides/source/layouts_and_rendering.textile +++ b/railties/guides/source/layouts_and_rendering.textile @@ -2,7 +2,7 @@ h2. Layouts and Rendering in Rails This guide covers the basic layout features of Action Controller and Action View. By referring to this guide, you will be able to: -* Use the various rendering methods built in to Rails +* Use the various rendering methods built into Rails * Create layouts with multiple content sections * Use partials to DRY up your views * Use nested layouts (sub-templates) diff --git a/railties/guides/source/plugins.textile b/railties/guides/source/plugins.textile index a12434a95b..e853ba79e9 100644 --- a/railties/guides/source/plugins.textile +++ b/railties/guides/source/plugins.textile @@ -1284,7 +1284,7 @@ class YaffleMigrationGenerator < Rails::Generator::NamedBase end </ruby> -The generator creates a new file in 'db/migrate' with a timestamp and an 'add_column' statement. It reuses the built in rails +migration_template+ method, and reuses the built-in rails migration template. +The generator creates a new file in 'db/migrate' with a timestamp and an 'add_column' statement. It reuses the built-in rails +migration_template+ method, and reuses the built-in rails migration template. It's courteous to check to see if table names are being pluralized whenever you create a generator that needs to be aware of table names. This way people using your generator won't have to manually change the generated files if they've turned pluralization off. diff --git a/railties/guides/source/routing.textile b/railties/guides/source/routing.textile index 9ff06856c3..72a76e25bb 100644 --- a/railties/guides/source/routing.textile +++ b/railties/guides/source/routing.textile @@ -382,6 +382,12 @@ match ':controller/:action/:id/:user_id' An incoming URL of +/photos/show/1/2+ will be dispatched to the +show+ action of the +PhotosController+. +params[:id]+ will be +"1"+, and +params[:user_id]+ will be +"2"+. +NOTE: You can't use +namespace+ or +:module+ with a +:controller+ path segment. If you need to do this then use a constraint on :controller that matches the namespace you require. e.g: + +<ruby> +match ':controller(/:action(/:id))', :controller => /admin\/[^\/]+/ +</ruby> + h4. Static Segments You can specify static segments when creating a route: @@ -645,13 +651,13 @@ scope :path_names => { :new => "make" } do end </ruby> -h4. Overriding the Named Helper Prefix +h4. Prefixing the Named Route Helpers -You can use the :name_prefix option to add a prefix to the named route helpers that Rails generates for a route. You can use this option to prevent collisions between routes using a path scope. +You can use the +:as+ option to prefix the named route helpers that Rails generates for a route. Use this option to prevent name collisions between routes using a path scope. <ruby> scope "admin" do - resources :photos, :name_prefix => "admin" + resources :photos, :as => "admin_photos" end resources :photos @@ -659,17 +665,17 @@ resources :photos This will provide route helpers such as +admin_photos_path+, +new_admin_photo_path+ etc. -You could specify a name prefix to use for a group of routes in the scope: +To prefix a group of routes, use +:as+ with +scope+: <ruby> -scope "admin", :name_prefix => "admin" do +scope "admin", :as => "admin" do resources :photos, :accounts end resources :photos, :accounts </ruby> -NOTE: The +namespace+ scope will automatically add a +:name_prefix+ as well as +:module+ and +:path+ prefixes. +NOTE: The +namespace+ scope will automatically add +:as+ as well as +:module+ and +:path+ prefixes. h4. Restricting the Routes Created @@ -714,21 +720,21 @@ Rails now creates routes to the +CategoriesControlleR+. h4. Overriding the Singular Form -If you want to customize the singular name of the route in the named helpers, you can use the +:singular+ option. +If you want to define the singular form of a resource, you should add additional rules to the +Inflector+. <ruby> -resources :teeth, :singular => "tooth" +ActiveSupport::Inflector.inflections do |inflect| + inflect.irregular 'tooth', 'teeth' +end </ruby> -TIP: If you want to define the singular form of a word for your entire application, you should add additional rules to the +Inflector+ instead. - -h4(#nested-name-prefix). Using +:name_prefix+ in Nested Resources +h4(#nested-names). Using +:as+ in Nested Resources -The +:name_prefix+ option overrides the automatically-generated prefix for the parent resource in nested route helpers. For example, +The +:as+ option overrides the automatically-generated name for the resource in nested route helpers. For example, <ruby> resources :magazines do - resources :ads, :name_prefix => 'periodical' + resources :ads, :as => 'periodical_ads' end </ruby> diff --git a/railties/guides/source/security.textile b/railties/guides/source/security.textile index b45514f66d..60108d5ab3 100644 --- a/railties/guides/source/security.textile +++ b/railties/guides/source/security.textile @@ -670,7 +670,7 @@ Also, the second query renames some columns with the AS statement so that the we h5(#sql-injection-countermeasures). Countermeasures -Ruby on Rails has a built in filter for special SQL characters, which will escape ' , " , NULL character and line breaks. <em class="highlight">Using +Model.find(id)+ or +Model.find_by_some thing(something)+ automatically applies this countermeasure</em>. But in SQL fragments, especially <em class="highlight">in conditions fragments (+:conditions => "..."+), the +connection.execute()+ or +Model.find_by_sql()+ methods, it has to be applied manually</em>. +Ruby on Rails has a built-in filter for special SQL characters, which will escape ' , " , NULL character and line breaks. <em class="highlight">Using +Model.find(id)+ or +Model.find_by_some thing(something)+ automatically applies this countermeasure</em>. But in SQL fragments, especially <em class="highlight">in conditions fragments (+:conditions => "..."+), the +connection.execute()+ or +Model.find_by_sql()+ methods, it has to be applied manually</em>. Instead of passing a string to the conditions option, you can pass an array to sanitize tainted strings like this: diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index 4a7ed2d028..458177b954 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -8,14 +8,6 @@ module Rails # In Rails 3.0, a Rails::Application object was introduced which is nothing more than # an Engine but with the responsibility of coordinating the whole boot process. # - # Opposite to Rails::Engine, you can only have one Rails::Application instance - # in your process and both Rails::Application and YourApplication::Application - # points to it. - # - # In other words, Rails::Application is Singleton and whenever you are accessing - # Rails::Application.config or YourApplication::Application.config, you are actually - # accessing YourApplication::Application.instance.config. - # # == Initialization # # Rails::Application is responsible for executing all railties, engines and plugin @@ -57,6 +49,10 @@ module Rails def instance if self == Rails::Application + if Rails.application + ActiveSupport::Deprecation.warn "Calling a method in Rails::Application is deprecated, " << + "please call it directly in your application constant #{Rails.application.class.name}.", caller + end Rails.application else @@instance ||= new @@ -125,14 +121,13 @@ module Rails end def reload_routes! - routes = Rails::Application.routes - routes.disable_clear_and_finalize = true - - routes.clear! + _routes = self.routes + _routes.disable_clear_and_finalize = true + _routes.clear! routes_reloader.paths.each { |path| load(path) } - ActiveSupport.on_load(:action_controller) { routes.finalize! } + ActiveSupport.on_load(:action_controller) { _routes.finalize! } ensure - routes.disable_clear_and_finalize = false + _routes.disable_clear_and_finalize = false end def initialize! diff --git a/railties/lib/rails/commands.rb b/railties/lib/rails/commands.rb index aad1170b56..b9353ba336 100644 --- a/railties/lib/rails/commands.rb +++ b/railties/lib/rails/commands.rb @@ -13,27 +13,27 @@ command = aliases[command] || command case command when 'generate', 'destroy', 'plugin', 'benchmarker', 'profiler' require APP_PATH - Rails::Application.require_environment! + Rails.application.require_environment! require "rails/commands/#{command}" when 'console' require 'rails/commands/console' require APP_PATH - Rails::Application.require_environment! - Rails::Console.start(Rails::Application) + Rails.application.require_environment! + Rails::Console.start(Rails.application) when 'server' require 'rails/commands/server' Rails::Server.new.tap { |server| require APP_PATH - Dir.chdir(Rails::Application.root) + Dir.chdir(Rails.application.root) server.start } when 'dbconsole' require 'rails/commands/dbconsole' require APP_PATH - Rails::DBConsole.start(Rails::Application) + Rails::DBConsole.start(Rails.application) when 'application', 'runner' require "rails/commands/#{command}" diff --git a/railties/lib/rails/commands/application.rb b/railties/lib/rails/commands/application.rb index a3a5aed399..47c6752ca3 100644 --- a/railties/lib/rails/commands/application.rb +++ b/railties/lib/rails/commands/application.rb @@ -4,7 +4,7 @@ if %w(--version -v).include? ARGV.first exit(0) end -if ARGV.first != "new" || ARGV.empty? +if ARGV.first != "new" ARGV[0] = "--help" else ARGV.shift diff --git a/railties/lib/rails/commands/plugin.rb b/railties/lib/rails/commands/plugin.rb index 8bcd92a33b..96b6f9c372 100644 --- a/railties/lib/rails/commands/plugin.rb +++ b/railties/lib/rails/commands/plugin.rb @@ -3,7 +3,7 @@ # Installing plugins: # # $ rails plugin install continuous_builder asset_timestamping -# +# # Specifying revisions: # # * Subversion revision is a single integer. @@ -14,12 +14,11 @@ # 'tag 1.8.0' (equivalent to 'refs/tags/1.8.0') # # -# This is Free Software, copyright 2005 by Ryan Tomayko (rtomayko@gmail.com) +# This is Free Software, copyright 2005 by Ryan Tomayko (rtomayko@gmail.com) # and is licensed MIT: (http://www.opensource.org/licenses/mit-license.php) $verbose = false - require 'open-uri' require 'fileutils' require 'tempfile' @@ -40,18 +39,18 @@ class RailsEnvironment dir = File.dirname(dir) end end - + def self.default @default ||= find end - + def self.default=(rails_env) @default = rails_env end - + def install(name_uri_or_plugin) if name_uri_or_plugin.is_a? String - if name_uri_or_plugin =~ /:\/\// + if name_uri_or_plugin =~ /:\/\// plugin = Plugin.new(name_uri_or_plugin) else plugin = Plugins[name_uri_or_plugin] @@ -65,7 +64,7 @@ class RailsEnvironment puts "Plugin not found: #{name_uri_or_plugin}" end end - + def use_svn? require 'active_support/core_ext/kernel' silence_stderr {`svn --version` rescue nil} @@ -97,7 +96,7 @@ class RailsEnvironment ext = `svn propget svn:externals "#{root}/vendor/plugins"` lines = ext.respond_to?(:lines) ? ext.lines : ext lines.reject{ |line| line.strip == '' }.map do |line| - line.strip.split(/\s+/, 2) + line.strip.split(/\s+/, 2) end end @@ -111,38 +110,37 @@ class RailsEnvironment system("svn propset -q svn:externals -F \"#{file.path}\" \"#{root}/vendor/plugins\"") end end - end class Plugin attr_reader :name, :uri - + def initialize(uri, name = nil) @uri = uri guess_name(uri) end - + def self.find(name) new(name) end - + def to_s "#{@name.ljust(30)}#{@uri}" end - + def svn_url? @uri =~ /svn(?:\+ssh)?:\/\/*/ end - + def git_url? @uri =~ /^git:\/\// || @uri =~ /\.git$/ end - + def installed? File.directory?("#{rails_env.root}/vendor/plugins/#{name}") \ or rails_env.externals.detect{ |name, repo| self.uri == repo } end - + def install(method=nil, options = {}) method ||= rails_env.best_install_method? if :http == method @@ -173,7 +171,7 @@ class Plugin if rails_env.use_externals? # clean up svn:externals externals = rails_env.externals - externals.reject!{|n,u| name == n or name == u} + externals.reject!{|n, u| name == n or name == u} rails_env.externals = externals end end @@ -192,7 +190,7 @@ class Plugin FileUtils.rm_rf tmp if svn_url? end - private + private def run_install_hook install_hook_file = "#{rails_env.root}/vendor/plugins/#{name}/install.rb" @@ -207,11 +205,11 @@ class Plugin def install_using_export(options = {}) svn_command :export, options end - + def install_using_checkout(options = {}) svn_command :checkout, options end - + def install_using_externals(options = {}) externals = rails_env.externals externals.push([@name, uri]) @@ -229,7 +227,7 @@ class Plugin fetcher.fetch end end - + def install_using_git(options = {}) root = rails_env.root mkdir_p(install_path = "#{root}/vendor/plugins/#{name}") @@ -268,7 +266,7 @@ class Plugin end @name.gsub!(/\.git$/, '') if @name =~ /\.git$/ end - + def rails_env @rails_env || RailsEnvironment.default end @@ -277,45 +275,44 @@ end # load default environment and parse arguments require 'optparse' module Commands - class Plugin attr_reader :environment, :script_name, :sources def initialize @environment = RailsEnvironment.default @rails_root = RailsEnvironment.default.root - @script_name = File.basename($0) + @script_name = File.basename($0) @sources = [] end - + def environment=(value) @environment = value RailsEnvironment.default = value end - + def options OptionParser.new do |o| o.set_summary_indent(' ') o.banner = "Usage: plugin [OPTIONS] command" o.define_head "Rails plugin manager." - - o.separator "" + + o.separator "" o.separator "GENERAL OPTIONS" - + o.on("-r", "--root=DIR", String, "Set an explicit rails app directory.", "Default: #{@rails_root}") { |rails_root| @rails_root = rails_root; self.environment = RailsEnvironment.new(@rails_root) } o.on("-s", "--source=URL1,URL2", Array, "Use the specified plugin repositories instead of the defaults.") { |sources| @sources = sources} - + o.on("-v", "--verbose", "Turn on verbose output.") { |verbose| $verbose = verbose } o.on("-h", "--help", "Show this help message.") { puts o; exit } - + o.separator "" o.separator "COMMANDS" - + o.separator " install Install plugin(s) from known repositories or URLs." o.separator " remove Uninstall plugins." - + o.separator "" o.separator "EXAMPLES" o.separator " Install a plugin:" @@ -328,41 +325,41 @@ module Commands o.separator " #{@script_name} plugin install -x continuous_builder\n" end end - + def parse!(args=ARGV) general, sub = split_args(args) options.parse!(general) - + command = general.shift if command =~ /^(install|remove)$/ command = Commands.const_get(command.capitalize).new(self) command.parse!(sub) else - puts "Unknown command: #{command}" + puts "Unknown command: #{command}" unless command.blank? puts options exit 1 end end - + def split_args(args) left = [] left << args.shift while args[0] and args[0] =~ /^-/ left << args.shift if args[0] - return [left, args] + [left, args] end - + def self.parse!(args=ARGV) Plugin.new.parse!(args) end end - + class Install def initialize(base_command) @base_command = base_command @method = :http @options = { :quiet => false, :revision => nil, :force => false } end - + def options OptionParser.new do |o| o.set_summary_indent(' ') @@ -370,8 +367,8 @@ module Commands o.define_head "Install one or more plugins." o.separator "" o.separator "Options:" - o.on( "-x", "--externals", - "Use svn:externals to grab the plugin.", + o.on( "-x", "--externals", + "Use svn:externals to grab the plugin.", "Enables plugin updates and plugin versioning.") { |v| @method = :externals } o.on( "-o", "--checkout", "Use svn checkout to grab the plugin.", @@ -392,7 +389,7 @@ module Commands o.separator "a plugin repository." end end - + def determine_install_method best = @base_command.environment.best_install_method @method = :http if best == :http and @method == :export @@ -410,9 +407,13 @@ module Commands end @method end - + def parse!(args) options.parse!(args) + if args.blank? + puts options + exit 1 + end environment = @base_command.environment install_method = determine_install_method puts "Plugins will be installed using #{install_method}" if $verbose @@ -430,7 +431,7 @@ module Commands def initialize(base_command) @base_command = base_command end - + def options OptionParser.new do |o| o.set_summary_indent(' ') @@ -438,9 +439,13 @@ module Commands o.define_head "Remove plugins." end end - + def parse!(args) options.parse!(args) + if args.blank? + puts options + exit 1 + end root = @base_command.environment.root args.each do |name| ::Plugin.new(name).uninstall @@ -470,7 +475,7 @@ module Commands end end end - + class RecursiveHTTPFetcher attr_accessor :quiet def initialize(urls_to_fetch, level = 1, cwd = ".") @@ -511,7 +516,7 @@ class RecursiveHTTPFetcher end links end - + def download(link) puts "+ #{File.join(@cwd, File.basename(link))}" unless @quiet open(link) do |stream| @@ -520,13 +525,13 @@ class RecursiveHTTPFetcher end end end - + def fetch(links = @urls_to_fetch) links.each do |l| (l =~ /\/$/ || links == @urls_to_fetch) ? fetch_dir(l) : download(l) end end - + def fetch_dir(url) @level += 1 push_d(File.basename(url)) if @level > 0 diff --git a/railties/lib/rails/commands/runner.rb b/railties/lib/rails/commands/runner.rb index b97ff086b6..c43a233bc3 100644 --- a/railties/lib/rails/commands/runner.rb +++ b/railties/lib/rails/commands/runner.rb @@ -37,7 +37,7 @@ ARGV.delete(code_or_file) ENV["RAILS_ENV"] = options[:environment] require APP_PATH -Rails::Application.require_environment! +Rails.application.require_environment! begin if code_or_file.nil? diff --git a/railties/lib/rails/commands/server.rb b/railties/lib/rails/commands/server.rb index 9d9dd48ea9..c3927b6613 100644 --- a/railties/lib/rails/commands/server.rb +++ b/railties/lib/rails/commands/server.rb @@ -21,6 +21,9 @@ module Rails opts.on("-e", "--environment=name", String, "Specifies the environment to run this server under (test/development/production).", "Default: development") { |v| options[:environment] = v } + opts.on("-P","--pid=pid",String, + "Specifies the PID file.", + "Default: tmp/pids/server.pid") { |v| options[:pid] = v } opts.separator "" diff --git a/railties/lib/rails/generators/actions.rb b/railties/lib/rails/generators/actions.rb index 199afbdc30..a27d38e23a 100644 --- a/railties/lib/rails/generators/actions.rb +++ b/railties/lib/rails/generators/actions.rb @@ -267,7 +267,7 @@ module Rails ActiveSupport::Deprecation.warn "freeze! is deprecated since your rails app now comes bundled with Rails by default, please check your Gemfile" end - # Make an entry in Rails routing file conifg/routes.rb + # Make an entry in Rails routing file config/routes.rb # # === Example # diff --git a/railties/lib/rails/generators/rails/app/templates/README b/railties/lib/rails/generators/rails/app/templates/README index e2764dee03..6966fe987e 100644 --- a/railties/lib/rails/generators/rails/app/templates/README +++ b/railties/lib/rails/generators/rails/app/templates/README @@ -35,7 +35,7 @@ link:files/vendor/rails/actionpack/README.html. <tt>cd myapp; rails server</tt> (run with --help for options) 3. Go to http://localhost:3000/ and you'll see: - "Welcome aboard: You're riding the Rails!" + "Welcome aboard: You're riding Ruby on Rails!" 4. Follow the guidelines to start developing your application. You can find the following resources handy: diff --git a/railties/lib/rails/generators/rails/app/templates/Rakefile b/railties/lib/rails/generators/rails/app/templates/Rakefile index 13f1f9fa41..d83cafc3be 100644 --- a/railties/lib/rails/generators/rails/app/templates/Rakefile +++ b/railties/lib/rails/generators/rails/app/templates/Rakefile @@ -4,4 +4,4 @@ require File.expand_path('../config/application', __FILE__) require 'rake' -Rails::Application.load_tasks +<%= app_const %>.load_tasks 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 031466cb86..67a38ea1d5 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/application.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/application.rb @@ -21,14 +21,14 @@ module <%= app_const_base %> # 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 + # Custom directories with classes and modules you want to be autoloadable. # config.autoload_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 + # :all can be used as a placeholder for all plugins not explicitly named. # config.plugins = [ :exception_notification, :ssl_requirement, :all ] - # Activate observers that should always be running + # Activate observers that should always be running. # 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. diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/secret_token.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/secret_token.rb.tt index 22aa576f5d..a3143f1346 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/secret_token.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/secret_token.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. -Rails.application.config.secret_token = '<%= app_secret %>' +<%= app_const %>.config.secret_token = '<%= app_secret %>' 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 a869a21e2c..0aed79bb70 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 @@ -1,8 +1,8 @@ # Be sure to restart your server when you modify this file. -Rails.application.config.session_store :cookie_store, :key => '_<%= app_name %>_session' +<%= app_const %>.config.session_store :cookie_store, :key => '_<%= app_name %>_session' # Use the database for sessions instead of the cookie-based default, # which shouldn't be used to store highly confidential information # (create the session table with "rake db:sessions:create") -# Rails.application.config.session_store :active_record_store +# <%= app_const %>.config.session_store :active_record_store diff --git a/railties/lib/rails/info_routes.rb b/railties/lib/rails/info_routes.rb index bd58034d8f..b5c4e4c1e0 100644 --- a/railties/lib/rails/info_routes.rb +++ b/railties/lib/rails/info_routes.rb @@ -1,3 +1,3 @@ -Rails.application.routes.draw do |map| +Rails.application.routes.draw do match '/rails/info/properties' => "rails/info#properties" end diff --git a/railties/lib/rails/tasks/middleware.rake b/railties/lib/rails/tasks/middleware.rake index e670989345..cd35ffb19a 100644 --- a/railties/lib/rails/tasks/middleware.rake +++ b/railties/lib/rails/tasks/middleware.rake @@ -3,5 +3,5 @@ task :middleware => :environment do Rails.configuration.middleware.each do |middleware| puts "use #{middleware.inspect}" end - puts "run #{Rails::Application.instance.class.name}.routes" + puts "run #{Rails.application.class.name}.routes" end diff --git a/railties/lib/rails/tasks/routes.rake b/railties/lib/rails/tasks/routes.rake index 41619bc1f8..a99232c4be 100644 --- a/railties/lib/rails/tasks/routes.rake +++ b/railties/lib/rails/tasks/routes.rake @@ -1,6 +1,6 @@ desc 'Print out all defined routes in match order, with names. Target specific controller with CONTROLLER=x.' task :routes => :environment do - Rails::Application.reload_routes! + Rails.application.reload_routes! all_routes = ENV['CONTROLLER'] ? Rails.application.routes.routes.select { |route| route.defaults[:controller] == ENV['CONTROLLER'] } : Rails.application.routes.routes routes = all_routes.collect do |route| # TODO: The :index method is deprecated in 1.9 in favor of :key diff --git a/railties/railties.gemspec b/railties/railties.gemspec index 99537d6d24..247b926af9 100644 --- a/railties/railties.gemspec +++ b/railties/railties.gemspec @@ -20,7 +20,7 @@ Gem::Specification.new do |s| s.has_rdoc = false s.add_dependency('rake', '>= 0.8.3') - s.add_dependency('thor', '~> 0.13.6') + s.add_dependency('thor', '~> 0.13.7') s.add_dependency('activesupport', version) s.add_dependency('actionpack', version) end diff --git a/railties/test/application/initializers/frameworks_test.rb b/railties/test/application/initializers/frameworks_test.rb index 7269a7c5a8..4ff10091b1 100644 --- a/railties/test/application/initializers/frameworks_test.rb +++ b/railties/test/application/initializers/frameworks_test.rb @@ -36,7 +36,7 @@ module ApplicationTests test "allows me to configure default url options for ActionMailer" do app_file "config/environments/development.rb", <<-RUBY - Rails::Application.configure do + AppTemplate::Application.configure do config.action_mailer.default_url_options = { :host => "test.rails" } end RUBY @@ -45,6 +45,25 @@ module ApplicationTests assert_equal "test.rails", ActionMailer::Base.default_url_options[:host] end + test "does not include url helpers as action methods" do + app_file "config/routes.rb", <<-RUBY + AppTemplate::Application.routes.draw do + get "/foo", :to => lambda { |env| [200, {}, []] }, :as => :foo + end + RUBY + + app_file "app/mailers/foo.rb", <<-RUBY + class Foo < ActionMailer::Base + def notify + end + end + RUBY + + require "#{app_path}/config/environment" + assert Foo.method_defined?(:foo_path) + assert_equal ["notify"], Foo.action_methods + end + # AS test "if there's no config.active_support.bare, all of ActiveSupport is required" do use_frameworks [] diff --git a/railties/test/application/initializers/i18n_test.rb b/railties/test/application/initializers/i18n_test.rb index a1fcba3310..4baa8a8170 100644 --- a/railties/test/application/initializers/i18n_test.rb +++ b/railties/test/application/initializers/i18n_test.rb @@ -16,7 +16,7 @@ module ApplicationTests end def app - @app ||= Rails::Application + @app ||= Rails.application end def assert_fallbacks(fallbacks) diff --git a/railties/test/application/rake_test.rb b/railties/test/application/rake_test.rb index 40fb446b16..d6e100ffe3 100644 --- a/railties/test/application/rake_test.rb +++ b/railties/test/application/rake_test.rb @@ -24,11 +24,11 @@ module ApplicationTests app_file "config/environment.rb", <<-RUBY SuperMiddleware = Struct.new(:app) - Rails::Application.configure do + AppTemplate::Application.configure do config.middleware.use SuperMiddleware end - Rails::Application.initialize! + AppTemplate::Application.initialize! RUBY assert_match "SuperMiddleware", Dir.chdir(app_path){ `rake middleware` } diff --git a/railties/test/application/runner_test.rb b/railties/test/application/runner_test.rb index d37b7649e2..07a3d94120 100644 --- a/railties/test/application/runner_test.rb +++ b/railties/test/application/runner_test.rb @@ -19,7 +19,7 @@ module ApplicationTests end def test_should_run_ruby_statement - assert_match "42", Dir.chdir(app_path) { `bundle exec rails runner "puts User.count"` } + assert_match "42", Dir.chdir(app_path) { `bundle exec rails runner "puts User.count"` } end def test_should_run_file diff --git a/railties/test/rails_info_controller_test.rb b/railties/test/rails_info_controller_test.rb index aa76979c27..687c2d1568 100644 --- a/railties/test/rails_info_controller_test.rb +++ b/railties/test/rails_info_controller_test.rb @@ -11,7 +11,7 @@ class InfoControllerTest < ActionController::TestCase tests Rails::InfoController def setup - Rails.application.routes.draw do |map| + Rails.application.routes.draw do match '/rails/info/properties' => "rails/info#properties" end @controller.stubs(:consider_all_requests_local? => false, :local_request? => true) |