Reputation: 5797
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?
Upvotes: 0
Views: 500
Reputation: 80041
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.
Upvotes: 2
Reputation: 898
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.
Upvotes: 0