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:
parent
a6429f8c22
commit
e3832c6f79
187 changed files with 2316 additions and 891 deletions
8
vendor/rails/activerecord/CHANGELOG
vendored
8
vendor/rails/activerecord/CHANGELOG
vendored
|
@ -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]
|
||||
|
|
2
vendor/rails/activerecord/Rakefile
vendored
2
vendor/rails/activerecord/Rakefile
vendored
|
@ -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"
|
||||
|
|
1
vendor/rails/activerecord/examples/.gitignore
vendored
Normal file
1
vendor/rails/activerecord/examples/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
performance.sql
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 = {}
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -2,7 +2,7 @@ module ActiveRecord
|
|||
module VERSION #:nodoc:
|
||||
MAJOR = 2
|
||||
MINOR = 3
|
||||
TINY = 4
|
||||
TINY = 5
|
||||
|
||||
STRING = [MAJOR, MINOR, TINY].join('.')
|
||||
end
|
||||
|
|
|
@ -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.'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) }
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -2,6 +2,7 @@ signals37:
|
|||
id: 1
|
||||
firm_id: 1
|
||||
credit_limit: 50
|
||||
firm_name: 37signals
|
||||
|
||||
unknown:
|
||||
id: 2
|
||||
|
|
10
vendor/rails/activerecord/test/models/company.rb
vendored
10
vendor/rails/activerecord/test/models/company.rb
vendored
|
@ -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] = [] }
|
||||
|
|
11
vendor/rails/activerecord/test/models/pirate.rb
vendored
11
vendor/rails/activerecord/test/models/pirate.rb
vendored
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue