aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib
diff options
context:
space:
mode:
authorEmilio Tagua <miloops@gmail.com>2009-07-31 16:21:07 -0300
committerEmilio Tagua <miloops@gmail.com>2009-07-31 16:21:07 -0300
commit3de59e916d6a3d4eab202cf0c99b1f88905a3b43 (patch)
treedef6d6a808ebe187be1f37f8a739fd786cc11f02 /activerecord/lib
parentc1cbf02e3170f1004daf4a146cbc41176c2458d3 (diff)
parent62fd1d3716b4b5fd1d91cdcc77003efe80fc5a7e (diff)
downloadrails-3de59e916d6a3d4eab202cf0c99b1f88905a3b43.tar.gz
rails-3de59e916d6a3d4eab202cf0c99b1f88905a3b43.tar.bz2
rails-3de59e916d6a3d4eab202cf0c99b1f88905a3b43.zip
Merge commit 'rails/master'
Conflicts: activerecord/lib/active_record/associations.rb
Diffstat (limited to 'activerecord/lib')
-rw-r--r--activerecord/lib/active_record.rb13
-rw-r--r--activerecord/lib/active_record/association_preload.rb4
-rwxr-xr-xactiverecord/lib/active_record/associations.rb76
-rw-r--r--activerecord/lib/active_record/associations/association_collection.rb4
-rw-r--r--activerecord/lib/active_record/attribute_methods.rb309
-rw-r--r--activerecord/lib/active_record/attribute_methods/before_type_cast.rb33
-rw-r--r--activerecord/lib/active_record/attribute_methods/dirty.rb187
-rw-r--r--activerecord/lib/active_record/attribute_methods/primary_key.rb44
-rw-r--r--activerecord/lib/active_record/attribute_methods/query.rb37
-rw-r--r--activerecord/lib/active_record/attribute_methods/read.rb116
-rw-r--r--activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb60
-rw-r--r--activerecord/lib/active_record/attribute_methods/write.rb37
-rwxr-xr-xactiverecord/lib/active_record/base.rb145
-rw-r--r--activerecord/lib/active_record/callbacks.rb18
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb8
-rw-r--r--activerecord/lib/active_record/dirty.rb186
-rw-r--r--activerecord/lib/active_record/fixtures.rb2
-rw-r--r--activerecord/lib/active_record/migration.rb6
-rw-r--r--activerecord/lib/active_record/named_scope.rb7
-rw-r--r--activerecord/lib/active_record/nested_attributes.rb2
-rw-r--r--activerecord/lib/active_record/serializers/xml_serializer.rb15
-rw-r--r--activerecord/lib/active_record/validations.rb2
23 files changed, 757 insertions, 556 deletions
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb
index 009071e1d4..ab271291c5 100644
--- a/activerecord/lib/active_record.rb
+++ b/activerecord/lib/active_record.rb
@@ -37,7 +37,7 @@ rescue LoadError
end
module ActiveRecord
- # TODO: Review explicit loads to see if they will automatically be handled by the initilizer.
+ # TODO: Review explicit loads to see if they will automatically be handled by the initializer.
def self.load_all!
[Base, DynamicFinderMatch, ConnectionAdapters::AbstractAdapter]
end
@@ -57,7 +57,6 @@ module ActiveRecord
autoload :Batches, 'active_record/batches'
autoload :Calculations, 'active_record/calculations'
autoload :Callbacks, 'active_record/callbacks'
- autoload :Dirty, 'active_record/dirty'
autoload :DynamicFinderMatch, 'active_record/dynamic_finder_match'
autoload :DynamicScopeMatch, 'active_record/dynamic_scope_match'
autoload :Migration, 'active_record/migration'
@@ -76,6 +75,16 @@ module ActiveRecord
autoload :Transactions, 'active_record/transactions'
autoload :Validations, 'active_record/validations'
+ module AttributeMethods
+ autoload :BeforeTypeCast, 'active_record/attribute_methods/before_type_cast'
+ autoload :Dirty, 'active_record/attribute_methods/dirty'
+ autoload :PrimaryKey, 'active_record/attribute_methods/primary_key'
+ autoload :Query, 'active_record/attribute_methods/query'
+ autoload :Read, 'active_record/attribute_methods/read'
+ autoload :TimeZoneConversion, 'active_record/attribute_methods/time_zone_conversion'
+ autoload :Write, 'active_record/attribute_methods/write'
+ end
+
module Locking
autoload :Optimistic, 'active_record/locking/optimistic'
autoload :Pessimistic, 'active_record/locking/pessimistic'
diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb
index af80a579d6..e41fda7a4b 100644
--- a/activerecord/lib/active_record/association_preload.rb
+++ b/activerecord/lib/active_record/association_preload.rb
@@ -28,7 +28,7 @@ module ActiveRecord
# 'books' table is useful; the joined 'authors' data is just redundant, and
# processing this redundant data takes memory and CPU time. The problem
# quickly becomes worse and worse as the level of eager loading increases
- # (i.e. if ActiveRecord is to eager load the associations' assocations as
+ # (i.e. if ActiveRecord is to eager load the associations' associations as
# well).
#
# The second strategy is to use multiple database queries, one for each
@@ -58,7 +58,7 @@ module ActiveRecord
# +associations+ specifies one or more associations that you want to
# preload. It may be:
# - a Symbol or a String which specifies a single association name. For
- # example, specifiying +:books+ allows this method to preload all books
+ # example, specifying +:books+ allows this method to preload all books
# for an Author.
# - an Array which specifies multiple association names. This array
# is processed recursively. For example, specifying <tt>[:avatar, :books]</tt>
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index a0aeff68b6..60da632b3b 100755
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -519,13 +519,13 @@ module ActiveRecord
#
# Post.find(:all, :include => [ :author, :comments ], :conditions => ['comments.approved = ?', true])
#
- # will result in a single SQL query with joins along the lines of: <tt>LEFT OUTER JOIN comments ON comments.post_id = posts.id</tt> and
+ # This will result in a single SQL query with joins along the lines of: <tt>LEFT OUTER JOIN comments ON comments.post_id = posts.id</tt> and
# <tt>LEFT OUTER JOIN authors ON authors.id = posts.author_id</tt>. Note that using conditions like this can have unintended consequences.
# In the above example posts with no approved comments are not returned at all, because the conditions apply to the SQL statement as a whole
# and not just to the association. You must disambiguate column references for this fallback to happen, for example
# <tt>:order => "author.name DESC"</tt> will work but <tt>:order => "name DESC"</tt> will not.
#
- # If you do want eagerload only some members of an association it is usually more natural to <tt>:include</tt> an association
+ # If you do want eager load only some members of an association it is usually more natural to <tt>:include</tt> an association
# which has conditions defined on it:
#
# class Post < ActiveRecord::Base
@@ -534,7 +534,7 @@ module ActiveRecord
#
# Post.find(:all, :include => :approved_comments)
#
- # will load posts and eager load the +approved_comments+ association, which contains only those comments that have been approved.
+ # This will load posts and eager load the +approved_comments+ association, which contains only those comments that have been approved.
#
# If you eager load an association with a specified <tt>:limit</tt> option, it will be ignored, returning all the associated objects:
#
@@ -557,7 +557,7 @@ module ActiveRecord
#
# Address.find(:all, :include => :addressable)
#
- # will execute one query to load the addresses and load the addressables with one query per addressable type.
+ # This will execute one query to load the addresses and load the addressables with one query per addressable type.
# For example if all the addressables are either of class Person or Company then a total of 3 queries will be executed. The list of
# addressable types to load is determined on the back of the addresses loaded. This is not supported if Active Record has to fallback
# to the previous implementation of eager loading and will raise ActiveRecord::EagerLoadPolymorphicError. The reason is that the parent
@@ -641,6 +641,60 @@ module ActiveRecord
# end
# end
#
+ # == Bi-directional associations
+ #
+ # When you specify an association there is usually an association on the associated model that specifies the same
+ # relationship in reverse. For example, with the following models:
+ #
+ # class Dungeon < ActiveRecord::Base
+ # has_many :traps
+ # has_one :evil_wizard
+ # end
+ #
+ # class Trap < ActiveRecord::Base
+ # belongs_to :dungeon
+ # end
+ #
+ # class EvilWizard < ActiveRecord::Base
+ # belongs_to :dungeon
+ # end
+ #
+ # The +traps+ association on +Dungeon+ and the the +dungeon+ association on +Trap+ are the inverse of each other and the
+ # inverse of the +dungeon+ association on +EvilWizard+ is the +evil_wizard+ association on +Dungeon+ (and vice-versa). By default,
+ # +ActiveRecord+ doesn't do know anything about these inverse relationships and so no object loading optimisation is possible. For example:
+ #
+ # d = Dungeon.first
+ # t = d.traps.first
+ # d.level == t.dungeon.level # => true
+ # d.level = 10
+ # d.level == t.dungeon.level # => false
+ #
+ # The +Dungeon+ instances +d+ and <tt>t.dungeon</tt> in the above example refer to the same object data from the database, but are
+ # actually different in-memory copies of that data. Specifying the <tt>:inverse_of</tt> option on associations lets you tell
+ # +ActiveRecord+ about inverse relationships and it will optimise object loading. For example, if we changed our model definitions to:
+ #
+ # class Dungeon < ActiveRecord::Base
+ # has_many :traps, :inverse_of => :dungeon
+ # has_one :evil_wizard, :inverse_of => :dungeon
+ # end
+ #
+ # class Trap < ActiveRecord::Base
+ # belongs_to :dungeon, :inverse_of => :traps
+ # end
+ #
+ # class EvilWizard < ActiveRecord::Base
+ # belongs_to :dungeon, :inverse_of => :evil_wizard
+ # end
+ #
+ # Then, from our code snippet above, +d+ and <tt>t.dungeon</tt> are actually the same in-memory instance and our final <tt>d.level == t.dungeon.level</tt>
+ # will return +true+.
+ #
+ # There are limitations to <tt>:inverse_of</tt> support:
+ #
+ # * does not work with <tt>:through</tt> associations.
+ # * does not work with <tt>:polymorphic</tt> associations.
+ # * for +belongs_to+ associations +has_many+ inverse associations are ignored.
+ #
# == Type safety with <tt>ActiveRecord::AssociationTypeMismatch</tt>
#
# If you attempt to assign an object to an association that doesn't match the inferred or specified <tt>:class_name</tt>, you'll
@@ -781,6 +835,10 @@ module ActiveRecord
# If false, don't validate the associated objects when saving the parent object. true by default.
# [:autosave]
# If true, always save any loaded members and destroy members marked for destruction, when saving the parent object. Off by default.
+ # [:inverse_of]
+ # Specifies the name of the <tt>belongs_to</tt> association on the associated object that is the inverse of this <tt>has_many</tt>
+ # association. Does not work in combination with <tt>:through</tt> or <tt>:as</tt> options.
+ # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional assocations for more detail.
#
# Option examples:
# has_many :comments, :order => "posted_on"
@@ -890,6 +948,10 @@ module ActiveRecord
# If false, don't validate the associated object when saving the parent object. +false+ by default.
# [:autosave]
# If true, always save the associated object or destroy it if marked for destruction, when saving the parent object. Off by default.
+ # [:inverse_of]
+ # Specifies the name of the <tt>belongs_to</tt> association on the associated object that is the inverse of this <tt>has_one</tt>
+ # association. Does not work in combination with <tt>:through</tt> or <tt>:as</tt> options.
+ # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional assocations for more detail.
#
# Option examples:
# has_one :credit_card, :dependent => :destroy # destroys the associated credit card
@@ -992,6 +1054,10 @@ module ActiveRecord
# [:touch]
# If true, the associated object will be touched (the updated_at/on attributes set to now) when this record is either saved or
# destroyed. If you specify a symbol, that attribute will be updated with the current time instead of the updated_at/on attribute.
+ # [:inverse_of]
+ # Specifies the name of the <tt>has_one</tt> or <tt>has_many</tt> association on the associated object that is the inverse of this <tt>belongs_to</tt>
+ # association. Does not work in combination with the <tt>:polymorphic</tt> options.
+ # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional assocations for more detail.
#
# Option examples:
# belongs_to :firm, :foreign_key => "client_of"
@@ -1201,7 +1267,7 @@ module ActiveRecord
private
# Generates a join table name from two provided table names.
- # The names in the join table namesme end up in lexicographic order.
+ # The names in the join table names end up in lexicographic order.
#
# join_table_name("members", "clubs") # => "clubs_members"
# join_table_name("members", "special_clubs") # => "members_special_clubs"
diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb
index 84edaec15e..e67ccfb228 100644
--- a/activerecord/lib/active_record/associations/association_collection.rb
+++ b/activerecord/lib/active_record/associations/association_collection.rb
@@ -11,7 +11,7 @@ module ActiveRecord
# ones created with +build+ are added to the target. So, the target may be
# non-empty and still lack children waiting to be read from the database.
# If you look directly to the database you cannot assume that's the entire
- # collection because new records may have beed added to the target, etc.
+ # collection because new records may have been added to the target, etc.
#
# If you need to work on all current children, new and existing records,
# +load_target+ and the +loaded+ flag are your friends.
@@ -228,7 +228,7 @@ module ActiveRecord
self
end
- # Destory all the records from this association.
+ # Destroy all the records from this association.
#
# See destroy for more info.
def destroy_all
diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb
index ecd2d57a5a..5cb536af1f 100644
--- a/activerecord/lib/active_record/attribute_methods.rb
+++ b/activerecord/lib/active_record/attribute_methods.rb
@@ -4,22 +4,6 @@ module ActiveRecord
module AttributeMethods #:nodoc:
extend ActiveSupport::Concern
- DEFAULT_SUFFIXES = %w(= ? _before_type_cast)
- ATTRIBUTE_TYPES_CACHED_BY_DEFAULT = [:datetime, :timestamp, :time, :date]
-
- included do
- attribute_method_suffix(*DEFAULT_SUFFIXES)
-
- cattr_accessor :attribute_types_cached_by_default, :instance_writer => false
- self.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT
-
- cattr_accessor :time_zone_aware_attributes, :instance_writer => false
- self.time_zone_aware_attributes = false
-
- class_inheritable_accessor :skip_time_zone_conversion_for_attributes, :instance_writer => false
- self.skip_time_zone_conversion_for_attributes = []
- end
-
# Declare and check for suffixed attribute methods.
module ClassMethods
# Declares a method available for all attributes with the given suffix.
@@ -50,8 +34,39 @@ module ActiveRecord
# person.name = 'Hubert'
# person.name_changed? # => true
def attribute_method_suffix(*suffixes)
- attribute_method_suffixes.concat suffixes
+ attribute_method_suffixes.concat(suffixes)
rebuild_attribute_method_regexp
+ undefine_attribute_methods
+ 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
+ # return that value (as a string). Otherwise, the given block
+ # will be used to compute the value of the method.
+ #
+ # The original method will be aliased, with the new name being
+ # prefixed with "original_". This allows the new method to
+ # access the original value.
+ #
+ # Example:
+ #
+ # class A < ActiveRecord::Base
+ # define_attr_method :primary_key, "sysid"
+ # define_attr_method( :inheritance_column ) do
+ # original_inheritance_column + "_id"
+ # end
+ # end
+ def define_attr_method(name, value=nil, &block)
+ sing = metaclass
+ sing.send :alias_method, "original_#{name}", name
+ if block_given?
+ sing.send :define_method, name, &block
+ else
+ # use eval instead of a block to work around a memory leak in dev
+ # mode in fcgi
+ sing.class_eval "def #{name}; #{value.to_s.inspect}; end"
+ end
end
# Returns MatchData if method_name is an attribute method.
@@ -60,173 +75,77 @@ module ActiveRecord
@@attribute_method_regexp.match(method_name)
end
-
# Contains the names of the generated attribute methods.
def generated_methods #:nodoc:
@generated_methods ||= Set.new
end
-
+
def generated_methods?
!generated_methods.empty?
end
-
+
# Generates all the attribute related methods for columns in the database
# accessors, mutators and query methods.
def define_attribute_methods
return if generated_methods?
- columns_hash.each do |name, column|
- unless instance_method_already_implemented?(name)
- if self.serialized_attributes[name]
- define_read_method_for_serialized_attribute(name)
- elsif create_time_zone_conversion_attribute?(name, column)
- define_read_method_for_time_zone_conversion(name)
- else
- define_read_method(name.to_sym, name, column)
- end
- end
-
- unless instance_method_already_implemented?("#{name}=")
- if create_time_zone_conversion_attribute?(name, column)
- define_write_method_for_time_zone_conversion(name)
- else
- define_write_method(name.to_sym)
+ columns_hash.keys.each do |name|
+ attribute_method_suffixes.each do |suffix|
+ method_name = "#{name}#{suffix}"
+ unless instance_method_already_implemented?(method_name)
+ generate_method = "define_attribute_method#{suffix}"
+ if respond_to?(generate_method)
+ send(generate_method, name)
+ else
+ evaluate_attribute_method("def #{method_name}(*args); send(:attribute#{suffix}, '#{name}', *args); end", method_name)
+ end
end
end
-
- unless instance_method_already_implemented?("#{name}?")
- define_question_method(name)
- end
end
end
+ def undefine_attribute_methods
+ generated_methods.each { |name| undef_method(name) }
+ @generated_methods = nil
+ end
+
# Checks whether the method is defined in the model or any of its subclasses
# that also derive from Active Record. Raises DangerousAttributeError if the
# method is defined by Active Record though.
def instance_method_already_implemented?(method_name)
method_name = method_name.to_s
- return true if method_name =~ /^id(=$|\?$|$)/
@_defined_class_methods ||= ancestors.first(ancestors.index(ActiveRecord::Base)).sum([]) { |m| m.public_instance_methods(false) | m.private_instance_methods(false) | m.protected_instance_methods(false) }.map {|m| m.to_s }.to_set
@@_defined_activerecord_methods ||= (ActiveRecord::Base.public_instance_methods(false) | ActiveRecord::Base.private_instance_methods(false) | ActiveRecord::Base.protected_instance_methods(false)).map{|m| m.to_s }.to_set
raise DangerousAttributeError, "#{method_name} is defined by ActiveRecord" if @@_defined_activerecord_methods.include?(method_name)
@_defined_class_methods.include?(method_name)
end
-
- alias :define_read_methods :define_attribute_methods
-
- # +cache_attributes+ allows you to declare which converted attribute values should
- # be cached. Usually caching only pays off for attributes with expensive conversion
- # methods, like time related columns (e.g. +created_at+, +updated_at+).
- def cache_attributes(*attribute_names)
- attribute_names.each {|attr| cached_attributes << attr.to_s}
- end
-
- # Returns the attributes which are cached. By default time related columns
- # with datatype <tt>:datetime, :timestamp, :time, :date</tt> are cached.
- def cached_attributes
- @cached_attributes ||=
- columns.select{|c| attribute_types_cached_by_default.include?(c.type)}.map{|col| col.name}.to_set
- end
-
- # Returns +true+ if the provided attribute is being cached.
- def cache_attribute?(attr_name)
- cached_attributes.include?(attr_name)
- end
private
-
# Suffixes a, ?, c become regexp /(a|\?|c)$/
def rebuild_attribute_method_regexp
suffixes = attribute_method_suffixes.map { |s| Regexp.escape(s) }
@@attribute_method_regexp = /(#{suffixes.join('|')})$/.freeze
end
- # Default to =, ?, _before_type_cast
def attribute_method_suffixes
@@attribute_method_suffixes ||= []
end
-
- def create_time_zone_conversion_attribute?(name, column)
- time_zone_aware_attributes && !skip_time_zone_conversion_for_attributes.include?(name.to_sym) && [:datetime, :timestamp].include?(column.type)
- end
-
- # Define an attribute reader method. Cope with nil column.
- def define_read_method(symbol, attr_name, column)
- cast_code = column.type_cast_code('v') if column
- access_code = cast_code ? "(v=@attributes['#{attr_name}']) && #{cast_code}" : "@attributes['#{attr_name}']"
-
- unless attr_name.to_s == self.primary_key.to_s
- access_code = access_code.insert(0, "missing_attribute('#{attr_name}', caller) unless @attributes.has_key?('#{attr_name}'); ")
- end
-
- if cache_attribute?(attr_name)
- access_code = "@attributes_cache['#{attr_name}'] ||= (#{access_code})"
- end
- evaluate_attribute_method attr_name, "def #{symbol}; #{access_code}; end"
- end
-
- # Define read method for serialized attribute.
- def define_read_method_for_serialized_attribute(attr_name)
- evaluate_attribute_method attr_name, "def #{attr_name}; unserialize_attribute('#{attr_name}'); end"
- end
-
- # Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled.
- # This enhanced read method automatically converts the UTC time stored in the database to the time zone stored in Time.zone.
- def define_read_method_for_time_zone_conversion(attr_name)
- method_body = <<-EOV
- def #{attr_name}(reload = false)
- cached = @attributes_cache['#{attr_name}']
- return cached if cached && !reload
- time = read_attribute('#{attr_name}')
- @attributes_cache['#{attr_name}'] = time.acts_like?(:time) ? time.in_time_zone : time
- end
- EOV
- evaluate_attribute_method attr_name, method_body
- end
-
- # Defines a predicate method <tt>attr_name?</tt>.
- def define_question_method(attr_name)
- evaluate_attribute_method attr_name, "def #{attr_name}?; query_attribute('#{attr_name}'); end", "#{attr_name}?"
- end
-
- def define_write_method(attr_name)
- evaluate_attribute_method attr_name, "def #{attr_name}=(new_value);write_attribute('#{attr_name}', new_value);end", "#{attr_name}="
- end
-
- # Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled.
- # This enhanced write method will automatically convert the time passed to it to the zone stored in Time.zone.
- def define_write_method_for_time_zone_conversion(attr_name)
- method_body = <<-EOV
- def #{attr_name}=(time)
- unless time.acts_like?(:time)
- time = time.is_a?(String) ? Time.zone.parse(time) : time.to_time rescue time
- end
- time = time.in_time_zone rescue nil if time
- write_attribute(:#{attr_name}, time)
- end
- EOV
- evaluate_attribute_method attr_name, method_body, "#{attr_name}="
- end
# Evaluate the definition for an attribute related method
- def evaluate_attribute_method(attr_name, method_definition, method_name=attr_name)
-
- unless method_name.to_s == primary_key.to_s
- generated_methods << method_name
- end
+ def evaluate_attribute_method(method_definition, method_name)
+ generated_methods << method_name.to_s
begin
class_eval(method_definition, __FILE__, __LINE__)
rescue SyntaxError => err
- generated_methods.delete(attr_name)
+ generated_methods.delete(method_name.to_s)
if logger
logger.warn "Exception occurred during reader method compilation."
- logger.warn "Maybe #{attr_name} is not a valid Ruby identifier?"
+ logger.warn "Maybe #{method_name} is not a valid Ruby identifier?"
logger.warn err.message
end
end
end
- end # ClassMethods
-
+ end
# Allows access to the object attributes, which are held in the <tt>@attributes</tt> hash, as though they
# were first-class methods. So a Person class with a name attribute can use Person#name and
@@ -248,98 +167,17 @@ module ActiveRecord
return self.send(method_id, *args, &block)
end
end
-
- guard_private_attribute_method!(method_name, args)
- if self.class.primary_key.to_s == method_name
- id
- elsif md = self.class.match_attribute_method?(method_name)
- attribute_name, method_type = md.pre_match, md.to_s
- if @attributes.include?(attribute_name)
- __send__("attribute#{method_type}", attribute_name, *args, &block)
- else
- super
- end
- elsif @attributes.include?(method_name)
- read_attribute(method_name)
- else
- super
- end
- end
- # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example,
- # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)).
- def read_attribute(attr_name)
- attr_name = attr_name.to_s
- if !(value = @attributes[attr_name]).nil?
- if column = column_for_attribute(attr_name)
- if unserializable_attribute?(attr_name, column)
- unserialize_attribute(attr_name)
- else
- column.type_cast(value)
- end
- else
- value
+ if md = self.class.match_attribute_method?(method_name)
+ attribute_name, method_type = md.pre_match, md.to_s
+ if attribute_name == 'id' || @attributes.include?(attribute_name)
+ guard_private_attribute_method!(method_name, args)
+ return __send__("attribute#{method_type}", attribute_name, *args, &block)
end
- else
- nil
- end
- end
-
- def read_attribute_before_type_cast(attr_name)
- @attributes[attr_name]
- end
-
- # Returns true if the attribute is of a text column and marked for serialization.
- def unserializable_attribute?(attr_name, column)
- column.text? && self.class.serialized_attributes[attr_name]
- end
-
- # Returns the unserialized object of the attribute.
- def unserialize_attribute(attr_name)
- unserialized_object = object_from_yaml(@attributes[attr_name])
-
- if unserialized_object.is_a?(self.class.serialized_attributes[attr_name]) || unserialized_object.nil?
- @attributes.frozen? ? unserialized_object : @attributes[attr_name] = unserialized_object
- else
- raise SerializationTypeMismatch,
- "#{attr_name} was supposed to be a #{self.class.serialized_attributes[attr_name]}, but was a #{unserialized_object.class.to_s}"
- end
- end
-
-
- # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+. Empty strings for fixnum and float
- # columns are turned into +nil+.
- def write_attribute(attr_name, value)
- attr_name = attr_name.to_s
- @attributes_cache.delete(attr_name)
- if (column = column_for_attribute(attr_name)) && column.number?
- @attributes[attr_name] = convert_number_column_value(value)
- else
- @attributes[attr_name] = value
end
+ super
end
-
- def query_attribute(attr_name)
- unless value = read_attribute(attr_name)
- false
- else
- column = self.class.columns_hash[attr_name]
- if column.nil?
- if Numeric === value || value !~ /[^0-9]/
- !value.to_i.zero?
- else
- return false if ActiveRecord::ConnectionAdapters::Column::FALSE_VALUES.include?(value)
- !value.blank?
- end
- elsif column.number?
- !value.zero?
- else
- !value.blank?
- end
- end
- end
-
# A Person object with a name attribute can ask <tt>person.respond_to?(:name)</tt>,
# <tt>person.respond_to?(:name=)</tt>, and <tt>person.respond_to?(:name?)</tt>
# which will all return +true+.
@@ -358,13 +196,9 @@ module ActiveRecord
return true
end
end
-
- if @attributes.nil?
- return super
- elsif @attributes.include?(method_name)
- return true
- elsif md = self.class.match_attribute_method?(method_name)
- return true if @attributes.include?(md.pre_match)
+
+ if md = self.class.match_attribute_method?(method_name)
+ return true if md.pre_match == 'id' || @attributes.include?(md.pre_match)
end
super
end
@@ -376,24 +210,9 @@ module ActiveRecord
raise NoMethodError.new("Attempt to call private method", method_name, args)
end
end
-
+
def missing_attribute(attr_name, stack)
raise ActiveRecord::MissingAttributeError, "missing attribute: #{attr_name}", stack
end
-
- # Handle *? for method_missing.
- def attribute?(attribute_name)
- query_attribute(attribute_name)
- end
-
- # Handle *= for method_missing.
- def attribute=(attribute_name, value)
- write_attribute(attribute_name, value)
- end
-
- # Handle *_before_type_cast for method_missing.
- def attribute_before_type_cast(attribute_name)
- read_attribute_before_type_cast(attribute_name)
- end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb
new file mode 100644
index 0000000000..a4e144f233
--- /dev/null
+++ b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb
@@ -0,0 +1,33 @@
+module ActiveRecord
+ module AttributeMethods
+ module BeforeTypeCast
+ extend ActiveSupport::Concern
+
+ included do
+ attribute_method_suffix "_before_type_cast"
+ end
+
+ def read_attribute_before_type_cast(attr_name)
+ @attributes[attr_name]
+ end
+
+ # Returns a hash of attributes before typecasting and deserialization.
+ def attributes_before_type_cast
+ self.attribute_names.inject({}) do |attrs, name|
+ attrs[name] = read_attribute_before_type_cast(name)
+ attrs
+ end
+ end
+
+ private
+ # Handle *_before_type_cast for method_missing.
+ def attribute_before_type_cast(attribute_name)
+ if attribute_name == 'id'
+ read_attribute_before_type_cast(self.class.primary_key)
+ else
+ read_attribute_before_type_cast(attribute_name)
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb
new file mode 100644
index 0000000000..b88c84938d
--- /dev/null
+++ b/activerecord/lib/active_record/attribute_methods/dirty.rb
@@ -0,0 +1,187 @@
+module ActiveRecord
+ module AttributeMethods
+ # Track unsaved attribute changes.
+ #
+ # A newly instantiated object is unchanged:
+ # person = Person.find_by_name('uncle bob')
+ # person.changed? # => false
+ #
+ # Change the name:
+ # person.name = 'Bob'
+ # person.changed? # => true
+ # person.name_changed? # => true
+ # person.name_was # => 'uncle bob'
+ # person.name_change # => ['uncle bob', 'Bob']
+ # person.name = 'Bill'
+ # person.name_change # => ['uncle bob', 'Bill']
+ #
+ # Save the changes:
+ # person.save
+ # person.changed? # => false
+ # person.name_changed? # => false
+ #
+ # Assigning the same value leaves the attribute unchanged:
+ # person.name = 'Bill'
+ # person.name_changed? # => false
+ # person.name_change # => nil
+ #
+ # Which attributes have changed?
+ # person.name = 'bob'
+ # person.changed # => ['name']
+ # person.changes # => { 'name' => ['Bill', 'bob'] }
+ #
+ # Before modifying an attribute in-place:
+ # person.name_will_change!
+ # person.name << 'by'
+ # person.name_change # => ['uncle bob', 'uncle bobby']
+ module Dirty
+ extend ActiveSupport::Concern
+
+ DIRTY_SUFFIXES = ['_changed?', '_change', '_will_change!', '_was']
+
+ included do
+ attribute_method_suffix *DIRTY_SUFFIXES
+
+ alias_method_chain :save, :dirty
+ alias_method_chain :save!, :dirty
+ alias_method_chain :update, :dirty
+ alias_method_chain :reload, :dirty
+
+ superclass_delegating_accessor :partial_updates
+ self.partial_updates = true
+ end
+
+ # Do any attributes have unsaved changes?
+ # person.changed? # => false
+ # person.name = 'bob'
+ # person.changed? # => true
+ def changed?
+ !changed_attributes.empty?
+ end
+
+ # List of attributes with unsaved changes.
+ # person.changed # => []
+ # person.name = 'bob'
+ # person.changed # => ['name']
+ def changed
+ changed_attributes.keys
+ end
+
+ # Map of changed attrs => [original value, new value].
+ # person.changes # => {}
+ # person.name = 'bob'
+ # person.changes # => { 'name' => ['bill', 'bob'] }
+ def changes
+ changed.inject({}) { |h, attr| h[attr] = attribute_change(attr); h }
+ end
+
+ # Attempts to +save+ the record and clears changed attributes if successful.
+ def save_with_dirty(*args) #:nodoc:
+ if status = save_without_dirty(*args)
+ changed_attributes.clear
+ end
+ status
+ end
+
+ # Attempts to <tt>save!</tt> the record and clears changed attributes if successful.
+ def save_with_dirty!(*args) #:nodoc:
+ status = save_without_dirty!(*args)
+ changed_attributes.clear
+ status
+ end
+
+ # <tt>reload</tt> the record and clears changed attributes.
+ def reload_with_dirty(*args) #:nodoc:
+ record = reload_without_dirty(*args)
+ changed_attributes.clear
+ record
+ end
+
+ private
+ # Map of change <tt>attr => original value</tt>.
+ def changed_attributes
+ @changed_attributes ||= {}
+ end
+
+ # Handle <tt>*_changed?</tt> for +method_missing+.
+ def attribute_changed?(attr)
+ changed_attributes.include?(attr)
+ end
+
+ # Handle <tt>*_change</tt> for +method_missing+.
+ def attribute_change(attr)
+ [changed_attributes[attr], __send__(attr)] if attribute_changed?(attr)
+ end
+
+ # Handle <tt>*_was</tt> for +method_missing+.
+ def attribute_was(attr)
+ attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr)
+ end
+
+ # Handle <tt>*_will_change!</tt> for +method_missing+.
+ def attribute_will_change!(attr)
+ changed_attributes[attr] = clone_attribute_value(:read_attribute, attr)
+ end
+
+ # Wrap write_attribute to remember original attribute value.
+ def write_attribute(attr, value)
+ attr = attr.to_s
+
+ # The attribute already has an unsaved change.
+ if changed_attributes.include?(attr)
+ old = changed_attributes[attr]
+ changed_attributes.delete(attr) unless field_changed?(attr, old, value)
+ else
+ old = clone_attribute_value(:read_attribute, attr)
+ changed_attributes[attr] = old if field_changed?(attr, old, value)
+ end
+
+ # Carry on.
+ super(attr, value)
+ end
+
+ def update_with_dirty
+ if partial_updates?
+ # Serialized attributes should always be written in case they've been
+ # changed in place.
+ update_without_dirty(changed | self.class.serialized_attributes.keys)
+ else
+ update_without_dirty
+ end
+ end
+
+ def field_changed?(attr, old, value)
+ if column = column_for_attribute(attr)
+ if column.number? && column.null && (old.nil? || old == 0) && value.blank?
+ # For nullable numeric columns, NULL gets stored in database for blank (i.e. '') values.
+ # Hence we don't record it as a change if the value changes from nil to ''.
+ # If an old value of 0 is set to '' we want this to get changed to nil as otherwise it'll
+ # be typecast back to 0 (''.to_i => 0)
+ value = nil
+ else
+ value = column.type_cast(value)
+ end
+ end
+
+ old != value
+ end
+
+ module ClassMethods
+ def self.extended(base)
+ class << base
+ alias_method_chain :alias_attribute, :dirty
+ end
+ end
+
+ def alias_attribute_with_dirty(new_name, old_name)
+ alias_attribute_without_dirty(new_name, old_name)
+ DIRTY_SUFFIXES.each do |suffix|
+ module_eval <<-STR, __FILE__, __LINE__+1
+ def #{new_name}#{suffix}; self.#{old_name}#{suffix}; end # def subject_changed?; self.title_changed?; end
+ STR
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb
new file mode 100644
index 0000000000..365fdeb55a
--- /dev/null
+++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb
@@ -0,0 +1,44 @@
+module ActiveRecord
+ module AttributeMethods
+ module PrimaryKey
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ # Defines the primary key field -- can be overridden in subclasses. Overwriting will negate any effect of the
+ # primary_key_prefix_type setting, though.
+ def primary_key
+ reset_primary_key
+ end
+
+ def reset_primary_key #:nodoc:
+ key = get_primary_key(base_class.name)
+ set_primary_key(key)
+ key
+ end
+
+ def get_primary_key(base_name) #:nodoc:
+ key = 'id'
+ case primary_key_prefix_type
+ when :table_name
+ key = base_name.to_s.foreign_key(false)
+ when :table_name_with_underscore
+ key = base_name.to_s.foreign_key
+ end
+ key
+ end
+
+ # Sets the name of the primary key column to use to the given value,
+ # or (if the value is nil or false) to the value returned by the given
+ # block.
+ #
+ # class Project < ActiveRecord::Base
+ # set_primary_key "sysid"
+ # end
+ def set_primary_key(value = nil, &block)
+ define_attr_method :primary_key, value, &block
+ end
+ alias :primary_key= :set_primary_key
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/attribute_methods/query.rb b/activerecord/lib/active_record/attribute_methods/query.rb
new file mode 100644
index 0000000000..a949d80120
--- /dev/null
+++ b/activerecord/lib/active_record/attribute_methods/query.rb
@@ -0,0 +1,37 @@
+module ActiveRecord
+ module AttributeMethods
+ module Query
+ extend ActiveSupport::Concern
+
+ included do
+ attribute_method_suffix "?"
+ end
+
+ def query_attribute(attr_name)
+ unless value = read_attribute(attr_name)
+ false
+ else
+ column = self.class.columns_hash[attr_name]
+ if column.nil?
+ if Numeric === value || value !~ /[^0-9]/
+ !value.to_i.zero?
+ else
+ return false if ActiveRecord::ConnectionAdapters::Column::FALSE_VALUES.include?(value)
+ !value.blank?
+ end
+ elsif column.number?
+ !value.zero?
+ else
+ !value.blank?
+ end
+ end
+ end
+
+ private
+ # Handle *? for method_missing.
+ def attribute?(attribute_name)
+ query_attribute(attribute_name)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb
new file mode 100644
index 0000000000..bea332ef26
--- /dev/null
+++ b/activerecord/lib/active_record/attribute_methods/read.rb
@@ -0,0 +1,116 @@
+module ActiveRecord
+ module AttributeMethods
+ module Read
+ extend ActiveSupport::Concern
+
+ ATTRIBUTE_TYPES_CACHED_BY_DEFAULT = [:datetime, :timestamp, :time, :date]
+
+ included do
+ attribute_method_suffix ""
+
+ cattr_accessor :attribute_types_cached_by_default, :instance_writer => false
+ self.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT
+
+ # Undefine id so it can be used as an attribute name
+ undef_method :id
+ end
+
+ module ClassMethods
+ # +cache_attributes+ allows you to declare which converted attribute values should
+ # be cached. Usually caching only pays off for attributes with expensive conversion
+ # methods, like time related columns (e.g. +created_at+, +updated_at+).
+ def cache_attributes(*attribute_names)
+ attribute_names.each {|attr| cached_attributes << attr.to_s}
+ end
+
+ # Returns the attributes which are cached. By default time related columns
+ # with datatype <tt>:datetime, :timestamp, :time, :date</tt> are cached.
+ def cached_attributes
+ @cached_attributes ||=
+ columns.select{|c| attribute_types_cached_by_default.include?(c.type)}.map{|col| col.name}.to_set
+ end
+
+ # Returns +true+ if the provided attribute is being cached.
+ def cache_attribute?(attr_name)
+ cached_attributes.include?(attr_name)
+ end
+
+ protected
+ def define_attribute_method(attr_name)
+ if self.serialized_attributes[attr_name]
+ define_read_method_for_serialized_attribute(attr_name)
+ else
+ define_read_method(attr_name.to_sym, attr_name, columns_hash[attr_name])
+ end
+
+ if attr_name == primary_key && attr_name != "id"
+ define_read_method(:id, attr_name, columns_hash[attr_name])
+ end
+ end
+
+ private
+ # Define read method for serialized attribute.
+ def define_read_method_for_serialized_attribute(attr_name)
+ evaluate_attribute_method "def #{attr_name}; unserialize_attribute('#{attr_name}'); end", attr_name
+ end
+
+ # Define an attribute reader method. Cope with nil column.
+ def define_read_method(symbol, attr_name, column)
+ cast_code = column.type_cast_code('v') if column
+ access_code = cast_code ? "(v=@attributes['#{attr_name}']) && #{cast_code}" : "@attributes['#{attr_name}']"
+
+ unless attr_name.to_s == self.primary_key.to_s
+ access_code = access_code.insert(0, "missing_attribute('#{attr_name}', caller) unless @attributes.has_key?('#{attr_name}'); ")
+ end
+
+ if cache_attribute?(attr_name)
+ access_code = "@attributes_cache['#{attr_name}'] ||= (#{access_code})"
+ end
+ evaluate_attribute_method "def #{symbol}; #{access_code}; end", symbol
+ end
+ end
+
+ # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example,
+ # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)).
+ def read_attribute(attr_name)
+ attr_name = attr_name.to_s
+ attr_name = self.class.primary_key if attr_name == 'id'
+ if !(value = @attributes[attr_name]).nil?
+ if column = column_for_attribute(attr_name)
+ if unserializable_attribute?(attr_name, column)
+ unserialize_attribute(attr_name)
+ else
+ column.type_cast(value)
+ end
+ else
+ value
+ end
+ else
+ nil
+ end
+ end
+
+ # Returns true if the attribute is of a text column and marked for serialization.
+ def unserializable_attribute?(attr_name, column)
+ column.text? && self.class.serialized_attributes[attr_name]
+ end
+
+ # Returns the unserialized object of the attribute.
+ def unserialize_attribute(attr_name)
+ unserialized_object = object_from_yaml(@attributes[attr_name])
+
+ if unserialized_object.is_a?(self.class.serialized_attributes[attr_name]) || unserialized_object.nil?
+ @attributes.frozen? ? unserialized_object : @attributes[attr_name] = unserialized_object
+ else
+ raise SerializationTypeMismatch,
+ "#{attr_name} was supposed to be a #{self.class.serialized_attributes[attr_name]}, but was a #{unserialized_object.class.to_s}"
+ end
+ end
+
+ private
+ def attribute(attribute_name)
+ read_attribute(attribute_name)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
new file mode 100644
index 0000000000..9e2c6174c6
--- /dev/null
+++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
@@ -0,0 +1,60 @@
+module ActiveRecord
+ module AttributeMethods
+ module TimeZoneConversion
+ extend ActiveSupport::Concern
+
+ included do
+ cattr_accessor :time_zone_aware_attributes, :instance_writer => false
+ self.time_zone_aware_attributes = false
+
+ class_inheritable_accessor :skip_time_zone_conversion_for_attributes, :instance_writer => false
+ self.skip_time_zone_conversion_for_attributes = []
+ end
+
+ module ClassMethods
+ protected
+ # Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled.
+ # This enhanced read method automatically converts the UTC time stored in the database to the time zone stored in Time.zone.
+ def define_attribute_method(attr_name)
+ if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name])
+ method_body = <<-EOV
+ def #{attr_name}(reload = false)
+ cached = @attributes_cache['#{attr_name}']
+ return cached if cached && !reload
+ time = read_attribute('#{attr_name}')
+ @attributes_cache['#{attr_name}'] = time.acts_like?(:time) ? time.in_time_zone : time
+ end
+ EOV
+ evaluate_attribute_method method_body, attr_name
+ else
+ super
+ end
+ end
+
+ # Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled.
+ # This enhanced write method will automatically convert the time passed to it to the zone stored in Time.zone.
+ def define_attribute_method=(attr_name)
+ if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name])
+ method_body = <<-EOV
+ def #{attr_name}=(time)
+ unless time.acts_like?(:time)
+ time = time.is_a?(String) ? Time.zone.parse(time) : time.to_time rescue time
+ end
+ time = time.in_time_zone rescue nil if time
+ write_attribute(:#{attr_name}, time)
+ end
+ EOV
+ evaluate_attribute_method method_body, "#{attr_name}="
+ else
+ super
+ end
+ end
+
+ private
+ def create_time_zone_conversion_attribute?(name, column)
+ time_zone_aware_attributes && !skip_time_zone_conversion_for_attributes.include?(name.to_sym) && [:datetime, :timestamp].include?(column.type)
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb
new file mode 100644
index 0000000000..497e72ee4a
--- /dev/null
+++ b/activerecord/lib/active_record/attribute_methods/write.rb
@@ -0,0 +1,37 @@
+module ActiveRecord
+ module AttributeMethods
+ module Write
+ extend ActiveSupport::Concern
+
+ included do
+ attribute_method_suffix "="
+ end
+
+ module ClassMethods
+ protected
+ def define_attribute_method=(attr_name)
+ evaluate_attribute_method "def #{attr_name}=(new_value); write_attribute('#{attr_name}', new_value); end", "#{attr_name}="
+ end
+ end
+
+ # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+. Empty strings for fixnum and float
+ # columns are turned into +nil+.
+ def write_attribute(attr_name, value)
+ attr_name = attr_name.to_s
+ attr_name = self.class.primary_key if attr_name == 'id'
+ @attributes_cache.delete(attr_name)
+ if (column = column_for_attribute(attr_name)) && column.number?
+ @attributes[attr_name] = convert_number_column_value(value)
+ else
+ @attributes[attr_name] = value
+ end
+ end
+
+ private
+ # Handle *= for method_missing.
+ def attribute=(attribute_name, value)
+ write_attribute(attribute_name, value)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index 9a3a02870a..2b50333682 100755
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -256,6 +256,12 @@ module ActiveRecord #:nodoc:
#
# Student.find(:all, :conditions => { :grade => [9,11,12] })
#
+ # When joining tables, nested hashes or keys written in the form 'table_name.column_name' can be used to qualify the table name of a
+ # particular condition. For instance:
+ #
+ # Student.find(:all, :conditions => { :schools => { :type => 'public' }}, :joins => :schools)
+ # Student.find(:all, :conditions => { 'schools.type' => 'public' }, :joins => :schools)
+ #
# == Overwriting default accessors
#
# All column values are automatically available through basic accessors on the Active Record object, but sometimes you
@@ -858,7 +864,7 @@ module ActiveRecord #:nodoc:
# Book.update_all "author = 'David'", "title LIKE '%Rails%'"
#
# # Update all avatars migrated more than a week ago
- # Avatar.update_all ['migrated_at = ?, Time.now.utc], ['migrated_at > ?', 1.week.ago]
+ # Avatar.update_all ['migrated_at = ?', Time.now.utc], ['migrated_at > ?', 1.week.ago]
#
# # Update all books that match our conditions, but limit it to 5 ordered by date
# Book.update_all "author = 'David'", "title LIKE '%Rails%'", :order => 'created_at', :limit => 5
@@ -1058,6 +1064,21 @@ module ActiveRecord #:nodoc:
#
# To start from an all-closed default and enable attributes as needed,
# have a look at +attr_accessible+.
+ #
+ # If the access logic of your application is richer you can use <tt>Hash#except</tt>
+ # or <tt>Hash#slice</tt> to sanitize the hash of parameters before they are
+ # passed to Active Record.
+ #
+ # For example, it could be the case that the list of protected attributes
+ # for a given model depends on the role of the user:
+ #
+ # # Assumes plan_id is not protected because it depends on the role.
+ # params[:account] = params[:account].except(:plan_id) unless admin?
+ # @account.update_attributes(params[:account])
+ #
+ # Note that +attr_protected+ is still applied to the received hash. Thus,
+ # with this technique you can at most _extend_ the list of protected
+ # attributes for a particular mass-assignment call.
def attr_protected(*attributes)
write_inheritable_attribute(:attr_protected, Set.new(attributes.map {|a| a.to_s}) + (protected_attributes || []))
end
@@ -1091,6 +1112,21 @@ module ActiveRecord #:nodoc:
#
# customer.credit_rating = "Average"
# customer.credit_rating # => "Average"
+ #
+ # If the access logic of your application is richer you can use <tt>Hash#except</tt>
+ # or <tt>Hash#slice</tt> to sanitize the hash of parameters before they are
+ # passed to Active Record.
+ #
+ # For example, it could be the case that the list of accessible attributes
+ # for a given model depends on the role of the user:
+ #
+ # # Assumes plan_id is accessible because it depends on the role.
+ # params[:account] = params[:account].except(:plan_id) unless admin?
+ # @account.update_attributes(params[:account])
+ #
+ # Note that +attr_accessible+ is still applied to the received hash. Thus,
+ # with this technique you can at most _narrow_ the list of accessible
+ # attributes for a particular mass-assignment call.
def attr_accessible(*attributes)
write_inheritable_attribute(:attr_accessible, Set.new(attributes.map(&:to_s)) + (accessible_attributes || []))
end
@@ -1192,29 +1228,6 @@ module ActiveRecord #:nodoc:
name
end
- # Defines the primary key field -- can be overridden in subclasses. Overwriting will negate any effect of the
- # primary_key_prefix_type setting, though.
- def primary_key
- reset_primary_key
- end
-
- def reset_primary_key #:nodoc:
- key = get_primary_key(base_class.name)
- set_primary_key(key)
- key
- end
-
- def get_primary_key(base_name) #:nodoc:
- key = 'id'
- case primary_key_prefix_type
- when :table_name
- key = base_name.to_s.foreign_key(false)
- when :table_name_with_underscore
- key = base_name.to_s.foreign_key
- end
- key
- end
-
# Defines the column name for use with single table inheritance
# -- can be set in subclasses like so: self.inheritance_column = "type_id"
def inheritance_column
@@ -1244,18 +1257,6 @@ module ActiveRecord #:nodoc:
end
alias :table_name= :set_table_name
- # Sets the name of the primary key column to use to the given value,
- # or (if the value is nil or false) to the value returned by the given
- # block.
- #
- # class Project < ActiveRecord::Base
- # set_primary_key "sysid"
- # end
- def set_primary_key(value = nil, &block)
- define_attr_method :primary_key, value, &block
- end
- alias :primary_key= :set_primary_key
-
# Sets the name of the inheritance column to use to the given value,
# or (if the value # is nil or false) to the value returned by the
# given block.
@@ -1368,8 +1369,8 @@ module ActiveRecord #:nodoc:
# end
# end
def reset_column_information
- generated_methods.each { |name| undef_method(name) }
- @column_names = @columns = @columns_hash = @content_columns = @dynamic_methods_hash = @generated_methods = @inheritance_column = nil
+ undefine_attribute_methods
+ @column_names = @columns = @columns_hash = @content_columns = @dynamic_methods_hash = @inheritance_column = nil
end
def reset_column_information_and_inheritable_attributes_for_all_subclasses#:nodoc:
@@ -1385,14 +1386,14 @@ module ActiveRecord #:nodoc:
classes
rescue
# OPTIMIZE this rescue is to fix this test: ./test/cases/reflection_test.rb:56:in `test_human_name_for_column'
- # Appearantly the method base_class causes some trouble.
+ # Apparently the method base_class causes some trouble.
# It now works for sure.
[self]
end
# Transforms attribute key names into a more humane format, such as "First name" instead of "first_name". Example:
# Person.human_attribute_name("first_name") # => "First name"
- # This used to be depricated in favor of humanize, but is now preferred, because it automatically uses the I18n
+ # This used to be deprecated in favor of humanize, but is now preferred, because it automatically uses the I18n
# module now.
# Specify +options+ with additional translating options.
def human_attribute_name(attribute_key_name, options = {})
@@ -2043,36 +2044,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
- # return that value (as a string). Otherwise, the given block
- # will be used to compute the value of the method.
- #
- # The original method will be aliased, with the new name being
- # prefixed with "original_". This allows the new method to
- # access the original value.
- #
- # Example:
- #
- # class A < ActiveRecord::Base
- # define_attr_method :primary_key, "sysid"
- # define_attr_method( :inheritance_column ) do
- # original_inheritance_column + "_id"
- # end
- # end
- def define_attr_method(name, value=nil, &block)
- sing = metaclass
- sing.send :alias_method, "original_#{name}", name
- if block_given?
- sing.send :define_method, name, &block
- else
- # use eval instead of a block to work around a memory leak in dev
- # mode in fcgi
- sing.class_eval "def #{name}; #{value.to_s.inspect}; end"
- end
- end
-
protected
# Scope parameters to method calls within the block. Takes a hash of method_name => parameters hash.
# method_name may be <tt>:find</tt> or <tt>:create</tt>. <tt>:find</tt> parameters may include the <tt>:conditions</tt>, <tt>:joins</tt>,
@@ -2474,18 +2445,6 @@ module ActiveRecord #:nodoc:
result
end
- # A model instance's primary key is always available as model.id
- # whether you name it the default 'id' or set it to something else.
- def id
- attr_name = self.class.primary_key
- column = column_for_attribute(attr_name)
-
- self.class.send(:define_read_method, :id, attr_name, column)
- # now that the method exists, call it
- self.send attr_name.to_sym
-
- end
-
# Returns a String, which Action Pack uses for constructing an URL to this
# object. The default implementation returns this record's id as a String,
# or nil if this record's unsaved.
@@ -2531,19 +2490,10 @@ module ActiveRecord #:nodoc:
end
end
- def id_before_type_cast #:nodoc:
- read_attribute_before_type_cast(self.class.primary_key)
- end
-
def quoted_id #:nodoc:
quote_value(id, column_for_attribute(self.class.primary_key))
end
- # Sets the primary ID.
- def id=(value)
- write_attribute(self.class.primary_key, value)
- end
-
# Returns true if this object hasn't been saved yet -- that is, a record for the object doesn't exist yet; otherwise, returns false.
def new_record?
@new_record || false
@@ -2784,14 +2734,6 @@ module ActiveRecord #:nodoc:
end
end
- # Returns a hash of attributes before typecasting and deserialization.
- def attributes_before_type_cast
- self.attribute_names.inject({}) do |attrs, name|
- attrs[name] = read_attribute_before_type_cast(name)
- attrs
- end
- end
-
# Returns an <tt>#inspect</tt>-like string for the value of the
# attribute +attr_name+. String attributes are elided after 50
# characters, and Date and Time attributes are returned in the
@@ -3191,7 +3133,10 @@ module ActiveRecord #:nodoc:
include Validations
include Locking::Optimistic, Locking::Pessimistic
include AttributeMethods
- include Dirty
+ include AttributeMethods::Read, AttributeMethods::Write, AttributeMethods::BeforeTypeCast, AttributeMethods::Query
+ include AttributeMethods::PrimaryKey
+ include AttributeMethods::TimeZoneConversion
+ include AttributeMethods::Dirty
include Callbacks, ActiveModel::Observing, Timestamp
include Associations, AssociationPreload, NamedScope
include ActiveModel::Conversion
diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb
index 3aa0b8f1b5..4a2ec5bf95 100644
--- a/activerecord/lib/active_record/callbacks.rb
+++ b/activerecord/lib/active_record/callbacks.rb
@@ -22,7 +22,7 @@ module ActiveRecord
# * (8) <tt>after_save</tt>
#
# That's a total of eight callbacks, which gives you immense power to react and prepare for each state in the
- # Active Record lifecycle. The sequence for calling <tt>Base#save</tt> an existing record is similar, except that each
+ # Active Record lifecycle. The sequence for calling <tt>Base#save</tt> for an existing record is similar, except that each
# <tt>_on_create</tt> callback is replaced by the corresponding <tt>_on_update</tt> callback.
#
# Examples:
@@ -262,6 +262,10 @@ module ActiveRecord
# Is called _after_ <tt>Base.save</tt> on new objects that haven't been saved yet (no record exists).
# Note that this callback is still wrapped in the transaction around +save+. For example, if you
# invoke an external indexer at this point it won't see the changes in the database.
+ #
+ # class Contact < ActiveRecord::Base
+ # after_create { |record| logger.info( "Contact #{record.id} was created." ) }
+ # end
def after_create() end
def create_with_callbacks #:nodoc:
return false if callback(:before_create) == false
@@ -272,11 +276,19 @@ module ActiveRecord
private :create_with_callbacks
# Is called _before_ <tt>Base.save</tt> on existing objects that have a record.
+ #
+ # class Contact < ActiveRecord::Base
+ # before_update { |record| logger.info( "Contact #{record.id} is about to be updated." ) }
+ # end
def before_update() end
# Is called _after_ <tt>Base.save</tt> on existing objects that have a record.
# Note that this callback is still wrapped in the transaction around +save+. For example, if you
# invoke an external indexer at this point it won't see the changes in the database.
+ #
+ # class Contact < ActiveRecord::Base
+ # after_update { |record| logger.info( "Contact #{record.id} was updated." ) }
+ # end
def after_update() end
def update_with_callbacks(*args) #:nodoc:
@@ -326,6 +338,10 @@ module ActiveRecord
#
# Note: If you need to _destroy_ or _nullify_ associated records first,
# use the <tt>:dependent</tt> option on your associations.
+ #
+ # class Contact < ActiveRecord::Base
+ # after_destroy { |record| logger.info( "Contact #{record.id} is about to be destroyed." ) }
+ # end
def before_destroy() end
# Is called _after_ <tt>Base.destroy</tt> (and all the attributes have been frozen).
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
index 500dafdc2e..12253eac3f 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
@@ -362,7 +362,7 @@ module ActiveRecord
def call(env)
@app.call(env)
ensure
- # Don't return connection (and peform implicit rollback) if
+ # Don't return connection (and perform implicit rollback) if
# this request is a part of integration test
unless env.key?("rack.test")
ActiveRecord::Base.clear_active_connections!
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
index 8e33681772..b2c5c78bf7 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -41,11 +41,19 @@ module ActiveRecord
# # create_table() passes a TableDefinition object to the block.
# # This form will not only create the table, but also columns for the
# # table.
+ #
# create_table(:suppliers) do |t|
# t.column :name, :string, :limit => 60
# # Other fields here
# end
#
+ # === Block form, with shorthand
+ # # You can also use the column types as method calls, rather than calling the column method.
+ # create_table(:suppliers) do |t|
+ # t.string :name, :limit => 60
+ # # Other fields here
+ # end
+ #
# === Regular form
# # Creates a table called 'suppliers' with no columns.
# create_table(:suppliers)
diff --git a/activerecord/lib/active_record/dirty.rb b/activerecord/lib/active_record/dirty.rb
deleted file mode 100644
index 178767e0c3..0000000000
--- a/activerecord/lib/active_record/dirty.rb
+++ /dev/null
@@ -1,186 +0,0 @@
-module ActiveRecord
- # Track unsaved attribute changes.
- #
- # A newly instantiated object is unchanged:
- # person = Person.find_by_name('uncle bob')
- # person.changed? # => false
- #
- # Change the name:
- # person.name = 'Bob'
- # person.changed? # => true
- # person.name_changed? # => true
- # person.name_was # => 'uncle bob'
- # person.name_change # => ['uncle bob', 'Bob']
- # person.name = 'Bill'
- # person.name_change # => ['uncle bob', 'Bill']
- #
- # Save the changes:
- # person.save
- # person.changed? # => false
- # person.name_changed? # => false
- #
- # Assigning the same value leaves the attribute unchanged:
- # person.name = 'Bill'
- # person.name_changed? # => false
- # person.name_change # => nil
- #
- # Which attributes have changed?
- # person.name = 'bob'
- # person.changed # => ['name']
- # person.changes # => { 'name' => ['Bill', 'bob'] }
- #
- # Before modifying an attribute in-place:
- # person.name_will_change!
- # person.name << 'by'
- # person.name_change # => ['uncle bob', 'uncle bobby']
- module Dirty
- extend ActiveSupport::Concern
-
- DIRTY_SUFFIXES = ['_changed?', '_change', '_will_change!', '_was']
-
- included do
- attribute_method_suffix *DIRTY_SUFFIXES
-
- alias_method_chain :write_attribute, :dirty
- alias_method_chain :save, :dirty
- alias_method_chain :save!, :dirty
- alias_method_chain :update, :dirty
- alias_method_chain :reload, :dirty
-
- superclass_delegating_accessor :partial_updates
- self.partial_updates = true
- end
-
- # Do any attributes have unsaved changes?
- # person.changed? # => false
- # person.name = 'bob'
- # person.changed? # => true
- def changed?
- !changed_attributes.empty?
- end
-
- # List of attributes with unsaved changes.
- # person.changed # => []
- # person.name = 'bob'
- # person.changed # => ['name']
- def changed
- changed_attributes.keys
- end
-
- # Map of changed attrs => [original value, new value].
- # person.changes # => {}
- # person.name = 'bob'
- # person.changes # => { 'name' => ['bill', 'bob'] }
- def changes
- changed.inject({}) { |h, attr| h[attr] = attribute_change(attr); h }
- end
-
- # Attempts to +save+ the record and clears changed attributes if successful.
- def save_with_dirty(*args) #:nodoc:
- if status = save_without_dirty(*args)
- changed_attributes.clear
- end
- status
- end
-
- # Attempts to <tt>save!</tt> the record and clears changed attributes if successful.
- def save_with_dirty!(*args) #:nodoc:
- status = save_without_dirty!(*args)
- changed_attributes.clear
- status
- end
-
- # <tt>reload</tt> the record and clears changed attributes.
- def reload_with_dirty(*args) #:nodoc:
- record = reload_without_dirty(*args)
- changed_attributes.clear
- record
- end
-
- private
- # Map of change <tt>attr => original value</tt>.
- def changed_attributes
- @changed_attributes ||= {}
- end
-
- # Handle <tt>*_changed?</tt> for +method_missing+.
- def attribute_changed?(attr)
- changed_attributes.include?(attr)
- end
-
- # Handle <tt>*_change</tt> for +method_missing+.
- def attribute_change(attr)
- [changed_attributes[attr], __send__(attr)] if attribute_changed?(attr)
- end
-
- # Handle <tt>*_was</tt> for +method_missing+.
- def attribute_was(attr)
- attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr)
- end
-
- # Handle <tt>*_will_change!</tt> for +method_missing+.
- def attribute_will_change!(attr)
- changed_attributes[attr] = clone_attribute_value(:read_attribute, attr)
- end
-
- # Wrap write_attribute to remember original attribute value.
- def write_attribute_with_dirty(attr, value)
- attr = attr.to_s
-
- # The attribute already has an unsaved change.
- if changed_attributes.include?(attr)
- old = changed_attributes[attr]
- changed_attributes.delete(attr) unless field_changed?(attr, old, value)
- else
- old = clone_attribute_value(:read_attribute, attr)
- changed_attributes[attr] = old if field_changed?(attr, old, value)
- end
-
- # Carry on.
- write_attribute_without_dirty(attr, value)
- end
-
- def update_with_dirty
- if partial_updates?
- # Serialized attributes should always be written in case they've been
- # changed in place.
- update_without_dirty(changed | self.class.serialized_attributes.keys)
- else
- update_without_dirty
- end
- end
-
- def field_changed?(attr, old, value)
- if column = column_for_attribute(attr)
- if column.number? && column.null && (old.nil? || old == 0) && value.blank?
- # For nullable numeric columns, NULL gets stored in database for blank (i.e. '') values.
- # Hence we don't record it as a change if the value changes from nil to ''.
- # If an old value of 0 is set to '' we want this to get changed to nil as otherwise it'll
- # be typecast back to 0 (''.to_i => 0)
- value = nil
- else
- value = column.type_cast(value)
- end
- end
-
- old != value
- end
-
- module ClassMethods
- def self.extended(base)
- class << base
- alias_method_chain :alias_attribute, :dirty
- end
- end
-
- def alias_attribute_with_dirty(new_name, old_name)
- alias_attribute_without_dirty(new_name, old_name)
- DIRTY_SUFFIXES.each do |suffix|
- module_eval <<-STR, __FILE__, __LINE__+1
- def #{new_name}#{suffix}; self.#{old_name}#{suffix}; end # def subject_changed?; self.title_changed?; end
- STR
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb
index 2b0cfc2c3b..6eeeddc9e1 100644
--- a/activerecord/lib/active_record/fixtures.rb
+++ b/activerecord/lib/active_record/fixtures.rb
@@ -409,7 +409,7 @@ end
# subdomain: $LABEL
#
# Also, sometimes (like when porting older join table fixtures) you'll need
-# to be able to get ahold of the identifier for a given label. ERB
+# to be able to get a hold of the identifier for a given label. ERB
# to the rescue:
#
# george_reginald:
diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb
index f7cc244471..7631f3ec35 100644
--- a/activerecord/lib/active_record/migration.rb
+++ b/activerecord/lib/active_record/migration.rb
@@ -109,8 +109,8 @@ module ActiveRecord
# script/generate migration MyNewMigration
#
# where MyNewMigration is the name of your migration. The generator will
- # create an empty migration file <tt>nnn_my_new_migration.rb</tt> in the <tt>db/migrate/</tt>
- # directory where <tt>nnn</tt> is the next largest migration number.
+ # create an empty migration file <tt>timestamp_my_new_migration.rb</tt> in the <tt>db/migrate/</tt>
+ # directory where <tt>timestamp</tt> is the UTC formatted date and time that the migration was generated.
#
# You may then edit the <tt>self.up</tt> and <tt>self.down</tt> methods of
# MyNewMigration.
@@ -118,7 +118,7 @@ module ActiveRecord
# There is a special syntactic shortcut to generate migrations that add fields to a table.
# script/generate migration add_fieldname_to_tablename fieldname:string
#
- # This will generate the file <tt>nnn_add_fieldname_to_tablename</tt>, which will look like this:
+ # This will generate the file <tt>timestamp_add_fieldname_to_tablename</tt>, which will look like this:
# class AddFieldnameToTablename < ActiveRecord::Migration
# def self.up
# add_column :tablenames, :fieldname, :string
diff --git a/activerecord/lib/active_record/named_scope.rb b/activerecord/lib/active_record/named_scope.rb
index dd2a90b8e5..bbe2d1f205 100644
--- a/activerecord/lib/active_record/named_scope.rb
+++ b/activerecord/lib/active_record/named_scope.rb
@@ -92,12 +92,7 @@ module ActiveRecord
when Hash
options
when Proc
- case parent_scope
- when Scope
- with_scope(:find => parent_scope.proxy_options) { options.call(*args) }
- else
- options.call(*args)
- end
+ options.call(*args)
end, &block)
end
metaclass.instance_eval do
diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb
index 0beb4321a2..bc4cca7855 100644
--- a/activerecord/lib/active_record/nested_attributes.rb
+++ b/activerecord/lib/active_record/nested_attributes.rb
@@ -284,7 +284,7 @@ module ActiveRecord
# })
#
# Will update the name of the Person with ID 1, build a new associated
- # person with the name `John', and mark the associatied Person with ID 2
+ # person with the name `John', and mark the associated Person with ID 2
# for destruction.
#
# Also accepts an Array of attribute hashes:
diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb
index 253fa03785..4e172bd2b6 100644
--- a/activerecord/lib/active_record/serializers/xml_serializer.rb
+++ b/activerecord/lib/active_record/serializers/xml_serializer.rb
@@ -71,6 +71,21 @@ module ActiveRecord #:nodoc:
# </account>
# </firm>
#
+ # Additionally, the record being serialized will be passed to a Proc's second
+ # parameter. This allows for ad hoc additions to the resultant document that
+ # incorporate the context of the record being serialized. And by leveraging the
+ # closure created by a Proc, to_xml can be used to add elements that normally fall
+ # outside of the scope of the model -- for example, generating and appending URLs
+ # associated with models.
+ #
+ # proc = Proc.new { |options, record| options[:builder].tag!('name-reverse', record.name.reverse) }
+ # firm.to_xml :procs => [ proc ]
+ #
+ # <firm>
+ # # ... normal attributes as shown above ...
+ # <name-reverse>slangis73</name-reverse>
+ # </firm>
+ #
# To include deeper levels of associations pass a hash like this:
#
# firm.to_xml :include => {:account => {}, :clients => {:include => :address}}
diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb
index 7ac6f6fe3b..a7fa98756e 100644
--- a/activerecord/lib/active_record/validations.rb
+++ b/activerecord/lib/active_record/validations.rb
@@ -63,7 +63,7 @@ module ActiveRecord
# default message (e.g. <tt>activerecord.errors.messages.MESSAGE</tt>). The translated model name,
# translated attribute name and the value are available for interpolation.
#
- # When using inheritence in your models, it will check all the inherited models too, but only if the model itself
+ # When using inheritance in your models, it will check all the inherited models too, but only if the model itself
# hasn't been found. Say you have <tt>class Admin < User; end</tt> and you wanted the translation for the <tt>:blank</tt>
# error +message+ for the <tt>title</tt> +attribute+, it looks for these translations:
#