Wondering if there’s a plugin or best way of setting up an ActiveRecord class so that, for example, when a record enter the "published" state, certain attributes are frozen so that they could not be tampered with.
问题:
回答1:
You can freeze an entire AR::B object by setting @readonly to true (in a method), but that will lock out all attributes.
The way I would recommend is by defining attribute setter methods that check for the current state before passing to super:
class Post < ActiveRecord::Base def author=(author) super unless self.published? end def content=(content) super unless self.published? end end
[EDIT] Or for a large amount of attributes:
class Post < ActiveRecord::Base %w(author content comments others).each do |method| class_eval <<-"end_eval", binding, __FILE__, __LINE__ def #{method}=(val) super unless self.published? end end_eval end end
Which of course I would advocate pulling into a plugin to share with others, and add a nice DSL for accessing like: disable_attributes :author, :content, :comments, :when => :published?
回答2:
Editing attributes which shouldn't be edited is a validation error:
class Post < ActiveRecord::Base validate :lock_down_attributes_when_published private def lock_down_attributes_when_published return unless published? message = "must not change when published" errors.add(:title, message) if title_changed? errors.add(:published_at, message) if published_at_changed? end end
This uses the ActiveRecord::Dirty extensions introduced in 2.2 or so.
回答3:
You could add a custom validation to block changes to attributes if you're in a certain state. You could hard code things directly into the validation. But I prefer the slightly more robust approach using constants defining a whitelist (list of attributes that are allowed to change in a state) or a blacklist (list of attributes not allowed to change in a state).
Here's an example of both approaches. Each approach assumes there is a state method in your model that returns the current/new state as a string.
White List Approach
WhiteListStateLockMap = { "state_1" => [ "first_attribute_allowed_to_change_in_state_1", "second_attribute_allowed_to_change_in_state_1", ... ], "state_2" => [ "first_attribute_allowed_to_change_in_state_2", "second_attribute_allowed_to_change_in_state_2", ... ], ... } validates :state_lock def state_lock # ensure that all changed elements are on the white list for this state. unless changed & WhiteListStateLockMap[state] == changed # add an error for each changed attribute absent from the white list for this state. (changed - WhiteListStateLockMap[state]).each do |attr| errors.add attr, "Locked while #{state}" end end end
Black List Approach
BlackListStateLockMap = { "state_1" => [ "first_attribute_not_allowed_to_change_in_state_1, "second_attribute_not_allowed_to_change_in_state_1, ... ], "state_2" => [ "first_attribute_not_allowed_to_change_in_state_2", "second_attribute_not_allowed_to_change_in_state_2", ... ], ... } validates :state_lock def state_lock # ensure that no changed attributes are on the black list for this state. unless (changed & BlackListStateLockMap[state]).empty? # add an error for all changed attributes on the black list for this state. (BlackListStateLockMap[state] & changed).each do |attr| errors.add attr, "Locked while #{state}" end end end
回答4:
If the particular state is merely persisted?
, then attr_readonly
is the best option.
attr_readonly
(*attributes)
public
Attributes listed as readonly will be used to create a new record but update operations will ignore these fields.
To test (courtesy of THAiSi):
class MyModel < ActiveRecord::Base attr_readonly :important_type_thingie end #RSpec describe MyModel do its('class.readonly_attributes') { should include "important_type_thingie" } it "should not update the thingie" do m = create :my_model, :important_type_thingie => 'foo' m.update_attributes :important_type_thingie => 'bar' m.reload.important_type_thingie.should eql 'foo' end end