aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord
diff options
context:
space:
mode:
authorPratik Naik <pratiknaik@gmail.com>2009-07-25 12:55:01 +0100
committerPratik Naik <pratiknaik@gmail.com>2009-07-25 12:55:01 +0100
commitc6b16fc2aada37aab1949cd9a1e46a2d9ccc8381 (patch)
treeea13a4e420a94ed7b552529ca9dc4b02680203d2 /activerecord
parentf2d65a456fd93fd3a220f85c1001f0180bfdd6be (diff)
parent0c68d23f19010379a9320690ca17a26743c8f071 (diff)
downloadrails-c6b16fc2aada37aab1949cd9a1e46a2d9ccc8381.tar.gz
rails-c6b16fc2aada37aab1949cd9a1e46a2d9ccc8381.tar.bz2
rails-c6b16fc2aada37aab1949cd9a1e46a2d9ccc8381.zip
Merge commit 'mainstream/master'
Diffstat (limited to 'activerecord')
-rw-r--r--activerecord/CHANGELOG4
-rwxr-xr-xactiverecord/lib/active_record/associations.rb18
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_association.rb26
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb6
-rw-r--r--activerecord/lib/active_record/attribute_methods.rb13
-rw-r--r--activerecord/lib/active_record/autosave_association.rb3
-rwxr-xr-xactiverecord/lib/active_record/base.rb1
-rw-r--r--activerecord/lib/active_record/callbacks.rb2
-rw-r--r--activerecord/lib/active_record/schema_dumper.rb6
-rw-r--r--activerecord/lib/active_record/serialization.rb68
-rw-r--r--activerecord/lib/active_record/serializers/json_serializer.rb85
-rw-r--r--activerecord/lib/active_record/serializers/xml_serializer.rb134
-rw-r--r--activerecord/test/cases/associations/belongs_to_associations_test.rb98
-rw-r--r--activerecord/test/cases/associations/has_many_through_associations_test.rb8
-rw-r--r--activerecord/test/cases/attribute_methods_test.rb16
-rwxr-xr-xactiverecord/test/cases/base_test.rb2
-rw-r--r--activerecord/test/cases/reflection_test.rb10
-rw-r--r--activerecord/test/cases/schema_dumper_test.rb14
-rw-r--r--activerecord/test/cases/xml_serialization_test.rb6
-rw-r--r--activerecord/test/models/author.rb2
-rw-r--r--activerecord/test/models/company.rb1
-rw-r--r--activerecord/test/models/essay.rb3
-rw-r--r--activerecord/test/models/reply.rb3
-rw-r--r--activerecord/test/models/topic.rb1
-rw-r--r--activerecord/test/schema/schema.rb7
25 files changed, 258 insertions, 279 deletions
diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG
index 411b640c9e..659de99873 100644
--- a/activerecord/CHANGELOG
+++ b/activerecord/CHANGELOG
@@ -1,5 +1,9 @@
*Edge*
+* Added :primary_key option to belongs_to associations. #765 [Szymon Nowak, Philip Hallstrom, Noel Rocha]
+ # employees.company_name references companies.name
+ Employee.belongs_to :company, :primary_key => 'name', :foreign_key => 'company_name'
+
* Implement #many? for NamedScope and AssociationCollection using #size. #1500 [Chris Kampmeier]
* Added :touch option to belongs_to associations that will touch the parent record when the current record is saved or destroyed [DHH]
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index 860d1003a6..910b7d7730 100755
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -1031,6 +1031,8 @@ module ActiveRecord
# of the association with an "_id" suffix. So a class that defines a <tt>belongs_to :person</tt> association will use
# "person_id" as the default <tt>:foreign_key</tt>. Similarly, <tt>belongs_to :favorite_person, :class_name => "Person"</tt>
# will use a foreign key of "favorite_person_id".
+ # [:primary_key]
+ # Specify the method that returns the primary key of associated object used for the association. By default this is id.
# [:dependent]
# If set to <tt>:destroy</tt>, the associated object is destroyed when this object is. If set to
# <tt>:delete</tt>, the associated object is deleted *without* calling its destroy method. This option should not be specified when
@@ -1065,6 +1067,7 @@ module ActiveRecord
#
# Option examples:
# belongs_to :firm, :foreign_key => "client_of"
+ # belongs_to :person, :primary_key => "name", :foreign_key => "person_name"
# belongs_to :author, :class_name => "Person", :foreign_key => "author_id"
# belongs_to :valid_coupon, :class_name => "Coupon", :foreign_key => "coupon_id",
# :conditions => 'discounts > #{payments_count}'
@@ -1345,9 +1348,16 @@ module ActiveRecord
if send(reflection.name).loaded? || reflection.options[:finder_sql]
send(reflection.name).map(&:id)
else
- send(reflection.name).all(:select => "#{reflection.quoted_table_name}.#{reflection.klass.primary_key}").map(&:id)
+ if reflection.through_reflection && reflection.source_reflection.belongs_to?
+ through = reflection.through_reflection
+ primary_key = reflection.source_reflection.primary_key_name
+ send(through.name).all(:select => "DISTINCT #{through.quoted_table_name}.#{primary_key}").map!(&:"#{primary_key}")
+ else
+ send(reflection.name).all(:select => "#{reflection.quoted_table_name}.#{reflection.klass.primary_key}").map!(&:id)
+ end
end
end
+
end
def collection_accessor_methods(reflection, association_proxy_class, writer = true)
@@ -1393,14 +1403,14 @@ module ActiveRecord
method_name = "belongs_to_counter_cache_after_create_for_#{reflection.name}".to_sym
define_method(method_name) do
association = send(reflection.name)
- association.class.increment_counter(cache_column, send(reflection.primary_key_name)) unless association.nil?
+ association.class.increment_counter(cache_column, association.id) unless association.nil?
end
after_create(method_name)
method_name = "belongs_to_counter_cache_before_destroy_for_#{reflection.name}".to_sym
define_method(method_name) do
association = send(reflection.name)
- association.class.decrement_counter(cache_column, send(reflection.primary_key_name)) unless association.nil?
+ association.class.decrement_counter(cache_column, association.id) unless association.nil?
end
before_destroy(method_name)
@@ -1592,7 +1602,7 @@ module ActiveRecord
mattr_accessor :valid_keys_for_belongs_to_association
@@valid_keys_for_belongs_to_association = [
- :class_name, :foreign_key, :foreign_type, :remote, :select, :conditions,
+ :class_name, :primary_key, :foreign_key, :foreign_type, :remote, :select, :conditions,
:include, :dependent, :counter_cache, :extend, :polymorphic, :readonly,
:validate, :touch, :inverse_of
]
diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb
index c88575048a..628033c87a 100644
--- a/activerecord/lib/active_record/associations/belongs_to_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_association.rb
@@ -14,7 +14,7 @@ module ActiveRecord
if record.nil?
if counter_cache_name && !@owner.new_record?
- @reflection.klass.decrement_counter(counter_cache_name, @owner[@reflection.primary_key_name]) if @owner[@reflection.primary_key_name]
+ @reflection.klass.decrement_counter(counter_cache_name, previous_record_id) if @owner[@reflection.primary_key_name]
end
@target = @owner[@reflection.primary_key_name] = nil
@@ -27,7 +27,7 @@ module ActiveRecord
end
@target = (AssociationProxy === record ? record.target : record)
- @owner[@reflection.primary_key_name] = record.id unless record.new_record?
+ @owner[@reflection.primary_key_name] = record_id(record) unless record.new_record?
@updated = true
end
@@ -43,13 +43,18 @@ module ActiveRecord
private
def find_target
- the_target = @reflection.klass.find(
+ find_method = if @reflection.options[:primary_key]
+ "find_by_#{@reflection.options[:primary_key]}"
+ else
+ "find"
+ end
+ the_target = @reflection.klass.send(find_method,
@owner[@reflection.primary_key_name],
:select => @reflection.options[:select],
:conditions => conditions,
:include => @reflection.options[:include],
:readonly => @reflection.options[:readonly]
- )
+ ) if @owner[@reflection.primary_key_name]
set_inverse_instance(the_target, @owner)
the_target
end
@@ -63,6 +68,19 @@ module ActiveRecord
def we_can_set_the_inverse_on_this?(record)
@reflection.has_inverse? && @reflection.inverse_of.macro == :has_one
end
+
+ def record_id(record)
+ record.send(@reflection.options[:primary_key] || :id)
+ end
+
+ def previous_record_id
+ @previous_record_id ||= if @reflection.options[:primary_key]
+ previous_record = @owner.send(@reflection.name)
+ previous_record.nil? ? nil : previous_record.id
+ else
+ @owner[@reflection.primary_key_name]
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
index d8146daa54..67e18d692d 100644
--- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
@@ -7,7 +7,7 @@ module ActiveRecord
else
@target = (AssociationProxy === record ? record.target : record)
- @owner[@reflection.primary_key_name] = record.id
+ @owner[@reflection.primary_key_name] = record_id(record)
@owner[@reflection.options[:foreign_type]] = record.class.base_class.name.to_s
@updated = true
@@ -41,6 +41,10 @@ module ActiveRecord
!@owner[@reflection.primary_key_name].nil?
end
+ def record_id(record)
+ record.send(@reflection.options[:primary_key] || :id)
+ end
+
def association_class
@owner[@reflection.options[:foreign_type]] ? @owner[@reflection.options[:foreign_type]].constantize : nil
end
diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb
index 15358979c2..ecd2d57a5a 100644
--- a/activerecord/lib/active_record/attribute_methods.rb
+++ b/activerecord/lib/active_record/attribute_methods.rb
@@ -133,6 +133,7 @@ module ActiveRecord
end
private
+
# Suffixes a, ?, c become regexp /(a|\?|c)$/
def rebuild_attribute_method_regexp
suffixes = attribute_method_suffixes.map { |s| Regexp.escape(s) }
@@ -238,19 +239,17 @@ module ActiveRecord
def method_missing(method_id, *args, &block)
method_name = method_id.to_s
- if self.class.private_method_defined?(method_name)
- raise NoMethodError.new("Attempt to call private method", method_name, args)
- end
-
# If we haven't generated any methods yet, generate them, then
# see if we've created the method we're looking for.
if !self.class.generated_methods?
self.class.define_attribute_methods
+ guard_private_attribute_method!(method_name, args)
if self.class.generated_methods.include?(method_name)
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)
@@ -371,6 +370,12 @@ module ActiveRecord
end
private
+ # prevent method_missing from calling private methods with #send
+ def guard_private_attribute_method!(method_name, args)
+ if self.class.private_method_defined?(method_name)
+ 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
diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb
index a540570f42..c1bc8423a9 100644
--- a/activerecord/lib/active_record/autosave_association.rb
+++ b/activerecord/lib/active_record/autosave_association.rb
@@ -339,7 +339,8 @@ module ActiveRecord
association.save(!autosave) if association.new_record? || autosave
if association.updated?
- self[reflection.primary_key_name] = association.id
+ association_id = association.send(reflection.options[:primary_key] || :id)
+ self[reflection.primary_key_name] = association_id
# TODO: Removing this code doesn't seem to matter…
if reflection.options[:polymorphic]
self[reflection.options[:foreign_type]] = association.class.base_class.name.to_s
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index bb7342ca6e..5a36ff5ba2 100755
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -3215,6 +3215,7 @@ module ActiveRecord #:nodoc:
include Dirty
include Callbacks, ActiveModel::Observing, Timestamp
include Associations, AssociationPreload, NamedScope
+ include ActiveModel::Conversion
# AutosaveAssociation needs to be included before Transactions, because we want
# #save_with_autosave_associations to be wrapped inside a transaction.
diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb
index 0ad135e378..4a2ec5bf95 100644
--- a/activerecord/lib/active_record/callbacks.rb
+++ b/activerecord/lib/active_record/callbacks.rb
@@ -365,7 +365,7 @@ module ActiveRecord
result = send(method)
end
- notify(method)
+ notify_observers(method)
return result
end
diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb
index 2d90ef35aa..5d88012e4f 100644
--- a/activerecord/lib/active_record/schema_dumper.rb
+++ b/activerecord/lib/active_record/schema_dumper.rb
@@ -78,11 +78,14 @@ HEADER
begin
tbl = StringIO.new
+ # first dump primary key column
if @connection.respond_to?(:pk_and_sequence_for)
pk, pk_seq = @connection.pk_and_sequence_for(table)
+ elsif @connection.respond_to?(:primary_key)
+ pk = @connection.primary_key(table)
end
pk ||= 'id'
-
+
tbl.print " create_table #{table.inspect}"
if columns.detect { |c| c.name == pk }
if pk != 'id'
@@ -94,6 +97,7 @@ HEADER
tbl.print ", :force => true"
tbl.puts " do |t|"
+ # then dump all non-primary key columns
column_specs = columns.map do |column|
raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" if @types[column.type].nil?
next if column.name == pk
diff --git a/activerecord/lib/active_record/serialization.rb b/activerecord/lib/active_record/serialization.rb
index 23d085bea9..94f1e8f1fd 100644
--- a/activerecord/lib/active_record/serialization.rb
+++ b/activerecord/lib/active_record/serialization.rb
@@ -1,42 +1,9 @@
module ActiveRecord #:nodoc:
module Serialization
- class Serializer #:nodoc:
- attr_reader :options
-
- def initialize(record, options = nil)
- @record = record
- @options = options ? options.dup : {}
- end
-
- # To replicate the behavior in ActiveRecord#attributes,
- # <tt>:except</tt> takes precedence over <tt>:only</tt>. If <tt>:only</tt> is not set
- # for a N level model but is set for the N+1 level models,
- # then because <tt>:except</tt> is set to a default value, the second
- # level model can have both <tt>:except</tt> and <tt>:only</tt> set. So if
- # <tt>:only</tt> is set, always delete <tt>:except</tt>.
- def serializable_attribute_names
- attribute_names = @record.attribute_names
-
- if options[:only]
- options.delete(:except)
- attribute_names = attribute_names & Array.wrap(options[:only]).collect { |n| n.to_s }
- else
- options[:except] = Array.wrap(options[:except]) | Array.wrap(@record.class.inheritance_column)
- attribute_names = attribute_names - options[:except].collect { |n| n.to_s }
- end
-
- attribute_names
- end
-
- def serializable_method_names
- Array.wrap(options[:methods]).inject([]) do |method_attributes, name|
- method_attributes << name if @record.respond_to?(name.to_s)
- method_attributes
- end
- end
-
- def serializable_names
- serializable_attribute_names + serializable_method_names
+ module RecordSerializer #:nodoc:
+ def initialize(*args)
+ super
+ options[:except] |= Array.wrap(@serializable.class.inheritance_column)
end
# Add associations specified via the <tt>:includes</tt> option.
@@ -53,11 +20,11 @@ module ActiveRecord #:nodoc:
associations = include_has_options ? include_associations.keys : Array.wrap(include_associations)
for association in associations
- records = case @record.class.reflect_on_association(association).macro
+ records = case @serializable.class.reflect_on_association(association).macro
when :has_many, :has_and_belongs_to_many
- @record.send(association).to_a
+ @serializable.send(association).to_a
when :has_one, :belongs_to
- @record.send(association)
+ @serializable.send(association)
end
unless records.nil?
@@ -71,28 +38,19 @@ module ActiveRecord #:nodoc:
end
end
- def serializable_record
- record = {}
- serializable_names.each { |name| record[name] = @record.send(name) }
+ def serializable_hash
+ hash = super
add_includes do |association, records, opts|
- record[association] =
+ hash[association] =
if records.is_a?(Enumerable)
- records.collect { |r| self.class.new(r, opts).serializable_record }
+ records.collect { |r| self.class.new(r, opts).serializable_hash }
else
- self.class.new(records, opts).serializable_record
+ self.class.new(records, opts).serializable_hash
end
end
- record
- end
-
- def serialize
- # overwrite to implement
- end
-
- def to_s(&block)
- serialize(&block)
+ hash
end
end
end
diff --git a/activerecord/lib/active_record/serializers/json_serializer.rb b/activerecord/lib/active_record/serializers/json_serializer.rb
index 21afcd6e5c..63bf42c09d 100644
--- a/activerecord/lib/active_record/serializers/json_serializer.rb
+++ b/activerecord/lib/active_record/serializers/json_serializer.rb
@@ -1,91 +1,14 @@
-require 'active_support/json'
-require 'active_model/naming'
-
module ActiveRecord #:nodoc:
module Serialization
extend ActiveSupport::Concern
+ include ActiveModel::Serializers::JSON
- included do
- cattr_accessor :include_root_in_json, :instance_writer => false
+ class JSONSerializer < ActiveModel::Serializers::JSON::Serializer
+ include Serialization::RecordSerializer
end
- # Returns a JSON string representing the model. Some configuration is
- # available through +options+.
- #
- # The option <tt>ActiveRecord::Base.include_root_in_json</tt> controls the
- # top-level behavior of to_json. In a new Rails application, it is set to
- # <tt>true</tt> in initializers/new_rails_defaults.rb. When it is <tt>true</tt>,
- # to_json will emit a single root node named after the object's type. For example:
- #
- # konata = User.find(1)
- # ActiveRecord::Base.include_root_in_json = true
- # konata.to_json
- # # => { "user": {"id": 1, "name": "Konata Izumi", "age": 16,
- # "created_at": "2006/08/01", "awesome": true} }
- #
- # ActiveRecord::Base.include_root_in_json = false
- # konata.to_json
- # # => {"id": 1, "name": "Konata Izumi", "age": 16,
- # "created_at": "2006/08/01", "awesome": true}
- #
- # The remainder of the examples in this section assume include_root_in_json is set to
- # <tt>false</tt>.
- #
- # Without any +options+, the returned JSON string will include all
- # the model's attributes. For example:
- #
- # konata = User.find(1)
- # konata.to_json
- # # => {"id": 1, "name": "Konata Izumi", "age": 16,
- # "created_at": "2006/08/01", "awesome": true}
- #
- # The <tt>:only</tt> and <tt>:except</tt> options can be used to limit the attributes
- # included, and work similar to the +attributes+ method. For example:
- #
- # konata.to_json(:only => [ :id, :name ])
- # # => {"id": 1, "name": "Konata Izumi"}
- #
- # konata.to_json(:except => [ :id, :created_at, :age ])
- # # => {"name": "Konata Izumi", "awesome": true}
- #
- # To include any methods on the model, use <tt>:methods</tt>.
- #
- # konata.to_json(:methods => :permalink)
- # # => {"id": 1, "name": "Konata Izumi", "age": 16,
- # "created_at": "2006/08/01", "awesome": true,
- # "permalink": "1-konata-izumi"}
- #
- # To include associations, use <tt>:include</tt>.
- #
- # konata.to_json(:include => :posts)
- # # => {"id": 1, "name": "Konata Izumi", "age": 16,
- # "created_at": "2006/08/01", "awesome": true,
- # "posts": [{"id": 1, "author_id": 1, "title": "Welcome to the weblog"},
- # {"id": 2, author_id: 1, "title": "So I was thinking"}]}
- #
- # 2nd level and higher order associations work as well:
- #
- # konata.to_json(:include => { :posts => {
- # :include => { :comments => {
- # :only => :body } },
- # :only => :title } })
- # # => {"id": 1, "name": "Konata Izumi", "age": 16,
- # "created_at": "2006/08/01", "awesome": true,
- # "posts": [{"comments": [{"body": "1st post!"}, {"body": "Second!"}],
- # "title": "Welcome to the weblog"},
- # {"comments": [{"body": "Don't think too hard"}],
- # "title": "So I was thinking"}]}
def encode_json(encoder)
- hash = Serializer.new(self, encoder.options).serializable_record
- hash = { self.class.model_name.element => hash } if include_root_in_json
- ActiveSupport::JSON.encode(hash)
- end
-
- def as_json(options = nil) self end #:nodoc:
-
- def from_json(json)
- self.attributes = ActiveSupport::JSON.decode(json)
- self
+ JSONSerializer.new(self, encoder.options).to_s
end
end
end
diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb
index 4eaf9531e2..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}}
@@ -164,73 +179,20 @@ module ActiveRecord #:nodoc:
end
end
- class XmlSerializer < ActiveRecord::Serialization::Serializer #:nodoc:
- def builder
- @builder ||= begin
- require 'builder' unless defined? ::Builder
- options[:indent] ||= 2
- builder = options[:builder] ||= ::Builder::XmlMarkup.new(:indent => options[:indent])
-
- unless options[:skip_instruct]
- builder.instruct!
- options[:skip_instruct] = true
- end
-
- builder
- end
- end
-
- def root
- root = (options[:root] || @record.class.to_s.underscore).to_s
- reformat_name(root)
- end
-
- def dasherize?
- !options.has_key?(:dasherize) || options[:dasherize]
- end
-
- def camelize?
- options.has_key?(:camelize) && options[:camelize]
- end
-
- def reformat_name(name)
- name = name.camelize if camelize?
- dasherize? ? name.dasherize : name
- end
+ class XmlSerializer < ActiveModel::Serializers::Xml::Serializer #:nodoc:
+ include Serialization::RecordSerializer
def serializable_attributes
- serializable_attribute_names.collect { |name| Attribute.new(name, @record) }
+ serializable_attribute_names.collect { |name| Attribute.new(name, @serializable) }
end
def serializable_method_attributes
Array(options[:methods]).inject([]) do |method_attributes, name|
- method_attributes << MethodAttribute.new(name.to_s, @record) if @record.respond_to?(name.to_s)
+ method_attributes << MethodAttribute.new(name.to_s, @serializable) if @serializable.respond_to?(name.to_s)
method_attributes
end
end
- def add_attributes
- (serializable_attributes + serializable_method_attributes).each do |attribute|
- add_tag(attribute)
- end
- end
-
- def add_procs
- if procs = options.delete(:procs)
- [ *procs ].each do |proc|
- proc.call(options)
- end
- end
- end
-
- def add_tag(attribute)
- builder.tag!(
- reformat_name(attribute.name),
- attribute.value.to_s,
- attribute.decorations(!options[:skip_types])
- )
- end
-
def add_associations(association, records, opts)
if records.is_a?(Enumerable)
tag = reformat_name(association.to_s)
@@ -254,7 +216,7 @@ module ActiveRecord #:nodoc:
end
end
else
- if record = @record.send(association)
+ if record = @serializable.send(association)
record.to_xml(opts.merge(:root => association))
end
end
@@ -280,50 +242,10 @@ module ActiveRecord #:nodoc:
end
end
- class Attribute #:nodoc:
- attr_reader :name, :value, :type
-
- def initialize(name, record)
- @name, @record = name, record
-
- @type = compute_type
- @value = compute_value
- end
-
- # There is a significant speed improvement if the value
- # does not need to be escaped, as <tt>tag!</tt> escapes all values
- # to ensure that valid XML is generated. For known binary
- # values, it is at least an order of magnitude faster to
- # Base64 encode binary values and directly put them in the
- # output XML than to pass the original value or the Base64
- # encoded value to the <tt>tag!</tt> method. It definitely makes
- # no sense to Base64 encode the value and then give it to
- # <tt>tag!</tt>, since that just adds additional overhead.
- def needs_encoding?
- ![ :binary, :date, :datetime, :boolean, :float, :integer ].include?(type)
- end
-
- def decorations(include_types = true)
- decorations = {}
-
- if type == :binary
- decorations[:encoding] = 'base64'
- end
-
- if include_types && type != :string
- decorations[:type] = type
- end
-
- if value.nil?
- decorations[:nil] = true
- end
-
- decorations
- end
-
+ class Attribute < ActiveModel::Serializers::Xml::Serializer::Attribute #:nodoc:
protected
def compute_type
- type = @record.class.serialized_attributes.has_key?(name) ? :yaml : @record.class.columns_hash[name].type
+ type = @serializable.class.serialized_attributes.has_key?(name) ? :yaml : @serializable.class.columns_hash[name].type
case type
when :text
@@ -334,22 +256,12 @@ module ActiveRecord #:nodoc:
type
end
end
-
- def compute_value
- value = @record.send(name)
-
- if formatter = Hash::XML_FORMATTING[type.to_s]
- value ? formatter.call(value) : nil
- else
- value
- end
- end
end
class MethodAttribute < Attribute #:nodoc:
protected
def compute_type
- Hash::XML_TYPE_NAMES[@record.send(name).class.name] || :string
+ Hash::XML_TYPE_NAMES[@serializable.send(name).class.name] || :string
end
end
end
diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb
index 13a78a1890..ab6f752243 100644
--- a/activerecord/test/cases/associations/belongs_to_associations_test.rb
+++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb
@@ -14,6 +14,7 @@ require 'models/tagging'
require 'models/comment'
require 'models/sponsor'
require 'models/member'
+require 'models/essay'
class BelongsToAssociationsTest < ActiveRecord::TestCase
fixtures :accounts, :companies, :developers, :projects, :topics,
@@ -25,6 +26,11 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert !Client.find(3).firm.nil?, "Microsoft should have a firm"
end
+ def test_belongs_to_with_primary_key
+ client = Client.create(:name => "Primary key client", :firm_name => companies(:first_firm).name)
+ assert_equal companies(:first_firm).name, client.firm_with_primary_key.name
+ end
+
def test_proxy_assignment
account = Account.find(1)
assert_nothing_raised { account.firm = account.firm }
@@ -47,6 +53,13 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal apple.id, citibank.firm_id
end
+ def test_natural_assignment_with_primary_key
+ apple = Firm.create("name" => "Apple")
+ citibank = Client.create("name" => "Primary key client")
+ citibank.firm_with_primary_key = apple
+ assert_equal apple.name, citibank.firm_name
+ end
+
def test_no_unexpected_aliasing
first_firm = companies(:first_firm)
another_firm = companies(:another_firm)
@@ -69,6 +82,15 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal apple, citibank.firm
end
+ def test_creating_the_belonging_object_with_primary_key
+ client = Client.create(:name => "Primary key client")
+ apple = client.create_firm_with_primary_key("name" => "Apple")
+ assert_equal apple, client.firm_with_primary_key
+ client.save
+ client.reload
+ assert_equal apple, client.firm_with_primary_key
+ end
+
def test_building_the_belonging_object
citibank = Account.create("credit_limit" => 10)
apple = citibank.build_firm("name" => "Apple")
@@ -76,6 +98,13 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal apple.id, citibank.firm_id
end
+ def test_building_the_belonging_object_with_primary_key
+ client = Client.create(:name => "Primary key client")
+ apple = client.build_firm_with_primary_key("name" => "Apple")
+ client.save
+ assert_equal apple.name, client.firm_name
+ end
+
def test_natural_assignment_to_nil
client = Client.find(3)
client.firm = nil
@@ -84,6 +113,14 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_nil client.client_of
end
+ def test_natural_assignment_to_nil_with_primary_key
+ client = Client.create(:name => "Primary key client", :firm_name => companies(:first_firm).name)
+ client.firm_with_primary_key = nil
+ client.save
+ assert_nil client.firm_with_primary_key(true)
+ assert_nil client.client_of
+ end
+
def test_with_different_class_name
assert_equal Company.find(1).name, Company.find(3).firm_with_other_name.name
assert_not_nil Company.find(3).firm_with_other_name, "Microsoft should have a firm"
@@ -110,6 +147,17 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal 0, Topic.find(debate.id).send(:read_attribute, "replies_count"), "First reply deleted"
end
+ def test_belongs_to_with_primary_key_counter
+ debate = Topic.create("title" => "debate")
+ assert_equal 0, debate.send(:read_attribute, "replies_count"), "No replies yet"
+
+ trash = debate.replies_with_primary_key.create("title" => "blah!", "content" => "world around!")
+ assert_equal 1, Topic.find(debate.id).send(:read_attribute, "replies_count"), "First reply created"
+
+ trash.destroy
+ assert_equal 0, Topic.find(debate.id).send(:read_attribute, "replies_count"), "First reply deleted"
+ end
+
def test_belongs_to_counter_with_assigning_nil
p = Post.find(1)
c = Comment.find(1)
@@ -122,6 +170,18 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal 1, Post.find(p.id).comments.size
end
+ def test_belongs_to_with_primary_key_counter_with_assigning_nil
+ debate = Topic.create("title" => "debate")
+ reply = Reply.create("title" => "blah!", "content" => "world around!", "parent_title" => "debate")
+
+ assert_equal debate.title, reply.parent_title
+ assert_equal 1, Topic.find(debate.id).send(:read_attribute, "replies_count")
+
+ reply.topic_with_primary_key = nil
+
+ assert_equal 0, Topic.find(debate.id).send(:read_attribute, "replies_count")
+ end
+
def test_belongs_to_counter_with_reassigning
t1 = Topic.create("title" => "t1")
t2 = Topic.create("title" => "t2")
@@ -219,6 +279,18 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal firm, final_cut.firm(true)
end
+ def test_assignment_before_child_saved_with_primary_key
+ final_cut = Client.new("name" => "Final Cut")
+ firm = Firm.find(1)
+ final_cut.firm_with_primary_key = firm
+ assert final_cut.new_record?
+ assert final_cut.save
+ assert !final_cut.new_record?
+ assert !firm.new_record?
+ assert_equal firm, final_cut.firm_with_primary_key
+ assert_equal firm, final_cut.firm_with_primary_key(true)
+ end
+
def test_new_record_with_foreign_key_but_no_object
c = Client.new("firm_id" => 1)
assert_equal Firm.find(:first), c.firm_with_basic_id
@@ -304,6 +376,20 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
sponsor.sponsorable = member
assert_equal "Member", sponsor.sponsorable_type
end
+
+ def test_polymorphic_assignment_with_primary_key_foreign_type_field_updating
+ # should update when assigning a saved record
+ essay = Essay.new
+ writer = Author.create(:name => "David")
+ essay.writer = writer
+ assert_equal "Author", essay.writer_type
+
+ # should update when assigning a new record
+ essay = Essay.new
+ writer = Author.new
+ essay.writer = writer
+ assert_equal "Author", essay.writer_type
+ end
def test_polymorphic_assignment_updates_foreign_id_field_for_new_and_saved_records
sponsor = Sponsor.new
@@ -317,6 +403,18 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal nil, sponsor.sponsorable_id
end
+ def test_polymorphic_assignment_with_primary_key_updates_foreign_id_field_for_new_and_saved_records
+ essay = Essay.new
+ saved_writer = Author.create(:name => "David")
+ new_writer = Author.new
+
+ essay.writer = saved_writer
+ assert_equal saved_writer.name, essay.writer_id
+
+ essay.writer = new_writer
+ assert_equal nil, essay.writer_id
+ end
+
def test_belongs_to_proxy_should_not_respond_to_private_methods
assert_raise(NoMethodError) { companies(:first_firm).private_method }
assert_raise(NoMethodError) { companies(:second_client).firm.private_method }
diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb
index 7a4712d7c8..8529ff0285 100644
--- a/activerecord/test/cases/associations/has_many_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb
@@ -243,8 +243,12 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert_equal 2, people(:michael).jobs.size
end
- def test_get_ids
- assert_equal [posts(:welcome).id, posts(:authorless).id].sort, people(:michael).post_ids.sort
+ def test_get_ids_for_belongs_to_source
+ assert_sql(/DISTINCT/) { assert_equal [posts(:welcome).id, posts(:authorless).id].sort, people(:michael).post_ids.sort }
+ end
+
+ def test_get_ids_for_has_many_source
+ assert_equal [comments(:eager_other_comment1).id], authors(:mary).comment_ids
end
def test_get_ids_for_loaded_associations
diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb
index 17ed302465..183be1e2f9 100644
--- a/activerecord/test/cases/attribute_methods_test.rb
+++ b/activerecord/test/cases/attribute_methods_test.rb
@@ -277,6 +277,22 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert_raise(ActiveRecord::UnknownAttributeError) { @target.new.attributes = { :title => "Ants in pants" } }
end
+ def test_read_attribute_overwrites_private_method_not_considered_implemented
+ # simulate a model with a db column that shares its name an inherited
+ # private method (e.g. Object#system)
+ #
+ Object.class_eval do
+ private
+ def title; "private!"; end
+ end
+ assert !@target.instance_method_already_implemented?(:title)
+ topic = @target.new
+ assert_equal nil, topic.title
+
+ Object.send(:undef_method, :title) # remove test method from object
+ end
+
+
private
def time_related_columns_on_topic
Topic.columns.select{|c| [:time, :date, :datetime, :timestamp].include?(c.type)}.map(&:name)
diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb
index f9ac37cc87..e47f898485 100755
--- a/activerecord/test/cases/base_test.rb
+++ b/activerecord/test/cases/base_test.rb
@@ -2026,7 +2026,7 @@ class BasicsTest < ActiveRecord::TestCase
def test_inspect_instance
topic = topics(:first)
- assert_equal %(#<Topic id: 1, title: "The First Topic", author_name: "David", author_email_address: "david@loudthinking.com", written_on: "#{topic.written_on.to_s(:db)}", bonus_time: "#{topic.bonus_time.to_s(:db)}", last_read: "#{topic.last_read.to_s(:db)}", content: "Have a nice day", approved: false, replies_count: 1, parent_id: nil, type: nil>), topic.inspect
+ assert_equal %(#<Topic id: 1, title: "The First Topic", author_name: "David", author_email_address: "david@loudthinking.com", written_on: "#{topic.written_on.to_s(:db)}", bonus_time: "#{topic.bonus_time.to_s(:db)}", last_read: "#{topic.last_read.to_s(:db)}", content: "Have a nice day", approved: false, replies_count: 1, parent_id: nil, parent_title: nil, type: nil>), topic.inspect
end
def test_inspect_new_instance
diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb
index 194d5e9dff..4083b990d9 100644
--- a/activerecord/test/cases/reflection_test.rb
+++ b/activerecord/test/cases/reflection_test.rb
@@ -27,25 +27,25 @@ class ReflectionTest < ActiveRecord::TestCase
def test_read_attribute_names
assert_equal(
- %w( id title author_name author_email_address bonus_time written_on last_read content approved replies_count parent_id type ).sort,
+ %w( id title author_name author_email_address bonus_time written_on last_read content approved replies_count parent_id parent_title type ).sort,
@first.attribute_names
)
end
def test_columns
- assert_equal 12, Topic.columns.length
+ assert_equal 13, Topic.columns.length
end
def test_columns_are_returned_in_the_order_they_were_declared
column_names = Topic.columns.map { |column| column.name }
- assert_equal %w(id title author_name author_email_address written_on bonus_time last_read content approved replies_count parent_id type), column_names
+ assert_equal %w(id title author_name author_email_address written_on bonus_time last_read content approved replies_count parent_id parent_title type), column_names
end
def test_content_columns
content_columns = Topic.content_columns
content_column_names = content_columns.map {|column| column.name}
- assert_equal 8, content_columns.length
- assert_equal %w(title author_name author_email_address written_on bonus_time last_read content approved).sort, content_column_names.sort
+ assert_equal 9, content_columns.length
+ assert_equal %w(title author_name author_email_address written_on bonus_time last_read content approved parent_title).sort, content_column_names.sort
end
def test_column_string_type_and_limit
diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb
index 4f02be3c06..9612b0beb6 100644
--- a/activerecord/test/cases/schema_dumper_test.rb
+++ b/activerecord/test/cases/schema_dumper_test.rb
@@ -156,6 +156,13 @@ class SchemaDumperTest < ActiveRecord::TestCase
index_definition = standard_dump.split(/\n/).grep(/add_index.*companies/).first.strip
assert_equal 'add_index "companies", ["firm_id", "type", "rating", "ruby_type"], :name => "company_index"', index_definition
end
+
+ def test_schema_dump_should_honor_nonstandard_primary_keys
+ output = standard_dump
+ match = output.match(%r{create_table "movies"(.*)do})
+ assert_not_nil(match, "nonstandardpk table not found")
+ assert_match %r(:primary_key => "movieid"), match[1], "non-standard primary key not preserved"
+ end
if current_adapter?(:MysqlAdapter)
def test_schema_dump_should_not_add_default_value_for_mysql_text_field
@@ -163,13 +170,6 @@ class SchemaDumperTest < ActiveRecord::TestCase
assert_match %r{t.text\s+"body",\s+:null => false$}, output
end
- def test_mysql_schema_dump_should_honor_nonstandard_primary_keys
- output = standard_dump
- match = output.match(%r{create_table "movies"(.*)do})
- assert_not_nil(match, "nonstandardpk table not found")
- assert_match %r(:primary_key => "movieid"), match[1], "non-standard primary key not preserved"
- end
-
def test_schema_dump_includes_length_for_mysql_blob_and_text_fields
output = standard_dump
assert_match %r{t.binary\s+"tiny_blob",\s+:limit => 255$}, output
diff --git a/activerecord/test/cases/xml_serialization_test.rb b/activerecord/test/cases/xml_serialization_test.rb
index b49997669e..e1ad5c1685 100644
--- a/activerecord/test/cases/xml_serialization_test.rb
+++ b/activerecord/test/cases/xml_serialization_test.rb
@@ -174,6 +174,12 @@ class DatabaseConnectedXmlSerializationTest < ActiveRecord::TestCase
assert_match %r{<nationality>Danish</nationality>}, xml
end
+ def test_dual_arity_procs_are_called_on_object
+ proc = Proc.new { |options, record| options[:builder].tag!('name-reverse', record.name.reverse) }
+ xml = authors(:david).to_xml(:procs => [ proc ])
+ assert_match %r{<name-reverse>divaD</name-reverse>}, xml
+ end
+
def test_top_level_procs_arent_applied_to_associations
author_proc = Proc.new { |options| options[:builder].tag!('nationality', 'Danish') }
xml = authors(:david).to_xml(:procs => [ author_proc ], :include => :posts, :indent => 2)
diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb
index 0d9ee36b20..b844c7cce0 100644
--- a/activerecord/test/models/author.rb
+++ b/activerecord/test/models/author.rb
@@ -87,6 +87,8 @@ class Author < ActiveRecord::Base
has_many :tags, :through => :posts # through has_many :through
has_many :post_categories, :through => :posts, :source => :categories
+ has_one :essay, :primary_key => :name, :as => :writer
+
belongs_to :author_address, :dependent => :destroy
belongs_to :author_address_extra, :dependent => :delete, :class_name => "AuthorAddress"
diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb
index 840527ddeb..22168468a6 100644
--- a/activerecord/test/models/company.rb
+++ b/activerecord/test/models/company.rb
@@ -88,6 +88,7 @@ class Client < Company
belongs_to :firm_with_select, :class_name => "Firm", :foreign_key => "firm_id", :select => "id"
belongs_to :firm_with_other_name, :class_name => "Firm", :foreign_key => "client_of"
belongs_to :firm_with_condition, :class_name => "Firm", :foreign_key => "client_of", :conditions => ["1 = ?", 1]
+ belongs_to :firm_with_primary_key, :class_name => "Firm", :primary_key => "name", :foreign_key => "firm_name"
belongs_to :readonly_firm, :class_name => "Firm", :foreign_key => "firm_id", :readonly => true
# Record destruction so we can test whether firm.clients.clear has
diff --git a/activerecord/test/models/essay.rb b/activerecord/test/models/essay.rb
new file mode 100644
index 0000000000..6c28f5e49b
--- /dev/null
+++ b/activerecord/test/models/essay.rb
@@ -0,0 +1,3 @@
+class Essay < ActiveRecord::Base
+ belongs_to :writer, :primary_key => :name, :polymorphic => true
+end
diff --git a/activerecord/test/models/reply.rb b/activerecord/test/models/reply.rb
index 616c07687c..f5906dedd1 100644
--- a/activerecord/test/models/reply.rb
+++ b/activerecord/test/models/reply.rb
@@ -4,12 +4,13 @@ class Reply < Topic
named_scope :base
belongs_to :topic, :foreign_key => "parent_id", :counter_cache => true
+ belongs_to :topic_with_primary_key, :class_name => "Topic", :primary_key => "title", :foreign_key => "parent_title", :counter_cache => "replies_count"
has_many :replies, :class_name => "SillyReply", :dependent => :destroy, :foreign_key => "parent_id"
validate :errors_on_empty_content
validate_on_create :title_is_wrong_create
- attr_accessible :title, :author_name, :author_email_address, :written_on, :content, :last_read
+ attr_accessible :title, :author_name, :author_email_address, :written_on, :content, :last_read, :parent_title
validate :check_empty_title
validate_on_create :check_content_mismatch
diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb
index 51012d22ed..201d96dcd7 100644
--- a/activerecord/test/models/topic.rb
+++ b/activerecord/test/models/topic.rb
@@ -39,6 +39,7 @@ class Topic < ActiveRecord::Base
named_scope :by_rejected_ids, lambda {{ :conditions => { :id => all(:conditions => {:approved => false}).map(&:id) } }}
has_many :replies, :dependent => :destroy, :foreign_key => "parent_id"
+ has_many :replies_with_primary_key, :class_name => "Reply", :dependent => :destroy, :primary_key => "title", :foreign_key => "parent_title"
serialize :content
before_create :default_written_on
diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb
index d2d6d1f4b3..2b7d3856b7 100644
--- a/activerecord/test/schema/schema.rb
+++ b/activerecord/test/schema/schema.rb
@@ -161,6 +161,12 @@ ActiveRecord::Schema.define do
t.integer :course_id, :null => false
end
+ create_table :essays, :force => true do |t|
+ t.string :name
+ t.string :writer_id
+ t.string :writer_type
+ end
+
create_table :events, :force => true do |t|
t.string :title, :limit => 5
end
@@ -421,6 +427,7 @@ ActiveRecord::Schema.define do
t.boolean :approved, :default => true
t.integer :replies_count, :default => 0
t.integer :parent_id
+ t.string :parent_title
t.string :type
end