Search code examples
ruby-on-railsvalidationruby-on-rails-4rails-activerecordhas-many

rails new record validation with scopes


Let's assume I have Projects and People belonging to a Project. A Person can either be a leader or not and has a scope for this. A Project must have at least one leader person or else it is invalid. So I tried this:

class Project < ActiveRecord::Base
  has_many :people

  validate :has_a_leader

  def has_a_leader
    unless self.people.lead.size > 0
      puts 'Must have at least one leader'
      errors.add(:people, 'Must have at least one leader')
    end
  end
end

class Person < ActiveRecord::Base
  belongs_to :project
  scope :lead, -> { where(:is_lead => true) }
end

Unfortunately the validation only works with saved records, because the scope is always empty on new records:

p = Project.new
p.people.build(:is_lead => true)
=> #<Person ..., is_lead: true>
p.people
=> #<ActiveRecord::AssociationRelation [#<Person ..., is_lead: true>]>
p.people.lead
=> #<ActiveRecord::AssociationRelation []>
p.valid?
'Must have at least one leader'
=> false

Another try with another syntax:

p = Project.new
p.people.lead.build
=> #<Person ..., is_lead: true>
p.people.lead
=> #<ActiveRecord::AssociationRelation []>
p.people
=> #<ActiveRecord::AssociationRelation []> # <-- first syntax at least got something here
p.valid?
'Must have at least one leader'
=> false

So it looks like I have to rewrite the validation like this and use the first syntax when creating new projects:

  def has_a_leader
    unless self.people.find_all(&:is_lead).size > 0
      puts 'Must have at least one leader'
      errors.add(:people, 'Must have at least one leader')
    end
  end

But now I have two places where I have defined what a leader person is: in the validation method and in the scope lambda. I repeat myself. Works, but not the Rails way.

Is there a better way to do this?


Solution

  • You can solve your problem by adding another association:

    class Project < ActiveRecord::Base
      has_one :leader, -> { where(is_lead: true) }, class_name: 'Person'
      validates :leader, presence: true
    end
    

    When you create a Project you can set a lead pretty easily:

    def create
      project = Project.new(params[:project])
      project.leader.new(name: 'Corey') #=> uses the scope to set `is_lead` to `true`
    end
    

    You still have the lead scope duplicated in your Person model, but since that's already defined, let's just use it:

    class Project < ActiveRecord::Base
      has_one :leader, Person.method(:lead), class_name: 'Person'
    end
    

    This has the upside of making it a lot easier to grab the leader of a project, too.