Skip to content
Browse files

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
  • Loading branch information...
1 parent 30a652a commit 66d05f5e2c7ac6b18220956fbcf34efcd32638fc @technoweenie technoweenie committed Sep 30, 2007
View
11 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]
View
6 activerecord/lib/active_record/associations.rb
@@ -841,7 +841,11 @@ def belongs_to(association_id, options = {})
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
25 activerecord/lib/active_record/base.rb
@@ -636,6 +636,15 @@ def accessible_attributes # :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 @@ def create_or_update
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 @@ def remove_attributes_protected_from_mass_assignment(attributes)
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 @@ def attributes_protected_by_default
# 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
18 activerecord/test/associations_test.rb
@@ -1175,6 +1175,24 @@ def test_belongs_to_counter_after_update_attributes
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
42 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 @@ def test_mass_assignment_protection_inheritance
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 @@ def test_class_level_delete
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 @@ def test_increment_nil_attribute
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

0 comments on commit 66d05f5

Please sign in to comment.
Something went wrong with that request. Please try again.