Question

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?

Was it helpful?

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.

OTHER TIPS

Have you considered adding a leader_id or main_leader_id to your projects table? I understand that your project can have more than one leader, but a potential problem with your implementation is this: suppose you create a project and it has at one person assigned who is a leader, so it's valid -- great. Later, that person is taken off the project (by changing the Person's project_id attribute). Unless you put a callback on Person, your Project isn't going to know that it no longer has a leader, and it will be in an invalid state. That could cause problems if you have other code that assumes that Project is valid and has at least one leader (i.e., my_project.leaders.first.do_something). If you have something like a main_leader_id, then you can simply validate against that in your Project model (with presence: true), and you can still use a has_many relationship if you need to get all of the leaders.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top