aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--actionpack/lib/action_controller/metal/helpers.rb5
-rw-r--r--actionpack/test/abstract/helper_test.rb2
-rw-r--r--actionpack/test/controller/helper_test.rb6
-rw-r--r--activemodel/lib/active_model.rb1
-rw-r--r--activemodel/lib/active_model/mass_assignment_security.rb160
-rw-r--r--activemodel/lib/active_model/mass_assignment_security/permission_set.rb39
-rw-r--r--activemodel/lib/active_model/mass_assignment_security/sanitizer.rb23
-rw-r--r--activemodel/lib/active_model/observing.rb2
-rw-r--r--activemodel/test/cases/mass_assignment_security/black_list_test.rb28
-rw-r--r--activemodel/test/cases/mass_assignment_security/permission_set_test.rb30
-rw-r--r--activemodel/test/cases/mass_assignment_security/sanitizer_test.rb37
-rw-r--r--activemodel/test/cases/mass_assignment_security/white_list_test.rb28
-rw-r--r--activemodel/test/cases/mass_assignment_security_test.rb52
-rw-r--r--activemodel/test/models/mass_assignment_specific.rb57
-rw-r--r--activerecord/lib/active_record/association_preload.rb7
-rw-r--r--activerecord/lib/active_record/associations/association_collection.rb7
-rw-r--r--activerecord/lib/active_record/base.rb162
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb15
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb19
-rw-r--r--activerecord/lib/active_record/counter_cache.rb17
-rw-r--r--activerecord/lib/active_record/persistence.rb19
-rw-r--r--activerecord/lib/active_record/relation.rb3
-rw-r--r--activerecord/lib/active_record/relation/finder_methods.rb2
-rw-r--r--activerecord/lib/active_record/timestamp.rb29
-rw-r--r--activerecord/test/cases/adapters/sqlite/sqlite_adapter_test.rb100
-rw-r--r--activerecord/test/cases/associations/eager_test.rb6
-rw-r--r--activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb4
-rw-r--r--activerecord/test/cases/associations/has_many_associations_test.rb7
-rw-r--r--activerecord/test/cases/associations/has_one_through_associations_test.rb4
-rw-r--r--activerecord/test/cases/associations_test.rb10
-rw-r--r--activerecord/test/cases/base_test.rb193
-rw-r--r--activerecord/test/cases/counter_cache_test.rb19
-rw-r--r--activerecord/test/cases/dirty_test.rb7
-rw-r--r--activerecord/test/cases/mass_assignment_security_test.rb43
-rw-r--r--activerecord/test/cases/migration_test.rb14
-rw-r--r--activerecord/test/cases/nested_attributes_test.rb18
-rw-r--r--activerecord/test/fixtures/cars.yml4
-rw-r--r--activerecord/test/models/car.rb4
-rw-r--r--activerecord/test/models/category.rb2
-rw-r--r--activerecord/test/models/engine.rb3
-rw-r--r--activerecord/test/models/loose_person.rb24
-rw-r--r--activerecord/test/models/post.rb1
-rw-r--r--activerecord/test/models/wheel.rb3
-rw-r--r--activerecord/test/schema/schema.rb13
-rw-r--r--activesupport/lib/active_support/core_ext/string/multibyte.rb2
-rw-r--r--activesupport/lib/active_support/multibyte/chars.rb26
-rw-r--r--activesupport/test/multibyte_chars_test.rb41
-rw-r--r--railties/lib/rails/commands/plugin.rb109
-rw-r--r--railties/lib/rails/commands/server.rb3
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/application.rb6
50 files changed, 966 insertions, 450 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
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..66cd9fdde6
--- /dev/null
+++ b/activemodel/lib/active_model/mass_assignment_security.rb
@@ -0,0 +1,160 @@
+require 'active_support/core_ext/class/attribute.rb'
+require 'active_model/mass_assignment_security/permission_set'
+
+module ActiveModel
+ # = Active Model Mass-Assignment Security
+ module MassAssignmentSecurity
+ extend ActiveSupport::Concern
+
+ included do
+ class_attribute :_accessible_attributes
+ class_attribute :_protected_attributes
+ class_attribute :_active_authorizer
+ end
+
+ # Mass assignment security provides an interface for protecting attributes
+ # from end-user assignment. For more complex permissions, mass assignment security
+ # may be handled outside the model by extending a non-ActiveRecord class,
+ # such as a controller, with this behavior.
+ #
+ # For example, a logged in user may need to assign additional attributes depending
+ # on their role:
+ #
+ # class AccountsController < ApplicationController
+ # include ActiveModel::MassAssignmentSecurity
+ #
+ # attr_accessible :first_name, :last_name
+ #
+ # def self.admin_accessible_attributes
+ # accessible_attributes + [ :plan_id ]
+ # end
+ #
+ # def update
+ # ...
+ # @account.update_attributes(account_params)
+ # ...
+ # end
+ #
+ # protected
+ #
+ # def account_params
+ # sanitize_for_mass_assignment(params[:account])
+ # end
+ #
+ # def mass_assignment_authorizer
+ # admin ? admin_accessible_attributes : super
+ # end
+ #
+ # end
+ #
+ module ClassMethods
+ # Attributes named in this macro are protected from mass-assignment
+ # whenever attributes are sanitized before assignment.
+ #
+ # Mass-assignment to these attributes will simply be ignored, to assign
+ # to them you can use direct writer methods. This is meant to protect
+ # sensitive attributes from being overwritten by malicious users
+ # tampering with URLs or forms.
+ #
+ # == Example
+ #
+ # class Customer
+ # include ActiveModel::MassAssignmentSecurity
+ #
+ # attr_accessor :name, :credit_rating
+ # attr_protected :credit_rating
+ #
+ # def attributes=(values)
+ # sanitize_for_mass_assignment(values).each do |k, v|
+ # send("#{k}=", v)
+ # end
+ # end
+ # end
+ #
+ # customer = Customer.new
+ # customer.attributes = { "name" => "David", "credit_rating" => "Excellent" }
+ # customer.name # => "David"
+ # customer.credit_rating # => nil
+ #
+ # customer.credit_rating = "Average"
+ # customer.credit_rating # => "Average"
+ #
+ # To start from an all-closed default and enable attributes as needed,
+ # have a look at +attr_accessible+.
+ #
+ # Note that using <tt>Hash#except</tt> or <tt>Hash#slice</tt> in place of +attr_protected+
+ # to sanitize attributes won't provide sufficient protection.
+ def attr_protected(*names)
+ self._protected_attributes = self.protected_attributes + names
+ self._active_authorizer = self._protected_attributes
+ end
+
+ # Specifies a white list of model attributes that can be set via
+ # mass-assignment.
+ #
+ # This is the opposite of the +attr_protected+ macro: Mass-assignment
+ # will only set attributes in this list, to assign to the rest of
+ # attributes you can use direct writer methods. This is meant to protect
+ # sensitive attributes from being overwritten by malicious users
+ # tampering with URLs or forms. If you'd rather start from an all-open
+ # default and restrict attributes as needed, have a look at
+ # +attr_protected+.
+ #
+ # class Customer
+ # include ActiveModel::MassAssignmentSecurity
+ #
+ # attr_accessor :name, :credit_rating
+ # attr_accessible :name
+ #
+ # def attributes=(values)
+ # sanitize_for_mass_assignment(values).each do |k, v|
+ # send("#{k}=", v)
+ # end
+ # end
+ # end
+ #
+ # customer = Customer.new
+ # customer.attributes = { :name => "David", :credit_rating => "Excellent" }
+ # customer.name # => "David"
+ # customer.credit_rating # => nil
+ #
+ # customer.credit_rating = "Average"
+ # customer.credit_rating # => "Average"
+ #
+ # Note that using <tt>Hash#except</tt> or <tt>Hash#slice</tt> in place of +attr_accessible+
+ # to sanitize attributes won't provide sufficient protection.
+ def attr_accessible(*names)
+ self._accessible_attributes = self.accessible_attributes + names
+ self._active_authorizer = self._accessible_attributes
+ end
+
+ def protected_attributes
+ self._protected_attributes ||= BlackList.new(attributes_protected_by_default).tap do |w|
+ w.logger = self.logger if self.respond_to?(:logger)
+ end
+ end
+
+ def accessible_attributes
+ self._accessible_attributes ||= WhiteList.new.tap { |w| w.logger = self.logger if self.respond_to?(:logger) }
+ end
+
+ def active_authorizer
+ self._active_authorizer ||= protected_attributes
+ end
+
+ def attributes_protected_by_default
+ []
+ end
+ end
+
+ protected
+
+ def sanitize_for_mass_assignment(attributes)
+ mass_assignment_authorizer.sanitize(attributes)
+ end
+
+ def mass_assignment_authorizer
+ self.class.active_authorizer
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/mass_assignment_security/permission_set.rb b/activemodel/lib/active_model/mass_assignment_security/permission_set.rb
new file mode 100644
index 0000000000..7c48472799
--- /dev/null
+++ b/activemodel/lib/active_model/mass_assignment_security/permission_set.rb
@@ -0,0 +1,39 @@
+require 'active_model/mass_assignment_security/sanitizer'
+
+module ActiveModel
+ module MassAssignmentSecurity
+ class PermissionSet < Set
+ attr_accessor :logger
+
+ def +(values)
+ super(values.map(&:to_s))
+ end
+
+ def include?(key)
+ super(remove_multiparameter_id(key))
+ end
+
+ protected
+
+ def remove_multiparameter_id(key)
+ key.to_s.gsub(/\(.+/, '')
+ end
+ end
+
+ class WhiteList < PermissionSet
+ include Sanitizer
+
+ def deny?(key)
+ !include?(key)
+ end
+ end
+
+ class BlackList < PermissionSet
+ include Sanitizer
+
+ def deny?(key)
+ include?(key)
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/activemodel/lib/active_model/mass_assignment_security/sanitizer.rb b/activemodel/lib/active_model/mass_assignment_security/sanitizer.rb
new file mode 100644
index 0000000000..150beb1ff2
--- /dev/null
+++ b/activemodel/lib/active_model/mass_assignment_security/sanitizer.rb
@@ -0,0 +1,23 @@
+module ActiveModel
+ module MassAssignmentSecurity
+ module Sanitizer
+ # Returns all attributes not denied by the authorizer.
+ def sanitize(attributes)
+ sanitized_attributes = attributes.reject { |key, value| deny?(key) }
+ debug_protected_attribute_removal(attributes, sanitized_attributes)
+ sanitized_attributes
+ end
+
+ protected
+
+ def debug_protected_attribute_removal(attributes, sanitized_attributes)
+ removed_keys = attributes.keys - sanitized_attributes.keys
+ warn!(removed_keys) if removed_keys.any?
+ end
+
+ def warn!(attrs)
+ self.logger.debug "WARNING: Can't mass-assign protected attributes: #{attrs.join(', ')}" if self.logger
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/observing.rb b/activemodel/lib/active_model/observing.rb
index d0f36ce3b1..c6a79acf81 100644
--- a/activemodel/lib/active_model/observing.rb
+++ b/activemodel/lib/active_model/observing.rb
@@ -1,6 +1,7 @@
require 'singleton'
require 'active_support/core_ext/array/wrap'
require 'active_support/core_ext/module/aliasing'
+require 'active_support/core_ext/module/remove_method'
require 'active_support/core_ext/string/inflections'
module ActiveModel
@@ -157,6 +158,7 @@ module ActiveModel
def observe(*models)
models.flatten!
models.collect! { |model| model.respond_to?(:to_sym) ? model.to_s.camelize.constantize : model }
+ remove_possible_method(:observed_classes)
define_method(:observed_classes) { models }
end
diff --git a/activemodel/test/cases/mass_assignment_security/black_list_test.rb b/activemodel/test/cases/mass_assignment_security/black_list_test.rb
new file mode 100644
index 0000000000..ed168bc016
--- /dev/null
+++ b/activemodel/test/cases/mass_assignment_security/black_list_test.rb
@@ -0,0 +1,28 @@
+require "cases/helper"
+
+class BlackListTest < ActiveModel::TestCase
+
+ def setup
+ @black_list = ActiveModel::MassAssignmentSecurity::BlackList.new
+ @included_key = 'admin'
+ @black_list += [ @included_key ]
+ end
+
+ test "deny? is true for included items" do
+ assert_equal true, @black_list.deny?(@included_key)
+ end
+
+ test "deny? is false for non-included items" do
+ assert_equal false, @black_list.deny?('first_name')
+ end
+
+ test "sanitize attributes" do
+ original_attributes = { 'first_name' => 'allowed', 'admin' => 'denied', 'admin(1)' => 'denied' }
+ attributes = @black_list.sanitize(original_attributes)
+
+ assert attributes.key?('first_name'), "Allowed key shouldn't be rejected"
+ assert !attributes.key?('admin'), "Denied key should be rejected"
+ assert !attributes.key?('admin(1)'), "Multi-parameter key should be detected"
+ end
+
+end
diff --git a/activemodel/test/cases/mass_assignment_security/permission_set_test.rb b/activemodel/test/cases/mass_assignment_security/permission_set_test.rb
new file mode 100644
index 0000000000..d005b638e4
--- /dev/null
+++ b/activemodel/test/cases/mass_assignment_security/permission_set_test.rb
@@ -0,0 +1,30 @@
+require "cases/helper"
+
+class PermissionSetTest < ActiveModel::TestCase
+
+ def setup
+ @permission_list = ActiveModel::MassAssignmentSecurity::PermissionSet.new
+ end
+
+ test "+ stringifies added collection values" do
+ symbol_collection = [ :admin ]
+ new_list = @permission_list += symbol_collection
+
+ assert new_list.include?('admin'), "did not add collection to #{@permission_list.inspect}}"
+ end
+
+ test "include? normalizes multi-parameter keys" do
+ multi_param_key = 'admin(1)'
+ new_list = @permission_list += [ 'admin' ]
+
+ assert new_list.include?(multi_param_key), "#{multi_param_key} not found in #{@permission_list.inspect}"
+ end
+
+ test "include? normal keys" do
+ normal_key = 'admin'
+ new_list = @permission_list += [ normal_key ]
+
+ assert new_list.include?(normal_key), "#{normal_key} not found in #{@permission_list.inspect}"
+ end
+
+end
diff --git a/activemodel/test/cases/mass_assignment_security/sanitizer_test.rb b/activemodel/test/cases/mass_assignment_security/sanitizer_test.rb
new file mode 100644
index 0000000000..367207aab3
--- /dev/null
+++ b/activemodel/test/cases/mass_assignment_security/sanitizer_test.rb
@@ -0,0 +1,37 @@
+require "cases/helper"
+require 'logger'
+
+class SanitizerTest < ActiveModel::TestCase
+
+ class SanitizingAuthorizer
+ include ActiveModel::MassAssignmentSecurity::Sanitizer
+
+ attr_accessor :logger
+
+ def deny?(key)
+ [ 'admin' ].include?(key)
+ end
+
+ end
+
+ def setup
+ @sanitizer = SanitizingAuthorizer.new
+ end
+
+ test "sanitize attributes" do
+ original_attributes = { 'first_name' => 'allowed', 'admin' => 'denied' }
+ attributes = @sanitizer.sanitize(original_attributes)
+
+ assert attributes.key?('first_name'), "Allowed key shouldn't be rejected"
+ assert !attributes.key?('admin'), "Denied key should be rejected"
+ end
+
+ test "debug mass assignment removal" do
+ original_attributes = { 'first_name' => 'allowed', 'admin' => 'denied' }
+ log = StringIO.new
+ @sanitizer.logger = Logger.new(log)
+ @sanitizer.sanitize(original_attributes)
+ assert (log.string =~ /admin/), "Should log removed attributes: #{log.string}"
+ end
+
+end
diff --git a/activemodel/test/cases/mass_assignment_security/white_list_test.rb b/activemodel/test/cases/mass_assignment_security/white_list_test.rb
new file mode 100644
index 0000000000..aa3596ad2a
--- /dev/null
+++ b/activemodel/test/cases/mass_assignment_security/white_list_test.rb
@@ -0,0 +1,28 @@
+require "cases/helper"
+
+class WhiteListTest < ActiveModel::TestCase
+
+ def setup
+ @white_list = ActiveModel::MassAssignmentSecurity::WhiteList.new
+ @included_key = 'first_name'
+ @white_list += [ @included_key ]
+ end
+
+ test "deny? is false for included items" do
+ assert_equal false, @white_list.deny?(@included_key)
+ end
+
+ test "deny? is true for non-included items" do
+ assert_equal true, @white_list.deny?('admin')
+ end
+
+ test "sanitize attributes" do
+ original_attributes = { 'first_name' => 'allowed', 'admin' => 'denied', 'admin(1)' => 'denied' }
+ attributes = @white_list.sanitize(original_attributes)
+
+ assert attributes.key?('first_name'), "Allowed key shouldn't be rejected"
+ assert !attributes.key?('admin'), "Denied key should be rejected"
+ assert !attributes.key?('admin(1)'), "Multi-parameter key should be detected"
+ end
+
+end
diff --git a/activemodel/test/cases/mass_assignment_security_test.rb b/activemodel/test/cases/mass_assignment_security_test.rb
new file mode 100644
index 0000000000..0f7a38b0bc
--- /dev/null
+++ b/activemodel/test/cases/mass_assignment_security_test.rb
@@ -0,0 +1,52 @@
+require "cases/helper"
+require 'models/mass_assignment_specific'
+
+class MassAssignmentSecurityTest < ActiveModel::TestCase
+
+ def test_attribute_protection
+ user = User.new
+ expected = { "name" => "John Smith", "email" => "john@smith.com" }
+ sanitized = user.sanitize_for_mass_assignment(expected.merge("admin" => true))
+ assert_equal expected, sanitized
+ end
+
+ def test_attributes_accessible
+ user = Person.new
+ expected = { "name" => "John Smith", "email" => "john@smith.com" }
+ sanitized = user.sanitize_for_mass_assignment(expected.merge("super_powers" => true))
+ assert_equal expected, sanitized
+ end
+
+ def test_attributes_protected_by_default
+ firm = Firm.new
+ expected = { }
+ sanitized = firm.sanitize_for_mass_assignment({ "type" => "Client" })
+ assert_equal expected, sanitized
+ end
+
+ def test_mass_assignment_protection_inheritance
+ assert LoosePerson.accessible_attributes.blank?
+ assert_equal Set.new([ 'credit_rating', 'administrator']), LoosePerson.protected_attributes
+
+ assert LooseDescendant.accessible_attributes.blank?
+ assert_equal Set.new([ 'credit_rating', 'administrator', 'phone_number']), LooseDescendant.protected_attributes
+
+ assert LooseDescendantSecond.accessible_attributes.blank?
+ assert_equal Set.new([ 'credit_rating', 'administrator', 'phone_number', 'name']), LooseDescendantSecond.protected_attributes,
+ 'Running attr_protected twice in one class should merge the protections'
+
+ assert (TightPerson.protected_attributes - TightPerson.attributes_protected_by_default).blank?
+ assert_equal Set.new([ 'name', 'address' ]), TightPerson.accessible_attributes
+
+ assert (TightDescendant.protected_attributes - TightDescendant.attributes_protected_by_default).blank?
+ assert_equal Set.new([ 'name', 'address', 'phone_number' ]), TightDescendant.accessible_attributes
+ end
+
+ def test_mass_assignment_multiparameter_protector
+ task = Task.new
+ attributes = { "starting(1i)" => "2004", "starting(2i)" => "6", "starting(3i)" => "24" }
+ sanitized = task.sanitize_for_mass_assignment(attributes)
+ assert_equal sanitized, { }
+ end
+
+end \ No newline at end of file
diff --git a/activemodel/test/models/mass_assignment_specific.rb b/activemodel/test/models/mass_assignment_specific.rb
new file mode 100644
index 0000000000..2a8fe170c2
--- /dev/null
+++ b/activemodel/test/models/mass_assignment_specific.rb
@@ -0,0 +1,57 @@
+class User
+ include ActiveModel::MassAssignmentSecurity
+ attr_protected :admin
+
+ public :sanitize_for_mass_assignment
+end
+
+class Person
+ include ActiveModel::MassAssignmentSecurity
+ attr_accessible :name, :email
+
+ public :sanitize_for_mass_assignment
+end
+
+class Firm
+ include ActiveModel::MassAssignmentSecurity
+
+ public :sanitize_for_mass_assignment
+
+ def self.attributes_protected_by_default
+ ["type"]
+ end
+end
+
+class Task
+ include ActiveModel::MassAssignmentSecurity
+ attr_protected :starting
+
+ public :sanitize_for_mass_assignment
+end
+
+class LoosePerson
+ include ActiveModel::MassAssignmentSecurity
+ attr_protected :credit_rating, :administrator
+end
+
+class LooseDescendant < LoosePerson
+ attr_protected :phone_number
+end
+
+class LooseDescendantSecond< LoosePerson
+ attr_protected :phone_number
+ attr_protected :name
+end
+
+class TightPerson
+ include ActiveModel::MassAssignmentSecurity
+ attr_accessible :name, :address
+
+ def self.attributes_protected_by_default
+ ["mobile_number"]
+ end
+end
+
+class TightDescendant < TightPerson
+ attr_accessible :phone_number
+end \ No newline at end of file
diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb
index f13c250ca4..cbec5789fd 100644
--- a/activerecord/lib/active_record/association_preload.rb
+++ b/activerecord/lib/active_record/association_preload.rb
@@ -282,7 +282,12 @@ module ActiveRecord
end
end
else
- records.first.class.preload_associations(records, through_association)
+ options = {}
+ options[:include] = reflection.options[:include] || reflection.options[:source] if reflection.options[:conditions]
+ options[:order] = reflection.options[:order]
+ options[:conditions] = reflection.options[:conditions]
+ records.first.class.preload_associations(records, through_association, options)
+
records.each do |record|
through_records.concat Array.wrap(record.send(through_association))
end
diff --git a/activerecord/lib/active_record/associations/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/base.rb b/activerecord/lib/active_record/base.rb
index 3f1015dd4b..c78060c956 100644
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -24,7 +24,7 @@ require 'active_record/errors'
require 'active_record/log_subscriber'
module ActiveRecord #:nodoc:
- # = Active Record
+ # = Active Record
#
# Active Record objects don't specify their attributes directly, but rather infer them from the table definition with
# which they're linked. Adding, removing, and changing attributes and their type is done directly in the database. Any change
@@ -476,112 +476,16 @@ module ActiveRecord #:nodoc:
connection.select_value(sql, "#{name} Count").to_i
end
- # Attributes named in this macro are protected from mass-assignment,
- # such as <tt>new(attributes)</tt>,
- # <tt>update_attributes(attributes)</tt>, or
- # <tt>attributes=(attributes)</tt>.
- #
- # Mass-assignment to these attributes will simply be ignored, to assign
- # to them you can use direct writer methods. This is meant to protect
- # sensitive attributes from being overwritten by malicious users
- # tampering with URLs or forms.
- #
- # class Customer < ActiveRecord::Base
- # attr_protected :credit_rating
- # end
- #
- # customer = Customer.new("name" => David, "credit_rating" => "Excellent")
- # customer.credit_rating # => nil
- # customer.attributes = { "description" => "Jolly fellow", "credit_rating" => "Superb" }
- # customer.credit_rating # => nil
- #
- # customer.credit_rating = "Average"
- # customer.credit_rating # => "Average"
- #
- # To start from an all-closed default and enable attributes as needed,
- # have a look at +attr_accessible+.
- #
- # If the access logic of your application is richer you can use <tt>Hash#except</tt>
- # or <tt>Hash#slice</tt> to sanitize the hash of parameters before they are
- # passed to Active Record.
- #
- # For example, it could be the case that the list of protected attributes
- # for a given model depends on the role of the user:
- #
- # # Assumes plan_id is not protected because it depends on the role.
- # params[:account] = params[:account].except(:plan_id) unless admin?
- # @account.update_attributes(params[:account])
- #
- # Note that +attr_protected+ is still applied to the received hash. Thus,
- # with this technique you can at most _extend_ the list of protected
- # attributes for a particular mass-assignment call.
- def attr_protected(*attributes)
- write_inheritable_attribute(:attr_protected, Set.new(attributes.map {|a| a.to_s}) + (protected_attributes || []))
- end
-
- # Returns an array of all the attributes that have been protected from mass-assignment.
- def protected_attributes # :nodoc:
- read_inheritable_attribute(:attr_protected)
+ # 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
- # Specifies a white list of model attributes that can be set via
- # mass-assignment, such as <tt>new(attributes)</tt>,
- # <tt>update_attributes(attributes)</tt>, or
- # <tt>attributes=(attributes)</tt>
- #
- # This is the opposite of the +attr_protected+ macro: Mass-assignment
- # will only set attributes in this list, to assign to the rest of
- # attributes you can use direct writer methods. This is meant to protect
- # sensitive attributes from being overwritten by malicious users
- # tampering with URLs or forms. If you'd rather start from an all-open
- # default and restrict attributes as needed, have a look at
- # +attr_protected+.
- #
- # class Customer < ActiveRecord::Base
- # attr_accessible :name, :nickname
- # end
- #
- # customer = Customer.new(:name => "David", :nickname => "Dave", :credit_rating => "Excellent")
- # customer.credit_rating # => nil
- # customer.attributes = { :name => "Jolly fellow", :credit_rating => "Superb" }
- # customer.credit_rating # => nil
- #
- # customer.credit_rating = "Average"
- # customer.credit_rating # => "Average"
- #
- # If the access logic of your application is richer you can use <tt>Hash#except</tt>
- # or <tt>Hash#slice</tt> to sanitize the hash of parameters before they are
- # passed to Active Record.
- #
- # For example, it could be the case that the list of accessible attributes
- # for a given model depends on the role of the user:
- #
- # # Assumes plan_id is accessible because it depends on the role.
- # params[:account] = params[:account].except(:plan_id) unless admin?
- # @account.update_attributes(params[:account])
- #
- # Note that +attr_accessible+ is still applied to the received hash. Thus,
- # with this technique you can at most _narrow_ the list of accessible
- # attributes for a particular mass-assignment call.
- def attr_accessible(*attributes)
- write_inheritable_attribute(:attr_accessible, Set.new(attributes.map(&:to_s)) + (accessible_attributes || []))
+ # Returns an array of all the attributes that have been specified as readonly.
+ def readonly_attributes
+ read_inheritable_attribute(:attr_readonly) || []
end
- # Returns an array of all the attributes that have been made accessible to mass-assignment.
- def accessible_attributes # :nodoc:
- read_inheritable_attribute(:attr_accessible)
- end
-
- # Attributes listed as readonly can be set for a new record, but will be ignored in database updates afterwards.
- def attr_readonly(*attributes)
- write_inheritable_attribute(:attr_readonly, Set.new(attributes.map(&:to_s)) + (readonly_attributes || []))
- end
-
- # Returns an array of all the attributes that have been specified as readonly.
- def readonly_attributes
- read_inheritable_attribute(:attr_readonly) || []
- end
-
# If you have an attribute that needs to be saved to the database as an object, and retrieved as the same object,
# then specify the name of that attribute using this method and it will be handled automatically.
# The serialization is done through YAML. If +class_name+ is specified, the serialized object must be of that
@@ -1248,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'"
@@ -1572,11 +1471,11 @@ MSG
# user.send(:attributes=, { :username => 'Phusion', :is_admin => true }, false)
# user.is_admin? # => true
def attributes=(new_attributes, guard_protected_attributes = true)
- return if new_attributes.nil?
+ return unless new_attributes.is_a? Hash
attributes = new_attributes.stringify_keys
multi_parameter_attributes = []
- attributes = remove_attributes_protected_from_mass_assignment(attributes) if guard_protected_attributes
+ attributes = sanitize_for_mass_assignment(attributes) if guard_protected_attributes
attributes.each do |k, v|
if k.include?("(")
@@ -1716,46 +1615,10 @@ MSG
end
end
- def remove_attributes_protected_from_mass_assignment(attributes)
- safe_attributes =
- if self.class.accessible_attributes.nil? && self.class.protected_attributes.nil?
- attributes.reject { |key, value| attributes_protected_by_default.include?(key.gsub(/\(.+/, "")) }
- elsif self.class.protected_attributes.nil?
- attributes.reject { |key, value| !self.class.accessible_attributes.include?(key.gsub(/\(.+/, "")) || attributes_protected_by_default.include?(key.gsub(/\(.+/, "")) }
- elsif self.class.accessible_attributes.nil?
- attributes.reject { |key, value| self.class.protected_attributes.include?(key.gsub(/\(.+/,"")) || attributes_protected_by_default.include?(key.gsub(/\(.+/, "")) }
- else
- raise "Declare either attr_protected or attr_accessible for #{self.class}, but not both."
- end
-
- removed_attributes = attributes.keys - safe_attributes.keys
-
- if removed_attributes.any?
- log_protected_attribute_removal(removed_attributes)
- end
-
- safe_attributes
- end
-
- # Removes attributes which have been marked as readonly.
- def remove_readonly_attributes(attributes)
- unless self.class.readonly_attributes.nil?
- attributes.delete_if { |key, value| self.class.readonly_attributes.include?(key.gsub(/\(.+/,"")) }
- else
- attributes
- end
- end
-
- def log_protected_attribute_removal(*attributes)
- if logger
- logger.debug "WARNING: Can't mass-assign these protected attributes: #{attributes.join(', ')}"
- end
- end
-
# The primary key and inheritance column can never be set by mass-assignment for security reasons.
- def attributes_protected_by_default
- default = [ self.class.primary_key, self.class.inheritance_column ]
- default << 'id' unless self.class.primary_key.eql? 'id'
+ def self.attributes_protected_by_default
+ default = [ primary_key, inheritance_column ]
+ default << 'id' unless primary_key.eql? 'id'
default
end
@@ -1920,6 +1783,7 @@ MSG
include AttributeMethods::PrimaryKey
include AttributeMethods::TimeZoneConversion
include AttributeMethods::Dirty
+ include ActiveModel::MassAssignmentSecurity
include Callbacks, ActiveModel::Observing, Timestamp
include Associations, AssociationPreload, NamedScope
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
index 0d9a86a1ea..e5e92f2b1c 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -4,7 +4,17 @@ module ActiveRecord
class Base
# sqlite3 adapter reuses sqlite_connection.
def self.sqlite3_connection(config) # :nodoc:
- parse_sqlite_config!(config)
+ # Require database.
+ unless config[:database]
+ raise ArgumentError, "No database file specified. Missing argument: database"
+ end
+
+ # Allow database path relative to Rails.root, but only if
+ # the database path is not the special path that tells
+ # Sqlite to build a database only in memory.
+ if defined?(Rails.root) && ':memory:' != config[:database]
+ config[:database] = File.expand_path(config[:database], Rails.root)
+ end
unless 'sqlite3' == config[:adapter]
raise ArgumentError, 'adapter name should be "sqlite3"'
@@ -16,8 +26,7 @@ module ActiveRecord
db = SQLite3::Database.new(
config[:database],
- :results_as_hash => true,
- :type_translation => false
+ :results_as_hash => true
)
db.busy_timeout(config[:timeout]) unless config[:timeout].nil?
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
index 1927585c49..117cf447df 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
@@ -2,25 +2,6 @@ require 'active_record/connection_adapters/abstract_adapter'
require 'active_support/core_ext/kernel/requires'
module ActiveRecord
- class Base
- class << self
- private
- def parse_sqlite_config!(config)
- # Require database.
- unless config[:database]
- raise ArgumentError, "No database file specified. Missing argument: database"
- end
-
- # Allow database path relative to Rails.root, but only if
- # the database path is not the special path that tells
- # Sqlite to build a database only in memory.
- if defined?(Rails.root) && ':memory:' != config[:database]
- config[:database] = File.expand_path(config[:database], Rails.root)
- end
- end
- end
- end
-
module ConnectionAdapters #:nodoc:
class SQLiteColumn < Column #:nodoc:
class << self
diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb
index 999322129a..237cd56683 100644
--- a/activerecord/lib/active_record/counter_cache.rb
+++ b/activerecord/lib/active_record/counter_cache.rb
@@ -17,9 +17,18 @@ module ActiveRecord
def reset_counters(id, *counters)
object = find(id)
counters.each do |association|
- child_class = reflect_on_association(association.to_sym).klass
- belongs_name = self.name.demodulize.underscore.to_sym
- counter_name = child_class.reflect_on_association(belongs_name).counter_cache_column
+ has_many_association = reflect_on_association(association.to_sym)
+
+ expected_name = if has_many_association.options[:as]
+ has_many_association.options[:as].to_s.classify
+ else
+ self.name
+ end
+
+ child_class = has_many_association.klass
+ belongs_to = child_class.reflect_on_all_associations(:belongs_to)
+ reflection = belongs_to.find { |e| e.class_name == expected_name }
+ counter_name = reflection.counter_cache_column
self.unscoped.where(arel_table[self.primary_key].eq(object.id)).arel.update({
arel_table[counter_name] => object.send(association).count
@@ -103,4 +112,4 @@ module ActiveRecord
update_counters(id, counter_name => -1)
end
end
-end \ No newline at end of file
+end
diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb
index 50166c4385..828a8b41b6 100644
--- a/activerecord/lib/active_record/persistence.rb
+++ b/activerecord/lib/active_record/persistence.rb
@@ -102,12 +102,21 @@ module ActiveRecord
became
end
- # Updates a single attribute and saves the record without going through the normal validation procedure.
- # This is especially useful for boolean flags on existing records. The regular +update_attribute+ method
- # in Base is replaced with this when the validations module is mixed in, which it is by default.
+ # Updates a single attribute and saves the record without going through the normal validation procedure
+ # or callbacks. This is especially useful for boolean flags on existing records.
def update_attribute(name, value)
send("#{name}=", value)
- save(:validate => false)
+ hash = { name => read_attribute(name) }
+
+ if record_update_timestamps
+ timestamp_attributes_for_update_in_model.each do |column|
+ hash[column] = read_attribute(column)
+ end
+ end
+
+ @changed_attributes.delete(name.to_s)
+ primary_key = self.class.primary_key
+ self.class.update_all(hash, { primary_key => self[primary_key] }) == 1
end
# Updates all the attributes from the passed-in Hash and saves the record.
@@ -234,4 +243,4 @@ module ActiveRecord
end
end
end
-end \ No newline at end of file
+end
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index bc708b573f..d9fc1b4940 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -213,8 +213,7 @@ module ActiveRecord
if conditions
where(conditions).destroy_all
else
- to_a.each {|object| object.destroy}
- reset
+ to_a.each {|object| object.destroy }.tap { reset }
end
end
diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb
index f39951e16c..3bf4c5bdd1 100644
--- a/activerecord/lib/active_record/relation/finder_methods.rb
+++ b/activerecord/lib/active_record/relation/finder_methods.rb
@@ -339,7 +339,7 @@ module ActiveRecord
end
def using_limitable_reflections?(reflections)
- reflections.collect(&:collection?).length.zero?
+ reflections.none?(&:collection?)
end
end
diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb
index ffd12d2082..1075a60f07 100644
--- a/activerecord/lib/active_record/timestamp.rb
+++ b/activerecord/lib/active_record/timestamp.rb
@@ -35,8 +35,7 @@ module ActiveRecord
if attribute
write_attribute(attribute, current_time)
else
- write_attribute('updated_at', current_time) if respond_to?(:updated_at)
- write_attribute('updated_on', current_time) if respond_to?(:updated_on)
+ timestamp_attributes_for_update_in_model.each { |column| write_attribute(column.to_s, current_time) }
end
save!
@@ -50,26 +49,36 @@ module ActiveRecord
write_attribute('created_at', current_time) if respond_to?(:created_at) && created_at.nil?
write_attribute('created_on', current_time) if respond_to?(:created_on) && created_on.nil?
- write_attribute('updated_at', current_time) if respond_to?(:updated_at) && updated_at.nil?
- write_attribute('updated_on', current_time) if respond_to?(:updated_on) && updated_on.nil?
+ timestamp_attributes_for_update_in_model.each do |column|
+ write_attribute(column.to_s, current_time) if self.send(column).nil?
+ end
end
super
end
def update(*args) #:nodoc:
+ record_update_timestamps
+ super
+ end
+
+ def record_update_timestamps
if record_timestamps && (!partial_updates? || changed?)
current_time = current_time_from_proper_timezone
-
- write_attribute('updated_at', current_time) if respond_to?(:updated_at)
- write_attribute('updated_on', current_time) if respond_to?(:updated_on)
+ timestamp_attributes_for_update_in_model.each { |column| write_attribute(column.to_s, current_time) }
+ true
+ else
+ false
end
+ end
- super
+ def timestamp_attributes_for_update_in_model #:nodoc:
+ [:updated_at, :updated_on].select { |elem| respond_to?(elem) }
end
- def current_time_from_proper_timezone
+ def current_time_from_proper_timezone #:nodoc:
self.class.default_timezone == :utc ? Time.now.utc : Time.now
end
end
-end \ No newline at end of file
+end
+
diff --git a/activerecord/test/cases/adapters/sqlite/sqlite_adapter_test.rb b/activerecord/test/cases/adapters/sqlite/sqlite_adapter_test.rb
new file mode 100644
index 0000000000..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
diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb
index 445e6889c0..40859d425f 100644
--- a/activerecord/test/cases/associations/eager_test.rb
+++ b/activerecord/test/cases/associations/eager_test.rb
@@ -357,6 +357,12 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
end
+ def test_eager_with_has_many_through_with_conditions_join_model_with_include
+ post_tags = Post.find(posts(:welcome).id).misc_tags
+ eager_post_tags = Post.find(1, :include => :misc_tags).misc_tags
+ assert_equal post_tags, eager_post_tags
+ end
+
def test_eager_with_has_many_and_limit
posts = Post.find(:all, :order => 'posts.id asc', :include => [ :author, :comments ], :limit => 2)
assert_equal 2, posts.size
diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb
index 004d0156e1..b11969a841 100644
--- a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb
+++ b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb
@@ -684,8 +684,8 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
end
def test_find_scoped_grouped
- assert_equal 4, categories(:general).posts_gruoped_by_title.size
- assert_equal 1, categories(:technology).posts_gruoped_by_title.size
+ assert_equal 4, categories(:general).posts_grouped_by_title.size
+ assert_equal 1, categories(:technology).posts_grouped_by_title.size
end
def test_find_scoped_grouped_having
diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb
index 5e3ba778f3..a52cedd8c2 100644
--- a/activerecord/test/cases/associations/has_many_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_associations_test.rb
@@ -817,8 +817,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_destroy_all
force_signal37_to_load_all_clients_of_firm
- assert !companies(:first_firm).clients_of_firm.empty?, "37signals has clients after load"
- companies(:first_firm).clients_of_firm.destroy_all
+ clients = companies(:first_firm).clients_of_firm.to_a
+ assert !clients.empty?, "37signals has clients after load"
+ destroyed = companies(:first_firm).clients_of_firm.destroy_all
+ assert_equal clients.sort_by(&:id), destroyed.sort_by(&:id)
+ assert destroyed.all? { |client| client.frozen? }, "destroyed clients should be frozen"
assert companies(:first_firm).clients_of_firm.empty?, "37signals has no clients after destroy all"
assert companies(:first_firm).clients_of_firm(true).empty?, "37signals has no clients after destroy all and refresh"
end
diff --git a/activerecord/test/cases/associations/has_one_through_associations_test.rb b/activerecord/test/cases/associations/has_one_through_associations_test.rb
index 9aef3eb374..178c57435b 100644
--- a/activerecord/test/cases/associations/has_one_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb
@@ -65,10 +65,6 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
assert_equal clubs(:moustache_club), @member.sponsor_club
end
- def has_one_through_to_has_many
- assert_equal 2, @member.fellow_members.size
- end
-
def test_has_one_through_eager_loading
members = assert_queries(3) do #base table, through table, clubs table
Member.find(:all, :include => :club, :conditions => ["name = ?", "Groucho Marx"])
diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb
index d99fb44f01..4ae776c35a 100644
--- a/activerecord/test/cases/associations_test.rb
+++ b/activerecord/test/cases/associations_test.rb
@@ -65,6 +65,16 @@ class AssociationsTest < ActiveRecord::TestCase
assert_equal 1, firm.clients(true).size, "New firm should have reloaded clients count"
end
+ def test_using_limitable_reflections_helper
+ using_limitable_reflections = lambda { |reflections| Tagging.scoped.send :using_limitable_reflections?, reflections }
+ belongs_to_reflections = [Tagging.reflect_on_association(:tag), Tagging.reflect_on_association(:super_tag)]
+ has_many_reflections = [Tag.reflect_on_association(:taggings), Developer.reflect_on_association(:projects)]
+ mixed_reflections = (belongs_to_reflections + has_many_reflections).uniq
+ assert using_limitable_reflections.call(belongs_to_reflections), "Belong to associations are limitable"
+ assert !using_limitable_reflections.call(has_many_reflections), "All has many style associations are not limitable"
+ assert !using_limitable_reflections.call(mixed_reflections), "No collection associations (has many style) should pass"
+ end
+
def test_force_reload_is_uncached
firm = Firm.create!("name" => "A New Firm, Inc")
client = Client.create!("name" => "TheClient.com", :firm => firm)
diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb
index ba7db838ca..a4cf5120e1 100644
--- a/activerecord/test/cases/base_test.rb
+++ b/activerecord/test/cases/base_test.rb
@@ -17,6 +17,7 @@ require 'models/comment'
require 'models/minimalistic'
require 'models/warehouse_thing'
require 'models/parrot'
+require 'models/loose_person'
require 'rexml/document'
require 'active_support/core_ext/exception'
@@ -37,46 +38,12 @@ class Computer < ActiveRecord::Base; end
class NonExistentTable < ActiveRecord::Base; end
class TestOracleDefault < ActiveRecord::Base; end
-class LoosePerson < ActiveRecord::Base
- self.table_name = 'people'
- self.abstract_class = true
- attr_protected :credit_rating, :administrator
-end
-
-class LooseDescendant < LoosePerson
- attr_protected :phone_number
-end
-
-class LooseDescendantSecond< LoosePerson
- attr_protected :phone_number
- attr_protected :name
-end
-
-class TightPerson < ActiveRecord::Base
- self.table_name = 'people'
- attr_accessible :name, :address
-end
-
-class TightDescendant < TightPerson
- attr_accessible :phone_number
-end
-
class ReadonlyTitlePost < Post
attr_readonly :title
end
class Booleantest < ActiveRecord::Base; end
-class Task < ActiveRecord::Base
- attr_protected :starting
-end
-
-class TopicWithProtectedContentAndAccessibleAuthorName < ActiveRecord::Base
- self.table_name = 'topics'
- attr_accessible :author_name
- attr_protected :content
-end
-
class BasicsTest < ActiveRecord::TestCase
fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :categorizations, :categories, :posts
@@ -94,6 +61,13 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal(topics(:first).author_email_address, Topic.find(1).author_email_address)
end
+ def test_set_attributes_without_hash
+ topic = Topic.new
+ assert_nothing_raised do
+ topic.attributes = ''
+ end
+ end
+
def test_integers_as_nil
test = AutoId.create('value' => '')
assert_nil AutoId.find(test.id).value
@@ -684,16 +658,24 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_destroy_all
- original_count = Topic.count
- topics_by_mary = Topic.count(:conditions => mary = "author_name = 'Mary'")
-
- Topic.destroy_all mary
- assert_equal original_count - topics_by_mary, Topic.count
+ conditions = "author_name = 'Mary'"
+ topics_by_mary = Topic.all(:conditions => conditions, :order => 'id')
+ assert ! topics_by_mary.empty?
+
+ assert_difference('Topic.count', -topics_by_mary.size) do
+ destroyed = Topic.destroy_all(conditions).sort_by(&:id)
+ assert_equal topics_by_mary, destroyed
+ assert destroyed.all? { |topic| topic.frozen? }, "destroyed topics should be frozen"
+ end
end
def test_destroy_many
+ clients = Client.find([2, 3], :order => 'id')
+
assert_difference('Client.count', -2) do
- Client.destroy([2, 3])
+ destroyed = Client.destroy([2, 3]).sort_by(&:id)
+ assert_equal clients, destroyed
+ assert destroyed.all? { |client| client.frozen? }, "destroyed clients should be frozen"
end
end
@@ -918,6 +900,46 @@ class BasicsTest < ActiveRecord::TestCase
assert !Topic.find(1).approved?
end
+ def test_update_attribute_with_one_changed_and_one_updated
+ t = Topic.order('id').limit(1).first
+ title, author_name = t.title, t.author_name
+ t.author_name = 'John'
+ t.update_attribute(:title, 'super_title')
+ assert_equal 'John', t.author_name
+ assert_equal 'super_title', t.title
+ assert t.changed?, "topic should have changed"
+ assert t.author_name_changed?, "author_name should have changed"
+ assert !t.title_changed?, "title should not have changed"
+ assert_nil t.title_change, 'title change should be nil'
+ assert_equal ['author_name'], t.changed
+
+ t.reload
+ assert_equal 'David', t.author_name
+ assert_equal 'super_title', t.title
+ end
+
+ def test_update_attribute_with_one_updated
+ t = Topic.first
+ title = t.title
+ t.update_attribute(:title, 'super_title')
+ assert_equal 'super_title', t.title
+ assert !t.changed?, "topic should not have changed"
+ assert !t.title_changed?, "title should not have changed"
+ assert_nil t.title_change, 'title change should be nil'
+
+ t.reload
+ assert_equal 'super_title', t.title
+ end
+
+ def test_update_attribute_for_udpated_at_on
+ developer = Developer.find(1)
+ updated_at = developer.updated_at
+ developer.update_attribute(:salary, 80001)
+ assert_not_equal updated_at, developer.updated_at
+ developer.reload
+ assert_not_equal updated_at, developer.updated_at
+ end
+
def test_update_attributes
topic = Topic.find(1)
assert !topic.approved?
@@ -955,88 +977,6 @@ class BasicsTest < ActiveRecord::TestCase
Reply.reset_callbacks(:validate)
end
- def test_mass_assignment_should_raise_exception_if_accessible_and_protected_attribute_writers_are_both_used
- topic = TopicWithProtectedContentAndAccessibleAuthorName.new
- assert_raise(RuntimeError) { topic.attributes = { "author_name" => "me" } }
- assert_raise(RuntimeError) { topic.attributes = { "content" => "stuff" } }
- end
-
- def test_mass_assignment_protection
- firm = Firm.new
- firm.attributes = { "name" => "Next Angle", "rating" => 5 }
- assert_equal 1, firm.rating
- end
-
- def test_mass_assignment_protection_against_class_attribute_writers
- [:logger, :configurations, :primary_key_prefix_type, :table_name_prefix, :table_name_suffix, :pluralize_table_names,
- :default_timezone, :schema_format, :lock_optimistically, :record_timestamps].each do |method|
- assert_respond_to Task, method
- assert_respond_to Task, "#{method}="
- assert_respond_to Task.new, method
- assert !Task.new.respond_to?("#{method}=")
- end
- end
-
- def test_customized_primary_key_remains_protected
- subscriber = Subscriber.new(:nick => 'webster123', :name => 'nice try')
- assert_nil subscriber.id
-
- keyboard = Keyboard.new(:key_number => 9, :name => 'nice try')
- assert_nil keyboard.id
- end
-
- def test_customized_primary_key_remains_protected_when_referred_to_as_id
- subscriber = Subscriber.new(:id => 'webster123', :name => 'nice try')
- assert_nil subscriber.id
-
- keyboard = Keyboard.new(:id => 9, :name => 'nice try')
- assert_nil keyboard.id
- end
-
- def test_mass_assigning_invalid_attribute
- firm = Firm.new
-
- assert_raise(ActiveRecord::UnknownAttributeError) do
- firm.attributes = { "id" => 5, "type" => "Client", "i_dont_even_exist" => 20 }
- end
- end
-
- def test_mass_assignment_protection_on_defaults
- firm = Firm.new
- firm.attributes = { "id" => 5, "type" => "Client" }
- assert_nil firm.id
- assert_equal "Firm", firm[:type]
- end
-
- def test_mass_assignment_accessible
- reply = Reply.new("title" => "hello", "content" => "world", "approved" => true)
- reply.save
-
- assert reply.approved?
-
- reply.approved = false
- reply.save
-
- assert !reply.approved?
- end
-
- def test_mass_assignment_protection_inheritance
- assert_nil LoosePerson.accessible_attributes
- assert_equal Set.new([ 'credit_rating', 'administrator' ]), LoosePerson.protected_attributes
-
- assert_nil LooseDescendant.accessible_attributes
- assert_equal Set.new([ 'credit_rating', 'administrator', 'phone_number' ]), LooseDescendant.protected_attributes
-
- assert_nil LooseDescendantSecond.accessible_attributes
- assert_equal Set.new([ 'credit_rating', 'administrator', 'phone_number', 'name' ]), LooseDescendantSecond.protected_attributes, 'Running attr_protected twice in one class should merge the protections'
-
- assert_nil TightPerson.protected_attributes
- assert_equal Set.new([ 'name', 'address' ]), TightPerson.accessible_attributes
-
- assert_nil TightDescendant.protected_attributes
- assert_equal Set.new([ 'name', 'address', 'phone_number' ]), TightDescendant.accessible_attributes
- end
-
def test_readonly_attributes
assert_equal Set.new([ 'title' , 'comments_count' ]), ReadonlyTitlePost.readonly_attributes
@@ -1239,15 +1179,6 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on
end
- def test_multiparameter_mass_assignment_protector
- task = Task.new
- time = Time.mktime(2000, 1, 1, 1)
- task.starting = time
- attributes = { "starting(1i)" => "2004", "starting(2i)" => "6", "starting(3i)" => "24" }
- task.attributes = attributes
- assert_equal time, task.starting
- end
-
def test_multiparameter_assignment_of_aggregation
customer = Customer.new
address = Address.new("The Street", "The City", "The Country")
diff --git a/activerecord/test/cases/counter_cache_test.rb b/activerecord/test/cases/counter_cache_test.rb
index 377de168b9..137236255d 100644
--- a/activerecord/test/cases/counter_cache_test.rb
+++ b/activerecord/test/cases/counter_cache_test.rb
@@ -1,17 +1,20 @@
require 'cases/helper'
require 'models/topic'
+require 'models/car'
+require 'models/wheel'
+require 'models/engine'
require 'models/reply'
require 'models/category'
require 'models/categorization'
class CounterCacheTest < ActiveRecord::TestCase
- fixtures :topics, :categories, :categorizations
+ fixtures :topics, :categories, :categorizations, :cars
- class SpecialTopic < ::Topic
+ class ::SpecialTopic < ::Topic
has_many :special_replies, :foreign_key => 'parent_id'
end
- class SpecialReply < ::Reply
+ class ::SpecialReply < ::Reply
belongs_to :special_topic, :foreign_key => 'parent_id', :counter_cache => 'replies_count'
end
@@ -58,6 +61,16 @@ class CounterCacheTest < ActiveRecord::TestCase
end
end
+ test "reset counter should with belongs_to which has class_name" do
+ car = cars(:honda)
+ assert_nothing_raised do
+ Car.reset_counters(car.id, :engines)
+ end
+ assert_nothing_raised do
+ Car.reset_counters(car.id, :wheels)
+ end
+ end
+
test "update counter with initial null value" do
category = categories(:general)
assert_equal 2, category.categorizations.count
diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb
index 75f7453aa9..837386ed24 100644
--- a/activerecord/test/cases/dirty_test.rb
+++ b/activerecord/test/cases/dirty_test.rb
@@ -475,10 +475,9 @@ class DirtyTest < ActiveRecord::TestCase
pirate = Pirate.find_by_catchphrase("Ahoy!")
pirate.update_attribute(:catchphrase, "Ninjas suck!")
- assert_equal 2, pirate.previous_changes.size
- assert_equal ["Ahoy!", "Ninjas suck!"], pirate.previous_changes['catchphrase']
- assert_not_nil pirate.previous_changes['updated_on'][0]
- assert_not_nil pirate.previous_changes['updated_on'][1]
+ assert_equal 0, pirate.previous_changes.size
+ assert_nil pirate.previous_changes['catchphrase']
+ assert_nil pirate.previous_changes['updated_on']
assert !pirate.previous_changes.key?('parrot_id')
assert !pirate.previous_changes.key?('created_on')
end
diff --git a/activerecord/test/cases/mass_assignment_security_test.rb b/activerecord/test/cases/mass_assignment_security_test.rb
new file mode 100644
index 0000000000..025ec1d3fa
--- /dev/null
+++ b/activerecord/test/cases/mass_assignment_security_test.rb
@@ -0,0 +1,43 @@
+require "cases/helper"
+require 'models/company'
+require 'models/subscriber'
+require 'models/keyboard'
+require 'models/task'
+
+class MassAssignmentSecurityTest < ActiveRecord::TestCase
+
+ def test_customized_primary_key_remains_protected
+ subscriber = Subscriber.new(:nick => 'webster123', :name => 'nice try')
+ assert_nil subscriber.id
+
+ keyboard = Keyboard.new(:key_number => 9, :name => 'nice try')
+ assert_nil keyboard.id
+ end
+
+ def test_customized_primary_key_remains_protected_when_referred_to_as_id
+ subscriber = Subscriber.new(:id => 'webster123', :name => 'nice try')
+ assert_nil subscriber.id
+
+ keyboard = Keyboard.new(:id => 9, :name => 'nice try')
+ assert_nil keyboard.id
+ end
+
+ def test_mass_assigning_invalid_attribute
+ firm = Firm.new
+
+ assert_raise(ActiveRecord::UnknownAttributeError) do
+ firm.attributes = { "id" => 5, "type" => "Client", "i_dont_even_exist" => 20 }
+ end
+ end
+
+ def test_protection_against_class_attribute_writers
+ [:logger, :configurations, :primary_key_prefix_type, :table_name_prefix, :table_name_suffix, :pluralize_table_names,
+ :default_timezone, :schema_format, :lock_optimistically, :record_timestamps].each do |method|
+ assert_respond_to Task, method
+ assert_respond_to Task, "#{method}="
+ assert_respond_to Task.new, method
+ assert !Task.new.respond_to?("#{method}=")
+ end
+ end
+
+end \ No newline at end of file
diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb
index 4ce9bdb46d..2c3fc46831 100644
--- a/activerecord/test/cases/migration_test.rb
+++ b/activerecord/test/cases/migration_test.rb
@@ -1358,6 +1358,20 @@ if ActiveRecord::Base.connection.supports_migrations?
ActiveRecord::Migrator.forward(MIGRATIONS_ROOT + "/valid")
assert_equal(3, ActiveRecord::Migrator.current_version)
end
+
+ def test_get_all_versions
+ ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/valid")
+ assert_equal([1,2,3], ActiveRecord::Migrator.get_all_versions)
+
+ ActiveRecord::Migrator.rollback(MIGRATIONS_ROOT + "/valid")
+ assert_equal([1,2], ActiveRecord::Migrator.get_all_versions)
+
+ ActiveRecord::Migrator.rollback(MIGRATIONS_ROOT + "/valid")
+ assert_equal([1], ActiveRecord::Migrator.get_all_versions)
+
+ ActiveRecord::Migrator.rollback(MIGRATIONS_ROOT + "/valid")
+ assert_equal([], ActiveRecord::Migrator.get_all_versions)
+ end
def test_schema_migrations_table_name
ActiveRecord::Base.table_name_prefix = "prefix_"
diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb
index 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
diff --git a/activerecord/test/fixtures/cars.yml b/activerecord/test/fixtures/cars.yml
new file mode 100644
index 0000000000..23c98e8144
--- /dev/null
+++ b/activerecord/test/fixtures/cars.yml
@@ -0,0 +1,4 @@
+honda:
+ id: 1
+ name: honda
+ engines_count: 0
diff --git a/activerecord/test/models/car.rb b/activerecord/test/models/car.rb
new file mode 100644
index 0000000000..1101180a67
--- /dev/null
+++ b/activerecord/test/models/car.rb
@@ -0,0 +1,4 @@
+class Car < ActiveRecord::Base
+ has_many :engines
+ has_many :wheels, :as => :wheelable
+end
diff --git a/activerecord/test/models/category.rb b/activerecord/test/models/category.rb
index 5efce6aaa6..48415846dd 100644
--- a/activerecord/test/models/category.rb
+++ b/activerecord/test/models/category.rb
@@ -15,7 +15,7 @@ class Category < ActiveRecord::Base
:conditions => { :title => 'Yet Another Testing Title' }
has_and_belongs_to_many :popular_grouped_posts, :class_name => "Post", :group => "posts.type", :having => "sum(comments.post_id) > 2", :include => :comments
- has_and_belongs_to_many :posts_gruoped_by_title, :class_name => "Post", :group => "title", :select => "title"
+ has_and_belongs_to_many :posts_grouped_by_title, :class_name => "Post", :group => "title", :select => "title"
def self.what_are_you
'a category...'
diff --git a/activerecord/test/models/engine.rb b/activerecord/test/models/engine.rb
new file mode 100644
index 0000000000..751c3f02d1
--- /dev/null
+++ b/activerecord/test/models/engine.rb
@@ -0,0 +1,3 @@
+class Engine < ActiveRecord::Base
+ belongs_to :my_car, :class_name => 'Car', :foreign_key => 'car_id', :counter_cache => :engines_count
+end
diff --git a/activerecord/test/models/loose_person.rb b/activerecord/test/models/loose_person.rb
new file mode 100644
index 0000000000..256c281d0d
--- /dev/null
+++ b/activerecord/test/models/loose_person.rb
@@ -0,0 +1,24 @@
+class LoosePerson < ActiveRecord::Base
+ self.table_name = 'people'
+ self.abstract_class = true
+
+ attr_protected :credit_rating, :administrator
+end
+
+class LooseDescendant < LoosePerson
+ attr_protected :phone_number
+end
+
+class LooseDescendantSecond< LoosePerson
+ attr_protected :phone_number
+ attr_protected :name
+end
+
+class TightPerson < ActiveRecord::Base
+ self.table_name = 'people'
+ attr_accessible :name, :address
+end
+
+class TightDescendant < TightPerson
+ attr_accessible :phone_number
+end \ No newline at end of file
diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb
index dd06822cfd..6c7b93be87 100644
--- a/activerecord/test/models/post.rb
+++ b/activerecord/test/models/post.rb
@@ -58,6 +58,7 @@ class Post < ActiveRecord::Base
end
end
+ has_many :misc_tags, :through => :taggings, :source => :tag, :conditions => "tags.name = 'Misc'"
has_many :funky_tags, :through => :taggings, :source => :tag
has_many :super_tags, :through => :taggings
has_one :tagging, :as => :taggable
diff --git a/activerecord/test/models/wheel.rb b/activerecord/test/models/wheel.rb
new file mode 100644
index 0000000000..26868bce5e
--- /dev/null
+++ b/activerecord/test/models/wheel.rb
@@ -0,0 +1,3 @@
+class Wheel < ActiveRecord::Base
+ belongs_to :wheelable, :polymorphic => true, :counter_cache => true
+end
diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb
index b212e7cff2..bea351b95a 100644
--- a/activerecord/test/schema/schema.rb
+++ b/activerecord/test/schema/schema.rb
@@ -82,6 +82,12 @@ ActiveRecord::Schema.define do
t.string :name
end
+ create_table :cars, :force => true do |t|
+ t.string :name
+ t.integer :engines_count
+ t.integer :wheels_count
+ end
+
create_table :categories, :force => true do |t|
t.string :name, :null => false
t.string :type
@@ -179,6 +185,9 @@ ActiveRecord::Schema.define do
end
add_index :edges, [:source_id, :sink_id], :unique => true, :name => 'unique_edge_index'
+ create_table :engines, :force => true do |t|
+ t.integer :car_id
+ end
create_table :entrants, :force => true do |t|
t.string :name, :null => false
@@ -566,6 +575,10 @@ ActiveRecord::Schema.define do
t.integer :zine_id
end
+ create_table :wheels, :force => true do |t|
+ t.references :wheelable, :polymorphic => true
+ end
+
create_table :zines, :force => true do |t|
t.string :title
end
diff --git a/activesupport/lib/active_support/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 <tt>String#slice!</tt>, except instead of byte offsets you specify character offsets.
- #
- # Example:
- # s = 'こんにちは'
- # s.mb_chars.slice!(2..3).to_s #=> "にち"
- # s #=> "こんは"
- def slice!(*args)
- slice = self[*args]
- self[*args] = ''
- slice
- end
-
# Limit the byte size of the string to a number of bytes without breaking characters. Usable
# when the storage for a string is limited for some reason.
#
@@ -425,14 +413,14 @@ module ActiveSupport #:nodoc:
chars(Unicode.tidy_bytes(@wrapped_string, force))
end
- %w(lstrip rstrip strip reverse upcase downcase tidy_bytes capitalize).each do |method|
- define_method("#{method}!") do |*args|
- unless args.nil?
- @wrapped_string = send(method, *args).to_s
- else
- @wrapped_string = send(method).to_s
+ %w(capitalize downcase lstrip reverse rstrip slice strip tidy_bytes upcase).each do |method|
+ # Only define a corresponding bang method for methods defined in the proxy; On 1.9 the proxy will
+ # exclude lstrip!, rstrip! and strip! because they are already work as expected on multibyte strings.
+ if public_method_defined?(method)
+ define_method("#{method}!") do |*args|
+ @wrapped_string = send(args.nil? ? method : method, *args).to_s
+ self
end
- self
end
end
diff --git a/activesupport/test/multibyte_chars_test.rb b/activesupport/test/multibyte_chars_test.rb
index 610295fa89..78232d8eb5 100644
--- a/activesupport/test/multibyte_chars_test.rb
+++ b/activesupport/test/multibyte_chars_test.rb
@@ -123,22 +123,30 @@ class MultibyteCharsUTF8BehaviourTest < Test::Unit::TestCase
assert_equal 'こに わ', @chars
end
- def test_overridden_bang_methods_return_self
- [:rstrip!, :lstrip!, :strip!, :reverse!, :upcase!, :downcase!, :capitalize!].each do |method|
- assert_equal @chars.object_id, @chars.send(method).object_id
- end
+ %w{capitalize downcase lstrip reverse rstrip strip upcase}.each do |method|
+ class_eval(<<-EOTESTS)
+ def test_#{method}_bang_should_return_self
+ assert_equal @chars.object_id, @chars.send("#{method}!").object_id
+ end
+
+ def test_#{method}_bang_should_change_wrapped_string
+ original = ' él piDió Un bUen café '
+ proxy = chars(original.dup)
+ proxy.send("#{method}!")
+ assert_not_equal original, proxy.to_s
+ end
+ EOTESTS
end
- def test_overridden_bang_methods_change_wrapped_string
- [:rstrip!, :lstrip!, :strip!, :reverse!, :upcase!, :downcase!].each do |method|
- original = ' Café '
- proxy = chars(original.dup)
- proxy.send(method)
- assert_not_equal original, proxy.to_s
- end
- proxy = chars('òu')
- proxy.capitalize!
- assert_equal 'Òu', proxy.to_s
+ def test_tidy_bytes_bang_should_return_self
+ assert_equal @chars.object_id, @chars.tidy_bytes!.object_id
+ end
+
+ def test_tidy_bytes_bang_should_change_wrapped_string
+ original = " Un bUen café \x92"
+ proxy = chars(original.dup)
+ proxy.tidy_bytes!
+ assert_not_equal original, proxy.to_s
end
if RUBY_VERSION >= '1.9'
@@ -417,8 +425,9 @@ class MultibyteCharsUTF8BehaviourTest < Test::Unit::TestCase
end
def test_slice_bang_removes_the_slice_from_the_receiver
- @chars.slice!(1..2)
- assert_equal 'こわ', @chars
+ chars = 'úüù'.mb_chars
+ chars.slice!(0,2)
+ assert_equal 'úü', chars
end
def test_slice_should_throw_exceptions_on_invalid_arguments
diff --git a/railties/lib/rails/commands/plugin.rb b/railties/lib/rails/commands/plugin.rb
index 8bcd92a33b..96b6f9c372 100644
--- a/railties/lib/rails/commands/plugin.rb
+++ b/railties/lib/rails/commands/plugin.rb
@@ -3,7 +3,7 @@
# Installing plugins:
#
# $ rails plugin install continuous_builder asset_timestamping
-#
+#
# Specifying revisions:
#
# * Subversion revision is a single integer.
@@ -14,12 +14,11 @@
# 'tag 1.8.0' (equivalent to 'refs/tags/1.8.0')
#
#
-# This is Free Software, copyright 2005 by Ryan Tomayko (rtomayko@gmail.com)
+# This is Free Software, copyright 2005 by Ryan Tomayko (rtomayko@gmail.com)
# and is licensed MIT: (http://www.opensource.org/licenses/mit-license.php)
$verbose = false
-
require 'open-uri'
require 'fileutils'
require 'tempfile'
@@ -40,18 +39,18 @@ class RailsEnvironment
dir = File.dirname(dir)
end
end
-
+
def self.default
@default ||= find
end
-
+
def self.default=(rails_env)
@default = rails_env
end
-
+
def install(name_uri_or_plugin)
if name_uri_or_plugin.is_a? String
- if name_uri_or_plugin =~ /:\/\//
+ if name_uri_or_plugin =~ /:\/\//
plugin = Plugin.new(name_uri_or_plugin)
else
plugin = Plugins[name_uri_or_plugin]
@@ -65,7 +64,7 @@ class RailsEnvironment
puts "Plugin not found: #{name_uri_or_plugin}"
end
end
-
+
def use_svn?
require 'active_support/core_ext/kernel'
silence_stderr {`svn --version` rescue nil}
@@ -97,7 +96,7 @@ class RailsEnvironment
ext = `svn propget svn:externals "#{root}/vendor/plugins"`
lines = ext.respond_to?(:lines) ? ext.lines : ext
lines.reject{ |line| line.strip == '' }.map do |line|
- line.strip.split(/\s+/, 2)
+ line.strip.split(/\s+/, 2)
end
end
@@ -111,38 +110,37 @@ class RailsEnvironment
system("svn propset -q svn:externals -F \"#{file.path}\" \"#{root}/vendor/plugins\"")
end
end
-
end
class Plugin
attr_reader :name, :uri
-
+
def initialize(uri, name = nil)
@uri = uri
guess_name(uri)
end
-
+
def self.find(name)
new(name)
end
-
+
def to_s
"#{@name.ljust(30)}#{@uri}"
end
-
+
def svn_url?
@uri =~ /svn(?:\+ssh)?:\/\/*/
end
-
+
def git_url?
@uri =~ /^git:\/\// || @uri =~ /\.git$/
end
-
+
def installed?
File.directory?("#{rails_env.root}/vendor/plugins/#{name}") \
or rails_env.externals.detect{ |name, repo| self.uri == repo }
end
-
+
def install(method=nil, options = {})
method ||= rails_env.best_install_method?
if :http == method
@@ -173,7 +171,7 @@ class Plugin
if rails_env.use_externals?
# clean up svn:externals
externals = rails_env.externals
- externals.reject!{|n,u| name == n or name == u}
+ externals.reject!{|n, u| name == n or name == u}
rails_env.externals = externals
end
end
@@ -192,7 +190,7 @@ class Plugin
FileUtils.rm_rf tmp if svn_url?
end
- private
+ private
def run_install_hook
install_hook_file = "#{rails_env.root}/vendor/plugins/#{name}/install.rb"
@@ -207,11 +205,11 @@ class Plugin
def install_using_export(options = {})
svn_command :export, options
end
-
+
def install_using_checkout(options = {})
svn_command :checkout, options
end
-
+
def install_using_externals(options = {})
externals = rails_env.externals
externals.push([@name, uri])
@@ -229,7 +227,7 @@ class Plugin
fetcher.fetch
end
end
-
+
def install_using_git(options = {})
root = rails_env.root
mkdir_p(install_path = "#{root}/vendor/plugins/#{name}")
@@ -268,7 +266,7 @@ class Plugin
end
@name.gsub!(/\.git$/, '') if @name =~ /\.git$/
end
-
+
def rails_env
@rails_env || RailsEnvironment.default
end
@@ -277,45 +275,44 @@ end
# load default environment and parse arguments
require 'optparse'
module Commands
-
class Plugin
attr_reader :environment, :script_name, :sources
def initialize
@environment = RailsEnvironment.default
@rails_root = RailsEnvironment.default.root
- @script_name = File.basename($0)
+ @script_name = File.basename($0)
@sources = []
end
-
+
def environment=(value)
@environment = value
RailsEnvironment.default = value
end
-
+
def options
OptionParser.new do |o|
o.set_summary_indent(' ')
o.banner = "Usage: plugin [OPTIONS] command"
o.define_head "Rails plugin manager."
-
- o.separator ""
+
+ o.separator ""
o.separator "GENERAL OPTIONS"
-
+
o.on("-r", "--root=DIR", String,
"Set an explicit rails app directory.",
"Default: #{@rails_root}") { |rails_root| @rails_root = rails_root; self.environment = RailsEnvironment.new(@rails_root) }
o.on("-s", "--source=URL1,URL2", Array,
"Use the specified plugin repositories instead of the defaults.") { |sources| @sources = sources}
-
+
o.on("-v", "--verbose", "Turn on verbose output.") { |verbose| $verbose = verbose }
o.on("-h", "--help", "Show this help message.") { puts o; exit }
-
+
o.separator ""
o.separator "COMMANDS"
-
+
o.separator " install Install plugin(s) from known repositories or URLs."
o.separator " remove Uninstall plugins."
-
+
o.separator ""
o.separator "EXAMPLES"
o.separator " Install a plugin:"
@@ -328,41 +325,41 @@ module Commands
o.separator " #{@script_name} plugin install -x continuous_builder\n"
end
end
-
+
def parse!(args=ARGV)
general, sub = split_args(args)
options.parse!(general)
-
+
command = general.shift
if command =~ /^(install|remove)$/
command = Commands.const_get(command.capitalize).new(self)
command.parse!(sub)
else
- puts "Unknown command: #{command}"
+ puts "Unknown command: #{command}" unless command.blank?
puts options
exit 1
end
end
-
+
def split_args(args)
left = []
left << args.shift while args[0] and args[0] =~ /^-/
left << args.shift if args[0]
- return [left, args]
+ [left, args]
end
-
+
def self.parse!(args=ARGV)
Plugin.new.parse!(args)
end
end
-
+
class Install
def initialize(base_command)
@base_command = base_command
@method = :http
@options = { :quiet => false, :revision => nil, :force => false }
end
-
+
def options
OptionParser.new do |o|
o.set_summary_indent(' ')
@@ -370,8 +367,8 @@ module Commands
o.define_head "Install one or more plugins."
o.separator ""
o.separator "Options:"
- o.on( "-x", "--externals",
- "Use svn:externals to grab the plugin.",
+ o.on( "-x", "--externals",
+ "Use svn:externals to grab the plugin.",
"Enables plugin updates and plugin versioning.") { |v| @method = :externals }
o.on( "-o", "--checkout",
"Use svn checkout to grab the plugin.",
@@ -392,7 +389,7 @@ module Commands
o.separator "a plugin repository."
end
end
-
+
def determine_install_method
best = @base_command.environment.best_install_method
@method = :http if best == :http and @method == :export
@@ -410,9 +407,13 @@ module Commands
end
@method
end
-
+
def parse!(args)
options.parse!(args)
+ if args.blank?
+ puts options
+ exit 1
+ end
environment = @base_command.environment
install_method = determine_install_method
puts "Plugins will be installed using #{install_method}" if $verbose
@@ -430,7 +431,7 @@ module Commands
def initialize(base_command)
@base_command = base_command
end
-
+
def options
OptionParser.new do |o|
o.set_summary_indent(' ')
@@ -438,9 +439,13 @@ module Commands
o.define_head "Remove plugins."
end
end
-
+
def parse!(args)
options.parse!(args)
+ if args.blank?
+ puts options
+ exit 1
+ end
root = @base_command.environment.root
args.each do |name|
::Plugin.new(name).uninstall
@@ -470,7 +475,7 @@ module Commands
end
end
end
-
+
class RecursiveHTTPFetcher
attr_accessor :quiet
def initialize(urls_to_fetch, level = 1, cwd = ".")
@@ -511,7 +516,7 @@ class RecursiveHTTPFetcher
end
links
end
-
+
def download(link)
puts "+ #{File.join(@cwd, File.basename(link))}" unless @quiet
open(link) do |stream|
@@ -520,13 +525,13 @@ class RecursiveHTTPFetcher
end
end
end
-
+
def fetch(links = @urls_to_fetch)
links.each do |l|
(l =~ /\/$/ || links == @urls_to_fetch) ? fetch_dir(l) : download(l)
end
end
-
+
def fetch_dir(url)
@level += 1
push_d(File.basename(url)) if @level > 0
diff --git a/railties/lib/rails/commands/server.rb b/railties/lib/rails/commands/server.rb
index 9d9dd48ea9..c3927b6613 100644
--- a/railties/lib/rails/commands/server.rb
+++ b/railties/lib/rails/commands/server.rb
@@ -21,6 +21,9 @@ module Rails
opts.on("-e", "--environment=name", String,
"Specifies the environment to run this server under (test/development/production).",
"Default: development") { |v| options[:environment] = v }
+ opts.on("-P","--pid=pid",String,
+ "Specifies the PID file.",
+ "Default: tmp/pids/server.pid") { |v| options[:pid] = v }
opts.separator ""
diff --git a/railties/lib/rails/generators/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.