Rails 2.3.5

Upgrade to Rails 2.3.5.
Also work around this bug:
 https://rails.lighthouseapp.com/projects/8994/tickets/3524
created by the aforementioned
Rails release.
This commit is contained in:
Jacques Distler 2009-11-30 19:38:34 -06:00
parent a6429f8c22
commit e3832c6f79
187 changed files with 2316 additions and 891 deletions

View file

@ -1,3 +1,11 @@
*2.3.5 (November 25, 2009)*
* Minor Bug Fixes and deprecation warnings
* 1.9 Compatibility
* Numerous fixes to the nested attributes functionality
*2.3.4 (September 4, 2009)*
* PostgreSQL: XML datatype support. #1874 [Leonardo Borges]

View file

@ -192,7 +192,7 @@ spec = Gem::Specification.new do |s|
s.files = s.files + Dir.glob( "#{dir}/**/*" ).delete_if { |item| item.include?( "\.svn" ) }
end
s.add_dependency('activesupport', '= 2.3.4' + PKG_BUILD)
s.add_dependency('activesupport', '= 2.3.5' + PKG_BUILD)
s.files.delete FIXTURES_ROOT + "/fixture_database.sqlite"
s.files.delete FIXTURES_ROOT + "/fixture_database_2.sqlite"

View file

@ -0,0 +1 @@
performance.sql

View file

@ -275,9 +275,10 @@ module ActiveRecord
# You can manipulate objects and associations before they are saved to the database, but there is some special behavior you should be
# aware of, mostly involving the saving of associated objects.
#
# Unless you enable the :autosave option on a <tt>has_one</tt>, <tt>belongs_to</tt>,
# <tt>has_many</tt>, or <tt>has_and_belongs_to_many</tt> association,
# in which case the members are always saved.
# Unless you set the :autosave option on a <tt>has_one</tt>, <tt>belongs_to</tt>,
# <tt>has_many</tt>, or <tt>has_and_belongs_to_many</tt> association. Setting it
# to +true+ will _always_ save the members, whereas setting it to +false+ will
# _never_ save the members.
#
# === One-to-one associations
#
@ -874,7 +875,9 @@ module ActiveRecord
# if the real class name is Person, you'll have to specify it with this option.
# [:conditions]
# Specify the conditions that the associated object must meet in order to be included as a +WHERE+
# SQL fragment, such as <tt>rank = 5</tt>.
# SQL fragment, such as <tt>rank = 5</tt>. Record creation from the association is scoped if a hash
# is used. <tt>has_one :account, :conditions => {:enabled => true}</tt> will create an enabled account with <tt>@company.create_account</tt>
# or <tt>@company.build_account</tt>.
# [:order]
# Specify the order in which the associated objects are returned as an <tt>ORDER BY</tt> SQL fragment,
# such as <tt>last_name, first_name DESC</tt>.
@ -1324,8 +1327,8 @@ module ActiveRecord
end
define_method("#{reflection.name.to_s.singularize}_ids=") do |new_value|
ids = (new_value || []).reject { |nid| nid.blank? }
send("#{reflection.name}=", reflection.klass.find(ids))
ids = (new_value || []).reject { |nid| nid.blank? }.map(&:to_i)
send("#{reflection.name}=", reflection.klass.find(ids).index_by(&:id).values_at(*ids))
end
end
end
@ -1408,7 +1411,7 @@ module ActiveRecord
if reflection.options.include?(:dependent)
# Add polymorphic type if the :as option is present
dependent_conditions = []
dependent_conditions << "#{reflection.primary_key_name} = \#{record.quoted_id}"
dependent_conditions << "#{reflection.primary_key_name} = \#{record.#{reflection.name}.send(:owner_quoted_id)}"
dependent_conditions << "#{reflection.options[:as]}_type = '#{base_class.name}'" if reflection.options[:as]
dependent_conditions << sanitize_sql(reflection.options[:conditions], reflection.quoted_table_name) if reflection.options[:conditions]
dependent_conditions << extra_conditions if extra_conditions

View file

@ -210,15 +210,14 @@ module ActiveRecord
# Forwards any missing method call to the \target.
def method_missing(method, *args)
if load_target
unless @target.respond_to?(method)
message = "undefined method `#{method.to_s}' for \"#{@target}\":#{@target.class.to_s}"
raise NoMethodError, message
end
if block_given?
@target.send(method, *args) { |*block_args| yield(*block_args) }
if @target.respond_to?(method)
if block_given?
@target.send(method, *args) { |*block_args| yield(*block_args) }
else
@target.send(method, *args)
end
else
@target.send(method, *args)
super
end
end
end

View file

@ -24,8 +24,8 @@ module ActiveRecord
def has_primary_key?
return @has_primary_key unless @has_primary_key.nil?
@has_primary_key = (ActiveRecord::Base.connection.supports_primary_key? &&
ActiveRecord::Base.connection.primary_key(@reflection.options[:join_table]))
@has_primary_key = (@owner.connection.supports_primary_key? &&
@owner.connection.primary_key(@reflection.options[:join_table]))
end
protected

View file

@ -8,18 +8,21 @@ module ActiveRecord
def create(attrs = {}, replace_existing = true)
new_record(replace_existing) do |reflection|
attrs = merge_with_conditions(attrs)
reflection.create_association(attrs)
end
end
def create!(attrs = {}, replace_existing = true)
new_record(replace_existing) do |reflection|
attrs = merge_with_conditions(attrs)
reflection.create_association!(attrs)
end
end
def build(attrs = {}, replace_existing = true)
new_record(replace_existing) do |reflection|
attrs = merge_with_conditions(attrs)
reflection.build_association(attrs)
end
end
@ -119,6 +122,12 @@ module ActiveRecord
record
end
def merge_with_conditions(attrs={})
attrs ||= {}
attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash)
attrs
end
end
end
end

View file

@ -159,7 +159,7 @@ module ActiveRecord
def add_autosave_association_callbacks(reflection)
save_method = "autosave_associated_records_for_#{reflection.name}"
validation_method = "validate_associated_records_for_#{reflection.name}"
validate validation_method
force_validation = (reflection.options[:validate] == true || reflection.options[:autosave] == true)
case reflection.macro
when :has_many, :has_and_belongs_to_many
@ -170,7 +170,10 @@ module ActiveRecord
after_create save_method
after_update save_method
define_method(validation_method) { validate_collection_association(reflection) }
if force_validation || (reflection.macro == :has_many && reflection.options[:validate] != false)
define_method(validation_method) { validate_collection_association(reflection) }
validate validation_method
end
else
case reflection.macro
when :has_one
@ -180,7 +183,11 @@ module ActiveRecord
define_method(save_method) { save_belongs_to_association(reflection) }
before_save save_method
end
define_method(validation_method) { validate_single_association(reflection) }
if force_validation
define_method(validation_method) { validate_single_association(reflection) }
validate validation_method
end
end
end
end
@ -224,10 +231,8 @@ module ActiveRecord
# Validate the association if <tt>:validate</tt> or <tt>:autosave</tt> is
# turned on for the association specified by +reflection+.
def validate_single_association(reflection)
if reflection.options[:validate] == true || reflection.options[:autosave] == true
if (association = association_instance_get(reflection.name)) && !association.target.nil?
association_valid?(reflection, association)
end
if (association = association_instance_get(reflection.name)) && !association.target.nil?
association_valid?(reflection, association)
end
end
@ -235,7 +240,7 @@ module ActiveRecord
# <tt>:autosave</tt> is turned on for the association specified by
# +reflection+.
def validate_collection_association(reflection)
if reflection.options[:validate] != false && association = association_instance_get(reflection.name)
if association = association_instance_get(reflection.name)
if records = associated_records_to_validate_or_save(association, new_record?, reflection.options[:autosave])
records.each { |record| association_valid?(reflection, record) }
end
@ -244,16 +249,15 @@ module ActiveRecord
# Returns whether or not the association is valid and applies any errors to
# the parent, <tt>self</tt>, if it wasn't. Skips any <tt>:autosave</tt>
# enabled records if they're marked_for_destruction?.
# enabled records if they're marked_for_destruction? or destroyed.
def association_valid?(reflection, association)
return true if association.destroyed? || association.marked_for_destruction?
unless valid = association.valid?
if reflection.options[:autosave]
unless association.marked_for_destruction?
association.errors.each_error do |attribute, error|
error = error.dup
error.attribute = "#{reflection.name}_#{attribute}"
errors.add(error) unless errors.on(error.attribute)
end
association.errors.each_error do |attribute, error|
attribute = "#{reflection.name}.#{attribute}"
errors.add(attribute, error.dup) unless errors.on(attribute)
end
else
errors.add(reflection.name)
@ -283,9 +287,11 @@ module ActiveRecord
if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave)
records.each do |record|
next if record.destroyed?
if autosave && record.marked_for_destruction?
association.destroy(record)
elsif @new_record_before_save || record.new_record?
elsif autosave != false && (@new_record_before_save || record.new_record?)
if autosave
association.send(:insert_record, record, false, false)
else
@ -311,14 +317,17 @@ module ActiveRecord
# This all happens inside a transaction, _if_ the Transactions module is included into
# ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
def save_has_one_association(reflection)
if (association = association_instance_get(reflection.name)) && !association.target.nil?
if (association = association_instance_get(reflection.name)) && !association.target.nil? && !association.destroyed?
autosave = reflection.options[:autosave]
if autosave && association.marked_for_destruction?
association.destroy
elsif new_record? || association.new_record? || association[reflection.primary_key_name] != id || autosave
association[reflection.primary_key_name] = id
association.save(!autosave)
else
key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id
if autosave != false && (new_record? || association.new_record? || association[reflection.primary_key_name] != key || autosave)
association[reflection.primary_key_name] = key
association.save(!autosave)
end
end
end
end
@ -332,12 +341,12 @@ module ActiveRecord
# This all happens inside a transaction, _if_ the Transactions module is included into
# ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
def save_belongs_to_association(reflection)
if association = association_instance_get(reflection.name)
if (association = association_instance_get(reflection.name)) && !association.destroyed?
autosave = reflection.options[:autosave]
if autosave && association.marked_for_destruction?
association.destroy
else
elsif autosave != false
association.save(!autosave) if association.new_record? || autosave
if association.updated?
@ -352,4 +361,4 @@ module ActiveRecord
end
end
end
end
end

View file

@ -2567,6 +2567,7 @@ module ActiveRecord #:nodoc:
# options, use <tt>#destroy</tt>.
def delete
self.class.delete(id) unless new_record?
@destroyed = true
freeze
end
@ -2581,6 +2582,7 @@ module ActiveRecord #:nodoc:
)
end
@destroyed = true
freeze
end
@ -2840,6 +2842,11 @@ module ActiveRecord #:nodoc:
@attributes.frozen?
end
# Returns +true+ if the record has been destroyed.
def destroyed?
@destroyed
end
# Returns +true+ if the record is read only. Records loaded through joins with piggy-back
# attributes will be marked as read only since they cannot be saved.
def readonly?

View file

@ -7,7 +7,8 @@ module MysqlCompat #:nodoc:
raise 'Mysql not loaded' unless defined?(::Mysql)
target = defined?(Mysql::Result) ? Mysql::Result : MysqlRes
return if target.instance_methods.include?('all_hashes')
return if target.instance_methods.include?('all_hashes') ||
target.instance_methods.include?(:all_hashes)
# Ruby driver has a version string and returns null values in each_hash
# C driver >= 2.7 returns null values in each_hash
@ -63,12 +64,15 @@ module ActiveRecord
raise
end
end
MysqlCompat.define_all_hashes_method!
mysql = Mysql.init
mysql.ssl_set(config[:sslkey], config[:sslcert], config[:sslca], config[:sslcapath], config[:sslcipher]) if config[:sslca] || config[:sslkey]
ConnectionAdapters::MysqlAdapter.new(mysql, logger, [host, username, password, database, port, socket], config)
default_flags = Mysql.const_defined?(:CLIENT_MULTI_RESULTS) ? Mysql::CLIENT_MULTI_RESULTS : 0
options = [host, username, password, database, port, socket, default_flags]
ConnectionAdapters::MysqlAdapter.new(mysql, logger, options, config)
end
end

View file

@ -434,7 +434,7 @@ end
# Any fixture labeled "DEFAULTS" is safely ignored.
class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash)
MAX_ID = 2 ** 31 - 1
MAX_ID = 2 ** 30 - 1
DEFAULT_FILTER_RE = /\.ya?ml$/
@@all_cached_fixtures = {}

View file

@ -23,16 +23,6 @@ module ActiveRecord
# p2.first_name = "should fail"
# p2.save # Raises a ActiveRecord::StaleObjectError
#
# Optimistic locking will also check for stale data when objects are destroyed. Example:
#
# p1 = Person.find(1)
# p2 = Person.find(1)
#
# p1.first_name = "Michael"
# p1.save
#
# p2.destroy # Raises a ActiveRecord::StaleObjectError
#
# You're then responsible for dealing with the conflict by rescuing the exception and either rolling back, merging,
# or otherwise apply the business logic needed to resolve the conflict.
#
@ -49,7 +39,6 @@ module ActiveRecord
base.lock_optimistically = true
base.alias_method_chain :update, :lock
base.alias_method_chain :destroy, :lock
base.alias_method_chain :attributes_from_column_definition, :lock
class << base
@ -109,28 +98,6 @@ module ActiveRecord
end
end
def destroy_with_lock #:nodoc:
return destroy_without_lock unless locking_enabled?
unless new_record?
lock_col = self.class.locking_column
previous_value = send(lock_col).to_i
affected_rows = connection.delete(
"DELETE FROM #{self.class.quoted_table_name} " +
"WHERE #{connection.quote_column_name(self.class.primary_key)} = #{quoted_id} " +
"AND #{self.class.quoted_locking_column} = #{quote_value(previous_value)}",
"#{self.class.name} Destroy"
)
unless affected_rows == 1
raise ActiveRecord::StaleObjectError, "Attempted to delete a stale object"
end
end
freeze
end
module ClassMethods
DEFAULT_LOCKING_COLUMN = 'lock_version'

View file

@ -1,25 +1,3 @@
# Copyright (c) 2006 Shugo Maeda <shugo@ruby-lang.org>
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject
# to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
# ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
# CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
module ActiveRecord
module Locking
# Locking::Pessimistic provides support for row-level locking using

View file

@ -1,9 +1,12 @@
module ActiveRecord
module NestedAttributes #:nodoc:
class TooManyRecords < ActiveRecordError
end
def self.included(base)
base.extend(ClassMethods)
base.class_inheritable_accessor :reject_new_nested_attributes_procs, :instance_writer => false
base.reject_new_nested_attributes_procs = {}
base.class_inheritable_accessor :nested_attributes_options, :instance_writer => false
base.nested_attributes_options = {}
end
# == Nested Attributes
@ -62,10 +65,10 @@ module ActiveRecord
# accepts_nested_attributes_for :avatar, :allow_destroy => true
# end
#
# Now, when you add the <tt>_delete</tt> key to the attributes hash, with a
# Now, when you add the <tt>_destroy</tt> key to the attributes hash, with a
# value that evaluates to +true+, you will destroy the associated model:
#
# member.avatar_attributes = { :id => '2', :_delete => '1' }
# member.avatar_attributes = { :id => '2', :_destroy => '1' }
# member.avatar.marked_for_destruction? # => true
# member.save
# member.avatar #=> nil
@ -85,14 +88,14 @@ module ActiveRecord
# the attribute hash.
#
# For each hash that does _not_ have an <tt>id</tt> key a new record will
# be instantiated, unless the hash also contains a <tt>_delete</tt> key
# be instantiated, unless the hash also contains a <tt>_destroy</tt> key
# that evaluates to +true+.
#
# params = { :member => {
# :name => 'joe', :posts_attributes => [
# { :title => 'Kari, the awesome Ruby documentation browser!' },
# { :title => 'The egalitarian assumption of the modern citizen' },
# { :title => '', :_delete => '1' } # this will be ignored
# { :title => '', :_destroy => '1' } # this will be ignored
# ]
# }}
#
@ -123,6 +126,22 @@ module ActiveRecord
# member.posts.first.title # => 'Kari, the awesome Ruby documentation browser!'
# member.posts.second.title # => 'The egalitarian assumption of the modern citizen'
#
# Alternatively, :reject_if also accepts a symbol for using methods:
#
# class Member < ActiveRecord::Base
# has_many :posts
# accepts_nested_attributes_for :posts, :reject_if => :new_record?
# end
#
# class Member < ActiveRecord::Base
# has_many :posts
# accepts_nested_attributes_for :posts, :reject_if => :reject_posts
#
# def reject_posts(attributed)
# attributed['title].blank?
# end
# end
#
# If the hash contains an <tt>id</tt> key that matches an already
# associated record, the matching record will be modified:
#
@ -140,7 +159,7 @@ module ActiveRecord
# By default the associated records are protected from being destroyed. If
# you want to destroy any of the associated records through the attributes
# hash, you have to enable it first using the <tt>:allow_destroy</tt>
# option. This will allow you to also use the <tt>_delete</tt> key to
# option. This will allow you to also use the <tt>_destroy</tt> key to
# destroy existing records:
#
# class Member < ActiveRecord::Base
@ -149,7 +168,7 @@ module ActiveRecord
# end
#
# params = { :member => {
# :posts_attributes => [{ :id => '2', :_delete => '1' }]
# :posts_attributes => [{ :id => '2', :_destroy => '1' }]
# }}
#
# member.attributes = params['member']
@ -172,14 +191,23 @@ module ActiveRecord
# Supported options:
# [:allow_destroy]
# If true, destroys any members from the attributes hash with a
# <tt>_delete</tt> key and a value that evaluates to +true+
# <tt>_destroy</tt> key and a value that evaluates to +true+
# (eg. 1, '1', true, or 'true'). This option is off by default.
# [:reject_if]
# Allows you to specify a Proc that checks whether a record should be
# built for a certain attribute hash. The hash is passed to the Proc
# and the Proc should return either +true+ or +false+. When no Proc
# is specified a record will be built for all attribute hashes that
# do not have a <tt>_delete</tt> that evaluates to true.
# Allows you to specify a Proc or a Symbol pointing to a method
# that checks whether a record should be built for a certain attribute
# hash. The hash is passed to the supplied Proc or the method
# and it should return either +true+ or +false+. When no :reject_if
# is specified, a record will be built for all attribute hashes that
# do not have a <tt>_destroy</tt> value that evaluates to true.
# Passing <tt>:all_blank</tt> instead of a Proc will create a proc
# that will reject a record where all the attributes are blank.
# [:limit]
# Allows you to specify the maximum number of the associated records that
# can be processes with the nested attributes. If the size of the
# nested attributes array exceeds the specified limit, NestedAttributes::TooManyRecords
# exception is raised. If omitted, any number associations can be processed.
# Note that the :limit option is only applicable to one-to-many associations.
#
# Examples:
# # creates avatar_attributes=
@ -189,7 +217,7 @@ module ActiveRecord
def accepts_nested_attributes_for(*attr_names)
options = { :allow_destroy => false }
options.update(attr_names.extract_options!)
options.assert_valid_keys(:allow_destroy, :reject_if)
options.assert_valid_keys(:allow_destroy, :reject_if, :limit)
attr_names.each do |association_name|
if reflection = reflect_on_association(association_name)
@ -201,16 +229,18 @@ module ActiveRecord
end
reflection.options[:autosave] = true
self.reject_new_nested_attributes_procs[association_name.to_sym] = options[:reject_if]
self.nested_attributes_options[association_name.to_sym] = options
# def pirate_attributes=(attributes)
# assign_nested_attributes_for_one_to_one_association(:pirate, attributes, false)
# end
class_eval %{
def #{association_name}_attributes=(attributes)
assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes, #{options[:allow_destroy]})
assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes)
end
}, __FILE__, __LINE__
add_autosave_association_callbacks(reflection)
else
raise ArgumentError, "No association found for name `#{association_name}'. Has it been defined yet?"
end
@ -223,15 +253,25 @@ module ActiveRecord
# destruction of this association.
#
# See ActionView::Helpers::FormHelper::fields_for for more info.
def _delete
def _destroy
marked_for_destruction?
end
# Deal with deprecated _delete.
#
def _delete #:nodoc:
ActiveSupport::Deprecation.warn "_delete is deprecated in nested attributes. Use _destroy instead."
_destroy
end
private
# Attribute hash keys that should not be assigned as normal attributes.
# These hash keys are nested attributes implementation details.
UNASSIGNABLE_KEYS = %w{ id _delete }
#
# TODO Remove _delete from UNASSIGNABLE_KEYS when deprecation warning are
# removed.
UNASSIGNABLE_KEYS = %w( id _destroy _delete )
# Assigns the given attributes to the association.
#
@ -240,17 +280,23 @@ module ActiveRecord
# record will be built.
#
# If the given attributes include a matching <tt>:id</tt> attribute _and_ a
# <tt>:_delete</tt> key set to a truthy value, then the existing record
# <tt>:_destroy</tt> key set to a truthy value, then the existing record
# will be marked for destruction.
def assign_nested_attributes_for_one_to_one_association(association_name, attributes, allow_destroy)
attributes = attributes.stringify_keys
def assign_nested_attributes_for_one_to_one_association(association_name, attributes)
options = self.nested_attributes_options[association_name]
attributes = attributes.with_indifferent_access
if attributes['id'].blank?
unless reject_new_record?(association_name, attributes)
send("build_#{association_name}", attributes.except(*UNASSIGNABLE_KEYS))
method = "build_#{association_name}"
if respond_to?(method)
send(method, attributes.except(*UNASSIGNABLE_KEYS))
else
raise ArgumentError, "Cannot build association #{association_name}. Are you trying to build a polymorphic one-to-one association?"
end
end
elsif (existing_record = send(association_name)) && existing_record.id.to_s == attributes['id'].to_s
assign_to_or_mark_for_destruction(existing_record, attributes, allow_destroy)
assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy])
end
end
@ -259,7 +305,7 @@ module ActiveRecord
# Hashes with an <tt>:id</tt> value matching an existing associated record
# will update that record. Hashes without an <tt>:id</tt> value will build
# a new record for the association. Hashes with a matching <tt>:id</tt>
# value and a <tt>:_delete</tt> key set to a truthy value will mark the
# value and a <tt>:_destroy</tt> key set to a truthy value will mark the
# matched record for destruction.
#
# For example:
@ -267,7 +313,7 @@ module ActiveRecord
# assign_nested_attributes_for_collection_association(:people, {
# '1' => { :id => '1', :name => 'Peter' },
# '2' => { :name => 'John' },
# '3' => { :id => '2', :_delete => true }
# '3' => { :id => '2', :_destroy => true }
# })
#
# Will update the name of the Person with ID 1, build a new associated
@ -279,51 +325,68 @@ module ActiveRecord
# assign_nested_attributes_for_collection_association(:people, [
# { :id => '1', :name => 'Peter' },
# { :name => 'John' },
# { :id => '2', :_delete => true }
# { :id => '2', :_destroy => true }
# ])
def assign_nested_attributes_for_collection_association(association_name, attributes_collection, allow_destroy)
def assign_nested_attributes_for_collection_association(association_name, attributes_collection)
options = self.nested_attributes_options[association_name]
unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array)
raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})"
end
if options[:limit] && attributes_collection.size > options[:limit]
raise TooManyRecords, "Maximum #{options[:limit]} records are allowed. Got #{attributes_collection.size} records instead."
end
if attributes_collection.is_a? Hash
attributes_collection = attributes_collection.sort_by { |index, _| index.to_i }.map { |_, attributes| attributes }
end
attributes_collection.each do |attributes|
attributes = attributes.stringify_keys
attributes = attributes.with_indifferent_access
if attributes['id'].blank?
unless reject_new_record?(association_name, attributes)
send(association_name).build(attributes.except(*UNASSIGNABLE_KEYS))
end
elsif existing_record = send(association_name).detect { |record| record.id.to_s == attributes['id'].to_s }
assign_to_or_mark_for_destruction(existing_record, attributes, allow_destroy)
assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy])
end
end
end
# Updates a record with the +attributes+ or marks it for destruction if
# +allow_destroy+ is +true+ and has_delete_flag? returns +true+.
# +allow_destroy+ is +true+ and has_destroy_flag? returns +true+.
def assign_to_or_mark_for_destruction(record, attributes, allow_destroy)
if has_delete_flag?(attributes) && allow_destroy
if has_destroy_flag?(attributes) && allow_destroy
record.mark_for_destruction
else
record.attributes = attributes.except(*UNASSIGNABLE_KEYS)
end
end
# Determines if a hash contains a truthy _delete key.
def has_delete_flag?(hash)
ConnectionAdapters::Column.value_to_boolean hash['_delete']
# Determines if a hash contains a truthy _destroy key.
def has_destroy_flag?(hash)
ConnectionAdapters::Column.value_to_boolean(hash['_destroy']) ||
ConnectionAdapters::Column.value_to_boolean(hash['_delete']) # TODO Remove after deprecation.
end
# Determines if a new record should be build by checking for
# has_delete_flag? or if a <tt>:reject_if</tt> proc exists for this
# has_destroy_flag? or if a <tt>:reject_if</tt> proc exists for this
# association and evaluates to +true+.
def reject_new_record?(association_name, attributes)
has_delete_flag?(attributes) ||
self.class.reject_new_nested_attributes_procs[association_name].try(:call, attributes)
has_destroy_flag?(attributes) || call_reject_if(association_name, attributes)
end
def call_reject_if(association_name, attributes)
callback = self.nested_attributes_options[association_name][:reject_if]
case callback
when Symbol
method(callback).arity == 0 ? send(callback) : send(callback, attributes)
when Proc
callback.try(:call, attributes)
end
end
end
end

View file

@ -27,11 +27,13 @@ module ActiveRecord
end
def message
generate_message(@message, options.dup)
# When type is a string, it means that we do not have to do a lookup, because
# the user already sent the "final" message.
type.is_a?(String) ? type : generate_message(default_options)
end
def full_message
attribute.to_s == 'base' ? message : generate_full_message(message, options.dup)
attribute.to_s == 'base' ? message : generate_full_message(default_options)
end
alias :to_s :message
@ -60,24 +62,19 @@ module ActiveRecord
# <li><tt>activerecord.errors.messages.blank</tt></li>
# <li>any default you provided through the +options+ hash (in the activerecord.errors scope)</li>
# </ol>
def generate_message(message, options = {})
def generate_message(options = {})
keys = @base.class.self_and_descendants_from_active_record.map do |klass|
[ :"models.#{klass.name.underscore}.attributes.#{attribute}.#{message}",
:"models.#{klass.name.underscore}.#{message}" ]
[ :"models.#{klass.name.underscore}.attributes.#{attribute}.#{@message}",
:"models.#{klass.name.underscore}.#{@message}" ]
end.flatten
keys << options.delete(:default)
keys << :"messages.#{message}"
keys << message if message.is_a?(String)
keys << @type unless @type == message
keys << :"messages.#{@message}"
keys << @message if @message.is_a?(String)
keys << @type unless @type == @message
keys.compact!
options.reverse_merge! :default => keys,
:scope => [:activerecord, :errors],
:model => @base.class.human_name,
:attribute => @base.class.human_attribute_name(attribute.to_s),
:value => value
options.merge!(:default => keys)
I18n.translate(keys.shift, options)
end
@ -108,16 +105,24 @@ module ActiveRecord
# full_messages:
# title:
# blank: This title is screwed!
def generate_full_message(message, options = {})
options.reverse_merge! :message => self.message,
:model => @base.class.human_name,
:attribute => @base.class.human_attribute_name(attribute.to_s),
:value => value
def generate_full_message(options = {})
keys = [
:"full_messages.#{@message}",
:'full_messages.format',
'{{attribute}} {{message}}'
]
key = :"full_messages.#{@message}"
defaults = [:'full_messages.format', '{{attribute}} {{message}}']
options.merge!(:default => keys, :message => self.message)
I18n.translate(keys.shift, options)
end
I18n.t(key, options.merge(:default => defaults, :scope => [:activerecord, :errors]))
# Return user options with default options.
#
def default_options
options.reverse_merge :scope => [:activerecord, :errors],
:model => @base.class.human_name,
:attribute => @base.class.human_attribute_name(attribute.to_s),
:value => value
end
end
@ -134,7 +139,8 @@ module ActiveRecord
end
def initialize(base) # :nodoc:
@base, @errors = base, {}
@base = base
clear
end
# Adds an error to the base object instead of any particular attribute. This is used
@ -150,16 +156,10 @@ module ActiveRecord
# error can be added to the same +attribute+ in which case an array will be returned on a call to <tt>on(attribute)</tt>.
# If no +messsage+ is supplied, :invalid is assumed.
# If +message+ is a Symbol, it will be translated, using the appropriate scope (see translate_error).
# def add(attribute, message = nil, options = {})
# message ||= :invalid
# message = generate_message(attribute, message, options)) if message.is_a?(Symbol)
# @errors[attribute.to_s] ||= []
# @errors[attribute.to_s] << message
# end
def add(error_or_attr, message = nil, options = {})
error, attribute = error_or_attr.is_a?(Error) ? [error_or_attr, error_or_attr.attribute] : [nil, error_or_attr]
#
def add(attribute, message = nil, options = {})
options[:message] = options.delete(:default) if options.has_key?(:default)
error, message = message, nil if message.is_a?(Error)
@errors[attribute.to_s] ||= []
@errors[attribute.to_s] << (error || Error.new(@base, attribute, message, options))
@ -283,7 +283,7 @@ module ActiveRecord
# Removes all errors that have been added.
def clear
@errors = {}
@errors = ActiveSupport::OrderedHash.new
end
# Returns the total number of errors added. Two errors added to the same attribute will be counted as such.
@ -321,7 +321,7 @@ module ActiveRecord
end
def generate_message(attribute, message = :invalid, options = {})
ActiveSupport::Deprecation.warn("ActiveRecord::Errors#generate_message has been deprecated. Please use ActiveRecord::Error#generate_message.")
ActiveSupport::Deprecation.warn("ActiveRecord::Errors#generate_message has been deprecated. Please use ActiveRecord::Error.new().to_s.")
Error.new(@base, attribute, message, options).to_s
end
end

View file

@ -2,7 +2,7 @@ module ActiveRecord
module VERSION #:nodoc:
MAJOR = 2
MINOR = 3
TINY = 4
TINY = 5
STRING = [MAJOR, MINOR, TINY].join('.')
end

View file

@ -1 +1,2 @@
require 'active_record'
ActiveSupport::Deprecation.warn 'require "activerecord" is deprecated and will be removed in Rails 3. Use require "active_record" instead.'

View file

@ -651,6 +651,18 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal 1, Client.find_all_by_client_of(firm.id).size
end
def test_delete_all_association_with_primary_key_deletes_correct_records
firm = Firm.find(:first)
# break the vanilla firm_id foreign key
assert_equal 2, firm.clients.count
firm.clients.first.update_attribute(:firm_id, nil)
assert_equal 1, firm.clients(true).count
assert_equal 1, firm.clients_using_primary_key_with_delete_all.count
old_record = firm.clients_using_primary_key_with_delete_all.first
firm = Firm.find(:first)
firm.destroy
assert Client.find_by_id(old_record.id).nil?
end
def test_creation_respects_hash_condition
ms_client = companies(:first_firm).clients_like_ms_with_hash_conditions.build

View file

@ -132,6 +132,28 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert !posts(:welcome).reload.people(true).include?(people(:michael))
end
def test_replace_order_is_preserved
posts(:welcome).people.clear
posts(:welcome).people = [people(:david), people(:michael)]
assert_equal [people(:david).id, people(:michael).id], posts(:welcome).readers.all(:order => 'id').map(&:person_id)
# Test the inverse order in case the first success was a coincidence
posts(:welcome).people.clear
posts(:welcome).people = [people(:michael), people(:david)]
assert_equal [people(:michael).id, people(:david).id], posts(:welcome).readers.all(:order => 'id').map(&:person_id)
end
def test_replace_by_id_order_is_preserved
posts(:welcome).people.clear
posts(:welcome).person_ids = [people(:david).id, people(:michael).id]
assert_equal [people(:david).id, people(:michael).id], posts(:welcome).readers.all(:order => 'id').map(&:person_id)
# Test the inverse order in case the first success was a coincidence
posts(:welcome).people.clear
posts(:welcome).person_ids = [people(:michael).id, people(:david).id]
assert_equal [people(:michael).id, people(:david).id], posts(:welcome).readers.all(:order => 'id').map(&:person_id)
end
def test_associate_with_create
assert_queries(1) { posts(:thinking) }

View file

@ -36,6 +36,15 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
assert_equal accounts(:rails_core_account), firm.account_using_primary_key
end
def test_update_with_foreign_and_primary_keys
firm = companies(:first_firm)
account = firm.account_using_foreign_and_primary_keys
assert_equal Account.find_by_firm_name(firm.name), account
firm.save
firm.reload
assert_equal account, firm.account_using_foreign_and_primary_keys
end
def test_can_marshal_has_one_association_with_nil_target
firm = Firm.new
assert_nothing_raised do
@ -306,4 +315,16 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
Firm.find(@firm.id, :include => :account).save!
end
end
def test_build_respects_hash_condition
account = companies(:first_firm).build_account_limit_500_with_hash_conditions
assert account.save
assert_equal 500, account.credit_limit
end
def test_create_respects_hash_condition
account = companies(:first_firm).create_account_limit_500_with_hash_conditions
assert !account.new_record?
assert_equal 500, account.credit_limit
end
end

View file

@ -436,6 +436,70 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa
end
end
class TestDefaultAutosaveAssociationOnNewRecord < ActiveRecord::TestCase
def test_autosave_new_record_on_belongs_to_can_be_disabled_per_relationship
new_account = Account.new("credit_limit" => 1000)
new_firm = Firm.new("name" => "some firm")
assert new_firm.new_record?
new_account.firm = new_firm
new_account.save!
assert !new_firm.new_record?
new_account = Account.new("credit_limit" => 1000)
new_autosaved_firm = Firm.new("name" => "some firm")
assert new_autosaved_firm.new_record?
new_account.unautosaved_firm = new_autosaved_firm
new_account.save!
assert new_autosaved_firm.new_record?
end
def test_autosave_new_record_on_has_one_can_be_disabled_per_relationship
firm = Firm.new("name" => "some firm")
account = Account.new("credit_limit" => 1000)
assert account.new_record?
firm.account = account
firm.save!
assert !account.new_record?
firm = Firm.new("name" => "some firm")
account = Account.new("credit_limit" => 1000)
firm.unautosaved_account = account
assert account.new_record?
firm.unautosaved_account = account
firm.save!
assert account.new_record?
end
def test_autosave_new_record_on_has_many_can_be_disabled_per_relationship
firm = Firm.new("name" => "some firm")
account = Account.new("credit_limit" => 1000)
assert account.new_record?
firm.accounts << account
firm.save!
assert !account.new_record?
firm = Firm.new("name" => "some firm")
account = Account.new("credit_limit" => 1000)
assert account.new_record?
firm.unautosaved_accounts << account
firm.save!
assert account.new_record?
end
end
class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
self.use_transactional_fixtures = false
@ -473,9 +537,17 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
assert !@pirate.valid?
@pirate.ship.mark_for_destruction
@pirate.ship.expects(:valid?).never
assert_difference('Ship.count', -1) { @pirate.save! }
end
def test_a_child_marked_for_destruction_should_not_be_destroyed_twice
@pirate.ship.mark_for_destruction
assert @pirate.save
@pirate.ship.expects(:destroy).never
assert @pirate.save
end
def test_should_rollback_destructions_if_an_exception_occurred_while_saving_a_child
# Stub the save method of the @pirate.ship instance to destroy and then raise an exception
class << @pirate.ship
@ -510,9 +582,17 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
assert !@ship.valid?
@ship.pirate.mark_for_destruction
@ship.pirate.expects(:valid?).never
assert_difference('Pirate.count', -1) { @ship.save! }
end
def test_a_parent_marked_for_destruction_should_not_be_destroyed_twice
@ship.pirate.mark_for_destruction
assert @ship.save
@ship.pirate.expects(:destroy).never
assert @ship.save
end
def test_should_rollback_destructions_if_an_exception_occurred_while_saving_a_parent
# Stub the save method of the @ship.pirate instance to destroy and then raise an exception
class << @ship.pirate
@ -553,9 +633,33 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
children.each { |child| child.name = '' }
assert !@pirate.valid?
children.each { |child| child.mark_for_destruction }
children.each do |child|
child.mark_for_destruction
child.expects(:valid?).never
end
assert_difference("#{association_name.classify}.count", -2) { @pirate.save! }
end
define_method("test_should_skip_validation_on_the_#{association_name}_association_if_destroyed") do
@pirate.send(association_name).create!(:name => "#{association_name}_1")
children = @pirate.send(association_name)
children.each { |child| child.name = '' }
assert !@pirate.valid?
children.each { |child| child.destroy }
assert @pirate.valid?
end
define_method("test_a_child_marked_for_destruction_should_not_be_destroyed_twice_while_saving_#{association_name}") do
@pirate.send(association_name).create!(:name => "#{association_name}_1")
children = @pirate.send(association_name)
children.each { |child| child.mark_for_destruction }
assert @pirate.save
children.each { |child| child.expects(:destroy).never }
assert @pirate.save
end
define_method("test_should_rollback_destructions_if_an_exception_occurred_while_saving_#{association_name}") do
2.times { |i| @pirate.send(association_name).create!(:name => "#{association_name}_#{i}") }
@ -646,15 +750,15 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase
def test_should_automatically_validate_the_associated_model
@pirate.ship.name = ''
assert !@pirate.valid?
assert !@pirate.errors.on(:ship_name).blank?
assert_equal "can't be blank", @pirate.errors.on(:"ship.name")
end
def test_should_merge_errors_on_the_associated_models_onto_the_parent_even_if_it_is_not_valid
@pirate.ship.name = nil
@pirate.catchphrase = nil
assert !@pirate.valid?
assert !@pirate.errors.on(:ship_name).blank?
assert !@pirate.errors.on(:catchphrase).blank?
assert @pirate.errors.full_messages.include?("Name can't be blank")
assert @pirate.errors.full_messages.include?("Catchphrase can't be blank")
end
def test_should_still_allow_to_bypass_validations_on_the_associated_model
@ -736,15 +840,15 @@ class TestAutosaveAssociationOnABelongsToAssociation < ActiveRecord::TestCase
def test_should_automatically_validate_the_associated_model
@ship.pirate.catchphrase = ''
assert !@ship.valid?
assert !@ship.errors.on(:pirate_catchphrase).blank?
assert_equal "can't be blank", @ship.errors.on(:"pirate.catchphrase")
end
def test_should_merge_errors_on_the_associated_model_onto_the_parent_even_if_it_is_not_valid
@ship.name = nil
@ship.pirate.catchphrase = nil
assert !@ship.valid?
assert !@ship.errors.on(:name).blank?
assert !@ship.errors.on(:pirate_catchphrase).blank?
assert @ship.errors.full_messages.include?("Name can't be blank")
assert @ship.errors.full_messages.include?("Catchphrase can't be blank")
end
def test_should_still_allow_to_bypass_validations_on_the_associated_model
@ -806,7 +910,7 @@ module AutosaveAssociationOnACollectionAssociationTests
@pirate.send(@association_name).each { |child| child.name = '' }
assert !@pirate.valid?
assert_equal "can't be blank", @pirate.errors.on("#{@association_name}_name")
assert @pirate.errors.full_messages.include?("Name can't be blank")
assert @pirate.errors.on(@association_name).blank?
end
@ -814,7 +918,7 @@ module AutosaveAssociationOnACollectionAssociationTests
@pirate.send(@association_name).build(:name => '')
assert !@pirate.valid?
assert_equal "can't be blank", @pirate.errors.on("#{@association_name}_name")
assert_equal "can't be blank", @pirate.errors.on("#{@association_name}.name")
assert @pirate.errors.on(@association_name).blank?
end
@ -823,7 +927,7 @@ module AutosaveAssociationOnACollectionAssociationTests
@pirate.catchphrase = nil
assert !@pirate.valid?
assert_equal "can't be blank", @pirate.errors.on("#{@association_name}_name")
assert_equal "can't be blank", @pirate.errors.on("#{@association_name}.name")
assert !@pirate.errors.on(:catchphrase).blank?
end
@ -920,4 +1024,119 @@ class TestAutosaveAssociationOnAHasAndBelongsToManyAssociation < ActiveRecord::T
end
include AutosaveAssociationOnACollectionAssociationTests
end
end
class TestAutosaveAssociationValidationsOnAHasManyAssocication < ActiveRecord::TestCase
self.use_transactional_fixtures = false
def setup
@pirate = Pirate.create(:catchphrase => "Don' botharrr talkin' like one, savvy?")
@pirate.birds.create(:name => 'cookoo')
end
test "should automatically validate associations" do
assert @pirate.valid?
@pirate.birds.each { |bird| bird.name = '' }
assert !@pirate.valid?
end
end
class TestAutosaveAssociationValidationsOnAHasOneAssocication < ActiveRecord::TestCase
self.use_transactional_fixtures = false
def setup
@pirate = Pirate.create(:catchphrase => "Don' botharrr talkin' like one, savvy?")
@pirate.create_ship(:name => 'titanic')
end
test "should automatically validate associations with :validate => true" do
assert @pirate.valid?
@pirate.ship.name = ''
assert !@pirate.valid?
end
test "should not automatically validate associations without :validate => true" do
assert @pirate.valid?
@pirate.non_validated_ship.name = ''
assert @pirate.valid?
end
end
class TestAutosaveAssociationValidationsOnABelongsToAssocication < ActiveRecord::TestCase
self.use_transactional_fixtures = false
def setup
@pirate = Pirate.create(:catchphrase => "Don' botharrr talkin' like one, savvy?")
end
test "should automatically validate associations with :validate => true" do
assert @pirate.valid?
@pirate.parrot = Parrot.new(:name => '')
assert !@pirate.valid?
end
test "should not automatically validate associations without :validate => true" do
assert @pirate.valid?
@pirate.non_validated_parrot = Parrot.new(:name => '')
assert @pirate.valid?
end
end
class TestAutosaveAssociationValidationsOnAHABTMAssocication < ActiveRecord::TestCase
self.use_transactional_fixtures = false
def setup
@pirate = Pirate.create(:catchphrase => "Don' botharrr talkin' like one, savvy?")
end
test "should automatically validate associations with :validate => true" do
assert @pirate.valid?
@pirate.parrots = [ Parrot.new(:name => 'popuga') ]
@pirate.parrots.each { |parrot| parrot.name = '' }
assert !@pirate.valid?
end
test "should not automatically validate associations without :validate => true" do
assert @pirate.valid?
@pirate.non_validated_parrots = [ Parrot.new(:name => 'popuga') ]
@pirate.non_validated_parrots.each { |parrot| parrot.name = '' }
assert @pirate.valid?
end
end
class TestAutosaveAssociationValidationMethodsGeneration < ActiveRecord::TestCase
self.use_transactional_fixtures = false
def setup
@pirate = Pirate.new
end
test "should generate validation methods for has_many associations" do
assert @pirate.respond_to?(:validate_associated_records_for_birds)
end
test "should generate validation methods for has_one associations with :validate => true" do
assert @pirate.respond_to?(:validate_associated_records_for_ship)
end
test "should not generate validation methods for has_one associations without :validate => true" do
assert !@pirate.respond_to?(:validate_associated_records_for_non_validated_ship)
end
test "should generate validation methods for belongs_to associations with :validate => true" do
assert @pirate.respond_to?(:validate_associated_records_for_parrot)
end
test "should not generate validation methods for belongs_to associations without :validate => true" do
assert !@pirate.respond_to?(:validate_associated_records_for_non_validated_parrot)
end
test "should generate validation methods for HABTM associations with :validate => true" do
assert @pirate.respond_to?(:validate_associated_records_for_parrots)
end
test "should not generate validation methods for HABTM associations without :validate => true" do
assert !@pirate.respond_to?(:validate_associated_records_for_non_validated_parrots)
end
end

View file

@ -474,6 +474,7 @@ class BasicsTest < ActiveRecord::TestCase
topic = Topic.find(1)
assert_equal topic, topic.delete, 'topic.delete did not return self'
assert topic.frozen?, 'topic not frozen after delete'
assert topic.destroyed?, 'topic not marked as being destroyed'
assert_raise(ActiveRecord::RecordNotFound) { Topic.find(topic.id) }
end
@ -486,6 +487,7 @@ class BasicsTest < ActiveRecord::TestCase
topic = Topic.find(1)
assert_equal topic, topic.destroy, 'topic.destroy did not return self'
assert topic.frozen?, 'topic not frozen after destroy'
assert topic.destroyed?, 'topic not marked as being destroyed'
assert_raise(ActiveRecord::RecordNotFound) { Topic.find(topic.id) }
end

View file

@ -41,6 +41,14 @@ class MysqlConnectionTest < ActiveRecord::TestCase
sleep 2
@connection.verify!
assert @connection.active?
end
# Test that MySQL allows multiple results for stored procedures
if Mysql.const_defined?(:CLIENT_MULTI_RESULTS)
def test_multi_results
rows = ActiveRecord::Base.connection.select_rows('CALL ten();')
assert_equal 10, rows[0][0].to_i, "ten() did not return 10 as expected: #{rows.inspect}"
end
end
private

View file

@ -519,8 +519,8 @@ class FoxyFixturesTest < ActiveRecord::TestCase
end
def test_identifies_consistently
assert_equal 1281023246, Fixtures.identify(:ruby)
assert_equal 2140105598, Fixtures.identify(:sapphire_2)
assert_equal 207281424, Fixtures.identify(:ruby)
assert_equal 1066363776, Fixtures.identify(:sapphire_2)
end
TIMESTAMP_COLUMNS = %w(created_at created_on updated_at updated_on)

View file

@ -38,24 +38,6 @@ class OptimisticLockingTest < ActiveRecord::TestCase
assert_raise(ActiveRecord::StaleObjectError) { p2.save! }
end
def test_lock_destroy
p1 = Person.find(1)
p2 = Person.find(1)
assert_equal 0, p1.lock_version
assert_equal 0, p2.lock_version
p1.first_name = 'stu'
p1.save!
assert_equal 1, p1.lock_version
assert_equal 0, p2.lock_version
assert_raises(ActiveRecord::StaleObjectError) { p2.destroy }
assert p1.destroy
assert_equal true, p1.frozen?
assert_raises(ActiveRecord::RecordNotFound) { Person.find(1) }
end
def test_lock_repeating
p1 = Person.find(1)
p2 = Person.find(1)

View file

@ -26,13 +26,13 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase
Pirate.accepts_nested_attributes_for :ship, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? }
end
def test_base_should_have_an_empty_reject_new_nested_attributes_procs
assert_equal Hash.new, ActiveRecord::Base.reject_new_nested_attributes_procs
def test_base_should_have_an_empty_nested_attributes_options
assert_equal Hash.new, ActiveRecord::Base.nested_attributes_options
end
def test_should_add_a_proc_to_reject_new_nested_attributes_procs
def test_should_add_a_proc_to_nested_attributes_options
[:parrots, :birds].each do |name|
assert_instance_of Proc, Pirate.reject_new_nested_attributes_procs[name]
assert_instance_of Proc, Pirate.nested_attributes_options[name][:reject_if]
end
end
@ -49,24 +49,66 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase
ship = pirate.create_ship(:name => 'Nights Dirty Lightning')
assert_no_difference('Ship.count') do
pirate.update_attributes(:ship_attributes => { '_delete' => true })
pirate.update_attributes(:ship_attributes => { '_destroy' => true })
end
end
def test_a_model_should_respond_to_underscore_delete_and_return_if_it_is_marked_for_destruction
def test_a_model_should_respond_to_underscore_destroy_and_return_if_it_is_marked_for_destruction
ship = Ship.create!(:name => 'Nights Dirty Lightning')
assert !ship._delete
assert !ship._destroy
ship.mark_for_destruction
assert ship._delete
assert ship._destroy
end
def test_underscore_delete_is_deprecated
ActiveSupport::Deprecation.expects(:warn)
ship = Ship.create!(:name => 'Nights Dirty Lightning')
ship._delete
end
def test_reject_if_method_without_arguments
Pirate.accepts_nested_attributes_for :ship, :reject_if => :new_record?
pirate = Pirate.new(:catchphrase => "Stop wastin' me time")
pirate.ship_attributes = { :name => 'Black Pearl' }
assert_no_difference('Ship.count') { pirate.save! }
end
def test_reject_if_method_with_arguments
Pirate.accepts_nested_attributes_for :ship, :reject_if => :reject_empty_ships_on_create
pirate = Pirate.new(:catchphrase => "Stop wastin' me time")
pirate.ship_attributes = { :name => 'Red Pearl', :_reject_me_if_new => true }
assert_no_difference('Ship.count') { pirate.save! }
# pirate.reject_empty_ships_on_create returns false for saved records
pirate.ship_attributes = { :name => 'Red Pearl', :_reject_me_if_new => true }
assert_difference('Ship.count') { pirate.save! }
end
def test_reject_if_with_indifferent_keys
Pirate.accepts_nested_attributes_for :ship, :reject_if => proc {|attributes| attributes[:name].blank? }
pirate = Pirate.new(:catchphrase => "Stop wastin' me time")
pirate.ship_attributes = { :name => 'Hello Pearl' }
assert_difference('Ship.count') { pirate.save! }
end
end
class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase
include AssertRaiseWithMessage
def setup
@pirate = Pirate.create!(:catchphrase => "Don' botharrr talkin' like one, savvy?")
@ship = @pirate.create_ship(:name => 'Nights Dirty Lightning')
end
def test_should_raise_argument_error_if_trying_to_build_polymorphic_belongs_to
assert_raise_with_message ArgumentError, "Cannot build association looter. Are you trying to build a polymorphic one-to-one association?" do
Treasure.new(:name => 'pearl', :looter_attributes => {:catchphrase => "Arrr"})
end
end
def test_should_define_an_attribute_writer_method_for_the_association
assert_respond_to @pirate, :ship_attributes=
end
@ -79,9 +121,9 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase
assert_equal 'Davy Jones Gold Dagger', @pirate.ship.name
end
def test_should_not_build_a_new_record_if_there_is_no_id_and_delete_is_truthy
def test_should_not_build_a_new_record_if_there_is_no_id_and_destroy_is_truthy
@ship.destroy
@pirate.reload.ship_attributes = { :name => 'Davy Jones Gold Dagger', :_delete => '1' }
@pirate.reload.ship_attributes = { :name => 'Davy Jones Gold Dagger', :_destroy => '1' }
assert_nil @pirate.ship
end
@ -101,8 +143,8 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase
assert_equal 'Nights Dirty Lightning', @ship.name
end
def test_should_not_replace_an_existing_record_if_there_is_no_id_and_delete_is_truthy
@pirate.reload.ship_attributes = { :name => 'Davy Jones Gold Dagger', :_delete => '1' }
def test_should_not_replace_an_existing_record_if_there_is_no_id_and_destroy_is_truthy
@pirate.reload.ship_attributes = { :name => 'Davy Jones Gold Dagger', :_destroy => '1' }
assert_equal @ship, @pirate.ship
assert_equal 'Nights Dirty Lightning', @pirate.ship.name
@ -129,29 +171,29 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase
assert_equal 'Davy Jones Gold Dagger', @pirate.ship.name
end
def test_should_delete_an_existing_record_if_there_is_a_matching_id_and_delete_is_truthy
def test_should_destroy_an_existing_record_if_there_is_a_matching_id_and_destroy_is_truthy
@pirate.ship.destroy
[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, :_delete => truth })
@pirate.update_attribute(:ship_attributes, { :id => @pirate.ship.id, :_destroy => truth })
end
end
end
def test_should_not_delete_an_existing_record_if_delete_is_not_truthy
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, :_delete => not_truth })
@pirate.update_attribute(:ship_attributes, { :id => @pirate.ship.id, :_destroy => not_truth })
end
end
end
def test_should_not_delete_an_existing_record_if_allow_destroy_is_false
def test_should_not_destroy_an_existing_record_if_allow_destroy_is_false
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, :_delete => '1' })
@pirate.update_attribute(:ship_attributes, { :id => @pirate.ship.id, :_destroy => '1' })
end
Pirate.accepts_nested_attributes_for :ship, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? }
@ -174,7 +216,7 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase
def test_should_not_destroy_the_associated_model_until_the_parent_is_saved
assert_no_difference('Ship.count') do
@pirate.attributes = { :ship_attributes => { :id => @ship.id, :_delete => '1' } }
@pirate.attributes = { :ship_attributes => { :id => @ship.id, :_destroy => '1' } }
end
assert_difference('Ship.count', -1) do
@pirate.save
@ -205,9 +247,9 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase
assert_equal 'Arr', @ship.pirate.catchphrase
end
def test_should_not_build_a_new_record_if_there_is_no_id_and_delete_is_truthy
def test_should_not_build_a_new_record_if_there_is_no_id_and_destroy_is_truthy
@pirate.destroy
@ship.reload.pirate_attributes = { :catchphrase => 'Arr', :_delete => '1' }
@ship.reload.pirate_attributes = { :catchphrase => 'Arr', :_destroy => '1' }
assert_nil @ship.pirate
end
@ -227,8 +269,8 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase
assert_equal 'Aye', @pirate.catchphrase
end
def test_should_not_replace_an_existing_record_if_there_is_no_id_and_delete_is_truthy
@ship.reload.pirate_attributes = { :catchphrase => 'Arr', :_delete => '1' }
def test_should_not_replace_an_existing_record_if_there_is_no_id_and_destroy_is_truthy
@ship.reload.pirate_attributes = { :catchphrase => 'Arr', :_destroy => '1' }
assert_equal @pirate, @ship.pirate
assert_equal 'Aye', @ship.pirate.catchphrase
@ -255,29 +297,29 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase
assert_equal 'Arr', @ship.pirate.catchphrase
end
def test_should_delete_an_existing_record_if_there_is_a_matching_id_and_delete_is_truthy
def test_should_destroy_an_existing_record_if_there_is_a_matching_id_and_destroy_is_truthy
@ship.pirate.destroy
[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, :_delete => truth })
@ship.update_attribute(:pirate_attributes, { :id => @ship.pirate.id, :_destroy => truth })
end
end
end
def test_should_not_delete_an_existing_record_if_delete_is_not_truthy
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, :_delete => not_truth })
@ship.update_attribute(:pirate_attributes, { :id => @ship.pirate.id, :_destroy => not_truth })
end
end
end
def test_should_not_delete_an_existing_record_if_allow_destroy_is_false
def test_should_not_destroy_an_existing_record_if_allow_destroy_is_false
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, :_delete => '1' })
@ship.update_attribute(:pirate_attributes, { :id => @ship.pirate.id, :_destroy => '1' })
end
Ship.accepts_nested_attributes_for :pirate, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? }
@ -293,7 +335,7 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase
def test_should_not_destroy_the_associated_model_until_the_parent_is_saved
assert_no_difference('Pirate.count') do
@ship.attributes = { :pirate_attributes => { :id => @ship.pirate.id, '_delete' => true } }
@ship.attributes = { :pirate_attributes => { :id => @ship.pirate.id, '_destroy' => true } }
end
assert_difference('Pirate.count', -1) { @ship.save }
end
@ -361,18 +403,18 @@ module NestedAttributesOnACollectionAssociationTests
assert_equal 'Privateers Greed', @pirate.send(@association_name).last.name
end
def test_should_not_assign_delete_key_to_a_record
def test_should_not_assign_destroy_key_to_a_record
assert_nothing_raised ActiveRecord::UnknownAttributeError do
@pirate.send(association_setter, { 'foo' => { '_delete' => '0' }})
@pirate.send(association_setter, { 'foo' => { '_destroy' => '0' }})
end
end
def test_should_ignore_new_associated_records_with_truthy_delete_attribute
def test_should_ignore_new_associated_records_with_truthy_destroy_attribute
@pirate.send(@association_name).destroy_all
@pirate.reload.attributes = {
association_getter => {
'foo' => { :name => 'Grace OMalley' },
'bar' => { :name => 'Privateers Greed', '_delete' => '1' }
'bar' => { :name => 'Privateers Greed', '_destroy' => '1' }
}
}
@ -424,7 +466,7 @@ module NestedAttributesOnACollectionAssociationTests
['1', 1, 'true', true].each do |true_variable|
record = @pirate.reload.send(@association_name).create!(:name => 'Grace OMalley')
@pirate.send(association_setter,
@alternate_params[association_getter].merge('baz' => { :id => record.id, '_delete' => true_variable })
@alternate_params[association_getter].merge('baz' => { :id => record.id, '_destroy' => true_variable })
)
assert_difference('@pirate.send(@association_name).count', -1) do
@ -435,7 +477,7 @@ module NestedAttributesOnACollectionAssociationTests
def test_should_not_destroy_the_associated_model_with_a_non_truthy_argument
[nil, '', '0', 0, 'false', false].each do |false_variable|
@alternate_params[association_getter]['foo']['_delete'] = false_variable
@alternate_params[association_getter]['foo']['_destroy'] = false_variable
assert_no_difference('@pirate.send(@association_name).count') do
@pirate.update_attributes(@alternate_params)
end
@ -444,7 +486,7 @@ module NestedAttributesOnACollectionAssociationTests
def test_should_not_destroy_the_associated_model_until_the_parent_is_saved
assert_no_difference('@pirate.send(@association_name).count') do
@pirate.send(association_setter, @alternate_params[association_getter].merge('baz' => { :id => @child_1.id, '_delete' => true }))
@pirate.send(association_setter, @alternate_params[association_getter].merge('baz' => { :id => @child_1.id, '_destroy' => true }))
end
assert_difference('@pirate.send(@association_name).count', -1) { @pirate.save }
end
@ -507,3 +549,33 @@ class TestNestedAttributesOnAHasAndBelongsToManyAssociation < ActiveRecord::Test
include NestedAttributesOnACollectionAssociationTests
end
class TestNestedAttributesLimit < ActiveRecord::TestCase
def setup
Pirate.accepts_nested_attributes_for :parrots, :limit => 2
@pirate = Pirate.create!(:catchphrase => "Don' botharrr talkin' like one, savvy?")
end
def teardown
Pirate.accepts_nested_attributes_for :parrots, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? }
end
def test_limit_with_less_records
@pirate.attributes = { :parrots_attributes => { 'foo' => { :name => 'Big Big Love' } } }
assert_difference('Parrot.count') { @pirate.save! }
end
def test_limit_with_number_exact_records
@pirate.attributes = { :parrots_attributes => { 'foo' => { :name => 'Lovely Day' }, 'bar' => { :name => 'Blown Away' } } }
assert_difference('Parrot.count', 2) { @pirate.save! }
end
def test_limit_with_exceeding_records
assert_raises(ActiveRecord::NestedAttributes::TooManyRecords) do
@pirate.attributes = { :parrots_attributes => { 'foo' => { :name => 'Lovely Day' },
'bar' => { :name => 'Blown Away' },
'car' => { :name => 'The Happening' }} }
end
end
end

View file

@ -170,9 +170,9 @@ class ReflectionTest < ActiveRecord::TestCase
def test_reflection_of_all_associations
# FIXME these assertions bust a lot
assert_equal 30, Firm.reflect_on_all_associations.size
assert_equal 23, Firm.reflect_on_all_associations(:has_many).size
assert_equal 7, Firm.reflect_on_all_associations(:has_one).size
assert_equal 36, Firm.reflect_on_all_associations.size
assert_equal 26, Firm.reflect_on_all_associations(:has_many).size
assert_equal 10, Firm.reflect_on_all_associations(:has_one).size
assert_equal 0, Firm.reflect_on_all_associations(:belongs_to).size
end

View file

@ -651,6 +651,14 @@ class ActiveRecordErrorI18nTests < ActiveSupport::TestCase
assert_full_message 'Title is kaputt?!', :title, :kaputt, :message => :broken
end
test "#full_message with different scope" do
store_translations(:my_errors => { :messages => { :kaputt => 'is kaputt' } })
assert_full_message 'Title is kaputt', :title, :kaputt, :scope => [:activerecord, :my_errors]
store_translations(:my_errors => { :full_messages => { :kaputt => '{{attribute}} {{message}}!' } })
assert_full_message 'Title is kaputt!', :title, :kaputt, :scope => [:activerecord, :my_errors]
end
# switch locales
test "#message allows to switch locales" do

View file

@ -28,6 +28,12 @@ class ProtectedPerson < ActiveRecord::Base
set_table_name 'people'
attr_accessor :addon
attr_protected :first_name
def special_error
this_method_does_not_exist!
rescue
errors.add(:special_error, "This method does not exist")
end
end
class UniqueReply < Reply
@ -172,6 +178,14 @@ class ValidationsTest < ActiveRecord::TestCase
end
end
def test_values_are_not_retrieved_unless_needed
assert_nothing_raised do
person = ProtectedPerson.new
person.special_error
assert_equal "This method does not exist", person.errors[:special_error]
end
end
def test_single_error_per_attr_iteration
r = Reply.new
r.save
@ -905,14 +919,18 @@ class ValidationsTest < ActiveRecord::TestCase
end
def test_validates_length_with_globally_modified_error_message
ActiveSupport::Deprecation.silence do
ActiveRecord::Errors.default_error_messages[:too_short] = 'tu est trops petit hombre {{count}}'
end
defaults = ActiveSupport::Deprecation.silence { ActiveRecord::Errors.default_error_messages }
original_message = defaults[:too_short]
defaults[:too_short] = 'tu est trops petit hombre {{count}}'
Topic.validates_length_of :title, :minimum => 10
t = Topic.create(:title => 'too short')
assert !t.valid?
assert_equal 'tu est trops petit hombre 10', t.errors['title']
ensure
defaults[:too_short] = original_message
end
def test_validates_size_of_association
@ -1432,12 +1450,22 @@ class ValidationsTest < ActiveRecord::TestCase
end
def test_validation_order
Topic.validates_presence_of :title
Topic.validates_length_of :title, :minimum => 2
Topic.validates_presence_of :title, :author_name
Topic.validate {|topic| topic.errors.add('author_email_address', 'will never be valid')}
Topic.validates_length_of :title, :content, :minimum => 2
t = Topic.new("title" => "")
assert !t.valid?
assert_equal "can't be blank", t.errors.on("title").first
t = Topic.new :title => ''
t.valid?
e = t.errors.instance_variable_get '@errors'
assert_equal 'title', key = e.keys.first
assert_equal "can't be blank", t.errors.on(key).first
assert_equal 'is too short (minimum is 2 characters)', t.errors.on(key).second
assert_equal 'author_name', key = e.keys.second
assert_equal "can't be blank", t.errors.on(key)
assert_equal 'author_email_address', key = e.keys.third
assert_equal 'will never be valid', t.errors.on(key)
assert_equal 'content', key = e.keys.fourth
assert_equal 'is too short (minimum is 2 characters)', t.errors.on(key)
end
def test_invalid_should_be_the_opposite_of_valid

View file

@ -2,6 +2,7 @@ signals37:
id: 1
firm_id: 1
credit_limit: 50
firm_name: 37signals
unknown:
id: 2

View file

@ -64,6 +64,8 @@ class Firm < Company
has_many :readonly_clients, :class_name => 'Client', :readonly => true
has_many :clients_using_primary_key, :class_name => 'Client',
:primary_key => 'name', :foreign_key => 'firm_name'
has_many :clients_using_primary_key_with_delete_all, :class_name => 'Client',
:primary_key => 'name', :foreign_key => 'firm_name', :dependent => :delete_all
has_many :clients_grouped_by_firm_id, :class_name => "Client", :group => "firm_id", :select => "firm_id"
has_many :clients_grouped_by_name, :class_name => "Client", :group => "name", :select => "name"
@ -72,7 +74,14 @@ class Firm < Company
has_one :account_with_select, :foreign_key => "firm_id", :select => "id, firm_id", :class_name=>'Account'
has_one :readonly_account, :foreign_key => "firm_id", :class_name => "Account", :readonly => true
has_one :account_using_primary_key, :primary_key => "firm_id", :class_name => "Account"
has_one :account_using_foreign_and_primary_keys, :foreign_key => "firm_name", :primary_key => "name", :class_name => "Account"
has_one :deletable_account, :foreign_key => "firm_id", :class_name => "Account", :dependent => :delete
has_one :account_limit_500_with_hash_conditions, :foreign_key => "firm_id", :class_name => "Account", :conditions => { :credit_limit => 500 }
has_one :unautosaved_account, :foreign_key => "firm_id", :class_name => 'Account', :autosave => false
has_many :accounts
has_many :unautosaved_accounts, :foreign_key => "firm_id", :class_name => 'Account', :autosave => false
end
class DependentFirm < Company
@ -136,6 +145,7 @@ end
class Account < ActiveRecord::Base
belongs_to :firm
belongs_to :unautosaved_firm, :foreign_key => "firm_id", :class_name => "Firm", :autosave => false
def self.destroyed_account_ids
@destroyed_account_ids ||= Hash.new { |h,k| h[k] = [] }

View file

@ -1,6 +1,8 @@
class Pirate < ActiveRecord::Base
belongs_to :parrot
has_and_belongs_to_many :parrots
belongs_to :parrot, :validate => true
belongs_to :non_validated_parrot, :class_name => 'Parrot'
has_and_belongs_to_many :parrots, :validate => true
has_and_belongs_to_many :non_validated_parrots, :class_name => 'Parrot'
has_and_belongs_to_many :parrots_with_method_callbacks, :class_name => "Parrot",
:before_add => :log_before_add,
:after_add => :log_after_add,
@ -17,6 +19,7 @@ class Pirate < ActiveRecord::Base
# These both have :autosave enabled because accepts_nested_attributes_for is used on them.
has_one :ship
has_one :non_validated_ship, :class_name => 'Ship'
has_many :birds
has_many :birds_with_method_callbacks, :class_name => "Bird",
:before_add => :log_before_add,
@ -40,6 +43,10 @@ class Pirate < ActiveRecord::Base
@ship_log ||= []
end
def reject_empty_ships_on_create(attributes)
attributes.delete('_reject_me_if_new').present? && new_record?
end
private
def log_before_add(record)
log(record, "before_adding_method")

View file

@ -3,4 +3,6 @@ class Treasure < ActiveRecord::Base
belongs_to :looter, :polymorphic => true
has_many :price_estimates, :as => :estimate_of
accepts_nested_attributes_for :looter
end

View file

@ -9,4 +9,16 @@ ActiveRecord::Schema.define do
t.text :medium_text, :limit => 16777215
t.text :long_text, :limit => 2147483647
end
ActiveRecord::Base.connection.execute <<-SQL
DROP PROCEDURE IF EXISTS ten;
SQL
ActiveRecord::Base.connection.execute <<-SQL
CREATE PROCEDURE ten() SQL SECURITY INVOKER
BEGIN
select 10;
END
SQL
end

View file

@ -23,6 +23,7 @@ ActiveRecord::Schema.define do
# unless the ordering matters. In which case, define them below
create_table :accounts, :force => true do |t|
t.integer :firm_id
t.string :firm_name
t.integer :credit_limit
end