Search code examples
ruby-on-railsvalidationactiverecordparent

ActiveRecord validation by parent attribute


I have model Article, for example. Article has_one ArticleContent. ArticleContent has validation of all it's attributes by default. But I need additional functionality - to save draft Article, without any validation. So I pass :draft => false as one of a parameter in Article.new(), next I do @article.build_article_content(). There is some not working code in ArticleContent:

  def draft?
    raise self.article.draft
  end

  validates_presence_of :header, :message => "We have no fuckin' header!", :unless => :draft?

Of course it's not work. At the moment of draft? execution there is no any suitable Article object anywhere, so self.article returns nil. Nice try, codemonkey...

Anyone have some sweet ideas? I think to make @content.save! is not a very good idea

UPDATE

I tried so:

def draft
    self[:draft]
end

def draft=(value)
    self[:draft] = value
end

def draft?
    self[:draft]
end

validates_presence_of :field1, :message => "msg1", :unless => :draft?
validates_presence_of :field2, :message => "msg2", :unless => :draft?
validates_presence_of :field3, :message => "msg3", :unless => :draft?

It works, but how can I group this?

unless self.draft?
    validates_presence_of :field1, :message => "msg1"
    validates_presence_of :field2, :message => "msg2"
    validates_presence_of :field3, :message => "msg3"
end

Says that draft? method is not found. Also i should do

@article.content.draft = @article.draft

And it looks like dirty-dirty hack too


Solution

  • This is a common use case for a state machine. There are several rails plugins that provide for those.

    http://ruby-toolbox.com/categories/state_machines.html

    If you don't need a full state machine implementation it could still be instructive to have a state column in your ArticleContent model. Its values would be "new", "draft", "published" and so on. Your validations would look at that column's value when deciding what to do, like:

    validates :content, :presence => true, :unless => Proc.new { |a| a.state == "Draft" }
    

    (I'm pretty sure that's not the correct syntax but you should get what I'm aiming at.)

    To answer your UPDATE

    Try with_options.

    with_options :unless => :draft? do |o|
        o.validates_presence_of :field1, :message => "msg1"
        o.validates_presence_of :field2, :message => "msg2"
        o.validates_presence_of :field3, :message => "msg3"
    end
    

    Looking at your code there's a couple of smells. In order to flunk a validation the thing to do is errors.add(blah), not raise an exception. Also, your methods defined for accessing the draft column look a little redundant. They're just doing what AR would do anyway.