Rails ActiveRecord: Locking down attributes when record enters a particular state

匿名 (未验证) 提交于 2019-12-03 02:51:02

问题:

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 


标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!