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
This commit is contained in:
Rick Olson
2007-09-30 07:09:44 +00:00
parent 30a652ad41
commit 66d05f5e2c
5 changed files with 85 additions and 17 deletions

View File

@@ -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]

View File

@@ -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

View File

@@ -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.

View File

@@ -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)

View File

@@ -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