Rails ActiveRecord: блокировка атрибутов, когда запись входит в определенное состояние

Удивительно, что если theres является плагином или лучший способ настроить класс ActiveRecord, чтобы, например, когда запись попала в "опубликованное" состояние, некоторые атрибуты были заморожены, так что их нельзя было подделать.

Ответ 1

Вы можете заморозить весь объект AR:: B, установив @readonly в true (в методе), но это заблокирует все атрибуты.

То, что я бы рекомендовал, - это определить методы установки атрибутов, которые проверяют текущее состояние перед переходом к супер:

class Post < ActiveRecord::Base
  def author=(author)
    super unless self.published?
  end

  def content=(content)
    super unless self.published?
  end
end

[EDIT] Или для большого количества атрибутов:

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

Который, конечно, я бы защищал бы вставку в плагин, чтобы делиться с другими, и добавить хороший DSL для доступа, например: disable_attributes :author, :content, :comments, :when => :published?

Ответ 2

Редактирование атрибутов, которые не следует редактировать, является ошибкой проверки:

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

Здесь используются расширения ActiveRecord:: Dirty, введенные в 2.2 или около того.

Ответ 3

Вы можете добавить настраиваемую проверку, чтобы блокировать изменения атрибутов, если вы находитесь в определенном состоянии. Вы можете жестко закодировать вещи непосредственно в валидацию. Но я предпочитаю немного более надежный подход, используя константы, определяющие белый список (список атрибутов, которые разрешено изменять в состоянии) или черный список (список атрибутов, которые не разрешено изменять в состоянии).

Вот пример обоих подходов. Каждый подход предполагает, что в вашей модели есть метод состояния, который возвращает текущее/новое состояние как строку.

Подход белого списка

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

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

Если конкретное состояние просто persisted?, то attr_readonly является наилучшим вариантом.

attr_readonly (*attributes) public

Атрибуты, перечисленные как readonly, будут использоваться для создания новой записи, но операции обновления будут игнорировать эти поля.

Чтобы проверить (любезно предоставлено 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