Nested Attributes, Forms and Validations
I have a simple app where people can apply for a position. The table for this is applicants. Then, all users are allowed to rate each applicant. The ratings are in a table called ratings, which just has an applicant_id, user_id and score. So our tables are:
class Applicant < ActiveRecord::Base has_many :ratings accepts_nested_attributes_for :references, :allow_destroy => true class User < ActiveRecord::Base has_many :ratings class Rating < ActiveRecord::Base belongs_to :user belongs_to :applicant validates_uniqueness_of :applicant_id, :scope => :user_id
The validates_uniqueness_of line is there to make sure that each user can only give one rating for each applicant.
Users edit the applicant to add their score. Since the rating is a nested attribute, we use a fields_for line in the form. Originally, I had something that looked like this:
<%= f.fields_for :ratings, @my_rating do |builder| %> <%= builder.collection_select :score, Rating::SCORES, :to_s, :humanize %> <%= builder.hidden_field :user_id, :value => current_user.id %> <% end %>
And, in the controller, my edit method looked like this:
def edit @applicant = Applicant.find(params[:id]) @applicant.ratings.build end
This worked, in that the ratings field was updated as the form was updated. But there was a problem. When a user went to edit their score (or put in their first score), scores from all users for that applicant showed in the form. This was NOT the behavior I wanted. Users should see only their own rating, if they want to edit it. And if they haven’t yet rated that applicant, they should just see one spot for their rating.
I fixed things by changing the controller to this:
def edit @applicant = Applicant.find(params[:id]) @my_rating = Rating.where(:applicant_id => params[:id]).where(:user_id => current_user.id) if @my_rating.empty? @my_rating = @applicant.ratings.build end end
and the form to this:
<%= f.fields_for :ratings, @my_rating do |builder| %> <%= builder.collection_select :score, Rating::SCORES, :to_s, :humanize %> 1 is the highest ranking, 5 is for candidates to reject
<% if builder.object.user_id.nil?%> <%= builder.hidden_field :user_id, :value => current_user.id %> <% end %> <% end %>
This then works as expected.