あるテーブルのカラムがシステムのみによって編集される値で、ユーザから編集させたくない場合、そのvalidation が欲しいと思った。
例えば、はてなブックマークのエントリーのタイトルは、ユーザは編集できず、ページのtitle タグの内容で固定したいみたいな場合。
そんなん、attr_protected とか使えよって話なのですが、CSV を使っての編集みたいな場合、データをCSV でDL して、編集してUP というパターンがあると思います。その際に、編集不可能項目を編集しようとする人が1人はいる訳です。
注意書き書いたとしても、見落とす。で、「エラーでだせよヾ(`д´)ノシ」って怒られる訳なんです。はい。
そんな時にこのテクニックの出番。
ぱっと思いついたけど、実際にできなかった方法
- 単純にvalidate メソッドを使っての実装はできない
これは、元々の値を参照できないからです。
def validate # self.title とtitle は常に同じ値。self.title はDB の内容を参照できるということはない errors.add(:title, 'は編集できません') if title != self.title end
- errros.add + アクセサのオーバーライドでもできない
def title=(val) if !title.blank? && title != val errors.add(:title, 'は編集できません') else write_attribute(:title, val) end end
これは、valiadte系 メソッド以外の場所でのerros.add は無効なため。
def valid? errors.clear # 他の場所でerros.add してもここで消される run_callbacks(:validate) validate if new_record? run_callbacks(:validate_on_create) validate_on_create else run_callbacks(:validate_on_update) validate_on_update end errors.empty? end
rails 2.1 で実装されたDirty Trackingを使えばできる
Dirty Tracking の機能は、特定のカラムの値が変更されたかを調査できるメソッドです。
@entry.title #=> 'ほげほげ についてそろそろ言っておくか' @entry.title_changed? #=> false @entry.title = 'ふがふが についてそろそろ言っておくか' @entry.title_changed? #=> true
これつかって、validate_on_update に編集禁止の旨を書けば、要求された機能を実装できます。
class Entry < ActiveRecord::Base def validate_on_update errors.add(:title, 'は編集できません') if title_changed? end end
Rails のバージョンが2.1 未満の場合
今回、僕の場合も開発に使っているRails は1.0でしたので、上の方法がデフォルトでは使えません。この場合、changed? メソッドを2.1 からport します。
ちょっと変更して、編集を禁止するカラムを指定するようにしました。
(1.0 なのでalias_method_chain も使えない・・・)
- dirty_tracking.rb
# Partial porting from Rails 2.1.0 active_record/dirty.rb module DirtyTracking def self.included(c) c.extend ClassMethods end module ClassMethods def dirty_tracking(*attrs) attrs.each do |attr| class_eval <<-DEFINE def #{attr}_changed? attribute_changed?("#{attr}") end def #{attr}=(val) if !changed_attributes.include?("#{attr}") old = read_attribute("#{attr}") changed_attributes["#{attr}"] = val if old != val end write_attribute("#{attr}", val) end DEFINE end class_eval <<-DEFINE def save_with_dirty_tracking(*args) if status = save_without_dirty_tracking(*args) changed_attributes.clear end status end def reload_with_dirty_tracking(*args) record = reload_without_dirty_tracking(*args) changed_attributes.clear record end def changed_attributes @changed_attributes ||= {} end def attribute_changed?(attr) changed_attributes.include?(attr) end alias save_without_dirty_tracking save alias save save_with_dirty_tracking alias reload_without_dirty_tracking reload alias reload reload_with_dirty_tracking DEFINE end end end
使い方
class Entry < ActiveRecord::Base include DirtyTracking dirty_tracking :title def validate_on_update errors.add(:title, 'は編集できません') if title_changed? end end
こういう要求はあまりないのかもしれませんが、2.1 の機構の中身にも興味があったので実装してみました。