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?
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.