Instiki 0.16.5

Update to Rails 2.3.2 (the stable Rails 2.3 release).
Add audio/speex support
Update CHANGELOG
Bump version number
This commit is contained in:
Jacques Distler 2009-03-16 09:55:30 -05:00
parent 801d307405
commit e2ccdfd812
264 changed files with 4850 additions and 1906 deletions

View file

@ -1,12 +1,9 @@
*2.3.1 [RC2] (March 5, 2009)*
*2.3.2 [Final] (March 15, 2009)*
* Added ActiveRecord::Base.each and ActiveRecord::Base.find_in_batches for batch processing [DHH/Jamis Buck]
* Added ActiveRecord::Base.find_each and ActiveRecord::Base.find_in_batches for batch processing [DHH/Jamis Buck]
* Added that ActiveRecord::Base.exists? can be called with no arguments #1817 [Scott Taylor]
*2.3.0 [RC1] (February 1st, 2009)*
* Add Support for updating deeply nested models from a single form. #1202 [Eloy Duran]
class Book < ActiveRecord::Base

View file

@ -177,7 +177,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.1' + PKG_BUILD)
s.add_dependency('activesupport', '= 2.3.2' + PKG_BUILD)
s.files.delete FIXTURES_ROOT + "/fixture_database.sqlite"
s.files.delete FIXTURES_ROOT + "/fixture_database_2.sqlite"

View file

@ -51,6 +51,12 @@ module ActiveRecord
end
end
class HasAndBelongsToManyAssociationForeignKeyNeeded < ActiveRecordError #:nodoc:
def initialize(reflection)
super("Cannot create self referential has_and_belongs_to_many association on '#{reflection.class_name rescue nil}##{reflection.name rescue nil}'. :association_foreign_key cannot be the same as the :foreign_key.")
end
end
class EagerLoadPolymorphicError < ActiveRecordError #:nodoc:
def initialize(reflection)
super("Can not eagerly load the polymorphic association #{reflection.name.inspect}")
@ -65,7 +71,7 @@ module ActiveRecord
# See ActiveRecord::Associations::ClassMethods for documentation.
module Associations # :nodoc:
# These classes will be loaded when associatoins are created.
# These classes will be loaded when associations are created.
# So there is no need to eager load them.
autoload :AssociationCollection, 'active_record/associations/association_collection'
autoload :AssociationProxy, 'active_record/associations/association_proxy'
@ -997,9 +1003,7 @@ module ActiveRecord
# Create the callbacks to update counter cache
if options[:counter_cache]
cache_column = options[:counter_cache] == true ?
"#{self.to_s.demodulize.underscore.pluralize}_count" :
options[:counter_cache]
cache_column = reflection.counter_cache_column
method_name = "belongs_to_counter_cache_after_create_for_#{reflection.name}".to_sym
define_method(method_name) do
@ -1526,6 +1530,10 @@ module ActiveRecord
options[:extend] = create_extension_modules(association_id, extension, options[:extend])
reflection = create_reflection(:has_and_belongs_to_many, association_id, options, self)
if reflection.association_foreign_key == reflection.primary_key_name
raise HasAndBelongsToManyAssociationForeignKeyNeeded.new(reflection)
end
reflection.options[:join_table] ||= join_table_name(undecorated_table_name(self.to_s), undecorated_table_name(reflection.class_name))
@ -1848,9 +1856,10 @@ module ActiveRecord
def construct(parent, associations, joins, row)
case associations
when Symbol, String
while (join = joins.shift).reflection.name.to_s != associations.to_s
raise ConfigurationError, "Not Enough Associations" if joins.empty?
end
join = joins.detect{|j| j.reflection.name.to_s == associations.to_s && j.parent_table_name == parent.class.table_name }
raise(ConfigurationError, "No such association") if join.nil?
joins.delete(join)
construct_association(parent, join, row)
when Array
associations.each do |association|
@ -1858,7 +1867,11 @@ module ActiveRecord
end
when Hash
associations.keys.sort{|a,b|a.to_s<=>b.to_s}.each do |name|
association = construct_association(parent, joins.shift, row)
join = joins.detect{|j| j.reflection.name.to_s == name.to_s && j.parent_table_name == parent.class.table_name }
raise(ConfigurationError, "No such association") if join.nil?
association = construct_association(parent, join, row)
joins.delete(join)
construct(association, associations[name], joins, row) if association
end
else

View file

@ -60,7 +60,7 @@ module ActiveRecord
@reflection.klass.find(*args)
end
end
# Fetches the first one using SQL if possible.
def first(*args)
if fetch_first_or_last_using_find?(args)
@ -143,6 +143,8 @@ module ActiveRecord
end
# Remove all records from this association
#
# See delete for more info.
def delete_all
load_target
delete(@target)
@ -186,7 +188,6 @@ module ActiveRecord
end
end
# Removes +records+ from this association calling +before_remove+ and
# +after_remove+ callbacks.
#
@ -195,22 +196,25 @@ module ActiveRecord
# are actually removed from the database, that depends precisely on
# +delete_records+. They are in any case removed from the collection.
def delete(*records)
records = flatten_deeper(records)
records.each { |record| raise_on_type_mismatch(record) }
transaction do
records.each { |record| callback(:before_remove, record) }
old_records = records.reject {|r| r.new_record? }
remove_records(records) do |records, old_records|
delete_records(old_records) if old_records.any?
records.each do |record|
@target.delete(record)
callback(:after_remove, record)
end
records.each { |record| @target.delete(record) }
end
end
# Destroy +records+ and remove them from this association calling
# +before_remove+ and +after_remove+ callbacks.
#
# Note that this method will _always_ remove records from the database
# ignoring the +:dependent+ option.
def destroy(*records)
remove_records(records) do |records, old_records|
old_records.each { |record| record.destroy }
end
load_target
end
# Removes all records from this association. Returns +self+ so method calls may be chained.
def clear
return self if length.zero? # forces load_target if it hasn't happened already
@ -223,15 +227,16 @@ module ActiveRecord
self
end
def destroy_all
transaction do
each { |record| record.destroy }
end
# Destory all the records from this association.
#
# See destroy for more info.
def destroy_all
load_target
destroy(@target)
reset_target!
end
def create(attrs = {})
if attrs.is_a?(Array)
attrs.collect { |attr| create(attr) }
@ -431,6 +436,18 @@ module ActiveRecord
record
end
def remove_records(*records)
records = flatten_deeper(records)
records.each { |record| raise_on_type_mismatch(record) }
transaction do
records.each { |record| callback(:before_remove, record) }
old_records = records.reject { |r| r.new_record? }
yield(records, old_records)
records.each { |record| callback(:after_remove, record) }
end
end
def callback(method, record)
callbacks_for(method).each do |callback|
ActiveSupport::Callbacks::Callback.new(method, callback, record).call(@owner, record)

View file

@ -23,8 +23,8 @@ module ActiveRecord
end
# Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been loaded and
# calling collection.size if it has. If it's more likely than not that the collection does have a size larger than zero
# and you need to fetch that collection afterwards, it'll take one less SELECT query if you use length.
# calling collection.size if it has. If it's more likely than not that the collection does have a size larger than zero,
# and you need to fetch that collection afterwards, it'll take one fewer SELECT query if you use #length.
def size
return @owner.send(:read_attribute, cached_counter_attribute_name) if has_cached_counter?
return @target.size if loaded?
@ -150,7 +150,7 @@ module ActiveRecord
end
else
reflection_primary_key = @reflection.source_reflection.primary_key_name
source_primary_key = @reflection.klass.primary_key
source_primary_key = @reflection.through_reflection.klass.primary_key
if @reflection.source_reflection.options[:as]
polymorphic_join = "AND %s.%s = %s" % [
@reflection.quoted_table_name, "#{@reflection.source_reflection.options[:as]}_type",

View file

@ -29,8 +29,17 @@ module ActiveRecord
unless @target.nil? || @target == obj
if dependent? && !dont_save
@target.destroy unless @target.new_record?
@owner.clear_association_cache
case @reflection.options[:dependent]
when :delete
@target.delete unless @target.new_record?
@owner.clear_association_cache
when :destroy
@target.destroy unless @target.new_record?
@owner.clear_association_cache
when :nullify
@target[@reflection.primary_key_name] = nil
@target.save unless @owner.new_record? || @target.new_record?
end
else
@target[@reflection.primary_key_name] = nil
@target.save unless @owner.new_record? || @target.new_record?

View file

@ -283,7 +283,7 @@ module ActiveRecord
if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave)
records.each do |record|
if autosave && record.marked_for_destruction?
record.destroy
association.destroy(record)
elsif @new_record_before_save || record.new_record?
if autosave
association.send(:insert_record, record, false, false)
@ -310,7 +310,7 @@ 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)
if (association = association_instance_get(reflection.name)) && !association.target.nil?
if reflection.options[:autosave] && association.marked_for_destruction?
association.destroy
elsif new_record? || association.new_record? || association[reflection.primary_key_name] != id || reflection.options[:autosave]

View file

@ -416,7 +416,7 @@ module ActiveRecord #:nodoc:
end
@@subclasses = {}
##
# :singleton-method:
# Contains the database configuration - as is typically stored in config/database.yml -
@ -661,7 +661,6 @@ module ActiveRecord #:nodoc:
connection.select_all(sanitize_sql(sql), "#{name} Load").collect! { |record| instantiate(record) }
end
# Returns true if a record exists in the table that matches the +id+ or
# conditions given, or false otherwise. The argument can take five forms:
#
@ -737,12 +736,12 @@ module ActiveRecord #:nodoc:
# ==== Parameters
#
# * +id+ - This should be the id or an array of ids to be updated.
# * +attributes+ - This should be a Hash of attributes to be set on the object, or an array of Hashes.
# * +attributes+ - This should be a hash of attributes to be set on the object, or an array of hashes.
#
# ==== Examples
#
# # Updating one record:
# Person.update(15, { :user_name => 'Samuel', :group => 'expert' })
# Person.update(15, :user_name => 'Samuel', :group => 'expert')
#
# # Updating multiple records:
# people = { 1 => { "first_name" => "David" }, 2 => { "first_name" => "Jeremy" } }
@ -1003,7 +1002,6 @@ module ActiveRecord #:nodoc:
update_counters(id, counter_name => -1)
end
# Attributes named in this macro are protected from mass-assignment,
# such as <tt>new(attributes)</tt>,
# <tt>update_attributes(attributes)</tt>, or
@ -1104,7 +1102,6 @@ module ActiveRecord #:nodoc:
read_inheritable_attribute(:attr_serialized) or write_inheritable_attribute(:attr_serialized, {})
end
# Guesses the table name (in forced lower-case) based on the name of the class in the inheritance hierarchy descending
# directly from ActiveRecord::Base. So if the hierarchy looks like: Reply < Message < ActiveRecord::Base, then Message is used
# to guess the table name even when called on Reply. The rules used to do the guess are handled by the Inflector class
@ -1347,7 +1344,7 @@ module ActiveRecord #:nodoc:
subclasses.each { |klass| klass.reset_inheritable_attributes; klass.reset_column_information }
end
def self_and_descendents_from_active_record#nodoc:
def self_and_descendants_from_active_record#nodoc:
klass = self
classes = [klass]
while klass != klass.base_class
@ -1367,7 +1364,7 @@ module ActiveRecord #:nodoc:
# module now.
# Specify +options+ with additional translating options.
def human_attribute_name(attribute_key_name, options = {})
defaults = self_and_descendents_from_active_record.map do |klass|
defaults = self_and_descendants_from_active_record.map do |klass|
:"#{klass.name.underscore}.#{attribute_key_name}"
end
defaults << options[:default] if options[:default]
@ -1382,7 +1379,7 @@ module ActiveRecord #:nodoc:
# Default scope of the translation is activerecord.models
# Specify +options+ with additional translating options.
def human_name(options = {})
defaults = self_and_descendents_from_active_record.map do |klass|
defaults = self_and_descendants_from_active_record.map do |klass|
:"#{klass.name.underscore}"
end
defaults << self.name.humanize
@ -1417,7 +1414,6 @@ module ActiveRecord #:nodoc:
end
end
def quote_value(value, column = nil) #:nodoc:
connection.quote(value,column)
end
@ -1486,7 +1482,7 @@ module ActiveRecord #:nodoc:
elsif match = DynamicScopeMatch.match(method_id)
return true if all_attributes_exists?(match.attribute_names)
end
super
end
@ -1537,7 +1533,7 @@ module ActiveRecord #:nodoc:
end
def reverse_sql_order(order_query)
reversed_query = order_query.split(/,/).each { |s|
reversed_query = order_query.to_s.split(/,/).each { |s|
if s.match(/\s(asc|ASC)$/)
s.gsub!(/\s(asc|ASC)$/, ' DESC')
elsif s.match(/\s(desc|DESC)$/)
@ -1690,7 +1686,7 @@ module ActiveRecord #:nodoc:
def construct_finder_sql(options)
scope = scope(:find)
sql = "SELECT #{options[:select] || (scope && scope[:select]) || default_select(options[:joins] || (scope && scope[:joins]))} "
sql << "FROM #{(scope && scope[:from]) || options[:from] || quoted_table_name} "
sql << "FROM #{options[:from] || (scope && scope[:from]) || quoted_table_name} "
add_joins!(sql, options[:joins], scope)
add_conditions!(sql, options[:conditions], scope)
@ -1745,7 +1741,9 @@ module ActiveRecord #:nodoc:
scoped_order = scope[:order] if scope
if order
sql << " ORDER BY #{order}"
sql << ", #{scoped_order}" if scoped_order
if scoped_order && scoped_order != order
sql << ", #{scoped_order}"
end
else
sql << " ORDER BY #{scoped_order}" if scoped_order
end
@ -1754,12 +1752,12 @@ module ActiveRecord #:nodoc:
def add_group!(sql, group, having, scope = :auto)
if group
sql << " GROUP BY #{group}"
sql << " HAVING #{having}" if having
sql << " HAVING #{sanitize_sql_for_conditions(having)}" if having
else
scope = scope(:find) if :auto == scope
if scope && (scoped_group = scope[:group])
sql << " GROUP BY #{scoped_group}"
sql << " HAVING #{scope[:having]}" if scope[:having]
sql << " HAVING #{sanitize_sql_for_conditions(scope[:having])}" if scope[:having]
end
end
end
@ -2014,7 +2012,6 @@ module ActiveRecord #:nodoc:
end
end
# Defines an "attribute" method (like +inheritance_column+ or
# +table_name+). A new (class) method will be created with the
# given name. If a value is specified, the new method will
@ -2111,7 +2108,7 @@ module ActiveRecord #:nodoc:
end
# Merge scopings
if action == :merge && current_scoped_methods
if [:merge, :reverse_merge].include?(action) && current_scoped_methods
method_scoping = current_scoped_methods.inject(method_scoping) do |hash, (method, params)|
case hash[method]
when Hash
@ -2133,7 +2130,11 @@ module ActiveRecord #:nodoc:
end
end
else
hash[method] = hash[method].merge(params)
if action == :reverse_merge
hash[method] = hash[method].merge(params)
else
hash[method] = params.merge(hash[method])
end
end
else
hash[method] = params
@ -2143,7 +2144,6 @@ module ActiveRecord #:nodoc:
end
self.scoped_methods << method_scoping
begin
yield
ensure
@ -2174,7 +2174,7 @@ module ActiveRecord #:nodoc:
# Test whether the given method and optional key are scoped.
def scoped?(method, key = nil) #:nodoc:
if current_scoped_methods && (scope = current_scoped_methods[method])
!key || scope.has_key?(key)
!key || !scope[key].nil?
end
end
@ -2749,7 +2749,6 @@ module ActiveRecord #:nodoc:
assign_multiparameter_attributes(multi_parameter_attributes)
end
# Returns a hash of all the attributes with their names as keys and the values of the attributes as values.
def attributes
self.attribute_names.inject({}) do |attrs, name|

View file

@ -4,21 +4,24 @@ module ActiveRecord
base.extend(ClassMethods)
end
# When processing large numbers of records, it's often a good idea to do so in batches to prevent memory ballooning.
# When processing large numbers of records, it's often a good idea to do
# so in batches to prevent memory ballooning.
module ClassMethods
# Yields each record that was found by the find +options+. The find is performed by find_in_batches
# with a batch size of 1000 (or as specified by the +batch_size+ option).
# Yields each record that was found by the find +options+. The find is
# performed by find_in_batches with a batch size of 1000 (or as
# specified by the <tt>:batch_size</tt> option).
#
# Example:
#
# Person.each(:conditions => "age > 21") do |person|
# Person.find_each(:conditions => "age > 21") do |person|
# person.party_all_night!
# end
#
# Note: This method is only intended to use for batch processing of large amounts of records that wouldn't fit in
# memory all at once. If you just need to loop over less than 1000 records, it's probably better just to use the
# regular find methods.
def each(options = {})
# Note: This method is only intended to use for batch processing of
# large amounts of records that wouldn't fit in memory all at once. If
# you just need to loop over less than 1000 records, it's probably
# better just to use the regular find methods.
def find_each(options = {})
find_in_batches(options) do |records|
records.each { |record| yield record }
end
@ -26,17 +29,22 @@ module ActiveRecord
self
end
# Yields each batch of records that was found by the find +options+ as an array. The size of each batch is
# set by the +batch_size+ option; the default is 1000.
# Yields each batch of records that was found by the find +options+ as
# an array. The size of each batch is set by the <tt>:batch_size</tt>
# option; the default is 1000.
#
# You can control the starting point for the batch processing by supplying the +start+ option. This is especially
# useful if you want multiple workers dealing with the same processing queue. You can make worker 1 handle all the
# records between id 0 and 10,000 and worker 2 handle from 10,000 and beyond (by setting the +start+ option on that
# worker).
# You can control the starting point for the batch processing by
# supplying the <tt>:start</tt> option. This is especially useful if you
# want multiple workers dealing with the same processing queue. You can
# make worker 1 handle all the records between id 0 and 10,000 and
# worker 2 handle from 10,000 and beyond (by setting the <tt>:start</tt>
# option on that worker).
#
# It's not possible to set the order. That is automatically set to ascending on the primary key ("id ASC")
# to make the batch ordering work. This also mean that this method only works with integer-based primary keys.
# You can't set the limit either, that's used to control the the batch sizes.
# It's not possible to set the order. That is automatically set to
# ascending on the primary key ("id ASC") to make the batch ordering
# work. This also mean that this method only works with integer-based
# primary keys. You can't set the limit either, that's used to control
# the the batch sizes.
#
# Example:
#
@ -49,12 +57,15 @@ module ActiveRecord
raise "You can't specify a limit, it's forced to be the batch_size" if options[:limit]
start = options.delete(:start).to_i
batch_size = options.delete(:batch_size) || 1000
with_scope(:find => options.merge(:order => batch_order, :limit => options.delete(:batch_size) || 1000)) do
with_scope(:find => options.merge(:order => batch_order, :limit => batch_size)) do
records = find(:all, :conditions => [ "#{table_name}.#{primary_key} >= ?", start ])
while records.any?
yield records
break if records.size < batch_size
records = find(:all, :conditions => [ "#{table_name}.#{primary_key} > ?", records.last.id ])
end
end

View file

@ -141,22 +141,30 @@ module ActiveRecord
def construct_count_options_from_args(*args)
options = {}
column_name = :all
# We need to handle
# count()
# count(:column_name=:all)
# count(options={})
# count(column_name=:all, options={})
# selects specified by scopes
case args.size
when 0
column_name = scope(:find)[:select] if scope(:find)
when 1
args[0].is_a?(Hash) ? options = args[0] : column_name = args[0]
if args[0].is_a?(Hash)
column_name = scope(:find)[:select] if scope(:find)
options = args[0]
else
column_name = args[0]
end
when 2
column_name, options = args
else
raise ArgumentError, "Unexpected parameters passed to count(): #{args.inspect}"
end if args.size > 0
[column_name, options]
end
[column_name || :all, options]
end
def construct_calculation_sql(operation, column_name, options) #:nodoc:
@ -214,13 +222,15 @@ module ActiveRecord
end
if options[:group] && options[:having]
having = sanitize_sql_for_conditions(options[:having])
# FrontBase requires identifiers in the HAVING clause and chokes on function calls
if connection.adapter_name == 'FrontBase'
options[:having].downcase!
options[:having].gsub!(/#{operation}\s*\(\s*#{column_name}\s*\)/, aggregate_alias)
having.downcase!
having.gsub!(/#{operation}\s*\(\s*#{column_name}\s*\)/, aggregate_alias)
end
sql << " HAVING #{options[:having]} "
sql << " HAVING #{having} "
end
sql << " ORDER BY #{options[:order]} " if options[:order]

View file

@ -18,7 +18,7 @@ module ActiveRecord
db.busy_timeout(config[:timeout]) unless config[:timeout].nil?
ConnectionAdapters::SQLite3Adapter.new(db, logger)
ConnectionAdapters::SQLite3Adapter.new(db, logger, config)
end
end

View file

@ -17,9 +17,9 @@ module ActiveRecord
# "Downgrade" deprecated sqlite API
if SQLite.const_defined?(:Version)
ConnectionAdapters::SQLite2Adapter.new(db, logger)
ConnectionAdapters::SQLite2Adapter.new(db, logger, config)
else
ConnectionAdapters::DeprecatedSQLiteAdapter.new(db, logger)
ConnectionAdapters::DeprecatedSQLiteAdapter.new(db, logger, config)
end
end
end
@ -72,10 +72,31 @@ module ActiveRecord
#
# * <tt>:database</tt> - Path to the database file.
class SQLiteAdapter < AbstractAdapter
class Version
include Comparable
def initialize(version_string)
@version = version_string.split('.').map(&:to_i)
end
def <=>(version_string)
@version <=> version_string.split('.').map(&:to_i)
end
end
def initialize(connection, logger, config)
super(connection, logger)
@config = config
end
def adapter_name #:nodoc:
'SQLite'
end
def supports_ddl_transactions?
sqlite_version >= '2.0.0'
end
def supports_migrations? #:nodoc:
true
end
@ -83,6 +104,10 @@ module ActiveRecord
def requires_reloading?
true
end
def supports_add_column?
sqlite_version >= '3.1.6'
end
def disconnect!
super
@ -164,7 +189,6 @@ module ActiveRecord
catch_schema_changes { @connection.rollback }
end
# SELECT ... FOR UPDATE is redundant since the table is locked.
def add_lock!(sql, options) #:nodoc:
sql
@ -213,14 +237,20 @@ module ActiveRecord
execute "ALTER TABLE #{name} RENAME TO #{new_name}"
end
# See: http://www.sqlite.org/lang_altertable.html
# SQLite has an additional restriction on the ALTER TABLE statement
def valid_alter_table_options( type, options)
type.to_sym != :primary_key
end
def add_column(table_name, column_name, type, options = {}) #:nodoc:
if @connection.respond_to?(:transaction_active?) && @connection.transaction_active?
raise StatementInvalid, 'Cannot add columns to a SQLite database while inside a transaction'
if supports_add_column? && valid_alter_table_options( type, options )
super(table_name, column_name, type, options)
else
alter_table(table_name) do |definition|
definition.column(column_name, type, options)
end
end
super(table_name, column_name, type, options)
# See last paragraph on http://www.sqlite.org/lang_altertable.html
execute "VACUUM"
end
def remove_column(table_name, *column_names) #:nodoc:
@ -380,7 +410,7 @@ module ActiveRecord
end
def sqlite_version
@sqlite_version ||= select_value('select sqlite_version(*)')
@sqlite_version ||= SQLiteAdapter::Version.new(select_value('select sqlite_version(*)'))
end
def default_primary_key_type
@ -393,23 +423,9 @@ module ActiveRecord
end
class SQLite2Adapter < SQLiteAdapter # :nodoc:
def supports_count_distinct? #:nodoc:
false
end
def rename_table(name, new_name)
move_table(name, new_name)
end
def add_column(table_name, column_name, type, options = {}) #:nodoc:
if @connection.respond_to?(:transaction_active?) && @connection.transaction_active?
raise StatementInvalid, 'Cannot add columns to a SQLite database while inside a transaction'
end
alter_table(table_name) do |definition|
definition.column(column_name, type, options)
end
end
end
class DeprecatedSQLiteAdapter < SQLite2Adapter # :nodoc:

View file

@ -23,6 +23,16 @@ 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.
#
@ -39,6 +49,7 @@ 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
@ -98,6 +109,28 @@ 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

@ -89,7 +89,12 @@ module ActiveRecord
when Hash
options
when Proc
options.call(*args)
case parent_scope
when Scope
with_scope(:find => parent_scope.proxy_options) { options.call(*args) }
else
options.call(*args)
end
end, &block)
end
(class << self; self end).instance_eval do
@ -99,7 +104,7 @@ module ActiveRecord
end
end
end
class Scope
attr_reader :proxy_scope, :proxy_options, :current_scoped_methods_when_defined
NON_DELEGATE_METHODS = %w(nil? send object_id class extend find size count sum average maximum minimum paginate first last empty? any? respond_to?).to_set
@ -112,6 +117,7 @@ module ActiveRecord
delegate :scopes, :with_scope, :to => :proxy_scope
def initialize(proxy_scope, options, &block)
options ||= {}
[options[:extend]].flatten.each { |extension| extend extension } if options[:extend]
extend Module.new(&block) if block_given?
unless Scope === proxy_scope
@ -170,7 +176,7 @@ module ActiveRecord
if scopes.include?(method)
scopes[method].call(self, *args)
else
with_scope :find => proxy_options, :create => proxy_options[:conditions].is_a?(Hash) ? proxy_options[:conditions] : {} do
with_scope({:find => proxy_options, :create => proxy_options[:conditions].is_a?(Hash) ? proxy_options[:conditions] : {}}, :reverse_merge) do
method = :new if method == :build
if current_scoped_methods_when_defined
with_scope current_scoped_methods_when_defined do

View file

@ -197,7 +197,7 @@ module ActiveRecord
def counter_cache_column
if options[:counter_cache] == true
"#{active_record.name.underscore.pluralize}_count"
"#{active_record.name.demodulize.underscore.pluralize}_count"
elsif options[:counter_cache]
options[:counter_cache]
end

View file

@ -231,16 +231,22 @@ module ActiveRecord #:nodoc:
def add_associations(association, records, opts)
if records.is_a?(Enumerable)
tag = reformat_name(association.to_s)
type = options[:skip_types] ? {} : {:type => "array"}
if records.empty?
builder.tag!(tag, :type => :array)
builder.tag!(tag, type)
else
builder.tag!(tag, :type => :array) do
builder.tag!(tag, type) do
association_name = association.to_s.singularize
records.each do |record|
record.to_xml opts.merge(
:root => association_name,
:type => (record.class.to_s.underscore == association_name ? nil : record.class.name)
)
if options[:skip_types]
record_type = {}
else
record_class = (record.class.to_s.underscore == association_name) ? nil : record.class.name
record_type = {:type => record_class}
end
record.to_xml opts.merge(:root => association_name).merge(record_type)
end
end
end

View file

@ -49,5 +49,18 @@ module ActiveRecord
ActiveRecord::Base.clear_all_connections!
ActiveRecord::Base.establish_connection(@connection)
end
def with_kcode(kcode)
if RUBY_VERSION < '1.9'
orig_kcode, $KCODE = $KCODE, kcode
begin
yield
ensure
$KCODE = orig_kcode
end
else
yield
end
end
end
end

View file

@ -89,7 +89,7 @@ module ActiveRecord
message, options[:default] = options[:default], message if options[:default].is_a?(Symbol)
defaults = @base.class.self_and_descendents_from_active_record.map do |klass|
defaults = @base.class.self_and_descendants_from_active_record.map do |klass|
[ :"models.#{klass.name.underscore}.attributes.#{attribute}.#{message}",
:"models.#{klass.name.underscore}.#{message}" ]
end
@ -720,20 +720,20 @@ module ActiveRecord
# class (which has a database table to query from).
finder_class = class_hierarchy.detect { |klass| !klass.abstract_class? }
is_text_column = finder_class.columns_hash[attr_name.to_s].text?
column = finder_class.columns_hash[attr_name.to_s]
if value.nil?
comparison_operator = "IS ?"
elsif is_text_column
elsif column.text?
comparison_operator = "#{connection.case_sensitive_equality_operator} ?"
value = value.to_s
value = column.limit ? value.to_s[0, column.limit] : value.to_s
else
comparison_operator = "= ?"
end
sql_attribute = "#{record.class.quoted_table_name}.#{connection.quote_column_name(attr_name)}"
if value.nil? || (configuration[:case_sensitive] || !is_text_column)
if value.nil? || (configuration[:case_sensitive] || !column.text?)
condition_sql = "#{sql_attribute} #{comparison_operator}"
condition_params = [value]
else
@ -802,7 +802,7 @@ module ActiveRecord
# Validates whether the value of the specified attribute is available in a particular enumerable object.
#
# class Person < ActiveRecord::Base
# validates_inclusion_of :gender, :in => %w( m f ), :message => "woah! what are you then!??!!"
# validates_inclusion_of :gender, :in => %w( m f )
# validates_inclusion_of :age, :in => 0..99
# validates_inclusion_of :format, :in => %w( jpg gif png ), :message => "extension {{value}} is not included in the list"
# end
@ -1040,6 +1040,11 @@ module ActiveRecord
errors.empty?
end
# Performs the opposite of <tt>valid?</tt>. Returns true if errors were added, false otherwise.
def invalid?
!valid?
end
# Returns the Errors object that holds all information about attribute error messages.
def errors
@errors ||= Errors.new(self)

View file

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

View file

@ -154,6 +154,23 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal 0, Topic.find(t2.id).replies.size
end
def test_belongs_to_reassign_with_namespaced_models_and_counters
t1 = Web::Topic.create("title" => "t1")
t2 = Web::Topic.create("title" => "t2")
r1 = Web::Reply.new("title" => "r1", "content" => "r1")
r1.topic = t1
assert r1.save
assert_equal 1, Web::Topic.find(t1.id).replies.size
assert_equal 0, Web::Topic.find(t2.id).replies.size
r1.topic = Web::Topic.find(t2.id)
assert r1.save
assert_equal 0, Web::Topic.find(t1.id).replies.size
assert_equal 1, Web::Topic.find(t2.id).replies.size
end
def test_belongs_to_counter_after_save
topic = Topic.create!(:title => "monday night")
topic.replies.create!(:title => "re: monday night", :content => "football")
@ -301,12 +318,28 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
end
def test_belongs_to_proxy_should_not_respond_to_private_methods
assert_raises(NoMethodError) { companies(:first_firm).private_method }
assert_raises(NoMethodError) { companies(:second_client).firm.private_method }
assert_raise(NoMethodError) { companies(:first_firm).private_method }
assert_raise(NoMethodError) { companies(:second_client).firm.private_method }
end
def test_belongs_to_proxy_should_respond_to_private_methods_via_send
companies(:first_firm).send(:private_method)
companies(:second_client).firm.send(:private_method)
end
def test_save_of_record_with_loaded_belongs_to
@account = companies(:first_firm).account
assert_nothing_raised do
Account.find(@account.id).save!
Account.find(@account.id, :include => :firm).save!
end
@account.firm.delete
assert_nothing_raised do
Account.find(@account.id).save!
Account.find(@account.id, :include => :firm).save!
end
end
end

View file

@ -1,4 +1,9 @@
require 'cases/helper'
require 'models/author'
require 'models/post'
require 'models/comment'
require 'models/category'
require 'models/categorization'
module Remembered
def self.included(base)
@ -99,3 +104,27 @@ class EagerLoadPolyAssocsTest < ActiveRecord::TestCase
end
end
end
class EagerLoadNestedIncludeWithMissingDataTest < ActiveRecord::TestCase
def setup
@davey_mcdave = Author.create(:name => 'Davey McDave')
@first_post = @davey_mcdave.posts.create(:title => 'Davey Speaks', :body => 'Expressive wordage')
@first_comment = @first_post.comments.create(:body => 'Inflamatory doublespeak')
@first_categorization = @davey_mcdave.categorizations.create(:category => Category.first, :post => @first_post)
end
def teardown
@davey_mcdave.destroy
@first_post.destroy
@first_comment.destroy
@first_categorization.destroy
end
def test_missing_data_in_a_nested_include_should_not_cause_errors_when_constructing_objects
assert_nothing_raised do
# @davey_mcdave doesn't have any author_favorites
includes = {:posts => :comments, :categorizations => :category, :author_favorites => :favorite_author }
Author.all :include => includes, :conditions => {:authors => {:name => @davey_mcdave.name}}, :order => 'categories.name'
end
end
end

View file

@ -549,16 +549,16 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_eager_with_invalid_association_reference
assert_raises(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") {
assert_raise(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") {
post = Post.find(6, :include=> :monkeys )
}
assert_raises(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") {
assert_raise(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") {
post = Post.find(6, :include=>[ :monkeys ])
}
assert_raises(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") {
assert_raise(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") {
post = Post.find(6, :include=>[ 'monkeys' ])
}
assert_raises(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys, :elephants") {
assert_raise(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys, :elephants") {
post = Post.find(6, :include=>[ :monkeys, :elephants ])
}
end

View file

@ -381,6 +381,33 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_date_from_db Date.new(2004, 10, 10), Developer.find(1).projects.first.joined_on.to_date
end
def test_destroying
david = Developer.find(1)
active_record = Project.find(1)
david.projects.reload
assert_equal 2, david.projects.size
assert_equal 3, active_record.developers.size
assert_difference "Project.count", -1 do
david.projects.destroy(active_record)
end
assert_equal 1, david.reload.projects.size
assert_equal 1, david.projects(true).size
end
def test_destroying_array
david = Developer.find(1)
david.projects.reload
assert_difference "Project.count", -Project.count do
david.projects.destroy(Project.find(:all))
end
assert_equal 0, david.reload.projects.size
assert_equal 0, david.projects(true).size
end
def test_destroy_all
david = Developer.find(1)
david.projects.reload
@ -616,7 +643,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
def test_updating_attributes_on_rich_associations
david = projects(:action_controller).developers.first
david.name = "DHH"
assert_raises(ActiveRecord::ReadOnlyRecord) { david.save! }
assert_raise(ActiveRecord::ReadOnlyRecord) { david.save! }
end
def test_updating_attributes_on_rich_associations_with_limited_find_from_reflection
@ -740,6 +767,14 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_equal developer, project.developers.find(:first)
assert_equal project, developer.projects.find(:first)
end
def test_self_referential_habtm_without_foreign_key_set_should_raise_exception
assert_raise(ActiveRecord::HasAndBelongsToManyAssociationForeignKeyNeeded) {
Member.class_eval do
has_and_belongs_to_many :friends, :class_name => "Member", :join_table => "member_friends"
end
}
end
def test_dynamic_find_should_respect_association_include
# SQL error in sort clause if :include is not included

View file

@ -70,6 +70,10 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal 2, companies(:first_firm).limited_clients.find(:all, :limit => nil).size
end
def test_dynamic_find_last_without_specified_order
assert_equal companies(:second_client), companies(:first_firm).unsorted_clients.find_last_by_type('Client')
end
def test_dynamic_find_should_respect_association_order
assert_equal companies(:second_client), companies(:first_firm).clients_sorted_desc.find(:first, :conditions => "type = 'Client'")
assert_equal companies(:second_client), companies(:first_firm).clients_sorted_desc.find_by_type('Client')
@ -176,7 +180,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_find_ids
firm = Firm.find(:first)
assert_raises(ActiveRecord::RecordNotFound) { firm.clients.find }
assert_raise(ActiveRecord::RecordNotFound) { firm.clients.find }
client = firm.clients.find(2)
assert_kind_of Client, client
@ -190,7 +194,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal 2, client_ary.size
assert_equal client, client_ary.first
assert_raises(ActiveRecord::RecordNotFound) { firm.clients.find(2, 99) }
assert_raise(ActiveRecord::RecordNotFound) { firm.clients.find(2, 99) }
end
def test_find_string_ids_when_using_finder_sql
@ -215,6 +219,45 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal 1, firm.clients.find(:all, :conditions => "name = 'Summit'").length
end
def test_find_each
firm = companies(:first_firm)
assert ! firm.clients.loaded?
assert_queries(3) do
firm.clients.find_each(:batch_size => 1) {|c| assert_equal firm.id, c.firm_id }
end
assert ! firm.clients.loaded?
end
def test_find_each_with_conditions
firm = companies(:first_firm)
assert_queries(2) do
firm.clients.find_each(:batch_size => 1, :conditions => {:name => "Microsoft"}) do |c|
assert_equal firm.id, c.firm_id
assert_equal "Microsoft", c.name
end
end
assert ! firm.clients.loaded?
end
def test_find_in_batches
firm = companies(:first_firm)
assert ! firm.clients.loaded?
assert_queries(2) do
firm.clients.find_in_batches(:batch_size => 2) do |clients|
clients.each {|c| assert_equal firm.id, c.firm_id }
end
end
assert ! firm.clients.loaded?
end
def test_find_all_sanitized
firm = Firm.find(:first)
summit = firm.clients.find(:all, :conditions => "name = 'Summit'")
@ -238,7 +281,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_find_in_collection
assert_equal Client.find(2).name, companies(:first_firm).clients.find(2).name
assert_raises(ActiveRecord::RecordNotFound) { companies(:first_firm).clients.find(6) }
assert_raise(ActiveRecord::RecordNotFound) { companies(:first_firm).clients.find(6) }
end
def test_find_grouped
@ -278,36 +321,36 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_create_with_bang_on_has_many_when_parent_is_new_raises
assert_raises(ActiveRecord::RecordNotSaved) do
assert_raise(ActiveRecord::RecordNotSaved) do
firm = Firm.new
firm.plain_clients.create! :name=>"Whoever"
end
end
def test_regular_create_on_has_many_when_parent_is_new_raises
assert_raises(ActiveRecord::RecordNotSaved) do
assert_raise(ActiveRecord::RecordNotSaved) do
firm = Firm.new
firm.plain_clients.create :name=>"Whoever"
end
end
def test_create_with_bang_on_has_many_raises_when_record_not_saved
assert_raises(ActiveRecord::RecordInvalid) do
assert_raise(ActiveRecord::RecordInvalid) do
firm = Firm.find(:first)
firm.plain_clients.create!
end
end
def test_create_with_bang_on_habtm_when_parent_is_new_raises
assert_raises(ActiveRecord::RecordNotSaved) do
assert_raise(ActiveRecord::RecordNotSaved) do
Developer.new("name" => "Aredridel").projects.create!
end
end
def test_adding_a_mismatch_class
assert_raises(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).clients_of_firm << nil }
assert_raises(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).clients_of_firm << 1 }
assert_raises(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).clients_of_firm << Topic.find(1) }
assert_raise(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).clients_of_firm << nil }
assert_raise(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).clients_of_firm << 1 }
assert_raise(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).clients_of_firm << Topic.find(1) }
end
def test_adding_a_collection
@ -602,7 +645,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_invalid_belongs_to_dependent_option_raises_exception
assert_raises ArgumentError do
assert_raise ArgumentError do
Author.belongs_to :special_author_address, :dependent => :nullify
end
end
@ -628,13 +671,37 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_deleting_type_mismatch
david = Developer.find(1)
david.projects.reload
assert_raises(ActiveRecord::AssociationTypeMismatch) { david.projects.delete(1) }
assert_raise(ActiveRecord::AssociationTypeMismatch) { david.projects.delete(1) }
end
def test_deleting_self_type_mismatch
david = Developer.find(1)
david.projects.reload
assert_raises(ActiveRecord::AssociationTypeMismatch) { david.projects.delete(Project.find(1).developers) }
assert_raise(ActiveRecord::AssociationTypeMismatch) { david.projects.delete(Project.find(1).developers) }
end
def test_destroying
force_signal37_to_load_all_clients_of_firm
assert_difference "Client.count", -1 do
companies(:first_firm).clients_of_firm.destroy(companies(:first_firm).clients_of_firm.first)
end
assert_equal 0, companies(:first_firm).reload.clients_of_firm.size
assert_equal 0, companies(:first_firm).clients_of_firm(true).size
end
def test_destroying_a_collection
force_signal37_to_load_all_clients_of_firm
companies(:first_firm).clients_of_firm.create("name" => "Another Client")
assert_equal 2, companies(:first_firm).clients_of_firm.size
assert_difference "Client.count", -2 do
companies(:first_firm).clients_of_firm.destroy([companies(:first_firm).clients_of_firm[0], companies(:first_firm).clients_of_firm[1]])
end
assert_equal 0, companies(:first_firm).reload.clients_of_firm.size
assert_equal 0, companies(:first_firm).clients_of_firm(true).size
end
def test_destroy_all

View file

@ -1,14 +1,19 @@
require "cases/helper"
require 'models/post'
require 'models/person'
require 'models/reference'
require 'models/job'
require 'models/reader'
require 'models/comment'
require 'models/tag'
require 'models/tagging'
require 'models/author'
require 'models/owner'
require 'models/pet'
require 'models/toy'
class HasManyThroughAssociationsTest < ActiveRecord::TestCase
fixtures :posts, :readers, :people, :comments, :authors
fixtures :posts, :readers, :people, :comments, :authors, :owners, :pets, :toys
def test_associate_existing
assert_queries(2) { posts(:thinking);people(:david) }
@ -87,6 +92,24 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert posts(:welcome).reload.people(true).empty?
end
def test_destroy_association
assert_difference "Person.count", -1 do
posts(:welcome).people.destroy(people(:michael))
end
assert posts(:welcome).reload.people.empty?
assert posts(:welcome).people(true).empty?
end
def test_destroy_all
assert_difference "Person.count", -1 do
posts(:welcome).people.destroy_all
end
assert posts(:welcome).reload.people.empty?
assert posts(:welcome).people(true).empty?
end
def test_replace_association
assert_queries(4){posts(:welcome);people(:david);people(:michael); posts(:welcome).people(true)}
@ -249,4 +272,8 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
author.author_favorites.create(:favorite_author_id => 3)
assert_equal post.author.author_favorites, post.author_favorites
end
def test_has_many_association_through_a_has_many_association_with_nonstandard_primary_keys
assert_equal 1, owners(:blackbeard).toys.count
end
end

View file

@ -59,8 +59,8 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
end
def test_type_mismatch
assert_raises(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).account = 1 }
assert_raises(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).account = Project.find(1) }
assert_raise(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).account = 1 }
assert_raise(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).account = Project.find(1) }
end
def test_natural_assignment
@ -76,7 +76,25 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
companies(:first_firm).save
assert_nil companies(:first_firm).account
# account is dependent, therefore is destroyed when reference to owner is lost
assert_raises(ActiveRecord::RecordNotFound) { Account.find(old_account_id) }
assert_raise(ActiveRecord::RecordNotFound) { Account.find(old_account_id) }
end
def test_nullification_on_association_change
firm = companies(:rails_core)
old_account_id = firm.account.id
firm.account = Account.new
# account is dependent with nullify, therefore its firm_id should be nil
assert_nil Account.find(old_account_id).firm_id
end
def test_association_changecalls_delete
companies(:first_firm).deletable_account = Account.new
assert_equal [], Account.destroyed_account_ids[companies(:first_firm).id]
end
def test_association_change_calls_destroy
companies(:first_firm).account = Account.new
assert_equal [companies(:first_firm).id], Account.destroyed_account_ids[companies(:first_firm).id]
end
def test_natural_assignment_to_already_associated_record
@ -263,8 +281,8 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
end
def test_has_one_proxy_should_not_respond_to_private_methods
assert_raises(NoMethodError) { accounts(:signals37).private_method }
assert_raises(NoMethodError) { companies(:first_firm).account.private_method }
assert_raise(NoMethodError) { accounts(:signals37).private_method }
assert_raise(NoMethodError) { companies(:first_firm).account.private_method }
end
def test_has_one_proxy_should_respond_to_private_methods_via_send
@ -272,4 +290,20 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
companies(:first_firm).account.send(:private_method)
end
def test_save_of_record_with_loaded_has_one
@firm = companies(:first_firm)
assert_not_nil @firm.account
assert_nothing_raised do
Firm.find(@firm.id).save!
Firm.find(@firm.id, :include => :account).save!
end
@firm.account.destroy
assert_nothing_raised do
Firm.find(@firm.id).save!
Firm.find(@firm.id, :include => :account).save!
end
end
end

View file

@ -115,8 +115,8 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
end
def test_has_one_through_proxy_should_not_respond_to_private_methods
assert_raises(NoMethodError) { clubs(:moustache_club).private_method }
assert_raises(NoMethodError) { @member.club.private_method }
assert_raise(NoMethodError) { clubs(:moustache_club).private_method }
assert_raise(NoMethodError) { @member.club.private_method }
end
def test_has_one_through_proxy_should_respond_to_private_methods_via_send
@ -173,4 +173,20 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
assert_not_nil assert_no_queries { @new_detail.member_type }
end
def test_save_of_record_with_loaded_has_one_through
@club = @member.club
assert_not_nil @club.sponsored_member
assert_nothing_raised do
Club.find(@club.id).save!
Club.find(@club.id, :include => :sponsored_member).save!
end
@club.sponsor.destroy
assert_nothing_raised do
Club.find(@club.id).save!
Club.find(@club.id, :include => :sponsored_member).save!
end
end
end

View file

@ -100,7 +100,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
%w(save create_or_update).each do |method|
klass = Class.new ActiveRecord::Base
klass.class_eval "def #{method}() 'defined #{method}' end"
assert_raises ActiveRecord::DangerousAttributeError do
assert_raise ActiveRecord::DangerousAttributeError do
klass.instance_method_already_implemented?(method)
end
end

View file

@ -556,6 +556,41 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
assert_raise(RuntimeError) { assert !@pirate.save }
assert_equal before, @pirate.reload.send(association_name)
end
# Add and remove callbacks tests for association collections.
%w{ method proc }.each do |callback_type|
define_method("test_should_run_add_callback_#{callback_type}s_for_#{association_name}") do
association_name_with_callbacks = "#{association_name}_with_#{callback_type}_callbacks"
pirate = Pirate.new(:catchphrase => "Arr")
pirate.send(association_name_with_callbacks).build(:name => "Crowe the One-Eyed")
expected = [
"before_adding_#{callback_type}_#{association_name.singularize}_<new>",
"after_adding_#{callback_type}_#{association_name.singularize}_<new>"
]
assert_equal expected, pirate.ship_log
end
define_method("test_should_run_remove_callback_#{callback_type}s_for_#{association_name}") do
association_name_with_callbacks = "#{association_name}_with_#{callback_type}_callbacks"
@pirate.send(association_name_with_callbacks).create!(:name => "Crowe the One-Eyed")
@pirate.send(association_name_with_callbacks).each { |c| c.mark_for_destruction }
child_id = @pirate.send(association_name_with_callbacks).first.id
@pirate.ship_log.clear
@pirate.save
expected = [
"before_removing_#{callback_type}_#{association_name.singularize}_#{child_id}",
"after_removing_#{callback_type}_#{association_name.singularize}_#{child_id}"
]
assert_equal expected, @pirate.ship_log
end
end
end
end

View file

@ -424,8 +424,8 @@ class BasicsTest < ActiveRecord::TestCase
def test_non_attribute_access_and_assignment
topic = Topic.new
assert !topic.respond_to?("mumbo")
assert_raises(NoMethodError) { topic.mumbo }
assert_raises(NoMethodError) { topic.mumbo = 5 }
assert_raise(NoMethodError) { topic.mumbo }
assert_raise(NoMethodError) { topic.mumbo = 5 }
end
def test_preserving_date_objects
@ -490,7 +490,7 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_record_not_found_exception
assert_raises(ActiveRecord::RecordNotFound) { topicReloaded = Topic.find(99999) }
assert_raise(ActiveRecord::RecordNotFound) { topicReloaded = Topic.find(99999) }
end
def test_initialize_with_attributes
@ -848,7 +848,7 @@ class BasicsTest < ActiveRecord::TestCase
client.delete
assert client.frozen?
assert_kind_of Firm, client.firm
assert_raises(ActiveSupport::FrozenObjectError) { client.name = "something else" }
assert_raise(ActiveSupport::FrozenObjectError) { client.name = "something else" }
end
def test_destroy_new_record
@ -862,7 +862,7 @@ class BasicsTest < ActiveRecord::TestCase
client.destroy
assert client.frozen?
assert_kind_of Firm, client.firm
assert_raises(ActiveSupport::FrozenObjectError) { client.name = "something else" }
assert_raise(ActiveSupport::FrozenObjectError) { client.name = "something else" }
end
def test_update_attribute
@ -910,8 +910,8 @@ class BasicsTest < ActiveRecord::TestCase
def test_mass_assignment_should_raise_exception_if_accessible_and_protected_attribute_writers_are_both_used
topic = TopicWithProtectedContentAndAccessibleAuthorName.new
assert_raises(RuntimeError) { topic.attributes = { "author_name" => "me" } }
assert_raises(RuntimeError) { topic.attributes = { "content" => "stuff" } }
assert_raise(RuntimeError) { topic.attributes = { "author_name" => "me" } }
assert_raise(RuntimeError) { topic.attributes = { "content" => "stuff" } }
end
def test_mass_assignment_protection
@ -949,7 +949,7 @@ class BasicsTest < ActiveRecord::TestCase
def test_mass_assigning_invalid_attribute
firm = Firm.new
assert_raises(ActiveRecord::UnknownAttributeError) do
assert_raise(ActiveRecord::UnknownAttributeError) do
firm.attributes = { "id" => 5, "type" => "Client", "i_dont_even_exist" => 20 }
end
end
@ -1402,7 +1402,7 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_sql_injection_via_find
assert_raises(ActiveRecord::RecordNotFound, ActiveRecord::StatementInvalid) do
assert_raise(ActiveRecord::RecordNotFound, ActiveRecord::StatementInvalid) do
Topic.find("123456 OR id > 0")
end
end
@ -1790,6 +1790,11 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal last, Developer.find(:all, :order => 'developers.name, developers.salary DESC').last
end
def test_find_symbol_ordered_last
last = Developer.find :last, :order => :salary
assert_equal last, Developer.find(:all, :order => :salary).last
end
def test_find_scoped_ordered_last
last_developer = Developer.with_scope(:find => { :order => 'developers.salary ASC' }) do
Developer.find(:last)
@ -2099,18 +2104,4 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal custom_datetime, parrot[attribute]
end
end
private
def with_kcode(kcode)
if RUBY_VERSION < '1.9'
orig_kcode, $KCODE = $KCODE, kcode
begin
yield
ensure
$KCODE = orig_kcode
end
else
yield
end
end
end

View file

@ -11,7 +11,7 @@ class EachTest < ActiveRecord::TestCase
def test_each_should_excecute_one_query_per_batch
assert_queries(Post.count + 1) do
Post.each(:batch_size => 1) do |post|
Post.find_each(:batch_size => 1) do |post|
assert_kind_of Post, post
end
end
@ -19,13 +19,13 @@ class EachTest < ActiveRecord::TestCase
def test_each_should_raise_if_the_order_is_set
assert_raise(RuntimeError) do
Post.each(:order => "title") { |post| post }
Post.find_each(:order => "title") { |post| post }
end
end
def test_each_should_raise_if_the_limit_is_set
assert_raise(RuntimeError) do
Post.each(:limit => 1) { |post| post }
Post.find_each(:limit => 1) { |post| post }
end
end
@ -46,4 +46,16 @@ class EachTest < ActiveRecord::TestCase
end
end
end
def test_find_in_batches_shouldnt_excute_query_unless_needed
post_count = Post.count
assert_queries(2) do
Post.find_in_batches(:batch_size => post_count) {|batch| assert_kind_of Array, batch }
end
assert_queries(1) do
Post.find_in_batches(:batch_size => post_count + 1) {|batch| assert_kind_of Array, batch }
end
end
end

View file

@ -92,6 +92,14 @@ class CalculationsTest < ActiveRecord::TestCase
assert_equal 60, c[2]
end
def test_should_group_by_summed_field_having_sanitized_condition
c = Account.sum(:credit_limit, :group => :firm_id,
:having => ['sum(credit_limit) > ?', 50])
assert_nil c[1]
assert_equal 105, c[6]
assert_equal 60, c[2]
end
def test_should_group_by_summed_association
c = Account.sum(:credit_limit, :group => :firm)
assert_equal 50, c[companies(:first_firm)]
@ -247,8 +255,8 @@ class CalculationsTest < ActiveRecord::TestCase
Company.send(:validate_calculation_options, :count, :include => true)
end
assert_raises(ArgumentError) { Company.send(:validate_calculation_options, :sum, :foo => :bar) }
assert_raises(ArgumentError) { Company.send(:validate_calculation_options, :count, :foo => :bar) }
assert_raise(ArgumentError) { Company.send(:validate_calculation_options, :sum, :foo => :bar) }
assert_raise(ArgumentError) { Company.send(:validate_calculation_options, :count, :foo => :bar) }
end
def test_should_count_selected_field_with_include
@ -256,6 +264,19 @@ class CalculationsTest < ActiveRecord::TestCase
assert_equal 4, Account.count(:distinct => true, :include => :firm, :select => :credit_limit)
end
def test_should_count_scoped_select
Account.update_all("credit_limit = NULL")
assert_equal 0, Account.scoped(:select => "credit_limit").count
end
def test_should_count_scoped_select_with_options
Account.update_all("credit_limit = NULL")
Account.last.update_attribute('credit_limit', 49)
Account.first.update_attribute('credit_limit', 51)
assert_equal 1, Account.scoped(:select => "credit_limit").count(:conditions => ['credit_limit >= 50'])
end
def test_should_count_manual_select_with_include
assert_equal 6, Account.count(:select => "DISTINCT accounts.id", :include => :firm)
end

View file

@ -352,13 +352,13 @@ class CallbacksTest < ActiveRecord::TestCase
david = ImmutableDeveloper.find(1)
assert david.valid?
assert !david.save
assert_raises(ActiveRecord::RecordNotSaved) { david.save! }
assert_raise(ActiveRecord::RecordNotSaved) { david.save! }
david = ImmutableDeveloper.find(1)
david.salary = 10_000_000
assert !david.valid?
assert !david.save
assert_raises(ActiveRecord::RecordInvalid) { david.save! }
assert_raise(ActiveRecord::RecordInvalid) { david.save! }
someone = CallbackCancellationDeveloper.find(1)
someone.cancel_before_save = true

View file

@ -228,7 +228,7 @@ class DirtyTest < ActiveRecord::TestCase
pirate = Pirate.new
pirate.parrot_id = 1
assert_raises(ActiveRecord::RecordInvalid) { pirate.save! }
assert_raise(ActiveRecord::RecordInvalid) { pirate.save! }
check_pirate_after_save_failure(pirate)
end

View file

@ -146,7 +146,7 @@ class FinderTest < ActiveRecord::TestCase
end
def test_find_by_ids_missing_one
assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1, 2, 45) }
assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, 2, 45) }
end
def test_find_all_with_limit
@ -191,6 +191,13 @@ class FinderTest < ActiveRecord::TestCase
assert developers.all? { |developer| developer.salary > 10000 }
end
def test_find_with_group_and_sanitized_having
developers = Developer.find(:all, :group => "salary", :having => ["sum(salary) > ?", 10000], :select => "salary")
assert_equal 3, developers.size
assert_equal 3, developers.map(&:salary).uniq.size
assert developers.all? { |developer| developer.salary > 10000 }
end
def test_find_with_entire_select_statement
topics = Topic.find_by_sql "SELECT * FROM topics WHERE author_name = 'Mary'"
@ -229,7 +236,7 @@ class FinderTest < ActiveRecord::TestCase
end
def test_unexisting_record_exception_handling
assert_raises(ActiveRecord::RecordNotFound) {
assert_raise(ActiveRecord::RecordNotFound) {
Topic.find(1).parent
}
@ -238,7 +245,7 @@ class FinderTest < ActiveRecord::TestCase
def test_find_only_some_columns
topic = Topic.find(1, :select => "author_name")
assert_raises(ActiveRecord::MissingAttributeError) {topic.title}
assert_raise(ActiveRecord::MissingAttributeError) {topic.title}
assert_equal "David", topic.author_name
assert !topic.attribute_present?("title")
#assert !topic.respond_to?("title")
@ -260,22 +267,22 @@ class FinderTest < ActiveRecord::TestCase
def test_find_on_array_conditions
assert Topic.find(1, :conditions => ["approved = ?", false])
assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => ["approved = ?", true]) }
assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => ["approved = ?", true]) }
end
def test_find_on_hash_conditions
assert Topic.find(1, :conditions => { :approved => false })
assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :approved => true }) }
assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :approved => true }) }
end
def test_find_on_hash_conditions_with_explicit_table_name
assert Topic.find(1, :conditions => { 'topics.approved' => false })
assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { 'topics.approved' => true }) }
assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { 'topics.approved' => true }) }
end
def test_find_on_hash_conditions_with_hashed_table_name
assert Topic.find(1, :conditions => {:topics => { :approved => false }})
assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => {:topics => { :approved => true }}) }
assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => {:topics => { :approved => true }}) }
end
def test_find_with_hash_conditions_on_joined_table
@ -293,7 +300,7 @@ class FinderTest < ActiveRecord::TestCase
def test_find_on_hash_conditions_with_explicit_table_name_and_aggregate
david = customers(:david)
assert Customer.find(david.id, :conditions => { 'customers.name' => david.name, :address => david.address })
assert_raises(ActiveRecord::RecordNotFound) {
assert_raise(ActiveRecord::RecordNotFound) {
Customer.find(david.id, :conditions => { 'customers.name' => david.name + "1", :address => david.address })
}
end
@ -304,13 +311,13 @@ class FinderTest < ActiveRecord::TestCase
def test_find_on_hash_conditions_with_range
assert_equal [1,2], Topic.find(:all, :conditions => { :id => 1..2 }).map(&:id).sort
assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :id => 2..3 }) }
assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :id => 2..3 }) }
end
def test_find_on_hash_conditions_with_end_exclusive_range
assert_equal [1,2,3], Topic.find(:all, :conditions => { :id => 1..3 }).map(&:id).sort
assert_equal [1,2], Topic.find(:all, :conditions => { :id => 1...3 }).map(&:id).sort
assert_raises(ActiveRecord::RecordNotFound) { Topic.find(3, :conditions => { :id => 2...3 }) }
assert_raise(ActiveRecord::RecordNotFound) { Topic.find(3, :conditions => { :id => 2...3 }) }
end
def test_find_on_hash_conditions_with_multiple_ranges
@ -320,9 +327,9 @@ class FinderTest < ActiveRecord::TestCase
def test_find_on_multiple_hash_conditions
assert Topic.find(1, :conditions => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => false })
assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => true }) }
assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :author_name => "David", :title => "HHC", :replies_count => 1, :approved => false }) }
assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => true }) }
assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => true }) }
assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :author_name => "David", :title => "HHC", :replies_count => 1, :approved => false }) }
assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => true }) }
end
def test_condition_interpolation
@ -346,7 +353,7 @@ class FinderTest < ActiveRecord::TestCase
end
def test_hash_condition_find_malformed
assert_raises(ActiveRecord::StatementInvalid) {
assert_raise(ActiveRecord::StatementInvalid) {
Company.find(:first, :conditions => { :id => 2, :dhh => true })
}
end
@ -415,10 +422,10 @@ class FinderTest < ActiveRecord::TestCase
assert_nil Company.find(:first, :conditions => ["name = ?", "37signals!"])
assert_nil Company.find(:first, :conditions => ["name = ?", "37signals!' OR 1=1"])
assert_kind_of Time, Topic.find(:first, :conditions => ["id = ?", 1]).written_on
assert_raises(ActiveRecord::PreparedStatementInvalid) {
assert_raise(ActiveRecord::PreparedStatementInvalid) {
Company.find(:first, :conditions => ["id=? AND name = ?", 2])
}
assert_raises(ActiveRecord::PreparedStatementInvalid) {
assert_raise(ActiveRecord::PreparedStatementInvalid) {
Company.find(:first, :conditions => ["id=?", 2, 3, 4])
}
end
@ -435,11 +442,11 @@ class FinderTest < ActiveRecord::TestCase
def test_bind_arity
assert_nothing_raised { bind '' }
assert_raises(ActiveRecord::PreparedStatementInvalid) { bind '', 1 }
assert_raise(ActiveRecord::PreparedStatementInvalid) { bind '', 1 }
assert_raises(ActiveRecord::PreparedStatementInvalid) { bind '?' }
assert_raise(ActiveRecord::PreparedStatementInvalid) { bind '?' }
assert_nothing_raised { bind '?', 1 }
assert_raises(ActiveRecord::PreparedStatementInvalid) { bind '?', 1, 1 }
assert_raise(ActiveRecord::PreparedStatementInvalid) { bind '?', 1, 1 }
end
def test_named_bind_variables
@ -544,7 +551,7 @@ class FinderTest < ActiveRecord::TestCase
def test_find_by_one_attribute_bang
assert_equal topics(:first), Topic.find_by_title!("The First Topic")
assert_raises(ActiveRecord::RecordNotFound) { Topic.find_by_title!("The First Topic!") }
assert_raise(ActiveRecord::RecordNotFound) { Topic.find_by_title!("The First Topic!") }
end
def test_find_by_one_attribute_caches_dynamic_finder
@ -625,14 +632,14 @@ class FinderTest < ActiveRecord::TestCase
end
def test_find_by_one_missing_attribute
assert_raises(NoMethodError) { Topic.find_by_undertitle("The First Topic!") }
assert_raise(NoMethodError) { Topic.find_by_undertitle("The First Topic!") }
end
def test_find_by_invalid_method_syntax
assert_raises(NoMethodError) { Topic.fail_to_find_by_title("The First Topic") }
assert_raises(NoMethodError) { Topic.find_by_title?("The First Topic") }
assert_raises(NoMethodError) { Topic.fail_to_find_or_create_by_title("Nonexistent Title") }
assert_raises(NoMethodError) { Topic.find_or_create_by_title?("Nonexistent Title") }
assert_raise(NoMethodError) { Topic.fail_to_find_by_title("The First Topic") }
assert_raise(NoMethodError) { Topic.find_by_title?("The First Topic") }
assert_raise(NoMethodError) { Topic.fail_to_find_or_create_by_title("Nonexistent Title") }
assert_raise(NoMethodError) { Topic.find_or_create_by_title?("Nonexistent Title") }
end
def test_find_by_two_attributes
@ -654,8 +661,8 @@ class FinderTest < ActiveRecord::TestCase
end
def test_find_last_by_invalid_method_syntax
assert_raises(NoMethodError) { Topic.fail_to_find_last_by_title("The First Topic") }
assert_raises(NoMethodError) { Topic.find_last_by_title?("The First Topic") }
assert_raise(NoMethodError) { Topic.fail_to_find_last_by_title("The First Topic") }
assert_raise(NoMethodError) { Topic.find_last_by_title?("The First Topic") }
end
def test_find_last_by_one_attribute_with_several_options
@ -663,7 +670,7 @@ class FinderTest < ActiveRecord::TestCase
end
def test_find_last_by_one_missing_attribute
assert_raises(NoMethodError) { Topic.find_last_by_undertitle("The Last Topic!") }
assert_raise(NoMethodError) { Topic.find_last_by_undertitle("The Last Topic!") }
end
def test_find_last_by_two_attributes
@ -916,16 +923,16 @@ class FinderTest < ActiveRecord::TestCase
end
def test_find_with_bad_sql
assert_raises(ActiveRecord::StatementInvalid) { Topic.find_by_sql "select 1 from badtable" }
assert_raise(ActiveRecord::StatementInvalid) { Topic.find_by_sql "select 1 from badtable" }
end
def test_find_with_invalid_params
assert_raises(ArgumentError) { Topic.find :first, :join => "It should be `joins'" }
assert_raises(ArgumentError) { Topic.find :first, :conditions => '1 = 1', :join => "It should be `joins'" }
assert_raise(ArgumentError) { Topic.find :first, :join => "It should be `joins'" }
assert_raise(ArgumentError) { Topic.find :first, :conditions => '1 = 1', :join => "It should be `joins'" }
end
def test_dynamic_finder_with_invalid_params
assert_raises(ArgumentError) { Topic.find_by_title 'No Title', :join => "It should be `joins'" }
assert_raise(ArgumentError) { Topic.find_by_title 'No Title', :join => "It should be `joins'" }
end
def test_find_all_with_limit
@ -1057,6 +1064,14 @@ class FinderTest < ActiveRecord::TestCase
assert_equal [0, 1, 1], posts.map(&:author_id).sort
end
def test_finder_with_scoped_from
all_topics = Topic.all
Topic.with_scope(:find => { :from => 'fake_topics' }) do
assert_equal all_topics, Topic.all(:from => 'topics')
end
end
protected
def bind(statement, *vars)
if vars.first.is_a?(Hash)

View file

@ -151,7 +151,7 @@ class FixturesTest < ActiveRecord::TestCase
end
def test_dirty_dirty_yaml_file
assert_raises(Fixture::FormatError) do
assert_raise(Fixture::FormatError) do
Fixtures.new( Account.connection, "courses", 'Course', FIXTURES_ROOT + "/naked/yml/courses")
end
end
@ -420,7 +420,7 @@ class InvalidTableNameFixturesTest < ActiveRecord::TestCase
self.use_transactional_fixtures = false
def test_raises_error
assert_raises FixtureClassNotFound do
assert_raise FixtureClassNotFound do
funny_jokes(:a_joke)
end
end

View file

@ -68,7 +68,7 @@ class InheritanceTest < ActiveRecord::TestCase
if current_adapter?(:SybaseAdapter)
Company.connection.execute "SET IDENTITY_INSERT companies OFF"
end
assert_raises(ActiveRecord::SubclassNotFound) { Company.find(100) }
assert_raise(ActiveRecord::SubclassNotFound) { Company.find(100) }
end
def test_inheritance_find
@ -124,7 +124,7 @@ class InheritanceTest < ActiveRecord::TestCase
end
def test_finding_incorrect_type_data
assert_raises(ActiveRecord::RecordNotFound) { Firm.find(2) }
assert_raise(ActiveRecord::RecordNotFound) { Firm.find(2) }
assert_nothing_raised { Firm.find(1) }
end

View file

@ -35,7 +35,25 @@ class OptimisticLockingTest < ActiveRecord::TestCase
assert_equal 0, p2.lock_version
p2.first_name = 'sue'
assert_raises(ActiveRecord::StaleObjectError) { p2.save! }
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
@ -50,9 +68,9 @@ class OptimisticLockingTest < ActiveRecord::TestCase
assert_equal 0, p2.lock_version
p2.first_name = 'sue'
assert_raises(ActiveRecord::StaleObjectError) { p2.save! }
assert_raise(ActiveRecord::StaleObjectError) { p2.save! }
p2.first_name = 'sue2'
assert_raises(ActiveRecord::StaleObjectError) { p2.save! }
assert_raise(ActiveRecord::StaleObjectError) { p2.save! }
end
def test_lock_new
@ -71,7 +89,7 @@ class OptimisticLockingTest < ActiveRecord::TestCase
assert_equal 0, p2.lock_version
p2.first_name = 'sue'
assert_raises(ActiveRecord::StaleObjectError) { p2.save! }
assert_raise(ActiveRecord::StaleObjectError) { p2.save! }
end
def test_lock_new_with_nil
@ -95,7 +113,7 @@ class OptimisticLockingTest < ActiveRecord::TestCase
assert_equal 0, t2.version
t2.tps_report_number = 800
assert_raises(ActiveRecord::StaleObjectError) { t2.save! }
assert_raise(ActiveRecord::StaleObjectError) { t2.save! }
end
def test_lock_column_is_mass_assignable

View file

@ -262,6 +262,15 @@ class NestedScopingTest < ActiveRecord::TestCase
end
end
def test_merge_inner_scope_has_priority
Developer.with_scope(:find => { :limit => 5 }) do
Developer.with_scope(:find => { :limit => 10 }) do
merged_option = Developer.instance_eval('current_scoped_methods')[:find]
assert_equal({ :limit => 10 }, merged_option)
end
end
end
def test_replace_options
Developer.with_scope(:find => { :conditions => "name = 'David'" }) do
Developer.with_exclusive_scope(:find => { :conditions => "name = 'Jamis'" }) do
@ -369,8 +378,10 @@ class NestedScopingTest < ActiveRecord::TestCase
def test_merged_scoped_find
poor_jamis = developers(:poor_jamis)
Developer.with_scope(:find => { :conditions => "salary < 100000" }) do
Developer.with_scope(:find => { :offset => 1 }) do
assert_equal(poor_jamis, Developer.find(:first, :order => 'id asc'))
Developer.with_scope(:find => { :offset => 1, :order => 'id asc' }) do
assert_sql /ORDER BY id asc / do
assert_equal(poor_jamis, Developer.find(:first, :order => 'id asc'))
end
end
end
end
@ -400,6 +411,29 @@ class NestedScopingTest < ActiveRecord::TestCase
end
end
def test_nested_scoped_create
comment = nil
Comment.with_scope(:create => { :post_id => 1}) do
Comment.with_scope(:create => { :post_id => 2}) do
assert_equal({ :post_id => 2 }, Comment.send(:current_scoped_methods)[:create])
comment = Comment.create :body => "Hey guys, nested scopes are broken. Please fix!"
end
end
assert_equal 2, comment.post_id
end
def test_nested_exclusive_scope_for_create
comment = nil
Comment.with_scope(:create => { :body => "Hey guys, nested scopes are broken. Please fix!" }) do
Comment.with_exclusive_scope(:create => { :post_id => 1 }) do
assert_equal({ :post_id => 1 }, Comment.send(:current_scoped_methods)[:create])
comment = Comment.create :body => "Hey guys"
end
end
assert_equal 1, comment.post_id
assert_equal 'Hey guys', comment.body
end
def test_merged_scoped_find_on_blank_conditions
[nil, " ", [], {}].each do |blank|
Developer.with_scope(:find => {:conditions => blank}) do
@ -523,7 +557,6 @@ class HasManyScopingTest< ActiveRecord::TestCase
end
end
class HasAndBelongsToManyScopingTest< ActiveRecord::TestCase
fixtures :posts, :categories, :categories_posts
@ -549,7 +582,6 @@ class HasAndBelongsToManyScopingTest< ActiveRecord::TestCase
end
end
class DefaultScopingTest < ActiveRecord::TestCase
fixtures :developers
@ -577,7 +609,7 @@ class DefaultScopingTest < ActiveRecord::TestCase
# Scopes added on children should append to parent scope
expected_klass_scope = [{ :create => {}, :find => { :order => 'salary DESC' }}, { :create => {}, :find => {} }]
assert_equal expected_klass_scope, klass.send(:scoped_methods)
# Parent should still have the original scope
assert_equal scope, DeveloperOrderedBySalary.send(:scoped_methods)
end
@ -620,7 +652,6 @@ end
=begin
# We disabled the scoping for has_one and belongs_to as we can't think of a proper use case
class BelongsToScopingTest< ActiveRecord::TestCase
fixtures :comments, :posts
@ -640,7 +671,6 @@ class BelongsToScopingTest< ActiveRecord::TestCase
end
class HasOneScopingTest< ActiveRecord::TestCase
fixtures :comments, :posts

View file

@ -93,6 +93,30 @@ if ActiveRecord::Base.connection.supports_migrations?
end
end
def testing_table_with_only_foo_attribute
Person.connection.create_table :testings, :id => false do |t|
t.column :foo, :string
end
yield Person.connection
ensure
Person.connection.drop_table :testings rescue nil
end
protected :testing_table_with_only_foo_attribute
def test_create_table_without_id
testing_table_with_only_foo_attribute do |connection|
assert_equal connection.columns(:testings).size, 1
end
end
def test_add_column_with_primary_key_attribute
testing_table_with_only_foo_attribute do |connection|
assert_nothing_raised { connection.add_column :testings, :id, :primary_key }
assert_equal connection.columns(:testings).size, 2
end
end
def test_create_table_adds_id
Person.connection.create_table :testings do |t|
t.column :foo, :string
@ -111,7 +135,7 @@ if ActiveRecord::Base.connection.supports_migrations?
end
end
assert_raises(ActiveRecord::StatementInvalid) do
assert_raise(ActiveRecord::StatementInvalid) do
Person.connection.execute "insert into testings (foo) values (NULL)"
end
ensure
@ -278,7 +302,7 @@ if ActiveRecord::Base.connection.supports_migrations?
end
Person.connection.add_column :testings, :bar, :string, :null => false
assert_raises(ActiveRecord::StatementInvalid) do
assert_raise(ActiveRecord::StatementInvalid) do
Person.connection.execute "insert into testings (foo, bar) values ('hello', NULL)"
end
ensure
@ -297,7 +321,7 @@ if ActiveRecord::Base.connection.supports_migrations?
Person.connection.enable_identity_insert("testings", false) if current_adapter?(:SybaseAdapter)
assert_nothing_raised {Person.connection.add_column :testings, :bar, :string, :null => false, :default => "default" }
assert_raises(ActiveRecord::StatementInvalid) do
assert_raise(ActiveRecord::StatementInvalid) do
unless current_adapter?(:OpenBaseAdapter)
Person.connection.execute "insert into testings (#{con.quote_column_name('id')}, #{con.quote_column_name('foo')}, #{con.quote_column_name('bar')}) values (2, 'hello', NULL)"
else
@ -545,7 +569,7 @@ if ActiveRecord::Base.connection.supports_migrations?
else
ActiveRecord::ActiveRecordError
end
assert_raises(exception) do
assert_raise(exception) do
Person.connection.rename_column "hats", "nonexistent", "should_fail"
end
ensure
@ -795,7 +819,7 @@ if ActiveRecord::Base.connection.supports_migrations?
assert_equal "hello world", Reminder.find(:first).content
WeNeedReminders.down
assert_raises(ActiveRecord::StatementInvalid) { Reminder.find(:first) }
assert_raise(ActiveRecord::StatementInvalid) { Reminder.find(:first) }
end
def test_add_table_with_decimals
@ -856,7 +880,7 @@ if ActiveRecord::Base.connection.supports_migrations?
end
GiveMeBigNumbers.down
assert_raises(ActiveRecord::StatementInvalid) { BigNumber.find(:first) }
assert_raise(ActiveRecord::StatementInvalid) { BigNumber.find(:first) }
end
def test_migrator
@ -876,7 +900,7 @@ if ActiveRecord::Base.connection.supports_migrations?
assert_equal 0, ActiveRecord::Migrator.current_version
Person.reset_column_information
assert !Person.column_methods_hash.include?(:last_name)
assert_raises(ActiveRecord::StatementInvalid) { Reminder.find(:first) }
assert_raise(ActiveRecord::StatementInvalid) { Reminder.find(:first) }
end
def test_migrator_one_up
@ -928,11 +952,11 @@ if ActiveRecord::Base.connection.supports_migrations?
assert_equal(0, ActiveRecord::Migrator.current_version)
end
if current_adapter?(:PostgreSQLAdapter)
if ActiveRecord::Base.connection.supports_ddl_transactions?
def test_migrator_one_up_with_exception_and_rollback
assert !Person.column_methods_hash.include?(:last_name)
e = assert_raises(StandardError) do
e = assert_raise(StandardError) do
ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/broken", 100)
end
@ -945,20 +969,20 @@ if ActiveRecord::Base.connection.supports_migrations?
def test_finds_migrations
migrations = ActiveRecord::Migrator.new(:up, MIGRATIONS_ROOT + "/valid").migrations
[['1', 'people_have_last_names'],
['2', 'we_need_reminders'],
['3', 'innocent_jointable']].each_with_index do |pair, i|
migrations[i].version == pair.first
migrations[1].name == pair.last
[[1, 'PeopleHaveLastNames'], [2, 'WeNeedReminders'], [3, 'InnocentJointable']].each_with_index do |pair, i|
assert_equal migrations[i].version, pair.first
assert_equal migrations[i].name, pair.last
end
end
def test_finds_pending_migrations
ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/interleaved/pass_2", 1)
migrations = ActiveRecord::Migrator.new(:up, MIGRATIONS_ROOT + "/interleaved/pass_2").pending_migrations
assert_equal 1, migrations.size
migrations[0].version == '3'
migrations[0].name == 'innocent_jointable'
assert_equal migrations[0].version, 3
assert_equal migrations[0].name, 'InnocentJointable'
end
def test_only_loads_pending_migrations
@ -1107,7 +1131,7 @@ if ActiveRecord::Base.connection.supports_migrations?
assert_equal "hello world", Reminder.find(:first).content
WeNeedReminders.down
assert_raises(ActiveRecord::StatementInvalid) { Reminder.find(:first) }
assert_raise(ActiveRecord::StatementInvalid) { Reminder.find(:first) }
ensure
ActiveRecord::Base.table_name_prefix = ''
ActiveRecord::Base.table_name_suffix = ''
@ -1137,13 +1161,13 @@ if ActiveRecord::Base.connection.supports_migrations?
end
def test_migrator_with_duplicates
assert_raises(ActiveRecord::DuplicateMigrationVersionError) do
assert_raise(ActiveRecord::DuplicateMigrationVersionError) do
ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/duplicate", nil)
end
end
def test_migrator_with_duplicate_names
assert_raises(ActiveRecord::DuplicateMigrationNameError, "Multiple migrations have the name Chunky") do
assert_raise(ActiveRecord::DuplicateMigrationNameError, "Multiple migrations have the name Chunky") do
ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/duplicate_names", nil)
end
end
@ -1159,7 +1183,7 @@ if ActiveRecord::Base.connection.supports_migrations?
# table name is 29 chars, the standard sequence name will
# be 33 chars and fail
assert_raises(ActiveRecord::StatementInvalid) do
assert_raise(ActiveRecord::StatementInvalid) do
begin
Person.connection.create_table :table_with_name_thats_just_ok do |t|
t.column :foo, :string, :null => false
@ -1186,7 +1210,7 @@ if ActiveRecord::Base.connection.supports_migrations?
end
# confirm the custom sequence got dropped
assert_raises(ActiveRecord::StatementInvalid) do
assert_raise(ActiveRecord::StatementInvalid) do
Person.connection.execute("select suitably_short_seq.nextval from dual")
end
end

View file

@ -15,7 +15,7 @@ class NamedScopeTest < ActiveRecord::TestCase
assert_equal Topic.find(:all), Topic.base
assert_equal Topic.find(:all), Topic.base.to_a
assert_equal Topic.find(:first), Topic.base.first
assert_equal Topic.find(:all), Topic.base.each { |i| i }
assert_equal Topic.find(:all), Topic.base.map { |i| i }
end
def test_found_items_are_cached
@ -99,6 +99,12 @@ class NamedScopeTest < ActiveRecord::TestCase
assert_equal topics_written_before_the_second, Topic.written_before(topics(:second).written_on)
end
def test_procedural_scopes_returning_nil
all_topics = Topic.find(:all)
assert_equal all_topics, Topic.written_before(nil)
end
def test_scopes_with_joins
address = author_addresses(:david_address)
posts_with_authors_at_address = Post.find(
@ -247,7 +253,7 @@ class NamedScopeTest < ActiveRecord::TestCase
topic = Topic.approved.create!({})
assert topic.approved
end
def test_should_build_with_proxy_options_chained
topic = Topic.approved.by_lifo.build({})
assert topic.approved
@ -287,15 +293,21 @@ class NamedScopeTest < ActiveRecord::TestCase
assert_equal post.comments.size, Post.scoped(:joins => join).scoped(:joins => join, :conditions => "posts.id = #{post.id}").size
end
def test_chanining_should_use_latest_conditions_when_creating
post1 = Topic.rejected.approved.new
assert post1.approved?
def test_chaining_should_use_latest_conditions_when_creating
post = Topic.rejected.new
assert !post.approved?
post2 = Topic.approved.rejected.new
assert ! post2.approved?
post = Topic.rejected.approved.new
assert post.approved?
post = Topic.approved.rejected.new
assert !post.approved?
post = Topic.approved.rejected.approved.new
assert post.approved?
end
def test_chanining_should_use_latest_conditions_when_searching
def test_chaining_should_use_latest_conditions_when_searching
# Normal hash conditions
assert_equal Topic.all(:conditions => {:approved => true}), Topic.rejected.approved.all
assert_equal Topic.all(:conditions => {:approved => false}), Topic.approved.rejected.all
@ -306,6 +318,24 @@ class NamedScopeTest < ActiveRecord::TestCase
# Nested hash conditions with different keys
assert_equal [posts(:sti_comments)], Post.with_special_comments.with_post(4).all.uniq
end
def test_methods_invoked_within_scopes_should_respect_scope
assert_equal [], Topic.approved.by_rejected_ids.proxy_options[:conditions][:id]
end
def test_named_scopes_batch_finders
assert_equal 3, Topic.approved.count
assert_queries(4) do
Topic.approved.find_each(:batch_size => 1) {|t| assert t.approved? }
end
assert_queries(2) do
Topic.approved.find_in_batches(:batch_size => 2) do |group|
group.each {|t| assert t.approved? }
end
end
end
end
class DynamicScopeMatchTest < ActiveRecord::TestCase

View file

@ -4,6 +4,7 @@ require 'models/customer'
require 'models/company'
require 'models/company_in_module'
require 'models/subscriber'
require 'models/pirate'
class ReflectionTest < ActiveRecord::TestCase
fixtures :topics, :customers, :companies, :subscribers
@ -169,9 +170,9 @@ class ReflectionTest < ActiveRecord::TestCase
def test_reflection_of_all_associations
# FIXME these assertions bust a lot
assert_equal 26, Firm.reflect_on_all_associations.size
assert_equal 20, Firm.reflect_on_all_associations(:has_many).size
assert_equal 6, Firm.reflect_on_all_associations(:has_one).size
assert_equal 28, Firm.reflect_on_all_associations.size
assert_equal 21, Firm.reflect_on_all_associations(:has_many).size
assert_equal 7, Firm.reflect_on_all_associations(:has_one).size
assert_equal 0, Firm.reflect_on_all_associations(:belongs_to).size
end

View file

@ -214,7 +214,7 @@ class TransactionTest < ActiveRecord::TestCase
end
def test_invalid_keys_for_transaction
assert_raises ArgumentError do
assert_raise ArgumentError do
Topic.transaction :nested => true do
end
end
@ -349,7 +349,7 @@ class TransactionTest < ActiveRecord::TestCase
end
end
def test_sqlite_add_column_in_transaction_raises_statement_invalid
def test_sqlite_add_column_in_transaction
return true unless current_adapter?(:SQLite3Adapter, :SQLiteAdapter)
# Test first if column creation/deletion works correctly when no
@ -368,10 +368,15 @@ class TransactionTest < ActiveRecord::TestCase
assert !Topic.column_names.include?('stuff')
end
# Test now inside a transaction: add_column should raise a StatementInvalid
Topic.transaction do
assert_raises(ActiveRecord::StatementInvalid) { Topic.connection.add_column('topics', 'stuff', :string) }
raise ActiveRecord::Rollback
if Topic.connection.supports_ddl_transactions?
assert_nothing_raised do
Topic.transaction { Topic.connection.add_column('topics', 'stuff', :string) }
end
else
Topic.transaction do
assert_raise(ActiveRecord::StatementInvalid) { Topic.connection.add_column('topics', 'stuff', :string) }
raise ActiveRecord::Rollback
end
end
end

View file

@ -8,6 +8,7 @@ require 'models/warehouse_thing'
require 'models/guid'
require 'models/owner'
require 'models/pet'
require 'models/event'
# The following methods in Topic are used in test_conditional_validation_*
class Topic
@ -113,8 +114,8 @@ class ValidationsTest < ActiveRecord::TestCase
end
def test_invalid_record_exception
assert_raises(ActiveRecord::RecordInvalid) { Reply.create! }
assert_raises(ActiveRecord::RecordInvalid) { Reply.new.save! }
assert_raise(ActiveRecord::RecordInvalid) { Reply.create! }
assert_raise(ActiveRecord::RecordInvalid) { Reply.new.save! }
begin
r = Reply.new
@ -126,13 +127,13 @@ class ValidationsTest < ActiveRecord::TestCase
end
def test_exception_on_create_bang_many
assert_raises(ActiveRecord::RecordInvalid) do
assert_raise(ActiveRecord::RecordInvalid) do
Reply.create!([ { "title" => "OK" }, { "title" => "Wrong Create" }])
end
end
def test_exception_on_create_bang_with_block
assert_raises(ActiveRecord::RecordInvalid) do
assert_raise(ActiveRecord::RecordInvalid) do
Reply.create!({ "title" => "OK" }) do |r|
r.content = nil
end
@ -140,7 +141,7 @@ class ValidationsTest < ActiveRecord::TestCase
end
def test_exception_on_create_bang_many_with_block
assert_raises(ActiveRecord::RecordInvalid) do
assert_raise(ActiveRecord::RecordInvalid) do
Reply.create!([{ "title" => "OK" }, { "title" => "Wrong Create" }]) do |r|
r.content = nil
end
@ -149,7 +150,7 @@ class ValidationsTest < ActiveRecord::TestCase
def test_scoped_create_without_attributes
Reply.with_scope(:create => {}) do
assert_raises(ActiveRecord::RecordInvalid) { Reply.create! }
assert_raise(ActiveRecord::RecordInvalid) { Reply.create! }
end
end
@ -169,7 +170,7 @@ class ValidationsTest < ActiveRecord::TestCase
assert_equal person.first_name, "Mary", "should be ok when no attributes are passed to create!"
end
end
end
end
def test_single_error_per_attr_iteration
r = Reply.new
@ -530,6 +531,14 @@ class ValidationsTest < ActiveRecord::TestCase
end
end
def test_validate_uniqueness_with_limit
# Event.title is limited to 5 characters
e1 = Event.create(:title => "abcde")
assert e1.valid?, "Could not create an event with a unique, 5 character title"
e2 = Event.create(:title => "abcdefgh")
assert !e2.valid?, "Created an event whose title, with limit taken into account, is not unique"
end
def test_validate_straight_inheritance_uniqueness
w1 = IneptWizard.create(:name => "Rincewind", :city => "Ankh-Morpork")
assert w1.valid?, "Saving w1"
@ -1421,6 +1430,17 @@ class ValidationsTest < ActiveRecord::TestCase
assert_equal "can't be blank", t.errors.on("title").first
end
def test_invalid_should_be_the_opposite_of_valid
Topic.validates_presence_of :title
t = Topic.new
assert t.invalid?
assert t.errors.invalid?(:title)
t.title = 'Things are going to change'
assert !t.invalid?
end
# previous implementation of validates_presence_of eval'd the
# string with the wrong binding, this regression test is to
# ensure that it works correctly
@ -1442,20 +1462,6 @@ class ValidationsTest < ActiveRecord::TestCase
t.author_name = "Hubert J. Farnsworth"
assert t.valid?, "A topic with an important title and author should be valid"
end
private
def with_kcode(kcode)
if RUBY_VERSION < '1.9'
orig_kcode, $KCODE = $KCODE, kcode
begin
yield
ensure
$KCODE = orig_kcode
end
else
yield
end
end
end

View file

@ -38,11 +38,15 @@ class XmlSerializationTest < ActiveRecord::TestCase
assert_match %r{<CreatedAt}, @xml
end
def test_should_allow_skipped_types
@xml = Contact.new(:age => 25).to_xml :skip_types => true
assert %r{<age>25</age>}.match(@xml)
end
def test_should_include_yielded_additions
@xml = Contact.new.to_xml do |xml|
xml.creator "David"
end
assert_match %r{<creator>David</creator>}, @xml
end
end
@ -145,6 +149,13 @@ class DatabaseConnectedXmlSerializationTest < ActiveRecord::TestCase
assert_match %r{<hello-post type="StiPost">}, xml
end
def test_included_associations_should_skip_types
xml = authors(:david).to_xml :include=>:hello_posts, :indent => 0, :skip_types => true
assert_match %r{<hello-posts>}, xml
assert_match %r{<hello-post>}, xml
assert_match %r{<hello-post>}, xml
end
def test_methods_are_called_on_object
xml = authors(:david).to_xml :methods => :label, :indent => 0
assert_match %r{<label>.*</label>}, xml

View file

@ -0,0 +1,4 @@
bone:
toy_id: 1
name: Bone
pet_id: 1

View file

@ -37,6 +37,7 @@ class Firm < Company
has_many :clients, :order => "id", :dependent => :destroy, :counter_sql =>
"SELECT COUNT(*) FROM companies WHERE firm_id = 1 " +
"AND (#{QUOTED_TYPE} = 'Client' OR #{QUOTED_TYPE} = 'SpecialClient' OR #{QUOTED_TYPE} = 'VerySpecialClient' )"
has_many :unsorted_clients, :class_name => "Client"
has_many :clients_sorted_desc, :class_name => "Client", :order => "id DESC"
has_many :clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :order => "id"
has_many :unvalidated_clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :validate => false
@ -69,6 +70,7 @@ 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 :deletable_account, :foreign_key => "firm_id", :class_name => "Account", :dependent => :delete
end
class DependentFirm < Company

View file

@ -0,0 +1,3 @@
class Event < ActiveRecord::Base
validates_uniqueness_of :title
end

View file

@ -1,4 +1,5 @@
class Owner < ActiveRecord::Base
set_primary_key :owner_id
has_many :pets
end
has_many :toys, :through => :pets
end

View file

@ -1,4 +1,5 @@
class Pet < ActiveRecord::Base
set_primary_key :pet_id
belongs_to :owner
end
has_many :toys
end

View file

@ -1,16 +1,63 @@
class Pirate < ActiveRecord::Base
belongs_to :parrot
has_and_belongs_to_many :parrots
has_many :treasures, :as => :looter
has_and_belongs_to_many :parrots_with_method_callbacks, :class_name => "Parrot",
:before_add => :log_before_add,
:after_add => :log_after_add,
:before_remove => :log_before_remove,
:after_remove => :log_after_remove
has_and_belongs_to_many :parrots_with_proc_callbacks, :class_name => "Parrot",
:before_add => proc {|p,pa| p.ship_log << "before_adding_proc_parrot_#{pa.id || '<new>'}"},
:after_add => proc {|p,pa| p.ship_log << "after_adding_proc_parrot_#{pa.id || '<new>'}"},
:before_remove => proc {|p,pa| p.ship_log << "before_removing_proc_parrot_#{pa.id}"},
:after_remove => proc {|p,pa| p.ship_log << "after_removing_proc_parrot_#{pa.id}"}
has_many :treasures, :as => :looter
has_many :treasure_estimates, :through => :treasures, :source => :price_estimates
# These both have :autosave enabled because accepts_nested_attributes_for is used on them.
has_one :ship
has_many :birds
has_many :birds_with_method_callbacks, :class_name => "Bird",
:before_add => :log_before_add,
:after_add => :log_after_add,
:before_remove => :log_before_remove,
:after_remove => :log_after_remove
has_many :birds_with_proc_callbacks, :class_name => "Bird",
:before_add => proc {|p,b| p.ship_log << "before_adding_proc_bird_#{b.id || '<new>'}"},
:after_add => proc {|p,b| p.ship_log << "after_adding_proc_bird_#{b.id || '<new>'}"},
:before_remove => proc {|p,b| p.ship_log << "before_removing_proc_bird_#{b.id}"},
:after_remove => proc {|p,b| p.ship_log << "after_removing_proc_bird_#{b.id}"}
accepts_nested_attributes_for :parrots, :birds, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? }
accepts_nested_attributes_for :ship, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? }
accepts_nested_attributes_for :parrots_with_method_callbacks, :parrots_with_proc_callbacks,
:birds_with_method_callbacks, :birds_with_proc_callbacks, :allow_destroy => true
validates_presence_of :catchphrase
def ship_log
@ship_log ||= []
end
private
def log_before_add(record)
log(record, "before_adding_method")
end
def log_after_add(record)
log(record, "after_adding_method")
end
def log_before_remove(record)
log(record, "before_removing_method")
end
def log_after_remove(record)
log(record, "after_removing_method")
end
def log(record, callback)
ship_log << "#{callback}_#{record.class.name.downcase}_#{record.id || '<new>'}"
end
end

View file

@ -37,3 +37,9 @@ end
class SillyReply < Reply
belongs_to :reply, :foreign_key => "parent_id", :counter_cache => :replies_count
end
module Web
class Reply < Web::Topic
belongs_to :topic, :foreign_key => "parent_id", :counter_cache => true, :class_name => 'Web::Topic'
end
end

View file

@ -1,7 +1,9 @@
class Topic < ActiveRecord::Base
named_scope :base
named_scope :written_before, lambda { |time|
{ :conditions => ['written_on < ?', time] }
if time
{ :conditions => ['written_on < ?', time] }
end
}
named_scope :approved, :conditions => {:approved => true}
named_scope :rejected, :conditions => {:approved => false}
@ -33,6 +35,8 @@ class Topic < ActiveRecord::Base
end
named_scope :named_extension, :extend => NamedExtension
named_scope :multiple_extensions, :extend => [MultipleExtensionTwo, MultipleExtensionOne]
named_scope :by_rejected_ids, lambda {{ :conditions => { :id => all(:conditions => {:approved => false}).map(&:id) } }}
has_many :replies, :dependent => :destroy, :foreign_key => "parent_id"
serialize :content
@ -69,3 +73,9 @@ class Topic < ActiveRecord::Base
end
end
end
module Web
class Topic < ActiveRecord::Base
has_many :replies, :dependent => :destroy, :foreign_key => "parent_id", :class_name => 'Web::Reply'
end
end

View file

@ -0,0 +1,4 @@
class Toy < ActiveRecord::Base
set_primary_key :toy_id
belongs_to :pet
end

View file

@ -155,6 +155,10 @@ ActiveRecord::Schema.define do
t.integer :course_id, :null => false
end
create_table :events, :force => true do |t|
t.string :title, :limit => 5
end
create_table :funny_jokes, :force => true do |t|
t.string :name
end
@ -421,6 +425,11 @@ ActiveRecord::Schema.define do
t.column :taggings_count, :integer, :default => 0
end
create_table :toys, :primary_key => :toy_id ,:force => true do |t|
t.string :name
t.integer :pet_id, :integer
end
create_table :treasures, :force => true do |t|
t.column :name, :string
t.column :looter_id, :integer