diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG
index c3fdbe38c6..5cde7f465b 100644
--- a/activerecord/CHANGELOG
+++ b/activerecord/CHANGELOG
@@ -36,6 +36,33 @@
2004-04-15
+
+ You can even do load first-level associations as part of the document:
+
+ firm.to_xml :include => [ :account, :clients ]
+
+ ...that'll return something like:
+
+
+
+ 1
+ 1
+ 37signals
+
+
+ 1
+ Summit
+
+
+ 1
+ Microsoft
+
+
+
+ 1
+ 50
+
+
* Allow :counter_cache to take a column name for custom counter cache columns [Jamis Buck]
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index 61f7e94d89..c84f1c7778 100755
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -1418,8 +1418,24 @@ module ActiveRecord #:nodoc:
# Returns a hash of all the attributes with their names as keys and clones of their objects as values.
- def attributes
- clone_attributes :read_attribute
+ def attributes(options = nil)
+ attributes = clone_attributes :read_attribute
+
+ if options.nil?
+ attributes
+ else
+ if except = options[:except]
+ except = Array(except).collect { |attribute| attribute.to_s }
+ except.each { |attribute_name| attributes.delete(attribute_name) }
+ attributes
+ elsif only = options[:only]
+ only = Array(only).collect { |attribute| attribute.to_s }
+ attributes.delete_if { |key, value| !only.include?(key) }
+ attributes
+ else
+ raise ArgumentError, "Options does not specify :except or :only (#{options.keys.inspect})"
+ end
+ end
end
# Returns a hash of cloned attributes before typecasting and deserialization.
@@ -1506,16 +1522,31 @@ module ActiveRecord #:nodoc:
# Turns this record into XML
def to_xml(options = {})
- options[:skip_attributes] = Array(options[:skip_attributes])
- options[:skip_attributes] << :type
- options[:skip_attributes].collect! { |attribute| attribute.to_s }
-
- attributes_to_be_xmled = attributes
- options[:skip_attributes].each { |attribute_name| attributes_to_be_xmled.delete(attribute_name) }
-
options[:root] ||= self.class.to_s.underscore
+ options[:except] = Array(options[:except]) << self.class.inheritance_column unless options[:only]
+ only_or_except = { :only => options[:only], :except => options[:except] }
- attributes_to_be_xmled.to_xml(options)
+ attributes_for_xml = attributes(only_or_except)
+
+ if include_associations = options.delete(:include)
+ for association in Array(include_associations)
+ case self.class.reflect_on_association(association).macro
+ when :has_many, :has_and_belongs_to_many
+ records = send(association).to_a
+ unless records.empty?
+ attributes_for_xml[association] = records.collect do |record|
+ record.attributes(only_or_except)
+ end
+ end
+ when :has_one, :belongs_to
+ if record = send(association)
+ attributes_for_xml[association] = record.attributes(only_or_except)
+ end
+ end
+ end
+ end
+
+ attributes_for_xml.to_xml(options)
end
private
diff --git a/activerecord/test/base_test.rb b/activerecord/test/base_test.rb
index d82bde433d..3ad2d6e960 100755
--- a/activerecord/test/base_test.rb
+++ b/activerecord/test/base_test.rb
@@ -1155,12 +1155,12 @@ class BasicsTest < Test::Unit::TestCase
end
def test_to_xml
- xml = Topic.find(:first).to_xml(:indent => 0, :skip_instruct => true)
+ xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true)
assert_equal "", xml.first(7)
assert xml.include?(%(The First Topic))
assert xml.include?(%(David))
assert xml.include?(%(1))
- assert xml.include?(%(false))
+ assert xml.include?(%(false)), "Approved should be a boolean"
assert xml.include?(%(0))
assert xml.include?(%(2000-01-01 08:28:00))
assert xml.include?(%(2003-07-16 09:28:00))
@@ -1171,16 +1171,54 @@ class BasicsTest < Test::Unit::TestCase
end
def test_to_xml_skipping_attributes
- xml = Topic.find(:first).to_xml(:indent => 0, :skip_instruct => true, :skip_attributes => :title)
- breakpoint
+ xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :except => :title)
assert_equal "", xml.first(7)
assert !xml.include?(%(The First Topic))
assert xml.include?(%(David))
- xml = Topic.find(:first).to_xml(:indent => 0, :skip_instruct => true, :skip_attributes => [ :title, :author_name ])
+ xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :except => [ :title, :author_name ])
assert !xml.include?(%(The First Topic))
assert !xml.include?(%(David))
end
+
+ def test_to_xml_including_has_many_association
+ xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :include => :replies)
+ assert_equal "", xml.first(7)
+ assert xml.include?(%())
+ assert xml.include?(%(The Second Topic's of the day))
+ end
+
+ def test_to_xml_including_belongs_to_association
+ xml = companies(:first_client).to_xml(:indent => 0, :skip_instruct => true, :include => :firm)
+ assert !xml.include?("")
+
+ xml = companies(:second_client).to_xml(:indent => 0, :skip_instruct => true, :include => :firm)
+ assert xml.include?("")
+ end
+
+ def test_to_xml_including_multiple_associations
+ xml = companies(:first_firm).to_xml(:indent => 0, :skip_instruct => true, :include => [ :clients, :account ])
+ assert_equal "", xml.first(6)
+ assert xml.include?(%())
+ assert xml.include?(%())
+ end
+
+ def test_except_attributes
+ assert_equal(
+ %w( author_name type id approved replies_count bonus_time written_on content author_email_address parent_id last_read),
+ topics(:first).attributes(:except => :title).keys
+ )
+
+ assert_equal(
+ %w( replies_count bonus_time written_on content author_email_address parent_id last_read),
+ topics(:first).attributes(:except => [ :title, :id, :type, :approved, :author_name ]).keys
+ )
+ end
+
+ def test_include_attributes
+ assert_equal(%w( title ), topics(:first).attributes(:only => :title).keys)
+ assert_equal(%w( title author_name type id approved ), topics(:first).attributes(:only => [ :title, :id, :type, :approved, :author_name ]).keys)
+ end
# FIXME: this test ought to run, but it needs to run sandboxed so that it
# doesn't b0rk the current test environment by undefing everything.