From 66d05f5e2c7ac6b18220956fbcf34efcd32638fc Mon Sep 17 00:00:00 2001 From: Rick Olson Date: Sun, 30 Sep 2007 07:09:44 +0000 Subject: Add attr_readonly to specify columns that are skipped during a normal ActiveRecord #save operation. Closes #6896 [dcmanges] git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@7693 5ecf4fe2-1ee6-0310-87b1-e25e094e27de --- activerecord/CHANGELOG | 11 +++++++ activerecord/lib/active_record/associations.rb | 6 +++- activerecord/lib/active_record/base.rb | 25 +++++++++++++-- activerecord/test/associations_test.rb | 18 +++++++++++ activerecord/test/base_test.rb | 42 ++++++++++++++++++-------- 5 files changed, 85 insertions(+), 17 deletions(-) (limited to 'activerecord') diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index 9372adb583..f470fe7b84 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,5 +1,16 @@ *2.0.0 [Preview Release]* (September 29th, 2007) [Includes duplicates of changes from 1.14.2 - 1.15.3] +* Add attr_readonly to specify columns that are skipped during a normal ActiveRecord #save operation. Closes #6896 [dcmanges] + + class Comment < ActiveRecord::Base + # Automatically sets Article#comments_count as readonly. + belongs_to :article, :counter_cache => :comments_count + end + + class Article < ActiveRecord::Base + attr_readonly :approved_comments_count + end + * Make size for has_many :through use counter cache if it exists. Closes #9734 [xaviershay] * Remove DB2 adapter since IBM chooses to maintain their own adapter instead. [Jeremy Kemper] diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index e269775fd5..1ec7d6021f 100755 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -841,7 +841,11 @@ module ActiveRecord module_eval( "before_destroy '#{reflection.name}.class.decrement_counter(\"#{cache_column}\", #{reflection.primary_key_name})" + " unless #{reflection.name}.nil?'" - ) + ) + + module_eval( + "#{reflection.class_name}.send(:attr_readonly,\"#{cache_column}\".intern) if defined?(#{reflection.class_name})" + ) end end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index a4ebaf4e93..34685a78e1 100755 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -636,6 +636,15 @@ module ActiveRecord #:nodoc: read_inheritable_attribute("attr_accessible") end + # Attributes listed as readonly can be set for a new record, but will be ignored in database updates afterwards. + def attr_readonly(*attributes) + write_inheritable_array("attr_readonly", attributes - (readonly_attributes || [])) + end + + # Returns an array of all the attributes that have been specified as readonly. + def readonly_attributes + read_inheritable_attribute("attr_readonly") + end # If you have an attribute that needs to be saved to the database as an object, and retrieved as the same object, # then specify the name of that attribute using this method and it will be handled automatically. @@ -1953,7 +1962,7 @@ module ActiveRecord #:nodoc: def update connection.update( "UPDATE #{self.class.table_name} " + - "SET #{quoted_comma_pair_list(connection, attributes_with_quotes(false))} " + + "SET #{quoted_comma_pair_list(connection, attributes_with_quotes(false, false))} " + "WHERE #{connection.quote_column_name(self.class.primary_key)} = #{quote_value(id)}", "#{self.class.name} Update" ) @@ -2008,6 +2017,15 @@ module ActiveRecord #:nodoc: raise "Declare either attr_protected or attr_accessible for #{self.class}, but not both." end end + + # Removes attributes which have been marked as readonly. + def remove_readonly_attributes(attributes) + unless self.class.readonly_attributes.nil? + attributes.delete_if { |key, value| self.class.readonly_attributes.include?(key.gsub(/\(.+/,"").intern) } + else + attributes + end + end # The primary key and inheritance column can never be set by mass-assignment for security reasons. def attributes_protected_by_default @@ -2018,13 +2036,14 @@ module ActiveRecord #:nodoc: # Returns copy of the attributes hash where all the values have been safely quoted for use in # an SQL statement. - def attributes_with_quotes(include_primary_key = true) - attributes.inject({}) do |quoted, (name, value)| + def attributes_with_quotes(include_primary_key = true, include_readonly_attributes = true) + quoted = attributes.inject({}) do |quoted, (name, value)| if column = column_for_attribute(name) quoted[name] = quote_value(value, column) unless !include_primary_key && column.primary end quoted end + include_readonly_attributes ? quoted : remove_readonly_attributes(quoted) end # Quote strings appropriately for SQL statements. diff --git a/activerecord/test/associations_test.rb b/activerecord/test/associations_test.rb index 59951d47fb..85df401fb1 100755 --- a/activerecord/test/associations_test.rb +++ b/activerecord/test/associations_test.rb @@ -1175,6 +1175,24 @@ class BelongsToAssociationsTest < Test::Unit::TestCase topic.update_attributes(:title => "37signals") assert_equal 1, Topic.find(topic.id)[:replies_count] end + + def test_belongs_to_counter_after_save + topic = Topic.create("title" => "monday night") + topic.replies.create("title" => "re: monday night", "content" => "football") + assert_equal 1, Topic.find(topic.id).send(:read_attribute, "replies_count") + + topic.save + assert_equal 1, Topic.find(topic.id).send(:read_attribute, "replies_count") + end + + def test_belongs_to_counter_after_update_attributes + topic = Topic.create("title" => "37s") + topic.replies.create("title" => "re: 37s", "content" => "rails") + assert_equal 1, Topic.find(topic.id).send(:read_attribute, "replies_count") + + topic.update_attributes("title" => "37signals") + assert_equal 1, Topic.find(topic.id).send(:read_attribute, "replies_count") + end def test_assignment_before_parent_saved client = Client.find(:first) diff --git a/activerecord/test/base_test.rb b/activerecord/test/base_test.rb index 1883f369c3..4418b8d9e6 100755 --- a/activerecord/test/base_test.rb +++ b/activerecord/test/base_test.rb @@ -47,6 +47,10 @@ class TightDescendant < TightPerson attr_accessible :phone_number end +class ReadonlyTitlePost < Post + attr_readonly :title +end + class Booleantest < ActiveRecord::Base; end class Task < ActiveRecord::Base @@ -840,6 +844,19 @@ class BasicsTest < Test::Unit::TestCase assert_nil TightDescendant.protected_attributes assert_equal [ :name, :address, :phone_number ], TightDescendant.accessible_attributes end + + def test_readonly_attributes + assert_equal [ :title ], ReadonlyTitlePost.readonly_attributes + + post = ReadonlyTitlePost.create(:title => "cannot change this", :body => "changeable") + post.reload + assert_equal "cannot change this", post.title + + post.update_attributes(:title => "try to change", :body => "changed") + post.reload + assert_equal "cannot change this", post.title + assert_equal "changed", post.body + end def test_multiparameter_attributes_on_date attributes = { "last_read(1i)" => "2004", "last_read(2i)" => "6", "last_read(3i)" => "24" } @@ -1222,12 +1239,12 @@ class BasicsTest < Test::Unit::TestCase end def test_increment_attribute - assert_equal 1, topics(:first).replies_count - topics(:first).increment! :replies_count - assert_equal 2, topics(:first, :reload).replies_count - - topics(:first).increment(:replies_count).increment!(:replies_count) - assert_equal 4, topics(:first, :reload).replies_count + assert_equal 50, accounts(:signals37).credit_limit + accounts(:signals37).increment! :credit_limit + assert_equal 51, accounts(:signals37, :reload).credit_limit + + accounts(:signals37).increment(:credit_limit).increment!(:credit_limit) + assert_equal 53, accounts(:signals37, :reload).credit_limit end def test_increment_nil_attribute @@ -1237,14 +1254,13 @@ class BasicsTest < Test::Unit::TestCase end def test_decrement_attribute - topics(:first).increment(:replies_count).increment!(:replies_count) - assert_equal 3, topics(:first).replies_count - - topics(:first).decrement!(:replies_count) - assert_equal 2, topics(:first, :reload).replies_count + assert_equal 50, accounts(:signals37).credit_limit - topics(:first).decrement(:replies_count).decrement!(:replies_count) - assert_equal 0, topics(:first, :reload).replies_count + accounts(:signals37).decrement!(:credit_limit) + assert_equal 49, accounts(:signals37, :reload).credit_limit + + accounts(:signals37).decrement(:credit_limit).decrement!(:credit_limit) + assert_equal 47, accounts(:signals37, :reload).credit_limit end def test_toggle_attribute -- cgit v1.2.3