From 57d750edf7c71e001ac314fa188aa1fc6292f8ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 30 Jun 2010 19:38:20 +0200 Subject: Make relation a private method. --- activerecord/lib/active_record/base.rb | 11 ++++++----- activerecord/lib/active_record/named_scope.rb | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index c8795e4496..c868ff3ae8 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -896,11 +896,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 +936,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. 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 -- cgit v1.2.3 From f3fedd7f84c25d1d99a70af1e21e20abb48f100f Mon Sep 17 00:00:00 2001 From: James Le Cuirot Date: Wed, 30 Jun 2010 23:22:13 +0100 Subject: Don't remove scheduled destroys when loading an association. [#4642 state:resolved] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: José Valim --- .../lib/active_record/associations/association_collection.rb | 7 ++++++- activerecord/test/cases/nested_attributes_test.rb | 6 ++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index ddf4ce4058..a4e08c7d41 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -393,7 +393,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/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index 3c797076e0..62237f955b 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -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') -- cgit v1.2.3 From e596a8e14cfb1f2ca19cd20ae7e40047d4819b3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 1 Jul 2010 10:26:45 +0200 Subject: Add the possibility to have several behaviors in AS::Deprecation. --- activesupport/lib/active_support/deprecation/behaviors.rb | 3 ++- activesupport/lib/active_support/deprecation/reporting.rb | 5 +++-- activesupport/lib/active_support/i18n_railtie.rb | 1 + activesupport/test/deprecation_test.rb | 13 +++++++++++++ 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/activesupport/lib/active_support/deprecation/behaviors.rb b/activesupport/lib/active_support/deprecation/behaviors.rb index feb1508586..f0842f201d 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 @@ -11,7 +12,7 @@ module ActiveSupport 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/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/test/deprecation_test.rb b/activesupport/test/deprecation_test.rb index cf27357b32..44f1aebb37 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 } -- cgit v1.2.3 From 9024545a6b019e3a2596a4194e84e77963e31b05 Mon Sep 17 00:00:00 2001 From: Cyril Mougel Date: Thu, 1 Jul 2010 11:59:10 +0200 Subject: fix failure if behavior is not define and try use the default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: José Valim --- activesupport/lib/active_support/deprecation/behaviors.rb | 2 +- activesupport/test/deprecation_test.rb | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/activesupport/lib/active_support/deprecation/behaviors.rb b/activesupport/lib/active_support/deprecation/behaviors.rb index f0842f201d..f54f65dcf0 100644 --- a/activesupport/lib/active_support/deprecation/behaviors.rb +++ b/activesupport/lib/active_support/deprecation/behaviors.rb @@ -8,7 +8,7 @@ module ActiveSupport attr_accessor :debug def behavior - @behavior ||= DEFAULT_BEHAVIORS[:stderr] + @behavior ||= [DEFAULT_BEHAVIORS[:stderr]] end def behavior=(behavior) diff --git a/activesupport/test/deprecation_test.rb b/activesupport/test/deprecation_test.rb index 44f1aebb37..cad0810241 100644 --- a/activesupport/test/deprecation_test.rb +++ b/activesupport/test/deprecation_test.rb @@ -136,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 } -- cgit v1.2.3 From d167cfd62b31333e11fcb9de4ea33dfabde7fa82 Mon Sep 17 00:00:00 2001 From: Jaime Iniesta Date: Thu, 1 Jul 2010 12:10:07 +0200 Subject: Fixes README for generated apps with Rails 3 (rails *new* myapp, and Welcome aboard text) --- railties/README | 4 ++-- railties/lib/rails/generators/rails/app/templates/README | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) 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: - rails myapp (where myapp is the application name) + rails new myapp (where myapp is the application name) 2. Change directory to myapp and start the web server: cd myapp; rails server (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/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. cd myapp; rails server (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: -- cgit v1.2.3 From 53b34e84762b7f2d6b641f99dadbb1eab42907ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 1 Jul 2010 17:07:48 +0200 Subject: Avoid calls to Rails::Application since this is not the official API. Your application should *always* reference your application const (as Blog::Application) and Rails.application should be used just internally. --- actionpack/lib/action_dispatch/routing.rb | 2 +- .../lib/active_record/railties/databases.rake | 2 +- .../lib/active_support/file_update_checker.rb | 11 ++-- railties/guides/source/initialization.textile | 72 ++++++++-------------- railties/lib/rails/application.rb | 12 ++-- railties/lib/rails/commands.rb | 10 +-- railties/lib/rails/commands/runner.rb | 2 +- .../rails/generators/rails/app/templates/Rakefile | 2 +- .../config/initializers/secret_token.rb.tt | 2 +- .../config/initializers/session_store.rb.tt | 4 +- railties/lib/rails/info_routes.rb | 2 +- railties/lib/rails/tasks/middleware.rake | 2 +- railties/lib/rails/tasks/routes.rake | 2 +- .../application/initializers/frameworks_test.rb | 2 +- .../test/application/initializers/i18n_test.rb | 2 +- railties/test/application/rake_test.rb | 4 +- railties/test/rails_info_controller_test.rb | 2 +- 17 files changed, 55 insertions(+), 80 deletions(-) diff --git a/actionpack/lib/action_dispatch/routing.rb b/actionpack/lib/action_dispatch/routing.rb index 401d98b663..89007fab74 100644 --- a/actionpack/lib/action_dispatch/routing.rb +++ b/actionpack/lib/action_dispatch/routing.rb @@ -167,7 +167,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 reload!. 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/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/railties/guides/source/initialization.textile b/railties/guides/source/initialization.textile index e458413b35..cedf823bdc 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' @@ -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. - # 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) h3. Bundled Rails (3.x) @@ -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: - - - require 'rubygems' - require 'bundler' - Bundler.setup - - -+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_: - - - 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 - - - -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' -+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: - File.join(Rails.root, "app/controllers") + Rails.root.join("app/controllers") -You may do: +Pathname can also be converted to string, so the following syntax is preferred: - Rails.root.join("app/controllers") + "#{Rails.root}/app/controllers" -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 -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_): def inherited(base) diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index 4a7ed2d028..f61103c764 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 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/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/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/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/test/application/initializers/frameworks_test.rb b/railties/test/application/initializers/frameworks_test.rb index 7269a7c5a8..b1deb7a645 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 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/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) -- cgit v1.2.3 From d7c1057652cfc971bb35ef09b0b1560fcd28ed70 Mon Sep 17 00:00:00 2001 From: Jeremy Kemper Date: Thu, 1 Jul 2010 02:25:35 -0700 Subject: Bump bundler dependency to 1.0.0.beta.2 or later --- .gitignore | 1 + rails.gemspec | 2 +- railties/guides/source/initialization.textile | 2 +- 3 files changed, 3 insertions(+), 2 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/rails.gemspec b/rails.gemspec index 236203c626..819513c4b5 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.2') end diff --git a/railties/guides/source/initialization.textile b/railties/guides/source/initialization.textile index cedf823bdc..8498f79f4e 100644 --- a/railties/guides/source/initialization.textile +++ b/railties/guides/source/initialization.textile @@ -141,7 +141,7 @@ 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 +* bundler-1.0.0.beta.2.gem * erubis-2.6.5.gem * i18n-0.4.1.gem * mail-2.2.4.gem -- cgit v1.2.3 From a6913bf7eb614d9e5008cf4230104babfec7c6c4 Mon Sep 17 00:00:00 2001 From: Rizwan Reza Date: Thu, 1 Jul 2010 23:07:24 +0430 Subject: Added documentation for usage of associative resources with form_for --- actionpack/lib/action_view/helpers/form_helper.rb | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) 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: -- cgit v1.2.3 From 4a0c514eb48b8e5d4ceffb4817661c182c2368a3 Mon Sep 17 00:00:00 2001 From: Santiago Pastorino Date: Wed, 30 Jun 2010 20:04:35 -0300 Subject: AS json refactor, move to_json implementation to core_ext and a cleanup a bit the code --- activesupport/lib/active_support/all.rb | 1 - .../lib/active_support/core_ext/object.rb | 1 + .../lib/active_support/core_ext/object/to_json.rb | 19 +++++++++++ activesupport/lib/active_support/json/encoding.rb | 37 ++++------------------ activesupport/lib/active_support/json/variable.rb | 6 ++-- activesupport/test/ordered_hash_test.rb | 1 + 6 files changed, 29 insertions(+), 36 deletions(-) create mode 100644 activesupport/lib/active_support/core_ext/object/to_json.rb 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/core_ext/object.rb b/activesupport/lib/active_support/core_ext/object.rb index 3a6100f776..8922016cd7 100644 --- a/activesupport/lib/active_support/core_ext/object.rb +++ b/activesupport/lib/active_support/core_ext/object.rb @@ -9,6 +9,7 @@ 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/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/json/encoding.rb b/activesupport/lib/active_support/json/encoding.rb index dd94315111..3f266d1e96 100644 --- a/activesupport/lib/active_support/json/encoding.rb +++ b/activesupport/lib/active_support/json/encoding.rb @@ -1,21 +1,16 @@ -# 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 - module ActiveSupport class << self delegate :use_standard_json_time_format, :use_standard_json_time_format=, @@ -128,20 +123,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 +133,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/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 -- cgit v1.2.3 From f8720a04d129668c7554c1a7270fba5418510b47 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Thu, 1 Jul 2010 15:04:33 -0700 Subject: porting session.clear fix to master branch. [#5030 state:resolved] Signed-off-by: Jeremy Kemper --- .../middleware/session/abstract_store.rb | 5 +++++ .../test/dispatch/session/cookie_store_test.rb | 22 ++++++++++++++++++++++ 2 files changed, 27 insertions(+) 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/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 -- cgit v1.2.3 From f7ba614c2db31933cbc12eda87518de3eca0228c Mon Sep 17 00:00:00 2001 From: Piotr Sarnacki Date: Fri, 2 Jul 2010 00:29:20 +0200 Subject: Unify routes naming by renaming router to routes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: José Valim --- actionmailer/lib/action_mailer/base.rb | 4 ++-- actionpack/lib/abstract_controller/rendering.rb | 4 ++-- actionpack/lib/action_controller/deprecated/base.rb | 2 +- actionpack/lib/action_controller/metal.rb | 2 +- actionpack/lib/action_controller/metal/url_for.rb | 8 ++++---- .../lib/action_dispatch/routing/deprecated_mapper.rb | 4 ++-- actionpack/lib/action_dispatch/routing/mapper.rb | 2 +- actionpack/lib/action_dispatch/routing/route_set.rb | 6 +++--- actionpack/lib/action_dispatch/routing/url_for.rb | 2 +- actionpack/lib/action_view/base.rb | 2 +- actionpack/lib/action_view/helpers/url_helper.rb | 2 +- actionpack/lib/action_view/test_case.rb | 10 +++++----- actionpack/test/abstract_unit.rb | 8 ++++---- actionpack/test/controller/url_for_test.rb | 2 +- actionpack/test/dispatch/url_generation_test.rb | 16 ++++++++-------- 15 files changed, 37 insertions(+), 37 deletions(-) diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index 11fa978b9c..8b86d83301 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." + "url_helpers of a routes 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 url_helpers of the routes, which will override this module extend DeprecatedUrlOptions ActiveSupport.run_load_hooks(:action_mailer, self) 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/deprecated/base.rb b/actionpack/lib/action_controller/deprecated/base.rb index 3975afcaf0..16b67b8ee7 100644 --- a/actionpack/lib/action_controller/deprecated/base.rb +++ b/actionpack/lib/action_controller/deprecated/base.rb @@ -96,7 +96,7 @@ module ActionController def resource_action_separator=(val) ActiveSupport::Deprecation.warn "ActionController::Base.resource_action_separator is deprecated and only " \ - "works with the deprecated router DSL." + "works with the deprecated routes DSL." @resource_action_separator = val end diff --git a/actionpack/lib/action_controller/metal.rb b/actionpack/lib/action_controller/metal.rb index 2281c500c5..8166d31719 100644 --- a/actionpack/lib/action_controller/metal.rb +++ b/actionpack/lib/action_controller/metal.rb @@ -47,7 +47,7 @@ module ActionController # # In AbstractController, dispatching is triggered directly by calling #process on a new controller. # ActionController::Metal provides an #action method that returns a valid Rack application for a - # given action. Other rack builders, such as Rack::Builder, Rack::URLMap, and the Rails router, + # given action. Other rack builders, such as Rack::Builder, Rack::URLMap, and the Rails routes, # can dispatch directly to the action returned by FooController.action(:index). class Metal < AbstractController::Base abstract! diff --git a/actionpack/lib/action_controller/metal/url_for.rb b/actionpack/lib/action_controller/metal/url_for.rb index c465035ca1..0c1e2fbe80 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 + def _routes raise "In order to use #url_for, you must include the helpers of a particular " \ - "router. For instance, `include Rails.application.routes.url_helpers" + "routes. 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_dispatch/routing/deprecated_mapper.rb b/actionpack/lib/action_dispatch/routing/deprecated_mapper.rb index bc3f62fb48..9684b86ed4 100644 --- a/actionpack/lib/action_dispatch/routing/deprecated_mapper.rb +++ b/actionpack/lib/action_dispatch/routing/deprecated_mapper.rb @@ -30,8 +30,8 @@ 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/" + ActiveSupport::Deprecation.warn "You are using the old routes DSL which will be removed in Rails 3.1. " << + "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..0d760ed23a 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -308,7 +308,7 @@ module ActionDispatch if name_prefix = options.delete(:name_prefix) options[:as] ||= name_prefix - ActiveSupport::Deprecation.warn ":name_prefix was deprecated in the new router syntax. Use :as instead.", caller + ActiveSupport::Deprecation.warn ":name_prefix was deprecated in the new routes syntax. Use :as instead.", caller end case args.first diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 5ecad6bc04..7248ed03f6 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -268,10 +268,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 @@ -474,7 +474,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 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] 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| "
#{html_tag}
".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/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_unit.rb b/actionpack/test/abstract_unit.rb index c8477fb83f..89c4cb46a1 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) } @@ -275,9 +275,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/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/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 -- cgit v1.2.3 From cb321546b7aef33fcf5466b61f79bd9198cd12b5 Mon Sep 17 00:00:00 2001 From: Santiago Pastorino Date: Thu, 1 Jul 2010 19:03:18 -0300 Subject: Time has it own implementation of xmlschema, now AMo doesn't depend on TZInfo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [#4979 state:committed] Signed-off-by: José Valim --- actionmailer/test/base_test.rb | 1 + activesupport/lib/active_support/json/encoding.rb | 4 ++-- activesupport/test/json/encoding_test.rb | 6 ++++-- 3 files changed, 7 insertions(+), 4 deletions(-) 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/activesupport/lib/active_support/json/encoding.rb b/activesupport/lib/active_support/json/encoding.rb index 3f266d1e96..dbce7e710a 100644 --- a/activesupport/lib/active_support/json/encoding.rb +++ b/activesupport/lib/active_support/json/encoding.rb @@ -9,7 +9,7 @@ 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/object/instance_variables' -require 'active_support/time' +require 'time' module ActiveSupport class << self @@ -212,7 +212,7 @@ class Time if ActiveSupport.use_standard_json_time_format xmlschema else - %(#{strftime("%Y/%m/%d %H:%M:%S")} #{formatted_offset(false)}) + strftime("%Y/%m/%d %H:%M:%S %z") end end end 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 = { -- cgit v1.2.3 From 2ef8a2b403adcee21fe1a5bfadc125b24779cd53 Mon Sep 17 00:00:00 2001 From: Santiago Pastorino Date: Thu, 1 Jul 2010 16:59:31 -0300 Subject: bump erubis version to 2.6.6 and thor version to 0.13.7 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: José Valim --- actionpack/actionpack.gemspec | 2 +- railties/guides/source/initialization.textile | 4 ++-- railties/railties.gemspec | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) 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/railties/guides/source/initialization.textile b/railties/guides/source/initialization.textile index 8498f79f4e..9a0e23385d 100644 --- a/railties/guides/source/initialization.textile +++ b/railties/guides/source/initialization.textile @@ -142,7 +142,7 @@ Here the only two gems we need are +rails+ and +sqlite3-ruby+, so it seems. This * arel-0.4.0.gem * builder-2.1.2.gem * bundler-1.0.0.beta.2.gem -* erubis-2.6.5.gem +* erubis-2.6.6.gem * i18n-0.4.1.gem * mail-2.2.4.gem * memcache-client-1.8.3.gem @@ -157,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 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 -- cgit v1.2.3 From d10aaefcfd4141144eaf971c0560da5631e3dff5 Mon Sep 17 00:00:00 2001 From: Neeraj Singh Date: Thu, 1 Jul 2010 23:06:58 -0400 Subject: clarifying the comments regarding base_class declaration --- activerecord/lib/active_record/base.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index c8795e4496..9860c3c725 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -869,6 +869,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 +879,7 @@ module ActiveRecord #:nodoc: # Set this to true if this is an abstract class (see abstract_class?). 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 -- cgit v1.2.3 From 0189fb76e3c38ef9a0d292135683b0d135c4a369 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 2 Jul 2010 08:13:52 +0200 Subject: reload_routes! was still referencing old Rails::Application. --- railties/lib/rails/application.rb | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index f61103c764..458177b954 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -121,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! -- cgit v1.2.3 From 9e6e64873243ee707ecad3d523ceb0dbae0617cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 2 Jul 2010 19:13:00 +0200 Subject: Fix routes with :controller segment when namespaced [#5034 state:resolved] --- actionpack/lib/action_dispatch/routing/mapper.rb | 2 +- .../lib/action_dispatch/routing/route_set.rb | 50 ++++++++++++++-------- actionpack/test/abstract_unit.rb | 17 +++++--- actionpack/test/dispatch/routing_test.rb | 24 ++++++++--- 4 files changed, 63 insertions(+), 30 deletions(-) diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 0d760ed23a..d71005a852 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -91,7 +91,7 @@ module ActionDispatch def app Constraints.new( - to.respond_to?(:call) ? to : Routing::RouteSet::Dispatcher.new(:defaults => defaults), + to.respond_to?(:call) ? to : Routing::RouteSet::Dispatcher.new(:defaults => defaults, :module => @scope[:module]), blocks, @set.request_class ) diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 7248ed03f6..afa312889f 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -14,6 +14,7 @@ module ActionDispatch def initialize(options={}) @defaults = options[:defaults] @glob_param = options.delete(:glob) + @module = options.delete(:module) @controllers = {} end @@ -26,7 +27,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 +35,44 @@ 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_param = @module && !default_controller ? + "#{@module}/#{params[:controller]}" : params[:controller] + 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 diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb index 89c4cb46a1..5be47f7c96 100644 --- a/actionpack/test/abstract_unit.rb +++ b/actionpack/test/abstract_unit.rb @@ -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 diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index 26bd641cd6..1bfc92aa3d 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -310,11 +310,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" @@ -348,6 +343,12 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end end + namespace :private do + root :to => redirect('/private/index') + match "index", :to => 'private#index' + match ":controller(/:action(/:id))" + end + match '/:locale/*file.:format', :to => 'files#show', :file => /path\/to\/existing\/file/ end end @@ -470,6 +471,19 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end end + def test_namespace_with_controller_segment + with_test_routes do + get '/private/foo' + assert_equal 'private/foo#index', @response.body + + get '/private/foo/bar' + assert_equal 'private/foo#bar', @response.body + + get '/private/foo/bar/1' + assert_equal 'private/foo#bar', @response.body + end + end + def test_session_singleton_resource with_test_routes do get '/session' -- cgit v1.2.3 From aeaa4687ea5802089a99e0fd96b10ed5c463cd21 Mon Sep 17 00:00:00 2001 From: Jeremy Kemper Date: Fri, 2 Jul 2010 11:32:05 -0700 Subject: Fix indent --- railties/test/application/runner_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 -- cgit v1.2.3 From 227e1caea51279fe4687f17e5704d637a21ea15f Mon Sep 17 00:00:00 2001 From: Geoff Buesing Date: Fri, 2 Jul 2010 12:58:32 -0500 Subject: Time#as_json: use Time#formatted_offset instead of strftime %z directive, which is non-standard and inaccurate on some platforms (e.g., Mac OS X). [#4979] Signed-off-by: Jeremy Kemper --- activesupport/lib/active_support/json/encoding.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/activesupport/lib/active_support/json/encoding.rb b/activesupport/lib/active_support/json/encoding.rb index dbce7e710a..8ec3af3f50 100644 --- a/activesupport/lib/active_support/json/encoding.rb +++ b/activesupport/lib/active_support/json/encoding.rb @@ -212,7 +212,7 @@ class Time if ActiveSupport.use_standard_json_time_format xmlschema else - strftime("%Y/%m/%d %H:%M:%S %z") + %(#{strftime("%Y/%m/%d %H:%M:%S")} #{formatted_offset(false)}) end end end -- cgit v1.2.3 From 5360014a5d77e8b6db71530dae49ac651392536d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?U=C4=A3is=20Ozols?= Date: Sat, 3 Jul 2010 03:16:04 +0300 Subject: Changed passed argument to :validate => false in validation skipping section. --- railties/guides/source/active_record_validations_callbacks.textile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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?+ -- cgit v1.2.3 From 8cc746331c32a6951e5c73c8a21fd32f00680471 Mon Sep 17 00:00:00 2001 From: Santiago Pastorino Date: Fri, 2 Jul 2010 23:46:19 -0300 Subject: Time#formatted_offset is defined in core_ext/time/conversions [#4979] --- activesupport/lib/active_support/json/encoding.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/activesupport/lib/active_support/json/encoding.rb b/activesupport/lib/active_support/json/encoding.rb index 8ec3af3f50..315897b423 100644 --- a/activesupport/lib/active_support/json/encoding.rb +++ b/activesupport/lib/active_support/json/encoding.rb @@ -10,6 +10,7 @@ require 'active_support/core_ext/hash/except' require 'active_support/core_ext/hash/slice' require 'active_support/core_ext/object/instance_variables' require 'time' +require 'active_support/core_ext/time/conversions' module ActiveSupport class << self -- cgit v1.2.3 From 201f373e7a183f33264ae31e7ca66768dddd3ebe Mon Sep 17 00:00:00 2001 From: Santiago Pastorino Date: Sat, 3 Jul 2010 01:39:54 -0300 Subject: Refactor move some date, time and date_time methods to */zones and fixed some requires --- .../active_support/core_ext/date/calculations.rb | 3 ++- .../lib/active_support/core_ext/date/conversions.rb | 18 ------------------ .../lib/active_support/core_ext/date/zones.rb | 21 +++++++++++++++++++++ .../core_ext/date_time/calculations.rb | 1 + .../core_ext/date_time/conversions.rb | 1 + .../lib/active_support/core_ext/date_time/zones.rb | 2 ++ .../active_support/core_ext/time/calculations.rb | 5 +++++ .../lib/active_support/core_ext/time/zones.rb | 5 ----- activesupport/lib/active_support/time.rb | 1 + 9 files changed, 33 insertions(+), 24 deletions(-) create mode 100644 activesupport/lib/active_support/core_ext/date/zones.rb 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..48d474dd9a 100644 --- a/activesupport/lib/active_support/core_ext/date/conversions.rb +++ b/activesupport/lib/active_support/core_ext/date/conversions.rb @@ -1,6 +1,5 @@ require 'date' require 'active_support/inflector' -require 'active_support/core_ext/time/calculations' class Date DATE_FORMATS = { @@ -15,9 +14,6 @@ class Date # Ruby 1.9 has Date#to_time which converts to localtime only. remove_method :to_time if instance_methods.include?(: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) - # Convert to a formatted string. See DATE_FORMATS for predefined formats. # # This method is aliased to to_s. @@ -82,16 +78,6 @@ class Date ::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. # @@ -102,8 +88,4 @@ class Date def to_datetime ::DateTime.civil(year, month, day, 0, 0, 0, 0) end if RUBY_VERSION < '1.9' - - def xmlschema - to_time_in_current_zone.xmlschema - end end 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..722f086951 --- /dev/null +++ b/activesupport/lib/active_support/core_ext/date/zones.rb @@ -0,0 +1,21 @@ +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 + + # Ruby 1.9 has Date#xmlschema which converts to a string without the time component. + remove_method :xmlschema if instance_methods.include?(:xmlschema) + + def xmlschema + to_time_in_current_zone.xmlschema + 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 Time.zone. # 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 Time.zone.now when config.time_zone is set, otherwise just returns Time.now. + 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 Time.zone.now when config.time_zone is set, otherwise just returns Time.now. - 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/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' -- cgit v1.2.3 From f6d7a4d251cae3e425cb62898c50d3f070c69d4b Mon Sep 17 00:00:00 2001 From: Santiago Pastorino Date: Sat, 3 Jul 2010 02:26:12 -0300 Subject: Removes the dependency that AMo has on tzinfo [#4979 state:committed] --- activesupport/lib/active_support/values/time_zone.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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) -- cgit v1.2.3 From 52e526a8e3ed3682ae2b774e4a328b9aa8bd6dde Mon Sep 17 00:00:00 2001 From: Andrew White Date: Sat, 3 Jul 2010 09:01:01 +0100 Subject: :singular is no longer a valid option for resources [#5037 state:resolved] --- railties/guides/source/routing.textile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/railties/guides/source/routing.textile b/railties/guides/source/routing.textile index 9ff06856c3..3562030aa9 100644 --- a/railties/guides/source/routing.textile +++ b/railties/guides/source/routing.textile @@ -714,14 +714,14 @@ 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+. -resources :teeth, :singular => "tooth" +ActiveSupport::Inflector.inflections do |inflect| + inflect.irregular 'tooth', 'teeth' +end -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 The +:name_prefix+ option overrides the automatically-generated prefix for the parent resource in nested route helpers. For example, -- cgit v1.2.3 From 54250a5bfe6992afaaca6357d3b414e6c49651ba Mon Sep 17 00:00:00 2001 From: Andrew White Date: Sat, 3 Jul 2010 08:14:17 +0100 Subject: Refactor recall parameter normalization [#5021 state:resolved] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: José Valim --- .../lib/action_dispatch/routing/route_set.rb | 22 +++++----------- actionpack/test/template/url_helper_test.rb | 29 ++++++++++++++++++++-- 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index afa312889f..177a6db04e 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -318,7 +318,6 @@ module ActionDispatch @extras = extras normalize_options! - normalize_recall! normalize_controller_action_id! use_relative_controller! controller.sub!(%r{^/}, '') if controller @@ -335,7 +334,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 @@ -359,15 +362,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 @@ -440,12 +434,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 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 -- cgit v1.2.3 From 547199ee4a279cf885ee6c4e3a605f64f70c9310 Mon Sep 17 00:00:00 2001 From: Andrew White Date: Sat, 3 Jul 2010 09:18:02 +0100 Subject: Updated routing guide to reflect the fact that :name_prefix is now :as --- railties/guides/source/routing.textile | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/railties/guides/source/routing.textile b/railties/guides/source/routing.textile index 3562030aa9..4e55b07ed7 100644 --- a/railties/guides/source/routing.textile +++ b/railties/guides/source/routing.textile @@ -647,11 +647,11 @@ end h4. Overriding the Named Helper Prefix -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 rename the named route helpers that Rails generates for a route. You can use this option to prevent collisions between routes using a path scope. scope "admin" do - resources :photos, :name_prefix => "admin" + resources :photos, :as => "admin_photos" end resources :photos @@ -662,14 +662,14 @@ This will provide route helpers such as +admin_photos_path+, +new_admin_photo_pa You could specify a name prefix to use for a group of routes in the scope: -scope "admin", :name_prefix => "admin" do +scope "admin", :as => "admin" do resources :photos, :accounts end resources :photos, :accounts -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 @@ -722,13 +722,13 @@ ActiveSupport::Inflector.inflections do |inflect| end -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, resources :magazines do - resources :ads, :name_prefix => 'periodical' + resources :ads, :as => 'periodical_ads' end -- cgit v1.2.3 From 075f8b72418f2d2ce9c2038bd57139955f6e46ec Mon Sep 17 00:00:00 2001 From: Santiago Pastorino Date: Sat, 3 Jul 2010 09:59:51 -0300 Subject: Add a missing require to allow the usage of Array#to_xml --- activemodel/lib/active_model/errors.rb | 1 + 1 file changed, 1 insertion(+) 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' -- cgit v1.2.3 From 75b32a69a185e2e802934bc4000f6c0efbc5c2e7 Mon Sep 17 00:00:00 2001 From: Wincent Colaiuta Date: Sat, 3 Jul 2010 12:12:50 +0200 Subject: Fixes for "router" and "routes" terminology MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Commit f7ba614c2db improved the internal consistency of the different means of accessing routes, but it introduced some problems at the level of code comments and user-visible strings. This commit applies fixes on three levels: Firstly, we remove or replace grammatically invalid constructs such as "a routes" or "a particular routes". Secondly, we make sure that we always use "the router DSL" or "the router syntax", because this has always been the official terminology. Finally, we make sure that we only use "routes" when referring to the application-specific set of routes that are defined in the "config/routes.rb" file, we use "router" when referring on a more abstract level to "the code in Rails used to handle routing", and we use "routing" when we need an adjective to apply to nouns such as "url_helpers. Again this is consistent with historical practice and other places in the documentation. Note that this is not a sweep over the entire codebase to ensure consistent usage of language; it is just a revision of the changes introduced in commit f7ba614c2db. Signed-off-by: Wincent Colaiuta Signed-off-by: José Valim --- actionmailer/lib/action_mailer/base.rb | 4 ++-- actionpack/lib/action_controller/deprecated/base.rb | 2 +- actionpack/lib/action_controller/metal.rb | 2 +- actionpack/lib/action_controller/metal/url_for.rb | 4 ++-- actionpack/lib/action_dispatch/routing/deprecated_mapper.rb | 2 +- actionpack/lib/action_dispatch/routing/mapper.rb | 2 +- actionpack/lib/action_dispatch/routing/route_set.rb | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index 8b86d83301..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 routes 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 routes, 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/actionpack/lib/action_controller/deprecated/base.rb b/actionpack/lib/action_controller/deprecated/base.rb index 16b67b8ee7..3975afcaf0 100644 --- a/actionpack/lib/action_controller/deprecated/base.rb +++ b/actionpack/lib/action_controller/deprecated/base.rb @@ -96,7 +96,7 @@ module ActionController def resource_action_separator=(val) ActiveSupport::Deprecation.warn "ActionController::Base.resource_action_separator is deprecated and only " \ - "works with the deprecated routes DSL." + "works with the deprecated router DSL." @resource_action_separator = val end diff --git a/actionpack/lib/action_controller/metal.rb b/actionpack/lib/action_controller/metal.rb index 8166d31719..2281c500c5 100644 --- a/actionpack/lib/action_controller/metal.rb +++ b/actionpack/lib/action_controller/metal.rb @@ -47,7 +47,7 @@ module ActionController # # In AbstractController, dispatching is triggered directly by calling #process on a new controller. # ActionController::Metal provides an #action method that returns a valid Rack application for a - # given action. Other rack builders, such as Rack::Builder, Rack::URLMap, and the Rails routes, + # given action. Other rack builders, such as Rack::Builder, Rack::URLMap, and the Rails router, # can dispatch directly to the action returned by FooController.action(:index). class Metal < AbstractController::Base abstract! diff --git a/actionpack/lib/action_controller/metal/url_for.rb b/actionpack/lib/action_controller/metal/url_for.rb index 0c1e2fbe80..a51fc5b8e4 100644 --- a/actionpack/lib/action_controller/metal/url_for.rb +++ b/actionpack/lib/action_controller/metal/url_for.rb @@ -13,8 +13,8 @@ module ActionController end def _routes - raise "In order to use #url_for, you must include the helpers of a particular " \ - "routes. For instance, `include Rails.application.routes.url_helpers" + raise "In order to use #url_for, you must include routing helpers explicitly. " \ + "For instance, `include Rails.application.routes.url_helpers" end module ClassMethods diff --git a/actionpack/lib/action_dispatch/routing/deprecated_mapper.rb b/actionpack/lib/action_dispatch/routing/deprecated_mapper.rb index 9684b86ed4..e122bdf232 100644 --- a/actionpack/lib/action_dispatch/routing/deprecated_mapper.rb +++ b/actionpack/lib/action_dispatch/routing/deprecated_mapper.rb @@ -30,7 +30,7 @@ module ActionDispatch class DeprecatedMapper #:nodoc: def initialize(set) #:nodoc: - ActiveSupport::Deprecation.warn "You are using the old routes DSL which will be removed in Rails 3.1. " << + ActiveSupport::Deprecation.warn "You are using the old router DSL which will be removed in Rails 3.1. " << "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 d71005a852..64e12f960d 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -308,7 +308,7 @@ module ActionDispatch if name_prefix = options.delete(:name_prefix) options[:as] ||= name_prefix - ActiveSupport::Deprecation.warn ":name_prefix was deprecated in the new routes syntax. Use :as instead.", caller + ActiveSupport::Deprecation.warn ":name_prefix was deprecated in the new router syntax. Use :as instead.", caller end case args.first diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 177a6db04e..24f9981f4b 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -480,7 +480,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 routes + # 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] -- cgit v1.2.3 From a7988fcf4149ab61c83ade5d380ae5d30789efc8 Mon Sep 17 00:00:00 2001 From: Jeff Dean Date: Sun, 4 Jul 2010 00:35:34 -0400 Subject: Added release notes for 3 changes to helpers --- railties/guides/source/3_0_release_notes.textile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 -- cgit v1.2.3 From 7d04a4be6b744fd1de9dc906a2faf83fffc9e0cc Mon Sep 17 00:00:00 2001 From: Andrew White Date: Sun, 4 Jul 2010 08:05:11 +0100 Subject: Reword routing guide so that we talk about prefixing as a use of :as rather than as a specific prefixing option (which :name_prefix used to be). --- railties/guides/source/routing.textile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/railties/guides/source/routing.textile b/railties/guides/source/routing.textile index 4e55b07ed7..00755071c5 100644 --- a/railties/guides/source/routing.textile +++ b/railties/guides/source/routing.textile @@ -645,9 +645,9 @@ scope :path_names => { :new => "make" } do end
-h4. Overriding the Named Helper Prefix +h4. Prefixing the Named Route Helpers -You can use the :as option to rename 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. scope "admin" do @@ -659,7 +659,7 @@ 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+: scope "admin", :as => "admin" do -- cgit v1.2.3 From c6843e23373c626ae49ad9fa253d4f7538d3434f Mon Sep 17 00:00:00 2001 From: Andrew White Date: Sun, 4 Jul 2010 06:52:52 +0100 Subject: Refactor resource options and scoping. Resource classes are now only responsible for controlling how they are named. All other options passed to resources are pushed out to the scope. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: José Valim --- actionpack/lib/action_dispatch/routing/mapper.rb | 163 +++++++++-------------- actionpack/test/dispatch/routing_test.rb | 83 +++++++++++- 2 files changed, 145 insertions(+), 101 deletions(-) diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 64e12f960d..f44c10f533 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -444,7 +444,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] class Resource #:nodoc: DEFAULT_ACTIONS = [:index, :create, :new, :show, :update, :destroy, :edit] @@ -463,16 +463,6 @@ module ActionDispatch self.class::DEFAULT_ACTIONS end - def actions - if only = options[:only] - Array(only).map(&:to_sym) - elsif except = options[:except] - default_actions - Array(except).map(&:to_sym) - else - default_actions - end - end - def name @as || @name end @@ -489,68 +479,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 +512,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 +522,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: @@ -610,17 +549,17 @@ module ActionDispatch collection_scope do post :create - end if parent_resource.actions.include?(:create) + end if resource_actions.include?(:create) new_scope do get :new - end if parent_resource.actions.include?(:new) + end if resource_actions.include?(:new) member_scope do - get :show if parent_resource.actions.include?(:show) - put :update if parent_resource.actions.include?(:update) - delete :destroy if parent_resource.actions.include?(:destroy) - get :edit if parent_resource.actions.include?(:edit) + get :show if resource_actions.include?(:show) + put :update if resource_actions.include?(:update) + delete :destroy if resource_actions.include?(:destroy) + get :edit if resource_actions.include?(:edit) end end @@ -638,19 +577,19 @@ module ActionDispatch yield if block_given? collection_scope do - get :index if parent_resource.actions.include?(:index) - post :create if parent_resource.actions.include?(:create) + get :index if resource_actions.include?(:index) + post :create if resource_actions.include?(:create) end new_scope do get :new - end if parent_resource.actions.include?(:new) + end if resource_actions.include?(:new) member_scope do - get :show if parent_resource.actions.include?(:show) - put :update if parent_resource.actions.include?(:update) - delete :destroy if parent_resource.actions.include?(:destroy) - get :edit if parent_resource.actions.include?(:edit) + get :show if resource_actions.include?(:show) + put :update if resource_actions.include?(:update) + delete :destroy if resource_actions.include?(:destroy) + get :edit if resource_actions.include?(:edit) end end @@ -693,18 +632,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 +662,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) @@ -794,23 +737,30 @@ module ActionDispatch @scope[:scope_level_resource] end + def resource_actions + if only = @scope[:options][:only] + Array(only).map(&:to_sym) + elsif except = @scope[:options][:except] + parent_resource.default_actions - Array(except).map(&:to_sym) + else + parent_resource.default_actions + end + end + def apply_common_behavior_for(method, resources, options, &block) if resources.length > 1 resources.each { |r| send(method, r, options, &block) } 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? - if resource_scope? nested do send(method, resources.pop, options, &block) @@ -853,7 +803,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 +811,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 +819,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 +827,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) diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index 1bfc92aa3d..d56612d6ec 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -298,8 +298,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 @@ -349,6 +349,22 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest match ":controller(/:action(/:id))" end + scope :only => :index do + resources :clubs do + resources :players, :only => [:show] + resource :chairman, :only => [:show] + end + end + + scope :except => [:new, :create, :edit, :update] do + resources :sectors do + resources :companies, :except => :destroy do + resources :divisions, :only => :index + end + resource :leader, :except => :destroy + end + end + match '/:locale/*file.:format', :to => 'files#show', :file => /path\/to\/existing\/file/ end end @@ -1254,7 +1270,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'} @@ -1640,6 +1656,69 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest assert_equal 'Not Found', @response.body assert_raises(ActionController::RoutingError){ movie_trailer_path(:movie_id => '00001') } end + + def test_only_option_should_be_overwritten + get '/clubs' + assert_equal 'clubs#index', @response.body + assert_equal '/clubs', clubs_path + + get '/clubs/1' + assert_equal 'Not Found', @response.body + assert_raise(NameError) { club_path(:id => '1') } + + get '/clubs/1/players' + assert_equal 'Not Found', @response.body + assert_raise(NameError) { club_players_path(:club_id => '1') } + + get '/clubs/1/players/2' + assert_equal 'players#show', @response.body + assert_equal '/clubs/1/players/2', club_player_path(:club_id => '1', :id => '2') + + get '/clubs/1/chairman/new' + assert_equal 'Not Found', @response.body + assert_raise(NameError) { new_club_chairman_path(:club_id => '1') } + + get '/clubs/1/chairman' + assert_equal 'chairmen#show', @response.body + assert_equal '/clubs/1/chairman', club_chairman_path(:club_id => '1') + end + + def test_except_option_should_be_overwritten + get '/sectors' + assert_equal 'sectors#index', @response.body + assert_equal '/sectors', sectors_path + + get '/sectors/new' + assert_equal 'Not Found', @response.body + assert_raise(NameError) { new_sector_path } + + delete '/sectors/1' + assert_equal 'sectors#destroy', @response.body + + get '/sectors/1/companies/new' + assert_equal 'companies#new', @response.body + assert_equal '/sectors/1/companies/new', new_sector_company_path + + delete '/sectors/1/companies/1' + assert_equal 'Not Found', @response.body + + get '/sectors/1/leader/new' + assert_equal 'leaders#new', @response.body + assert_equal '/sectors/1/leader/new', new_sector_leader_path + + delete '/sectors/1/leader' + assert_equal 'Not Found', @response.body + end + + def test_only_option_should_overwrite_except_option + get '/sectors/1/companies/2/divisions' + assert_equal 'divisions#index', @response.body + assert_equal '/sectors/1/companies/2/divisions', sector_company_divisions_path + + get '/sectors/1/companies/2/divisions/3' + assert_equal 'Not Found', @response.body + assert_raise(NameError) { sector_company_division_path(:sector_id => '1', :company_id => '2', :id => '3') } + end end private -- cgit v1.2.3 From aa31a255c831808b42c28978a36ffa42ff03e68e Mon Sep 17 00:00:00 2001 From: Andrew White Date: Sun, 4 Jul 2010 17:35:34 +0100 Subject: Fix syntax of routing tests so they actually run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: José Valim --- actionpack/test/dispatch/routing_test.rb | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index d56612d6ec..1eb5d32ec3 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -1656,19 +1656,21 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest assert_equal 'Not Found', @response.body assert_raises(ActionController::RoutingError){ movie_trailer_path(:movie_id => '00001') } end + end - def test_only_option_should_be_overwritten + def test_only_option_should_be_overwritten + with_test_routes do get '/clubs' assert_equal 'clubs#index', @response.body assert_equal '/clubs', clubs_path get '/clubs/1' assert_equal 'Not Found', @response.body - assert_raise(NameError) { club_path(:id => '1') } + assert_raise(NoMethodError) { club_path(:id => '1') } get '/clubs/1/players' assert_equal 'Not Found', @response.body - assert_raise(NameError) { club_players_path(:club_id => '1') } + assert_raise(NoMethodError) { club_players_path(:club_id => '1') } get '/clubs/1/players/2' assert_equal 'players#show', @response.body @@ -1676,48 +1678,52 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest get '/clubs/1/chairman/new' assert_equal 'Not Found', @response.body - assert_raise(NameError) { new_club_chairman_path(:club_id => '1') } + assert_raise(NoMethodError) { new_club_chairman_path(:club_id => '1') } get '/clubs/1/chairman' assert_equal 'chairmen#show', @response.body assert_equal '/clubs/1/chairman', club_chairman_path(:club_id => '1') end + end - def test_except_option_should_be_overwritten + def test_except_option_should_be_overwritten + with_test_routes do get '/sectors' assert_equal 'sectors#index', @response.body assert_equal '/sectors', sectors_path - get '/sectors/new' + get '/sectors/1/edit' assert_equal 'Not Found', @response.body - assert_raise(NameError) { new_sector_path } + assert_raise(NoMethodError) { edit_sector_path(:id => '1') } delete '/sectors/1' assert_equal 'sectors#destroy', @response.body get '/sectors/1/companies/new' assert_equal 'companies#new', @response.body - assert_equal '/sectors/1/companies/new', new_sector_company_path + assert_equal '/sectors/1/companies/new', new_sector_company_path(:sector_id => '1') delete '/sectors/1/companies/1' assert_equal 'Not Found', @response.body get '/sectors/1/leader/new' assert_equal 'leaders#new', @response.body - assert_equal '/sectors/1/leader/new', new_sector_leader_path + assert_equal '/sectors/1/leader/new', new_sector_leader_path(:sector_id => '1') delete '/sectors/1/leader' assert_equal 'Not Found', @response.body end + end - def test_only_option_should_overwrite_except_option + def test_only_option_should_overwrite_except_option + with_test_routes do get '/sectors/1/companies/2/divisions' assert_equal 'divisions#index', @response.body - assert_equal '/sectors/1/companies/2/divisions', sector_company_divisions_path + assert_equal '/sectors/1/companies/2/divisions', sector_company_divisions_path(:sector_id => '1', :company_id => '2') get '/sectors/1/companies/2/divisions/3' assert_equal 'Not Found', @response.body - assert_raise(NameError) { sector_company_division_path(:sector_id => '1', :company_id => '2', :id => '3') } + assert_raise(NoMethodError) { sector_company_division_path(:sector_id => '1', :company_id => '2', :id => '3') } end end -- cgit v1.2.3 From 3cb53758324214ec8bc65979f47dd43d0e85a248 Mon Sep 17 00:00:00 2001 From: Madjo DIAPENA Date: Sun, 4 Jul 2010 17:41:08 +0200 Subject: ARGV.empty? is useless. If ARGV is empty, ARGV.first != "new" will always be true MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: José Valim --- railties/lib/rails/commands/application.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 -- cgit v1.2.3 From 7f7480f6fc1e88ce19bee8ac7f6fb2243343495e Mon Sep 17 00:00:00 2001 From: Patrik Stenmark Date: Sat, 15 May 2010 14:10:40 +0200 Subject: Adds tests for content negotiation change introduced in dc5300adb6d46252c26e Signed-off-by: wycats --- actionpack/test/controller/mime_responds_test.rb | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) 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" -- cgit v1.2.3 From 8a09ea6d6dbbd6a1325b3be429d96d9c45c046df Mon Sep 17 00:00:00 2001 From: Santiago Pastorino Date: Sat, 3 Jul 2010 21:12:49 -0300 Subject: Avoids deprecation warning running tests --- actionmailer/test/old_base/url_test.rb | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) 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 -- cgit v1.2.3 From 5bf3294c8b3aeb3afd426e8c182456c675829c1e Mon Sep 17 00:00:00 2001 From: Santiago Pastorino Date: Sat, 3 Jul 2010 21:30:33 -0300 Subject: Move Date#xmlschema to conversions and add a missing require --- .../lib/active_support/core_ext/date/conversions.rb | 12 ++++++++++-- activesupport/lib/active_support/core_ext/date/zones.rb | 7 ------- activesupport/lib/active_support/json/encoding.rb | 2 ++ 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/activesupport/lib/active_support/core_ext/date/conversions.rb b/activesupport/lib/active_support/core_ext/date/conversions.rb index 48d474dd9a..092f936961 100644 --- a/activesupport/lib/active_support/core_ext/date/conversions.rb +++ b/activesupport/lib/active_support/core_ext/date/conversions.rb @@ -1,5 +1,6 @@ require 'date' require 'active_support/inflector' +require 'active_support/core_ext/date/zones' class Date DATE_FORMATS = { @@ -12,7 +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 method_defined?(:xmlschema) # Convert to a formatted string. See DATE_FORMATS for predefined formats. # @@ -77,7 +81,7 @@ class Date def to_time(form = :local) ::Time.send("#{form}_time", year, month, day) 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. # @@ -88,4 +92,8 @@ class Date def to_datetime ::DateTime.civil(year, month, day, 0, 0, 0, 0) end if RUBY_VERSION < '1.9' + + def xmlschema + to_time_in_current_zone.xmlschema + end end diff --git a/activesupport/lib/active_support/core_ext/date/zones.rb b/activesupport/lib/active_support/core_ext/date/zones.rb index 722f086951..3a83af6be2 100644 --- a/activesupport/lib/active_support/core_ext/date/zones.rb +++ b/activesupport/lib/active_support/core_ext/date/zones.rb @@ -11,11 +11,4 @@ class Date to_time end end - - # Ruby 1.9 has Date#xmlschema which converts to a string without the time component. - remove_method :xmlschema if instance_methods.include?(:xmlschema) - - def xmlschema - to_time_in_current_zone.xmlschema - end end diff --git a/activesupport/lib/active_support/json/encoding.rb b/activesupport/lib/active_support/json/encoding.rb index 315897b423..2f9588e0f4 100644 --- a/activesupport/lib/active_support/json/encoding.rb +++ b/activesupport/lib/active_support/json/encoding.rb @@ -11,6 +11,8 @@ require 'active_support/core_ext/hash/slice' require 'active_support/core_ext/object/instance_variables' 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 -- cgit v1.2.3 From a5dda97602f2188a13cbcab5c7e9a5ba84ba876b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 5 Jul 2010 12:50:08 +0200 Subject: Define a convention for descendants and subclasses. The former should be symmetric with ancestors and include all children. However, it should not include self since ancestors + descendants should not have duplicated. The latter is symmetric to superclass in the sense it only includes direct children. By adopting a convention, we expect to have less conflict with other frameworks, as Datamapper. For this moment, to ensure ActiveModel::Validations can be used with Datamapper, we should always call ActiveSupport::DescendantsTracker.descendants(self) internally instead of self.descendants avoiding conflicts. --- actionpack/lib/action_controller/base.rb | 6 -- actionpack/lib/action_controller/railtie.rb | 1 - .../action_dispatch/routing/deprecated_mapper.rb | 4 +- activerecord/lib/active_record/observer.rb | 6 +- activesupport/lib/active_support/callbacks.rb | 4 +- .../active_support/core_ext/class/subclasses.rb | 57 +++++++-------- .../lib/active_support/core_ext/object.rb | 1 - .../active_support/core_ext/object/extending.rb | 11 --- .../lib/active_support/descendants_tracker.rb | 24 ++++--- activesupport/test/core_ext/class_test.rb | 40 +++++------ .../test/core_ext/object_and_class_ext_test.rb | 49 ------------- activesupport/test/descendants_tracker_test.rb | 8 ++- .../source/active_support_core_extensions.textile | 81 ++++++++-------------- 13 files changed, 100 insertions(+), 192 deletions(-) delete mode 100644 activesupport/lib/active_support/core_ext/object/extending.rb 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/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/routing/deprecated_mapper.rb b/actionpack/lib/action_dispatch/routing/deprecated_mapper.rb index e122bdf232..05e8dfded6 100644 --- a/actionpack/lib/action_dispatch/routing/deprecated_mapper.rb +++ b/actionpack/lib/action_dispatch/routing/deprecated_mapper.rb @@ -19,8 +19,8 @@ module ActionDispatch def in_memory_controller_namespaces namespaces = Set.new - ActionController::Base.subclasses.each do |klass| - controller_name = klass.underscore + ActionController::Base.descendants.each do |klass| + controller_name = klass.name.underscore namespaces << controller_name.split('/')[0...-1].join('/') end namespaces.delete('') 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/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/object.rb b/activesupport/lib/active_support/core_ext/object.rb index 8922016cd7..27618b55c6 100644 --- a/activesupport/lib/active_support/core_ext/object.rb +++ b/activesupport/lib/active_support/core_ext/object.rb @@ -6,7 +6,6 @@ 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' 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/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/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/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/railties/guides/source/active_support_core_extensions.textile b/railties/guides/source/active_support_core_extensions.textile index 844b9428bd..de0c00ac68 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: - - -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] - - -The order in which these classes are returned is unspecified. The returned collection may have duplicates: - - -subclasses_of(Numeric, Integer) -# => [Bignum, Float, Fixnum, Integer, Date::Infinity, Rational, BigDecimal, Bignum, Fixnum] - - -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,47 @@ 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. Descendants & Subclasses -h5. +subclasses+ +h5. +descendants+ -The +subclasses+ method returns the names of all the anonymous or reachable descendants of its receiver as an array of strings: +The +descendants+ method returns all classes, including its children, that inherits from self. class C; end -C.subclasses # => [] - -Integer.subclasses # => ["Bignum", "Fixnum"] +C.descendants #=> [] -module M - class A; end - class B1 < A; end - class B2 < A; end -end +class B < C; end +C.descendants #=> [B] -module N - class C < M::B1; end -end +class A < B; end +C.descendants #=> [B, A] -M::A.subclasses # => ["N::C", "M::B2", "M::B1"] +class D < C; end +C.descendants #=> [B, A, D] -The order in which these class names are returned is unspecified. +h5. +subclasses+ + +The +subclasses+ method returns all direct classes that inherits from self. + + +class C; end +C.subclasses #=> [] + +class B < C; end +C.subclasses #=> [B] + +class A < B; end +C.subclasses #=> [B] + +class D < C; end +C.subclasses #=> [B, D] + -See also +Object#subclasses_of+ in "Extensions to All Objects FIX THIS LINK":FIXME. +The order in which these class are returned is unspecified. -WARNING: This method is redefined in some Rails core classes. +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+. -- cgit v1.2.3 From 6671d9cdc1cc40a6cdd365902f76d4aca78a410c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 5 Jul 2010 21:44:49 +0200 Subject: RouteSet should also handle anonymous classes. --- actionpack/lib/action_dispatch/routing/deprecated_mapper.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/actionpack/lib/action_dispatch/routing/deprecated_mapper.rb b/actionpack/lib/action_dispatch/routing/deprecated_mapper.rb index 05e8dfded6..b3146a1c60 100644 --- a/actionpack/lib/action_dispatch/routing/deprecated_mapper.rb +++ b/actionpack/lib/action_dispatch/routing/deprecated_mapper.rb @@ -20,8 +20,8 @@ module ActionDispatch def in_memory_controller_namespaces namespaces = Set.new ActionController::Base.descendants.each do |klass| - controller_name = klass.name.underscore - namespaces << controller_name.split('/')[0...-1].join('/') + next if klass.anonymous? + namespaces << klass.name.underscore.split('/')[0...-1].join('/') end namespaces.delete('') namespaces -- cgit v1.2.3 From 8079484b118e6dc8ffe3575b50c3857acd5b1a6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 6 Jul 2010 00:39:13 +0200 Subject: Recognize should also work with route is wrapped in a constraint. --- actionpack/lib/action_dispatch/routing/mapper.rb | 2 ++ actionpack/lib/action_dispatch/routing/route_set.rb | 2 ++ actionpack/test/dispatch/routing_test.rb | 5 ++++- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index f44c10f533..6529cf6f31 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 diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 24f9981f4b..01a068a9f2 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -512,6 +512,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/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index 1eb5d32ec3..463a62cd53 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" -- cgit v1.2.3 From db0530e4ba48f20cd42b781d053cdb23e5e7d210 Mon Sep 17 00:00:00 2001 From: Mark Hayes Date: Mon, 5 Jul 2010 19:28:58 -0700 Subject: Fixed typo in Rails::Generators::Base [#5051 state:resolved] --- railties/lib/rails/generators/actions.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 # -- cgit v1.2.3 From e5c95b1871da61984b4cd109e39b605c447323dd Mon Sep 17 00:00:00 2001 From: l4u Date: Tue, 6 Jul 2010 21:15:14 +0800 Subject: Fixed minor typo app/view to app/views --- railties/guides/source/getting_started.textile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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+.

<%= notice %>

-- cgit v1.2.3 From c1fc59c7ac1726f54d54b2896004997892e15ec9 Mon Sep 17 00:00:00 2001 From: Neeraj Singh Date: Tue, 6 Jul 2010 10:09:54 -0400 Subject: added more info about << operation in associations --- activerecord/lib/active_record/associations.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 4caa434fc0..49da8595b2 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -763,6 +763,8 @@ 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 :dependent => :destroy, @@ -1186,6 +1188,8 @@ module ActiveRecord # [collection<<(object, ...)] # Adds one or more objects to the collection by creating associations in the join table # (collection.push and collection.concat 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. -- cgit v1.2.3 From 92ff71bb14b1b589a3d5c04d120e4b9210b243b1 Mon Sep 17 00:00:00 2001 From: Xavier Noria Date: Tue, 6 Jul 2010 17:29:19 +0200 Subject: documents automatic management of join models in hmt associations, in particular the gotcha that deletion is direct --- activerecord/lib/active_record/associations.rb | 15 +++++++++++---- railties/guides/source/association_basics.textile | 10 ++++++++++ 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 4caa434fc0..eebbd17f43 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -768,15 +768,20 @@ module ActiveRecord # Objects will be in addition destroyed if they're associated with :dependent => :destroy, # and deleted if they're associated with :dependent => :delete_all. # [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 :through + # 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 collection=. See above. # [collection.clear] # Removes every object from the collection. This destroys the associated objects if they # are associated with :dependent => :destroy, deletes them directly from the # database if :dependent => :delete_all, otherwise sets their foreign keys to +NULL+. + # If the :through 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 +874,11 @@ module ActiveRecord # [:as] # Specifies a polymorphic interface (See belongs_to). # [:through] - # Specifies a Join Model through which to perform the query. Options for :class_name and :foreign_key + # Specifies a join model through which to perform the query. Options for :class_name and :foreign_key # are ignored, as the association uses the source reflection. You can only use a :through query through a belongs_to - # has_one or has_many association on the join model. + # has_one or has_many 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 has_many :through queries. Only use it if the name cannot be # inferred from the association. has_many :subscribers, :through => :subscriptions will look for either :subscribers or 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 + + +physician.patients = patients + + +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: -- cgit v1.2.3 From f4be0041c605daad2406ec41fa7dfa4388af5f31 Mon Sep 17 00:00:00 2001 From: Andrew White Date: Tue, 6 Jul 2010 19:06:02 +0100 Subject: Refactor handling of :only and :except options. The rules are: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Don't inherit when specified as an option on a resource 2. Don't push into scope when specified as an option on a resource 2. Resources pull in :only or :except options from scope 3. Either :only or :except in nested scope overwrites parent scope [#5048 state:resolved] Signed-off-by: José Valim --- actionpack/lib/action_dispatch/routing/mapper.rb | 72 ++++--- actionpack/test/dispatch/routing_test.rb | 237 ++++++++++++++++++----- 2 files changed, 236 insertions(+), 73 deletions(-) diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 6529cf6f31..aab272f565 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -433,12 +433,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 @@ -446,7 +450,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] - RESOURCE_OPTIONS = [:as, :controller, :path] + RESOURCE_OPTIONS = [:as, :controller, :path, :only, :except] class Resource #:nodoc: DEFAULT_ACTIONS = [:index, :create, :new, :show, :update, :destroy, :edit] @@ -465,6 +469,16 @@ module ActionDispatch self.class::DEFAULT_ACTIONS end + def actions + if only = @options[:only] + Array(only).map(&:to_sym) + elsif except = @options[:except] + default_actions - Array(except).map(&:to_sym) + else + default_actions + end + end + def name @as || @name end @@ -551,17 +565,17 @@ module ActionDispatch collection_scope do post :create - end if resource_actions.include?(:create) + end if parent_resource.actions.include?(:create) new_scope do get :new - end if resource_actions.include?(:new) + end if parent_resource.actions.include?(:new) member_scope do - get :show if resource_actions.include?(:show) - put :update if resource_actions.include?(:update) - delete :destroy if resource_actions.include?(:destroy) - get :edit if resource_actions.include?(:edit) + get :show if parent_resource.actions.include?(:show) + put :update if parent_resource.actions.include?(:update) + delete :destroy if parent_resource.actions.include?(:destroy) + get :edit if parent_resource.actions.include?(:edit) end end @@ -579,19 +593,19 @@ module ActionDispatch yield if block_given? collection_scope do - get :index if resource_actions.include?(:index) - post :create if resource_actions.include?(:create) + get :index if parent_resource.actions.include?(:index) + post :create if parent_resource.actions.include?(:create) end new_scope do get :new - end if resource_actions.include?(:new) + end if parent_resource.actions.include?(:new) member_scope do - get :show if resource_actions.include?(:show) - put :update if resource_actions.include?(:update) - delete :destroy if resource_actions.include?(:destroy) - get :edit if resource_actions.include?(:edit) + get :show if parent_resource.actions.include?(:show) + put :update if parent_resource.actions.include?(:update) + delete :destroy if parent_resource.actions.include?(:destroy) + get :edit if parent_resource.actions.include?(:edit) end end @@ -739,16 +753,6 @@ module ActionDispatch @scope[:scope_level_resource] end - def resource_actions - if only = @scope[:options][:only] - Array(only).map(&:to_sym) - elsif except = @scope[:options][:except] - parent_resource.default_actions - Array(except).map(&:to_sym) - else - parent_resource.default_actions - end - end - def apply_common_behavior_for(method, resources, options, &block) if resources.length > 1 resources.each { |r| send(method, r, options, &block) } @@ -763,6 +767,10 @@ module ActionDispatch return true end + unless action_options?(options) + options.merge!(scope_action_options) if scope_action_options? + end + if resource_scope? nested do send(method, resources.pop, options, &block) @@ -773,6 +781,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 @@ -899,7 +919,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/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index 463a62cd53..90d05d4a67 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -352,19 +352,55 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest match ":controller(/:action(/:id))" end - scope :only => :index do - resources :clubs do - resources :players, :only => [:show] - resource :chairman, :only => [:show] + scope :only => [:index, :show] do + namespace :only do + resources :clubs do + resources :players + resource :chairman + end end end - scope :except => [:new, :create, :edit, :update] do - resources :sectors do - resources :companies, :except => :destroy do - resources :divisions, :only => :index + 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 - resource :leader, :except => :destroy end end @@ -1661,72 +1697,179 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end end - def test_only_option_should_be_overwritten + def test_only_should_be_read_from_scope with_test_routes do - get '/clubs' - assert_equal 'clubs#index', @response.body - assert_equal '/clubs', clubs_path + get '/only/clubs' + assert_equal 'only/clubs#index', @response.body + assert_equal '/only/clubs', only_clubs_path - get '/clubs/1' + get '/only/clubs/1/edit' assert_equal 'Not Found', @response.body - assert_raise(NoMethodError) { club_path(:id => '1') } + 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 '/clubs/1/players' + get '/only/clubs/1/players/2/edit' assert_equal 'Not Found', @response.body - assert_raise(NoMethodError) { club_players_path(:club_id => '1') } + assert_raise(NoMethodError) { edit_only_club_player_path(:club_id => '1', :id => '2') } - get '/clubs/1/players/2' - assert_equal 'players#show', @response.body - assert_equal '/clubs/1/players/2', 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 '/clubs/1/chairman/new' + get '/only/clubs/1/chairman/edit' assert_equal 'Not Found', @response.body - assert_raise(NoMethodError) { new_club_chairman_path(:club_id => '1') } + assert_raise(NoMethodError) { edit_only_club_chairman_path(:club_id => '1') } + end + end - get '/clubs/1/chairman' - assert_equal 'chairmen#show', @response.body - assert_equal '/clubs/1/chairman', club_chairman_path(:club_id => '1') + 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_except_option_should_be_overwritten + def test_only_option_should_override_scope with_test_routes do - get '/sectors' - assert_equal 'sectors#index', @response.body - assert_equal '/sectors', sectors_path + get '/only/sectors' + assert_equal 'only/sectors#index', @response.body + assert_equal '/only/sectors', only_sectors_path - get '/sectors/1/edit' + get '/only/sectors/1' assert_equal 'Not Found', @response.body - assert_raise(NoMethodError) { edit_sector_path(:id => '1') } + 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') - delete '/sectors/1' - assert_equal 'sectors#destroy', @response.body + 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 - get '/sectors/1/companies/new' - assert_equal 'companies#new', @response.body - assert_equal '/sectors/1/companies/new', new_sector_company_path(:sector_id => '1') + 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 - delete '/sectors/1/companies/1' + 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 - get '/sectors/1/leader/new' - assert_equal 'leaders#new', @response.body - assert_equal '/sectors/1/leader/new', new_sector_leader_path(:sector_id => '1') + 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') - delete '/sectors/1/leader' + 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_option_should_overwrite_except_option + def test_only_scope_should_override_parent_except_scope with_test_routes do - get '/sectors/1/companies/2/divisions' - assert_equal 'divisions#index', @response.body - assert_equal '/sectors/1/companies/2/divisions', sector_company_divisions_path(:sector_id => '1', :company_id => '2') + 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 '/sectors/1/companies/2/divisions/3' + get '/except/sectors/1/companies/2/departments/3' assert_equal 'Not Found', @response.body - assert_raise(NoMethodError) { sector_company_division_path(:sector_id => '1', :company_id => '2', :id => '3') } + assert_raise(NoMethodError) { except_sector_company_department_path(:sector_id => '1', :company_id => '2', :id => '3') } end end -- cgit v1.2.3 From b802a0d4c72fbd605b92e9f403c9b8765a6e480c Mon Sep 17 00:00:00 2001 From: Andrew White Date: Tue, 6 Jul 2010 22:20:06 +0100 Subject: When a dynamic :controller segment is present in the path add a Regexp constraint that allow matching on multiple path segments. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Using a namespace block isn't compatible with dynamic routes so we raise an ArgumentError if we detect a :module present in the scope. [#5052 state:resolved] Signed-off-by: José Valim --- actionpack/lib/action_dispatch/routing/mapper.rb | 25 +++++++++++---- .../lib/action_dispatch/routing/route_set.rb | 6 ++-- actionpack/test/dispatch/routing_test.rb | 36 +++++++++++++++------- 3 files changed, 46 insertions(+), 21 deletions(-) diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index aab272f565..430f6fc5a3 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -65,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\)$/, '') @@ -93,7 +103,7 @@ module ActionDispatch def app Constraints.new( - to.respond_to?(:call) ? to : Routing::RouteSet::Dispatcher.new(:defaults => defaults, :module => @scope[:module]), + to.respond_to?(:call) ? to : Routing::RouteSet::Dispatcher.new(:defaults => defaults), blocks, @set.request_class ) @@ -135,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" @@ -185,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 diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 01a068a9f2..1b1a221c60 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -14,7 +14,6 @@ module ActionDispatch def initialize(options={}) @defaults = options[:defaults] @glob_param = options.delete(:glob) - @module = options.delete(:module) @controllers = {} end @@ -39,12 +38,11 @@ module ActionDispatch # 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 + # 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 = @module && !default_controller ? - "#{@module}/#{params[:controller]}" : params[:controller] + controller_param = params[:controller] controller_reference(controller_param) end rescue NameError => e diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index 90d05d4a67..2a014bf976 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -323,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 @@ -349,7 +349,6 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest namespace :private do root :to => redirect('/private/index') match "index", :to => 'private#index' - match ":controller(/:action(/:id))" end scope :only => [:index, :show] do @@ -527,15 +526,14 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end def test_namespace_with_controller_segment - with_test_routes do - get '/private/foo' - assert_equal 'private/foo#index', @response.body - - get '/private/foo/bar' - assert_equal 'private/foo#bar', @response.body - - get '/private/foo/bar/1' - assert_equal 'private/foo#bar', @response.body + 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 @@ -1354,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") -- cgit v1.2.3 From 0f96cea322294f84df34c43000443b249a880126 Mon Sep 17 00:00:00 2001 From: Andrew White Date: Wed, 7 Jul 2010 05:31:54 +0100 Subject: Add note about incompatibility of namespace and :controller --- railties/guides/source/routing.textile | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/railties/guides/source/routing.textile b/railties/guides/source/routing.textile index 00755071c5..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: + + +match ':controller(/:action(/:id))', :controller => /admin\/[^\/]+/ + + h4. Static Segments You can specify static segments when creating a route: -- cgit v1.2.3 From 8735d15e61797d0e6879efbde4f1ca18b4403eff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 7 Jul 2010 12:50:38 +0200 Subject: Add a test to ensure url helpers are not action methods in ActionMailer. --- .../test/application/initializers/frameworks_test.rb | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/railties/test/application/initializers/frameworks_test.rb b/railties/test/application/initializers/frameworks_test.rb index b1deb7a645..4ff10091b1 100644 --- a/railties/test/application/initializers/frameworks_test.rb +++ b/railties/test/application/initializers/frameworks_test.rb @@ -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 [] -- cgit v1.2.3 From 81f398b804a68c8090c0fbea20f50c0ae253b708 Mon Sep 17 00:00:00 2001 From: Jeremy Kemper Date: Wed, 7 Jul 2010 12:03:01 -0700 Subject: Fix setting helpers_path to a string or pathname --- actionpack/lib/action_controller/metal/helpers.rb | 5 +++-- actionpack/test/abstract/helper_test.rb | 2 +- actionpack/test/controller/helper_test.rb | 6 +++--- 3 files changed, 7 insertions(+), 6 deletions(-) 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/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/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 -- cgit v1.2.3 From c58e7a71b52a38885352768c74b7563395566bb6 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Wed, 7 Jul 2010 14:23:16 -0700 Subject: adding some behavioral tests for the sqlite adapter. [#5065 state:resolved] Signed-off-by: wycats --- .../cases/adapters/sqlite/sqlite_adapter_test.rb | 100 +++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 activerecord/test/cases/adapters/sqlite/sqlite_adapter_test.rb 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..69cfb00faf --- /dev/null +++ b/activerecord/test/cases/adapters/sqlite/sqlite_adapter_test.rb @@ -0,0 +1,100 @@ +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 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 -- cgit v1.2.3 From de51cbccf8c9d4e59a128ca8dca8c42d8d7c4dc9 Mon Sep 17 00:00:00 2001 From: Ben Somers Date: Wed, 7 Jul 2010 10:05:58 -0700 Subject: Fixed gruoped_by_title spelling [#5063 state:committed] Signed-off-by: Xavier Noria --- .../cases/associations/has_and_belongs_to_many_associations_test.rb | 4 ++-- activerecord/test/models/category.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) 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/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...' -- cgit v1.2.3 From ff44cc284441be894cc6f2bbc1798a21e881414e Mon Sep 17 00:00:00 2001 From: Akira Matsuda Date: Sun, 4 Jul 2010 17:20:50 +0900 Subject: whitespace Signed-off-by: Jeremy Kemper --- railties/lib/rails/commands/plugin.rb | 97 +++++++++++++++++------------------ 1 file changed, 47 insertions(+), 50 deletions(-) diff --git a/railties/lib/rails/commands/plugin.rb b/railties/lib/rails/commands/plugin.rb index 8bcd92a33b..e0930cdcee 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,11 +325,11 @@ 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) @@ -343,26 +340,26 @@ module Commands 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] 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,7 +407,7 @@ module Commands end @method end - + def parse!(args) options.parse!(args) environment = @base_command.environment @@ -430,7 +427,7 @@ module Commands def initialize(base_command) @base_command = base_command end - + def options OptionParser.new do |o| o.set_summary_indent(' ') @@ -438,7 +435,7 @@ module Commands o.define_head "Remove plugins." end end - + def parse!(args) options.parse!(args) root = @base_command.environment.root @@ -470,7 +467,7 @@ module Commands end end end - + class RecursiveHTTPFetcher attr_accessor :quiet def initialize(urls_to_fetch, level = 1, cwd = ".") @@ -511,7 +508,7 @@ class RecursiveHTTPFetcher end links end - + def download(link) puts "+ #{File.join(@cwd, File.basename(link))}" unless @quiet open(link) do |stream| @@ -520,13 +517,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 -- cgit v1.2.3 From bf5d15456757e63598575db42917d702af9da729 Mon Sep 17 00:00:00 2001 From: Akira Matsuda Date: Sun, 4 Jul 2010 17:37:57 +0900 Subject: Print proper "Usage:" messages for "rails plugin" command * suppress outputting "Unknown command:" when no command were specified * output the "Usage:" message when no plugin names were given [#5043 state:committed] Signed-off-by: Jeremy Kemper --- railties/lib/rails/commands/plugin.rb | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/railties/lib/rails/commands/plugin.rb b/railties/lib/rails/commands/plugin.rb index e0930cdcee..96b6f9c372 100644 --- a/railties/lib/rails/commands/plugin.rb +++ b/railties/lib/rails/commands/plugin.rb @@ -335,7 +335,7 @@ module Commands 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 @@ -345,7 +345,7 @@ module Commands 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) @@ -410,6 +410,10 @@ module Commands 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 @@ -438,6 +442,10 @@ module Commands 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 -- cgit v1.2.3 From dc364fdc595405aa3d5735e60d46ad3f9544a65b Mon Sep 17 00:00:00 2001 From: Rohit Arondekar Date: Wed, 7 Jul 2010 22:15:15 -0700 Subject: API Docs: Fixes to the Routing docs --- actionpack/lib/action_dispatch/routing.rb | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/actionpack/lib/action_dispatch/routing.rb b/actionpack/lib/action_dispatch/routing.rb index 89007fab74..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 root as a shorthand to name a route for the root path "". + # Use root 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}/ -- cgit v1.2.3 From f6fa6cf6117e691899c821b8999bd7dfb3f48b38 Mon Sep 17 00:00:00 2001 From: Neeraj Singh Date: Thu, 8 Jul 2010 10:09:20 -0400 Subject: clarifying how to create non standard primary key --- .../connection_adapters/abstract/schema_statements.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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..555a611e68 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -106,7 +106,13 @@ module ActiveRecord # Join tables for +has_and_belongs_to_many+ should set :id => false. # [:primary_key] # The name of the primary key, if one is to be added automatically. - # Defaults to +id+. + # Defaults to +id+. You must NOT pass :id => false otherwise :primary_key option + # will have no effect. + # + # Also note that this just sets the primary_key in the table. You still need to + # add :set_primary_key => '' in the model to tell model what column is the + # primary_key. Models do NOT auto-detect the primary_key from table defintion. + # # [:options] # Any extra options you want appended to the table definition. # [:temporary] -- cgit v1.2.3 From a9587935dec6b5de01d51553ecc6d4157a8ec173 Mon Sep 17 00:00:00 2001 From: Xavier Noria Date: Thu, 8 Jul 2010 16:53:37 +0200 Subject: copy-edits some docs --- .../abstract/schema_statements.rb | 11 +++---- .../source/active_support_core_extensions.textile | 36 ++++++++++++---------- 2 files changed, 25 insertions(+), 22 deletions(-) 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 555a611e68..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,15 +103,14 @@ module ActiveRecord # The +options+ hash can include the following keys: # [:id] # Whether to automatically add a primary key column. Defaults to true. - # Join tables for +has_and_belongs_to_many+ should set :id => false. + # Join tables for +has_and_belongs_to_many+ should set it to false. # [:primary_key] # The name of the primary key, if one is to be added automatically. - # Defaults to +id+. You must NOT pass :id => false otherwise :primary_key option - # will have no effect. + # Defaults to +id+. If :id is false this option is ignored. # - # Also note that this just sets the primary_key in the table. You still need to - # add :set_primary_key => '' in the model to tell model what column is the - # primary_key. Models do NOT auto-detect the primary_key from table defintion. + # 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. # # [:options] # Any extra options you want appended to the table definition. diff --git a/railties/guides/source/active_support_core_extensions.textile b/railties/guides/source/active_support_core_extensions.textile index de0c00ac68..58824d7aeb 100644 --- a/railties/guides/source/active_support_core_extensions.textile +++ b/railties/guides/source/active_support_core_extensions.textile @@ -1107,47 +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 & Subclasses +h4. Subclasses & Descendants -h5. +descendants+ +h5. +subclasses+ -The +descendants+ method returns all classes, including its children, that inherits from self. +The +subclasses+ method returns the subclasses of the receiver: class C; end -C.descendants #=> [] +C.subclasses # => [] class B < C; end -C.descendants #=> [B] +C.subclasses # => [B] class A < B; end -C.descendants #=> [B, A] +C.subclasses # => [B] class D < C; end -C.descendants #=> [B, A, D] +C.subclasses # => [B, D] -h5. +subclasses+ +The order in which these classes are returned is unspecified. -The +subclasses+ method returns all direct classes that inherits from self. +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+. + +h5. +descendants+ + +The +descendants+ method returns all classes that are < than its receiver: class C; end -C.subclasses #=> [] +C.descendants # => [] class B < C; end -C.subclasses #=> [B] +C.descendants # => [B] class A < B; end -C.subclasses #=> [B] +C.descendants # => [B, A] class D < C; end -C.subclasses #=> [B, D] +C.descendants # => [B, A, D] -The order in which these class are returned is unspecified. - -WARNING: This method is redefined in some Rails core classes but should be all compatible in Rails 3.1. +The order in which these classes are returned is unspecified. NOTE: Defined in +active_support/core_ext/class/subclasses.rb+. -- cgit v1.2.3 From 723a0bbe3a8737a099cd995a397b919b1957413d Mon Sep 17 00:00:00 2001 From: Neeraj Singh Date: Thu, 8 Jul 2010 09:29:38 -0400 Subject: This test never runs and it has never run. Since the day this file was created this test has name not beginning with test_. Also this test is trying to use has_many on another has_many which is not supported. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: José Valim --- .../test/cases/associations/has_one_through_associations_test.rb | 4 ---- 1 file changed, 4 deletions(-) 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"]) -- cgit v1.2.3 From 606088df3f10dd8daec8ccc97d8279c119a503b5 Mon Sep 17 00:00:00 2001 From: Eric Chapweske Date: Fri, 29 Jan 2010 17:02:12 -0800 Subject: Mass assignment security refactoring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: José Valim --- activerecord/lib/active_record.rb | 1 + activerecord/lib/active_record/base.rb | 144 ++----------------- .../lib/active_record/mass_assignment_security.rb | 160 +++++++++++++++++++++ .../mass_assignment_security/permission_set.rb | 44 ++++++ .../mass_assignment_security/sanitizer.rb | 27 ++++ activerecord/test/cases/base_test.rb | 26 ++-- .../mass_assignment_security/black_list_test.rb | 28 ++++ .../permission_set_test.rb | 30 ++++ .../mass_assignment_security/sanitizer_test.rb | 36 +++++ .../mass_assignment_security/white_list_test.rb | 28 ++++ 10 files changed, 378 insertions(+), 146 deletions(-) create mode 100644 activerecord/lib/active_record/mass_assignment_security.rb create mode 100644 activerecord/lib/active_record/mass_assignment_security/permission_set.rb create mode 100644 activerecord/lib/active_record/mass_assignment_security/sanitizer.rb create mode 100644 activerecord/test/cases/mass_assignment_security/black_list_test.rb create mode 100644 activerecord/test/cases/mass_assignment_security/permission_set_test.rb create mode 100644 activerecord/test/cases/mass_assignment_security/sanitizer_test.rb create mode 100644 activerecord/test/cases/mass_assignment_security/white_list_test.rb diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index e2f2508ae8..9ab05a0548 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -64,6 +64,7 @@ module ActiveRecord autoload :CounterCache autoload :DynamicFinderMatch autoload :DynamicScopeMatch + autoload :MassAssignmentSecurity autoload :Migration autoload :Migrator, 'active_record/migration' autoload :NamedScope diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 3f1015dd4b..affb1fee3c 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 new(attributes), - # update_attributes(attributes), or - # attributes=(attributes). - # - # 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 Hash#except - # or Hash#slice 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 || [])) - end - - # Returns an array of all the attributes that have been protected from mass-assignment. - def protected_attributes # :nodoc: - read_inheritable_attribute(:attr_protected) - end - - # Specifies a white list of model attributes that can be set via - # mass-assignment, such as new(attributes), - # update_attributes(attributes), or - # attributes=(attributes) - # - # 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 Hash#except - # or Hash#slice 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 || [])) + # 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 made accessible to mass-assignment. - def accessible_attributes # :nodoc: - read_inheritable_attribute(:attr_accessible) + # Returns an array of all the attributes that have been specified as readonly. + def readonly_attributes + read_inheritable_attribute(:attr_readonly) || [] 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 @@ -1716,27 +1620,6 @@ 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? @@ -1746,16 +1629,10 @@ MSG 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 @@ -1920,6 +1797,7 @@ MSG include AttributeMethods::PrimaryKey include AttributeMethods::TimeZoneConversion include AttributeMethods::Dirty + extend MassAssignmentSecurity include Callbacks, ActiveModel::Observing, Timestamp include Associations, AssociationPreload, NamedScope diff --git a/activerecord/lib/active_record/mass_assignment_security.rb b/activerecord/lib/active_record/mass_assignment_security.rb new file mode 100644 index 0000000000..07beb6405e --- /dev/null +++ b/activerecord/lib/active_record/mass_assignment_security.rb @@ -0,0 +1,160 @@ +require 'active_record/mass_assignment_security/permission_set' + +module ActiveRecord + module MassAssignmentSecurity + # 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 + # extend ActiveRecord::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 + # remove_attributes_protected_from_mass_assignment(params[:account]) + # end + # + # def mass_assignment_authorizer + # admin ? admin_accessible_attributes : super + # end + # + # end + # + def self.extended(base) + base.send(:include, InstanceMethods) + end + + module InstanceMethods + + protected + + def remove_attributes_protected_from_mass_assignment(attributes) + mass_assignment_authorizer.sanitize(attributes) + end + + def mass_assignment_authorizer + self.class.mass_assignment_authorizer + end + + end + + # Attributes named in this macro are protected from mass-assignment, + # such as new(attributes), + # update_attributes(attributes), or + # attributes=(attributes). + # + # 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+. + # + # Note that using Hash#except or Hash#slice in place of +attr_protected+ + # to sanitize attributes won't provide sufficient protection. + def attr_protected(*keys) + use_authorizer(:protected_attributes) + protected_attributes.merge(keys) + end + + # Specifies a white list of model attributes that can be set via + # mass-assignment, such as new(attributes), + # update_attributes(attributes), or + # attributes=(attributes) + # + # 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" + # + # Note that using Hash#except or Hash#slice in place of +attr_accessible+ + # to sanitize attributes won't provide sufficient protection. + def attr_accessible(*keys) + use_authorizer(:accessible_attributes) + accessible_attributes.merge(keys) + end + + # Returns an array of all the attributes that have been protected from mass-assignment. + def protected_attributes + read_inheritable_attribute(:protected_attributes) || begin + authorizer = BlackList.new + authorizer += attributes_protected_by_default + authorizer.logger = logger + write_inheritable_attribute(:protected_attributes, authorizer) + end + end + + # Returns an array of all the attributes that have been made accessible to mass-assignment. + def accessible_attributes + read_inheritable_attribute(:accessible_attributes) || begin + authorizer = WhiteList.new + authorizer.logger = logger + write_inheritable_attribute(:accessible_attributes, authorizer) + end + end + + def mass_assignment_authorizer + protected_attributes + end + + private + + # Sets the active authorizer, (attr_protected or attr_accessible). Subsequent calls + # will raise an exception when using a different authorizer_id. + def use_authorizer(authorizer_id) # :nodoc: + if active_authorizer_id = read_inheritable_attribute(:active_authorizer_id) + unless authorizer_id == active_authorizer_id + raise("Already using #{active_authorizer_id}, cannot use #{authorizer_id}") + end + else + write_inheritable_attribute(:active_authorizer_id, authorizer_id) + end + end + + end +end diff --git a/activerecord/lib/active_record/mass_assignment_security/permission_set.rb b/activerecord/lib/active_record/mass_assignment_security/permission_set.rb new file mode 100644 index 0000000000..1d34dce02e --- /dev/null +++ b/activerecord/lib/active_record/mass_assignment_security/permission_set.rb @@ -0,0 +1,44 @@ +require 'active_record/mass_assignment_security/sanitizer' + +module ActiveRecord + module MassAssignmentSecurity + class PermissionSet < Set + + attr_accessor :logger + + def merge(values) + super(values.map(&:to_s)) + end + + def include?(key) + super(remove_multiparameter_id(key)) + end + + protected + + def remove_multiparameter_id(key) + key.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/activerecord/lib/active_record/mass_assignment_security/sanitizer.rb b/activerecord/lib/active_record/mass_assignment_security/sanitizer.rb new file mode 100644 index 0000000000..4a099a147c --- /dev/null +++ b/activerecord/lib/active_record/mass_assignment_security/sanitizer.rb @@ -0,0 +1,27 @@ +module ActiveRecord + 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) if debug? + sanitized_attributes + end + + protected + + def debug_protected_attribute_removal(attributes, sanitized_attributes) + removed_keys = attributes.keys - sanitized_attributes.keys + if removed_keys.any? + logger.debug "WARNING: Can't mass-assign protected attributes: #{removed_keys.join(', ')}" + end + end + + def debug? + logger.present? + end + + end + end +end diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index ba7db838ca..dff39cf54f 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -71,9 +71,8 @@ class Task < ActiveRecord::Base attr_protected :starting end -class TopicWithProtectedContentAndAccessibleAuthorName < ActiveRecord::Base +class TopicWithProtectedContent < ActiveRecord::Base self.table_name = 'topics' - attr_accessible :author_name attr_protected :content end @@ -956,9 +955,9 @@ class BasicsTest < ActiveRecord::TestCase 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" } } + assert_raise(RuntimeError) do + TopicWithProtectedContent.attr_accessible :author_name + end end def test_mass_assignment_protection @@ -1021,19 +1020,20 @@ class BasicsTest < ActiveRecord::TestCase end def test_mass_assignment_protection_inheritance - assert_nil LoosePerson.accessible_attributes - assert_equal Set.new([ 'credit_rating', 'administrator' ]), LoosePerson.protected_attributes + assert LoosePerson.accessible_attributes.blank? + assert_equal Set.new([ 'credit_rating', 'administrator', *LoosePerson.attributes_protected_by_default ]), LoosePerson.protected_attributes - assert_nil LooseDescendant.accessible_attributes - assert_equal Set.new([ 'credit_rating', 'administrator', 'phone_number' ]), LooseDescendant.protected_attributes + assert LooseDescendant.accessible_attributes.blank? + assert_equal Set.new([ 'credit_rating', 'administrator', 'phone_number', *LoosePerson.attributes_protected_by_default ]), 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 LooseDescendantSecond.accessible_attributes.blank? + assert_equal Set.new([ 'credit_rating', 'administrator', 'phone_number', 'name', *LoosePerson.attributes_protected_by_default ]), LooseDescendantSecond.protected_attributes, + 'Running attr_protected twice in one class should merge the protections' - assert_nil TightPerson.protected_attributes + assert (TightPerson.protected_attributes - TightPerson.attributes_protected_by_default).blank? assert_equal Set.new([ 'name', 'address' ]), TightPerson.accessible_attributes - assert_nil TightDescendant.protected_attributes + assert (TightDescendant.protected_attributes - TightDescendant.attributes_protected_by_default).blank? assert_equal Set.new([ 'name', 'address', 'phone_number' ]), TightDescendant.accessible_attributes end diff --git a/activerecord/test/cases/mass_assignment_security/black_list_test.rb b/activerecord/test/cases/mass_assignment_security/black_list_test.rb new file mode 100644 index 0000000000..8b7f48a5f6 --- /dev/null +++ b/activerecord/test/cases/mass_assignment_security/black_list_test.rb @@ -0,0 +1,28 @@ +require "cases/helper" + +class BlackListTest < ActiveRecord::TestCase + + def setup + @black_list = ActiveRecord::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/activerecord/test/cases/mass_assignment_security/permission_set_test.rb b/activerecord/test/cases/mass_assignment_security/permission_set_test.rb new file mode 100644 index 0000000000..de7f3982a2 --- /dev/null +++ b/activerecord/test/cases/mass_assignment_security/permission_set_test.rb @@ -0,0 +1,30 @@ +require "cases/helper" + +class PermissionSetTest < ActiveRecord::TestCase + + def setup + @permission_list = ActiveRecord::MassAssignmentSecurity::PermissionSet.new + end + + test "+ stringifies added collection values" do + symbol_collection = [ :admin ] + @permission_list += symbol_collection + + assert @permission_list.include?('admin'), "did not add collection to #{@permission_list.inspect}}" + end + + test "include? normalizes multi-parameter keys" do + multi_param_key = 'admin(1)' + @permission_list += [ 'admin' ] + + assert_equal true, @permission_list.include?(multi_param_key), "#{multi_param_key} not found in #{@permission_list.inspect}" + end + + test "include? normal keys" do + normal_key = 'admin' + @permission_list += [ normal_key ] + + assert_equal true, @permission_list.include?(normal_key), "#{normal_key} not found in #{@permission_list.inspect}" + end + +end diff --git a/activerecord/test/cases/mass_assignment_security/sanitizer_test.rb b/activerecord/test/cases/mass_assignment_security/sanitizer_test.rb new file mode 100644 index 0000000000..122bc7e114 --- /dev/null +++ b/activerecord/test/cases/mass_assignment_security/sanitizer_test.rb @@ -0,0 +1,36 @@ +require "cases/helper" + +class SanitizerTest < ActiveRecord::TestCase + + class SanitizingAuthorizer + include ActiveRecord::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/activerecord/test/cases/mass_assignment_security/white_list_test.rb b/activerecord/test/cases/mass_assignment_security/white_list_test.rb new file mode 100644 index 0000000000..4601263437 --- /dev/null +++ b/activerecord/test/cases/mass_assignment_security/white_list_test.rb @@ -0,0 +1,28 @@ +require "cases/helper" + +class WhiteListTest < ActiveRecord::TestCase + + def setup + @white_list = ActiveRecord::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 -- cgit v1.2.3 From 7c86e8e21ba6a1f88226ddd0cf012a563f234d06 Mon Sep 17 00:00:00 2001 From: Josh Kalderimis Date: Wed, 7 Jul 2010 17:05:42 +0200 Subject: minor changes to mass assignment security patch to bring it in line with rails standards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: José Valim --- activerecord/lib/active_record/base.rb | 4 +- .../lib/active_record/mass_assignment_security.rb | 198 ++++++++++----------- .../mass_assignment_security/permission_set.rb | 7 +- .../mass_assignment_security/sanitizer.rb | 8 +- activerecord/test/cases/base_test.rb | 126 +------------ .../permission_set_test.rb | 12 +- .../test/cases/mass_assignment_security_test.rb | 96 ++++++++++ .../test/models/mass_assignment_specific.rb | 32 ++++ 8 files changed, 234 insertions(+), 249 deletions(-) create mode 100644 activerecord/test/cases/mass_assignment_security_test.rb create mode 100644 activerecord/test/models/mass_assignment_specific.rb diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index affb1fee3c..56b4ba8260 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -1480,7 +1480,7 @@ MSG 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?("(") @@ -1797,7 +1797,7 @@ MSG include AttributeMethods::PrimaryKey include AttributeMethods::TimeZoneConversion include AttributeMethods::Dirty - extend MassAssignmentSecurity + include MassAssignmentSecurity include Callbacks, ActiveModel::Observing, Timestamp include Associations, AssociationPreload, NamedScope diff --git a/activerecord/lib/active_record/mass_assignment_security.rb b/activerecord/lib/active_record/mass_assignment_security.rb index 07beb6405e..8f4d6e1c74 100644 --- a/activerecord/lib/active_record/mass_assignment_security.rb +++ b/activerecord/lib/active_record/mass_assignment_security.rb @@ -1,7 +1,16 @@ require 'active_record/mass_assignment_security/permission_set' module ActiveRecord + # = Active Record 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, @@ -11,7 +20,7 @@ module ActiveRecord # on their role: # # class AccountsController < ApplicationController - # extend ActiveRecord::MassAssignmentSecurity + # include ActiveRecord::MassAssignmentSecurity # # attr_accessible :first_name, :last_name # @@ -28,7 +37,7 @@ module ActiveRecord # protected # # def account_params - # remove_attributes_protected_from_mass_assignment(params[:account]) + # sanitize_for_mass_assignment(params[:account]) # end # # def mass_assignment_authorizer @@ -37,123 +46,96 @@ module ActiveRecord # # end # - def self.extended(base) - base.send(:include, InstanceMethods) - end - - module InstanceMethods - - protected - - def remove_attributes_protected_from_mass_assignment(attributes) - mass_assignment_authorizer.sanitize(attributes) - end - - def mass_assignment_authorizer - self.class.mass_assignment_authorizer - end + module ClassMethods + # Attributes named in this macro are protected from mass-assignment, + # such as new(attributes), + # update_attributes(attributes), or + # attributes=(attributes). + # + # 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+. + # + # Note that using Hash#except or Hash#slice 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 - end + # Specifies a white list of model attributes that can be set via + # mass-assignment, such as new(attributes), + # update_attributes(attributes), or + # attributes=(attributes) + # + # 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" + # + # Note that using Hash#except or Hash#slice 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 - # Attributes named in this macro are protected from mass-assignment, - # such as new(attributes), - # update_attributes(attributes), or - # attributes=(attributes). - # - # 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+. - # - # Note that using Hash#except or Hash#slice in place of +attr_protected+ - # to sanitize attributes won't provide sufficient protection. - def attr_protected(*keys) - use_authorizer(:protected_attributes) - protected_attributes.merge(keys) - end + def protected_attributes + self._protected_attributes ||= BlackList.new(attributes_protected_by_default).tap { |w| w.logger = logger } + end - # Specifies a white list of model attributes that can be set via - # mass-assignment, such as new(attributes), - # update_attributes(attributes), or - # attributes=(attributes) - # - # 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" - # - # Note that using Hash#except or Hash#slice in place of +attr_accessible+ - # to sanitize attributes won't provide sufficient protection. - def attr_accessible(*keys) - use_authorizer(:accessible_attributes) - accessible_attributes.merge(keys) - end + def accessible_attributes + self._accessible_attributes ||= WhiteList.new.tap { |w| w.logger = logger } + end - # Returns an array of all the attributes that have been protected from mass-assignment. - def protected_attributes - read_inheritable_attribute(:protected_attributes) || begin - authorizer = BlackList.new - authorizer += attributes_protected_by_default - authorizer.logger = logger - write_inheritable_attribute(:protected_attributes, authorizer) + def active_authorizer + self._active_authorizer ||= protected_attributes end - end - # Returns an array of all the attributes that have been made accessible to mass-assignment. - def accessible_attributes - read_inheritable_attribute(:accessible_attributes) || begin - authorizer = WhiteList.new - authorizer.logger = logger - write_inheritable_attribute(:accessible_attributes, authorizer) + def attributes_protected_by_default + [] end end - def mass_assignment_authorizer - protected_attributes - end + protected - private + def sanitize_for_mass_assignment(attributes) + mass_assignment_authorizer.sanitize(attributes) + end - # Sets the active authorizer, (attr_protected or attr_accessible). Subsequent calls - # will raise an exception when using a different authorizer_id. - def use_authorizer(authorizer_id) # :nodoc: - if active_authorizer_id = read_inheritable_attribute(:active_authorizer_id) - unless authorizer_id == active_authorizer_id - raise("Already using #{active_authorizer_id}, cannot use #{authorizer_id}") - end - else - write_inheritable_attribute(:active_authorizer_id, authorizer_id) - end + def mass_assignment_authorizer + self.class.active_authorizer end end diff --git a/activerecord/lib/active_record/mass_assignment_security/permission_set.rb b/activerecord/lib/active_record/mass_assignment_security/permission_set.rb index 1d34dce02e..8446a4103b 100644 --- a/activerecord/lib/active_record/mass_assignment_security/permission_set.rb +++ b/activerecord/lib/active_record/mass_assignment_security/permission_set.rb @@ -2,11 +2,11 @@ require 'active_record/mass_assignment_security/sanitizer' module ActiveRecord module MassAssignmentSecurity - class PermissionSet < Set + class PermissionSet < Set attr_accessor :logger - def merge(values) + def +(values) super(values.map(&:to_s)) end @@ -19,7 +19,6 @@ module ActiveRecord def remove_multiparameter_id(key) key.gsub(/\(.+/, '') end - end class WhiteList < PermissionSet @@ -28,7 +27,6 @@ module ActiveRecord def deny?(key) !include?(key) end - end class BlackList < PermissionSet @@ -37,7 +35,6 @@ module ActiveRecord def deny?(key) include?(key) end - end end diff --git a/activerecord/lib/active_record/mass_assignment_security/sanitizer.rb b/activerecord/lib/active_record/mass_assignment_security/sanitizer.rb index 4a099a147c..11de35f9d6 100644 --- a/activerecord/lib/active_record/mass_assignment_security/sanitizer.rb +++ b/activerecord/lib/active_record/mass_assignment_security/sanitizer.rb @@ -13,15 +13,17 @@ module ActiveRecord def debug_protected_attribute_removal(attributes, sanitized_attributes) removed_keys = attributes.keys - sanitized_attributes.keys - if removed_keys.any? - logger.debug "WARNING: Can't mass-assign protected attributes: #{removed_keys.join(', ')}" - end + warn!(removed_keys) if removed_keys.any? end def debug? logger.present? end + def warn!(attrs) + logger.debug "WARNING: Can't mass-assign protected attributes: #{attrs.join(', ')}" + end + end end end diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index dff39cf54f..2eecb6e344 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/mass_assignment_specific' require 'rexml/document' require 'active_support/core_ext/exception' @@ -37,45 +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 TopicWithProtectedContent < ActiveRecord::Base - self.table_name = 'topics' - attr_protected :content -end - class BasicsTest < ActiveRecord::TestCase fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :categorizations, :categories, :posts @@ -954,89 +922,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 - assert_raise(RuntimeError) do - TopicWithProtectedContent.attr_accessible :author_name - end - 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 LoosePerson.accessible_attributes.blank? - assert_equal Set.new([ 'credit_rating', 'administrator', *LoosePerson.attributes_protected_by_default ]), LoosePerson.protected_attributes - - assert LooseDescendant.accessible_attributes.blank? - assert_equal Set.new([ 'credit_rating', 'administrator', 'phone_number', *LoosePerson.attributes_protected_by_default ]), LooseDescendant.protected_attributes - - assert LooseDescendantSecond.accessible_attributes.blank? - assert_equal Set.new([ 'credit_rating', 'administrator', 'phone_number', 'name', *LoosePerson.attributes_protected_by_default ]), 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_readonly_attributes assert_equal Set.new([ 'title' , 'comments_count' ]), ReadonlyTitlePost.readonly_attributes @@ -1239,15 +1124,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/mass_assignment_security/permission_set_test.rb b/activerecord/test/cases/mass_assignment_security/permission_set_test.rb index de7f3982a2..ca8985042a 100644 --- a/activerecord/test/cases/mass_assignment_security/permission_set_test.rb +++ b/activerecord/test/cases/mass_assignment_security/permission_set_test.rb @@ -8,23 +8,23 @@ class PermissionSetTest < ActiveRecord::TestCase test "+ stringifies added collection values" do symbol_collection = [ :admin ] - @permission_list += symbol_collection + new_list = @permission_list += symbol_collection - assert @permission_list.include?('admin'), "did not add collection to #{@permission_list.inspect}}" + 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)' - @permission_list += [ 'admin' ] + new_list = @permission_list += [ 'admin' ] - assert_equal true, @permission_list.include?(multi_param_key), "#{multi_param_key} not found in #{@permission_list.inspect}" + 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' - @permission_list += [ normal_key ] + new_list = @permission_list += [ normal_key ] - assert_equal true, @permission_list.include?(normal_key), "#{normal_key} not found in #{@permission_list.inspect}" + assert new_list.include?(normal_key), "#{normal_key} not found in #{@permission_list.inspect}" end 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..07154da93b --- /dev/null +++ b/activerecord/test/cases/mass_assignment_security_test.rb @@ -0,0 +1,96 @@ +require "cases/helper" +require 'models/reply' +require 'models/company' +require 'models/subscriber' +require 'models/keyboard' +require 'models/mass_assignment_specific' + +class MassAssignmentSecurityTest < ActiveRecord::TestCase + + 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 LoosePerson.accessible_attributes.blank? + assert_equal Set.new([ 'credit_rating', 'administrator', *LoosePerson.attributes_protected_by_default ]), LoosePerson.protected_attributes + + assert LooseDescendant.accessible_attributes.blank? + assert_equal Set.new([ 'credit_rating', 'administrator', 'phone_number', *LoosePerson.attributes_protected_by_default ]), LooseDescendant.protected_attributes + + assert LooseDescendantSecond.accessible_attributes.blank? + assert_equal Set.new([ 'credit_rating', 'administrator', 'phone_number', 'name', *LoosePerson.attributes_protected_by_default ]), + 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 + 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 + +end \ No newline at end of file diff --git a/activerecord/test/models/mass_assignment_specific.rb b/activerecord/test/models/mass_assignment_specific.rb new file mode 100644 index 0000000000..13a80e0197 --- /dev/null +++ b/activerecord/test/models/mass_assignment_specific.rb @@ -0,0 +1,32 @@ +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 Task < ActiveRecord::Base + attr_protected :starting +end + +class TopicWithProtectedContent < ActiveRecord::Base + self.table_name = 'topics' + attr_protected :content +end \ No newline at end of file -- cgit v1.2.3 From 4b66aab00fa0ea6bcc6ec81df19e44de34fd7864 Mon Sep 17 00:00:00 2001 From: Josh Kalderimis Date: Thu, 8 Jul 2010 18:16:36 +0200 Subject: mass_assignment_security moved from AR to AMo, and minor test cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: José Valim --- activemodel/lib/active_model.rb | 1 + .../lib/active_model/mass_assignment_security.rb | 145 +++++++++++++++++++++ .../mass_assignment_security/permission_set.rb | 41 ++++++ .../mass_assignment_security/sanitizer.rb | 29 +++++ .../mass_assignment_security/black_list_test.rb | 28 ++++ .../permission_set_test.rb | 30 +++++ .../mass_assignment_security/sanitizer_test.rb | 37 ++++++ .../mass_assignment_security/white_list_test.rb | 28 ++++ .../test/cases/mass_assignment_security_test.rb | 52 ++++++++ .../test/models/mass_assignment_specific.rb | 57 ++++++++ activerecord/lib/active_record.rb | 1 - activerecord/lib/active_record/base.rb | 2 +- .../lib/active_record/mass_assignment_security.rb | 142 -------------------- .../mass_assignment_security/permission_set.rb | 41 ------ .../mass_assignment_security/sanitizer.rb | 29 ----- activerecord/test/cases/base_test.rb | 2 +- .../mass_assignment_security/black_list_test.rb | 28 ---- .../permission_set_test.rb | 30 ----- .../mass_assignment_security/sanitizer_test.rb | 36 ----- .../mass_assignment_security/white_list_test.rb | 28 ---- .../test/cases/mass_assignment_security_test.rb | 71 ++-------- activerecord/test/models/loose_person.rb | 24 ++++ .../test/models/mass_assignment_specific.rb | 32 ----- 23 files changed, 483 insertions(+), 431 deletions(-) create mode 100644 activemodel/lib/active_model/mass_assignment_security.rb create mode 100644 activemodel/lib/active_model/mass_assignment_security/permission_set.rb create mode 100644 activemodel/lib/active_model/mass_assignment_security/sanitizer.rb create mode 100644 activemodel/test/cases/mass_assignment_security/black_list_test.rb create mode 100644 activemodel/test/cases/mass_assignment_security/permission_set_test.rb create mode 100644 activemodel/test/cases/mass_assignment_security/sanitizer_test.rb create mode 100644 activemodel/test/cases/mass_assignment_security/white_list_test.rb create mode 100644 activemodel/test/cases/mass_assignment_security_test.rb create mode 100644 activemodel/test/models/mass_assignment_specific.rb delete mode 100644 activerecord/lib/active_record/mass_assignment_security.rb delete mode 100644 activerecord/lib/active_record/mass_assignment_security/permission_set.rb delete mode 100644 activerecord/lib/active_record/mass_assignment_security/sanitizer.rb delete mode 100644 activerecord/test/cases/mass_assignment_security/black_list_test.rb delete mode 100644 activerecord/test/cases/mass_assignment_security/permission_set_test.rb delete mode 100644 activerecord/test/cases/mass_assignment_security/sanitizer_test.rb delete mode 100644 activerecord/test/cases/mass_assignment_security/white_list_test.rb create mode 100644 activerecord/test/models/loose_person.rb delete mode 100644 activerecord/test/models/mass_assignment_specific.rb 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/mass_assignment_security.rb b/activemodel/lib/active_model/mass_assignment_security.rb new file mode 100644 index 0000000000..c0549ba6c0 --- /dev/null +++ b/activemodel/lib/active_model/mass_assignment_security.rb @@ -0,0 +1,145 @@ +require 'active_support/core_ext/class/attribute.rb' +require 'active_model/mass_assignment_security/permission_set' + +module ActiveModel + # = Active Record 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 ActiveRecord::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, + # such as new(attributes), + # update_attributes(attributes), or + # attributes=(attributes). + # + # 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+. + # + # Note that using Hash#except or Hash#slice 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, such as new(attributes), + # update_attributes(attributes), or + # attributes=(attributes) + # + # 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" + # + # Note that using Hash#except or Hash#slice 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..978da493d7 --- /dev/null +++ b/activemodel/lib/active_model/mass_assignment_security/permission_set.rb @@ -0,0 +1,41 @@ +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.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..275e481fb8 --- /dev/null +++ b/activemodel/lib/active_model/mass_assignment_security/sanitizer.rb @@ -0,0 +1,29 @@ +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) if debug? + 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 debug? + self.logger.present? + end + + def warn!(attrs) + self.logger.debug "WARNING: Can't mass-assign protected attributes: #{attrs.join(', ')}" + end + + end + end +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.rb b/activerecord/lib/active_record.rb index 9ab05a0548..e2f2508ae8 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -64,7 +64,6 @@ module ActiveRecord autoload :CounterCache autoload :DynamicFinderMatch autoload :DynamicScopeMatch - autoload :MassAssignmentSecurity autoload :Migration autoload :Migrator, 'active_record/migration' autoload :NamedScope diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 56b4ba8260..f22a9de7b1 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -1797,7 +1797,7 @@ MSG include AttributeMethods::PrimaryKey include AttributeMethods::TimeZoneConversion include AttributeMethods::Dirty - include MassAssignmentSecurity + include ActiveModel::MassAssignmentSecurity include Callbacks, ActiveModel::Observing, Timestamp include Associations, AssociationPreload, NamedScope diff --git a/activerecord/lib/active_record/mass_assignment_security.rb b/activerecord/lib/active_record/mass_assignment_security.rb deleted file mode 100644 index 8f4d6e1c74..0000000000 --- a/activerecord/lib/active_record/mass_assignment_security.rb +++ /dev/null @@ -1,142 +0,0 @@ -require 'active_record/mass_assignment_security/permission_set' - -module ActiveRecord - # = Active Record 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 ActiveRecord::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, - # such as new(attributes), - # update_attributes(attributes), or - # attributes=(attributes). - # - # 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+. - # - # Note that using Hash#except or Hash#slice 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, such as new(attributes), - # update_attributes(attributes), or - # attributes=(attributes) - # - # 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" - # - # Note that using Hash#except or Hash#slice 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 { |w| w.logger = logger } - end - - def accessible_attributes - self._accessible_attributes ||= WhiteList.new.tap { |w| w.logger = 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/activerecord/lib/active_record/mass_assignment_security/permission_set.rb b/activerecord/lib/active_record/mass_assignment_security/permission_set.rb deleted file mode 100644 index 8446a4103b..0000000000 --- a/activerecord/lib/active_record/mass_assignment_security/permission_set.rb +++ /dev/null @@ -1,41 +0,0 @@ -require 'active_record/mass_assignment_security/sanitizer' - -module ActiveRecord - 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.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/activerecord/lib/active_record/mass_assignment_security/sanitizer.rb b/activerecord/lib/active_record/mass_assignment_security/sanitizer.rb deleted file mode 100644 index 11de35f9d6..0000000000 --- a/activerecord/lib/active_record/mass_assignment_security/sanitizer.rb +++ /dev/null @@ -1,29 +0,0 @@ -module ActiveRecord - 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) if debug? - 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 debug? - logger.present? - end - - def warn!(attrs) - logger.debug "WARNING: Can't mass-assign protected attributes: #{attrs.join(', ')}" - end - - end - end -end diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 2eecb6e344..5a72e9c6e0 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -17,7 +17,7 @@ require 'models/comment' require 'models/minimalistic' require 'models/warehouse_thing' require 'models/parrot' -require 'models/mass_assignment_specific' +require 'models/loose_person' require 'rexml/document' require 'active_support/core_ext/exception' diff --git a/activerecord/test/cases/mass_assignment_security/black_list_test.rb b/activerecord/test/cases/mass_assignment_security/black_list_test.rb deleted file mode 100644 index 8b7f48a5f6..0000000000 --- a/activerecord/test/cases/mass_assignment_security/black_list_test.rb +++ /dev/null @@ -1,28 +0,0 @@ -require "cases/helper" - -class BlackListTest < ActiveRecord::TestCase - - def setup - @black_list = ActiveRecord::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/activerecord/test/cases/mass_assignment_security/permission_set_test.rb b/activerecord/test/cases/mass_assignment_security/permission_set_test.rb deleted file mode 100644 index ca8985042a..0000000000 --- a/activerecord/test/cases/mass_assignment_security/permission_set_test.rb +++ /dev/null @@ -1,30 +0,0 @@ -require "cases/helper" - -class PermissionSetTest < ActiveRecord::TestCase - - def setup - @permission_list = ActiveRecord::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/activerecord/test/cases/mass_assignment_security/sanitizer_test.rb b/activerecord/test/cases/mass_assignment_security/sanitizer_test.rb deleted file mode 100644 index 122bc7e114..0000000000 --- a/activerecord/test/cases/mass_assignment_security/sanitizer_test.rb +++ /dev/null @@ -1,36 +0,0 @@ -require "cases/helper" - -class SanitizerTest < ActiveRecord::TestCase - - class SanitizingAuthorizer - include ActiveRecord::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/activerecord/test/cases/mass_assignment_security/white_list_test.rb b/activerecord/test/cases/mass_assignment_security/white_list_test.rb deleted file mode 100644 index 4601263437..0000000000 --- a/activerecord/test/cases/mass_assignment_security/white_list_test.rb +++ /dev/null @@ -1,28 +0,0 @@ -require "cases/helper" - -class WhiteListTest < ActiveRecord::TestCase - - def setup - @white_list = ActiveRecord::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/activerecord/test/cases/mass_assignment_security_test.rb b/activerecord/test/cases/mass_assignment_security_test.rb index 07154da93b..025ec1d3fa 100644 --- a/activerecord/test/cases/mass_assignment_security_test.rb +++ b/activerecord/test/cases/mass_assignment_security_test.rb @@ -1,28 +1,11 @@ require "cases/helper" -require 'models/reply' require 'models/company' require 'models/subscriber' require 'models/keyboard' -require 'models/mass_assignment_specific' +require 'models/task' class MassAssignmentSecurityTest < ActiveRecord::TestCase - 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 @@ -47,50 +30,14 @@ class MassAssignmentSecurityTest < ActiveRecord::TestCase 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 LoosePerson.accessible_attributes.blank? - assert_equal Set.new([ 'credit_rating', 'administrator', *LoosePerson.attributes_protected_by_default ]), LoosePerson.protected_attributes - - assert LooseDescendant.accessible_attributes.blank? - assert_equal Set.new([ 'credit_rating', 'administrator', 'phone_number', *LoosePerson.attributes_protected_by_default ]), LooseDescendant.protected_attributes - - assert LooseDescendantSecond.accessible_attributes.blank? - assert_equal Set.new([ 'credit_rating', 'administrator', 'phone_number', 'name', *LoosePerson.attributes_protected_by_default ]), - 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 - 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 + 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/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/mass_assignment_specific.rb b/activerecord/test/models/mass_assignment_specific.rb deleted file mode 100644 index 13a80e0197..0000000000 --- a/activerecord/test/models/mass_assignment_specific.rb +++ /dev/null @@ -1,32 +0,0 @@ -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 Task < ActiveRecord::Base - attr_protected :starting -end - -class TopicWithProtectedContent < ActiveRecord::Base - self.table_name = 'topics' - attr_protected :content -end \ No newline at end of file -- cgit v1.2.3 From c285f07a92c643729a1a6ae6282cd597fe8e20e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 8 Jul 2010 19:02:34 +0200 Subject: Change documentation for ActiveModel::MassAssignmentSecurity a bit and make debug always be called since some people may overwrite warn! to add extra behavior even if logger is not available. --- .../lib/active_model/mass_assignment_security.rb | 67 +++++++++++++--------- .../mass_assignment_security/permission_set.rb | 10 ++-- .../mass_assignment_security/sanitizer.rb | 24 +++----- 3 files changed, 54 insertions(+), 47 deletions(-) diff --git a/activemodel/lib/active_model/mass_assignment_security.rb b/activemodel/lib/active_model/mass_assignment_security.rb index c0549ba6c0..66cd9fdde6 100644 --- a/activemodel/lib/active_model/mass_assignment_security.rb +++ b/activemodel/lib/active_model/mass_assignment_security.rb @@ -2,7 +2,7 @@ require 'active_support/core_ext/class/attribute.rb' require 'active_model/mass_assignment_security/permission_set' module ActiveModel - # = Active Record Mass-Assignment Security + # = Active Model Mass-Assignment Security module MassAssignmentSecurity extend ActiveSupport::Concern @@ -21,7 +21,7 @@ module ActiveModel # on their role: # # class AccountsController < ApplicationController - # include ActiveRecord::MassAssignmentSecurity + # include ActiveModel::MassAssignmentSecurity # # attr_accessible :first_name, :last_name # @@ -48,23 +48,32 @@ module ActiveModel # end # module ClassMethods - # Attributes named in this macro are protected from mass-assignment, - # such as new(attributes), - # update_attributes(attributes), or - # attributes=(attributes). + # 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. # - # class Customer < ActiveRecord::Base + # == 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("name" => David, "credit_rating" => "Excellent") - # customer.credit_rating # => nil - # customer.attributes = { "description" => "Jolly fellow", "credit_rating" => "Superb" } + # customer = Customer.new + # customer.attributes = { "name" => "David", "credit_rating" => "Excellent" } + # customer.name # => "David" # customer.credit_rating # => nil # # customer.credit_rating = "Average" @@ -81,9 +90,7 @@ module ActiveModel end # Specifies a white list of model attributes that can be set via - # mass-assignment, such as new(attributes), - # update_attributes(attributes), or - # attributes=(attributes) + # 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 @@ -93,13 +100,22 @@ module ActiveModel # default and restrict attributes as needed, have a look at # +attr_protected+. # - # class Customer < ActiveRecord::Base - # attr_accessible :name, :nickname + # 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(:name => "David", :nickname => "Dave", :credit_rating => "Excellent") - # customer.credit_rating # => nil - # customer.attributes = { :name => "Jolly fellow", :credit_rating => "Superb" } + # customer = Customer.new + # customer.attributes = { :name => "David", :credit_rating => "Excellent" } + # customer.name # => "David" # customer.credit_rating # => nil # # customer.credit_rating = "Average" @@ -131,15 +147,14 @@ module ActiveModel end end - protected - - def sanitize_for_mass_assignment(attributes) - mass_assignment_authorizer.sanitize(attributes) - end + protected - def mass_assignment_authorizer - self.class.active_authorizer - end + 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 index 978da493d7..7c48472799 100644 --- a/activemodel/lib/active_model/mass_assignment_security/permission_set.rb +++ b/activemodel/lib/active_model/mass_assignment_security/permission_set.rb @@ -2,7 +2,6 @@ require 'active_model/mass_assignment_security/sanitizer' module ActiveModel module MassAssignmentSecurity - class PermissionSet < Set attr_accessor :logger @@ -14,11 +13,11 @@ module ActiveModel super(remove_multiparameter_id(key)) end - protected + protected - def remove_multiparameter_id(key) - key.gsub(/\(.+/, '') - end + def remove_multiparameter_id(key) + key.to_s.gsub(/\(.+/, '') + end end class WhiteList < PermissionSet @@ -36,6 +35,5 @@ module ActiveModel 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 index 275e481fb8..150beb1ff2 100644 --- a/activemodel/lib/active_model/mass_assignment_security/sanitizer.rb +++ b/activemodel/lib/active_model/mass_assignment_security/sanitizer.rb @@ -1,29 +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) if debug? + 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 debug? - self.logger.present? - end + protected - def warn!(attrs) - self.logger.debug "WARNING: Can't mass-assign protected attributes: #{attrs.join(', ')}" - end + 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 -- cgit v1.2.3 From 64cee90c0f6bbbb2a53ffe71655a7cee1e6c99be Mon Sep 17 00:00:00 2001 From: Jeremy Kemper Date: Thu, 8 Jul 2010 10:53:06 -0700 Subject: Bump bundler to 1.0.0.beta.3 --- rails.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rails.gemspec b/rails.gemspec index 819513c4b5..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', '>= 1.0.0.beta.2') + s.add_dependency('bundler', '>= 1.0.0.beta.3') end -- cgit v1.2.3 From 951dbf06b4177b1c3d912213166ca0b14374a48b Mon Sep 17 00:00:00 2001 From: Jeremy Kemper Date: Thu, 8 Jul 2010 10:58:58 -0700 Subject: Revert "Bump bundler to 1.0.0.beta.3" (It's not out yet) This reverts commit 64cee90c0f6bbbb2a53ffe71655a7cee1e6c99be. --- rails.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rails.gemspec b/rails.gemspec index 2d10ce78a0..819513c4b5 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', '>= 1.0.0.beta.3') + s.add_dependency('bundler', '>= 1.0.0.beta.2') end -- cgit v1.2.3 From 00f1cd71a97bfa79e6b62a870b09ca914c48e421 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Thu, 8 Jul 2010 13:58:23 +0200 Subject: fix ActiveRecord `destroy_all` so it returns destroyed records Signed-off-by: Jeremy Kemper --- .../associations/association_collection.rb | 7 ++++--- activerecord/lib/active_record/relation.rb | 3 +-- .../cases/associations/has_many_associations_test.rb | 7 +++++-- activerecord/test/cases/base_test.rb | 20 ++++++++++++++------ 4 files changed, 24 insertions(+), 13 deletions(-) diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index a4e08c7d41..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 = {}) 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/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/base_test.rb b/activerecord/test/cases/base_test.rb index 5a72e9c6e0..329ca38f99 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -651,16 +651,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 -- cgit v1.2.3 From 9ac9c35117a8f232d6df9009b8c55f4ed2ce077b Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Thu, 8 Jul 2010 11:44:19 -0700 Subject: removing useless code. [#5070 state:resolved] Signed-off-by: Jeremy Kemper --- activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 0d9a86a1ea..e4fa412a67 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -16,8 +16,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? -- cgit v1.2.3 From 7b0f8534c7a806fd04aac7c817c247433d02f641 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Thu, 8 Jul 2010 11:50:07 -0700 Subject: moving parse_sqlite_config to the sqlite3_connection method (where it belongs) [#5071 state:resolved] Signed-off-by: Jeremy Kemper --- .../connection_adapters/sqlite3_adapter.rb | 12 +++++++++++- .../connection_adapters/sqlite_adapter.rb | 19 ------------------- 2 files changed, 11 insertions(+), 20 deletions(-) diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index e4fa412a67..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"' 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 -- cgit v1.2.3 From a4f5f0547f205c6dbf264e8dadeb18492d5271ba Mon Sep 17 00:00:00 2001 From: Neeraj Singh Date: Thu, 1 Jul 2010 23:16:49 -0400 Subject: removing unused method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: José Valim --- activerecord/lib/active_record/base.rb | 5 ----- 1 file changed, 5 deletions(-) diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index f22a9de7b1..573220f460 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -1152,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'" -- cgit v1.2.3 From b09fd9ccbbe988e6edaf244d304458ad9d476c31 Mon Sep 17 00:00:00 2001 From: Josh Kalderimis Date: Thu, 8 Jul 2010 19:50:41 +0200 Subject: removed an old unused method in AR which removed readonly attributes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: José Valim --- activerecord/lib/active_record/base.rb | 9 --------- 1 file changed, 9 deletions(-) diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 573220f460..cb74a01388 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -1615,15 +1615,6 @@ MSG end 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 - # The primary key and inheritance column can never be set by mass-assignment for security reasons. def self.attributes_protected_by_default default = [ primary_key, inheritance_column ] -- cgit v1.2.3 From 4a26a292a0708df595662444e8c738ee1520ec57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Strza=C5=82kowski?= Date: Thu, 8 Jul 2010 21:34:05 +0200 Subject: Removing method before redefining it. It was causing warnings. [#5072 state:resolved] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: José Valim --- activemodel/lib/active_model/observing.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/activemodel/lib/active_model/observing.rb b/activemodel/lib/active_model/observing.rb index d0f36ce3b1..eb853f01ee 100644 --- a/activemodel/lib/active_model/observing.rb +++ b/activemodel/lib/active_model/observing.rb @@ -157,6 +157,7 @@ module ActiveModel def observe(*models) models.flatten! models.collect! { |model| model.respond_to?(:to_sym) ? model.to_s.camelize.constantize : model } + remove_method(:observed_classes) define_method(:observed_classes) { models } end -- cgit v1.2.3 From 8b2330ebc3dd2f65a605260366d2445ee7a3009b Mon Sep 17 00:00:00 2001 From: Thiago Pradi Date: Thu, 8 Jul 2010 03:26:37 -0300 Subject: Tests to specify the behaviour of ActiveRecord::Migrator.get_all_versions() [#5066 state:resolved] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: José Valim --- activerecord/test/cases/migration_test.rb | 14 ++++++++++++++ 1 file changed, 14 insertions(+) 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_" -- cgit v1.2.3 From 0e9bc23c0e5dba228626ffbc2bef069331b2e471 Mon Sep 17 00:00:00 2001 From: Ken Collins Date: Wed, 7 Jul 2010 15:47:57 -0400 Subject: Fix the #using_limitable_reflections? helper to work correctly by not examining the length of an array which contains false/true, hence always passing. [#4869 state:resolved] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: José Valim --- activerecord/lib/active_record/relation/finder_methods.rb | 2 +- activerecord/test/cases/associations_test.rb | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) 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/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) -- cgit v1.2.3 From e848ab527ca9da1a6cf2a8485163f01daf3f66d1 Mon Sep 17 00:00:00 2001 From: Sudara Date: Fri, 2 Jul 2010 00:20:26 +0000 Subject: Allow a PID file to be specified to rails server [#5031 state:resolved] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: José Valim --- railties/lib/rails/commands/server.rb | 3 +++ 1 file changed, 3 insertions(+) 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 "" -- cgit v1.2.3 From 17650e394fae26984d506fd0f705bc32e5a5de27 Mon Sep 17 00:00:00 2001 From: Grant Ammons Date: Mon, 28 Jun 2010 16:37:38 -0400 Subject: Eager loading :through associations will join the :source model if there are :conditions. [#2362 state:resolved] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: José Valim --- activerecord/lib/active_record/association_preload.rb | 7 ++++++- activerecord/test/cases/associations/eager_test.rb | 6 ++++++ activerecord/test/models/post.rb | 1 + 3 files changed, 13 insertions(+), 1 deletion(-) 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/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/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 -- cgit v1.2.3 From 690352dce492938ab54a4b7e2befbd0a6931de3c Mon Sep 17 00:00:00 2001 From: Neeraj Singh Date: Fri, 2 Jul 2010 14:36:13 -0400 Subject: consolidating updated_at and updated_on MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: José Valim --- activerecord/lib/active_record/timestamp.rb | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index ffd12d2082..da8324ddcc 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,8 +49,9 @@ 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.each do |column| + write_attribute(column.to_s, current_time) if respond_to?(column) && self.send(column).nil? + end end super @@ -60,16 +60,23 @@ module ActiveRecord def update(*args) #:nodoc: 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) } end super end + + def timestamp_attributes_for_update #:nodoc: + [:updated_at, :updated_on] + end + + def timestamp_attributes_for_update_in_model #:nodoc: + ([:updated_at, :updated_on].inject([]) { |sum, elem| respond_to?(elem) ? sum << elem : sum }) + 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 + -- cgit v1.2.3 From 01629d180468049d17a8be6900e27a4f0d2b18c4 Mon Sep 17 00:00:00 2001 From: Neeraj Singh Date: Wed, 30 Jun 2010 07:14:09 -0400 Subject: This patch changes update_attribute implementatino so: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - it will only save the attribute it has been asked to save and not all dirty attributes - it does not invoke callbacks - it does change updated_at/on Signed-off-by: José Valim --- activerecord/lib/active_record/persistence.rb | 17 +++++++--- activerecord/lib/active_record/timestamp.rb | 12 +++++-- activerecord/test/cases/base_test.rb | 40 +++++++++++++++++++++++ activerecord/test/cases/dirty_test.rb | 7 ++-- activerecord/test/cases/nested_attributes_test.rb | 18 +++++----- 5 files changed, 74 insertions(+), 20 deletions(-) diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 50166c4385..8d093c81de 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -102,12 +102,19 @@ 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) + primary_key = self.class.primary_key + h = {name => value} + if should_record_update_timestamps + self.send(:record_update_timestamps) + current_time = current_time_from_proper_timezone + timestamp_attributes_for_update_in_model.each { |column| h.merge!(column => current_time) } + end + self.class.update_all(h, {primary_key => self[primary_key]}) == 1 + @changed_attributes.delete(name.to_s) end # Updates all the attributes from the passed-in Hash and saves the record. @@ -234,4 +241,4 @@ module ActiveRecord end end end -end \ No newline at end of file +end diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index da8324ddcc..e6d52744df 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -58,14 +58,22 @@ module ActiveRecord end def update(*args) #:nodoc: - if record_timestamps && (!partial_updates? || changed?) + record_update_timestamps + super + end + + def record_update_timestamps + if should_record_update_timestamps current_time = current_time_from_proper_timezone timestamp_attributes_for_update_in_model.each { |column| write_attribute(column.to_s, current_time) } end + end - super + def should_record_update_timestamps + record_timestamps && (!partial_updates? || changed?) end + def timestamp_attributes_for_update #:nodoc: [:updated_at, :updated_on] end diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 329ca38f99..09bdd13cd4 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -893,6 +893,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? 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/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index 62237f955b..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 -- cgit v1.2.3 From 6f83a57ac7bf77565380b26b506972cfe751b717 Mon Sep 17 00:00:00 2001 From: Norman Clarke Date: Thu, 8 Jul 2010 17:42:14 -0300 Subject: Improve bang method defs, make slice! operate in-place. [#5028 state:resolved] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: José Valim --- .../active_support/core_ext/string/multibyte.rb | 2 +- .../lib/active_support/multibyte/chars.rb | 26 ++++---------- activesupport/test/multibyte_chars_test.rb | 41 +++++++++++++--------- 3 files changed, 33 insertions(+), 36 deletions(-) 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/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 String#slice!, 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/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 -- cgit v1.2.3 From 87f64ef05e48ad8b374022ca944e34a7ad68551d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 8 Jul 2010 22:59:41 +0200 Subject: Improve a bit the code in latest commits. --- activemodel/lib/active_model/observing.rb | 3 ++- activerecord/lib/active_record/persistence.rb | 16 +++++++++------- activerecord/lib/active_record/timestamp.rb | 20 +++++++------------- 3 files changed, 18 insertions(+), 21 deletions(-) diff --git a/activemodel/lib/active_model/observing.rb b/activemodel/lib/active_model/observing.rb index eb853f01ee..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,7 +158,7 @@ module ActiveModel def observe(*models) models.flatten! models.collect! { |model| model.respond_to?(:to_sym) ? model.to_s.camelize.constantize : model } - remove_method(:observed_classes) + remove_possible_method(:observed_classes) define_method(:observed_classes) { models } end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 8d093c81de..828a8b41b6 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -106,15 +106,17 @@ module ActiveRecord # or callbacks. This is especially useful for boolean flags on existing records. def update_attribute(name, value) send("#{name}=", value) - primary_key = self.class.primary_key - h = {name => value} - if should_record_update_timestamps - self.send(:record_update_timestamps) - current_time = current_time_from_proper_timezone - timestamp_attributes_for_update_in_model.each { |column| h.merge!(column => current_time) } + 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 - self.class.update_all(h, {primary_key => self[primary_key]}) == 1 + @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. diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index e6d52744df..1075a60f07 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -49,8 +49,8 @@ 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? - timestamp_attributes_for_update.each do |column| - write_attribute(column.to_s, current_time) if respond_to?(column) && self.send(column).nil? + timestamp_attributes_for_update_in_model.each do |column| + write_attribute(column.to_s, current_time) if self.send(column).nil? end end @@ -63,23 +63,17 @@ module ActiveRecord end def record_update_timestamps - if should_record_update_timestamps + if record_timestamps && (!partial_updates? || changed?) current_time = current_time_from_proper_timezone timestamp_attributes_for_update_in_model.each { |column| write_attribute(column.to_s, current_time) } + true + else + false end end - def should_record_update_timestamps - record_timestamps && (!partial_updates? || changed?) - end - - - def timestamp_attributes_for_update #:nodoc: - [:updated_at, :updated_on] - end - def timestamp_attributes_for_update_in_model #:nodoc: - ([:updated_at, :updated_on].inject([]) { |sum, elem| respond_to?(elem) ? sum << elem : sum }) + [:updated_at, :updated_on].select { |elem| respond_to?(elem) } end def current_time_from_proper_timezone #:nodoc: -- cgit v1.2.3 From 1e53404fe9c39ad0849894d73e431315be8c0bf0 Mon Sep 17 00:00:00 2001 From: Neeraj Singh Date: Sun, 27 Jun 2010 11:17:44 -0400 Subject: reset_counter should work with non-traditional belongs_to and polymorphic belongs_to MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [#4984 state:resolved] Signed-off-by: José Valim --- activerecord/lib/active_record/counter_cache.rb | 12 +++++++++--- activerecord/test/cases/counter_cache_test.rb | 19 ++++++++++++++++--- activerecord/test/fixtures/cars.yml | 4 ++++ activerecord/test/models/car.rb | 4 ++++ activerecord/test/models/engine.rb | 3 +++ activerecord/test/models/wheel.rb | 3 +++ activerecord/test/schema/schema.rb | 13 +++++++++++++ 7 files changed, 52 insertions(+), 6 deletions(-) create mode 100644 activerecord/test/fixtures/cars.yml create mode 100644 activerecord/test/models/car.rb create mode 100644 activerecord/test/models/engine.rb create mode 100644 activerecord/test/models/wheel.rb diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index 999322129a..808e70d7de 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -17,8 +17,14 @@ 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 + has_many_association = reflect_on_association(association.to_sym) + polymorphic_class = has_many_association.options[:as] + child_class = has_many_association.klass + belongs_to = child_class.reflect_on_all_associations(:belongs_to) + belongs_to_association = belongs_to.detect do |e| + polymorphic_class.nil? ? (e.class_name == self.name) : (e.class_name.to_s.downcase == polymorphic_class.to_s.downcase) + end + belongs_name = belongs_to_association.name counter_name = child_class.reflect_on_association(belongs_name).counter_cache_column self.unscoped.where(arel_table[self.primary_key].eq(object.id)).arel.update({ @@ -103,4 +109,4 @@ module ActiveRecord update_counters(id, counter_name => -1) end end -end \ No newline at end of file +end 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/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/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/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 -- cgit v1.2.3 From 786342e17f42799ef889cf6127fe97e9598272e0 Mon Sep 17 00:00:00 2001 From: David Trasbo Date: Wed, 30 Jun 2010 21:33:49 +0200 Subject: Return from ActiveRecord::Base#attributes= unless value is a hash [#4070 state:committed] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: José Valim --- activerecord/lib/active_record/base.rb | 2 +- activerecord/test/cases/base_test.rb | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index cb74a01388..c78060c956 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -1471,7 +1471,7 @@ 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 = [] diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 09bdd13cd4..a4cf5120e1 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -61,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 -- cgit v1.2.3 From d9ebc76d70a122544452189f0cb93d3854f2043f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 8 Jul 2010 23:45:10 +0200 Subject: Refactor previous commits a bit. --- activerecord/lib/active_record/counter_cache.rb | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index 808e70d7de..237cd56683 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -18,14 +18,17 @@ module ActiveRecord object = find(id) counters.each do |association| has_many_association = reflect_on_association(association.to_sym) - polymorphic_class = has_many_association.options[:as] - child_class = has_many_association.klass - belongs_to = child_class.reflect_on_all_associations(:belongs_to) - belongs_to_association = belongs_to.detect do |e| - polymorphic_class.nil? ? (e.class_name == self.name) : (e.class_name.to_s.downcase == polymorphic_class.to_s.downcase) + + expected_name = if has_many_association.options[:as] + has_many_association.options[:as].to_s.classify + else + self.name end - belongs_name = belongs_to_association.name - counter_name = child_class.reflect_on_association(belongs_name).counter_cache_column + + 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 -- cgit v1.2.3 From d7ffa3c077e08921ea7897455084fc2942fe0977 Mon Sep 17 00:00:00 2001 From: Norman Clarke Date: Thu, 8 Jul 2010 17:43:08 -0300 Subject: Describe recent changes in Multibyte::Chars. --- railties/guides/source/initialization.textile | 137 +++++++++++++++----------- 1 file changed, 81 insertions(+), 56 deletions(-) diff --git a/railties/guides/source/initialization.textile b/railties/guides/source/initialization.textile index 9a0e23385d..7a44ef7c77 100644 --- a/railties/guides/source/initialization.textile +++ b/railties/guides/source/initialization.textile @@ -3568,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: 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 -There's also these method definitions: +There are also these method definitions: - 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. @@ -3608,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: - - - 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 - - -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!+ + + + 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 - - 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 - +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 -- cgit v1.2.3 From c6f4c5916ef467814d970c70627a82c1df4d2686 Mon Sep 17 00:00:00 2001 From: Jaime Iniesta Date: Fri, 9 Jul 2010 17:53:47 +0200 Subject: Minor typos: 'built-in' instead of 'built in', 'built into' instead of 'built in to' --- activesupport/lib/active_support/core_ext/logger.rb | 2 +- railties/guides/source/index.html.erb | 2 +- railties/guides/source/layouts_and_rendering.textile | 2 +- railties/guides/source/plugins.textile | 2 +- railties/guides/source/security.textile | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) 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/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 %> -

Guide to using built in Form helpers.

+

Guide to using built-in Form helpers.

<% end %> 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
-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/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. Using +Model.find(id)+ or +Model.find_by_some thing(something)+ automatically applies this countermeasure. But in SQL fragments, especially in conditions fragments (+:conditions => "..."+), the +connection.execute()+ or +Model.find_by_sql()+ methods, it has to be applied manually. +Ruby on Rails has a built-in filter for special SQL characters, which will escape ' , " , NULL character and line breaks. Using +Model.find(id)+ or +Model.find_by_some thing(something)+ automatically applies this countermeasure. But in SQL fragments, especially in conditions fragments (+:conditions => "..."+), the +connection.execute()+ or +Model.find_by_sql()+ methods, it has to be applied manually. Instead of passing a string to the conditions option, you can pass an array to sanitize tainted strings like this: -- cgit v1.2.3 From c9ae2c11ebbf42d887dced2938a59e8d0634d60a Mon Sep 17 00:00:00 2001 From: Xavier Noria Date: Fri, 9 Jul 2010 19:09:47 +0200 Subject: application.rb: revises the comment for autoload_paths so that is assumes less from the user, and unifies punctuation --- .../lib/rails/generators/rails/app/templates/config/application.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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. -- cgit v1.2.3 From 1e1af8f612ce5649f529fd2dcb573c9b42b455ad Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 9 Jul 2010 17:12:29 -0700 Subject: adding more behavioral tests for the sqlite adapter --- .../cases/adapters/sqlite/sqlite_adapter_test.rb | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/activerecord/test/cases/adapters/sqlite/sqlite_adapter_test.rb b/activerecord/test/cases/adapters/sqlite/sqlite_adapter_test.rb index 69cfb00faf..2505372b7e 100644 --- a/activerecord/test/cases/adapters/sqlite/sqlite_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite/sqlite_adapter_test.rb @@ -83,6 +83,26 @@ module ActiveRecord 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 -- cgit v1.2.3 From 80e47d7b88dcc732ebeb5290faab6e529829dac6 Mon Sep 17 00:00:00 2001 From: Jeremy Kemper Date: Fri, 9 Jul 2010 20:55:59 -0700 Subject: Revert "Revert "Bump bundler to 1.0.0.beta.3"" It's out for reals! This reverts commit 951dbf06b4177b1c3d912213166ca0b14374a48b. --- rails.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rails.gemspec b/rails.gemspec index 819513c4b5..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', '>= 1.0.0.beta.2') + s.add_dependency('bundler', '>= 1.0.0.beta.3') end -- cgit v1.2.3