Update: See the comments for an explanation of why I'm not doing validates_presence_of :project_id (instead of validates_presence_of :project). I'm prepared to be proved wrong on my assertions there, but they seem to hold up to my testing.
A common Rails pattern is to have two models with a has_many/belongs_to association, e.g.
class Task < ActiveRecord::Base belongs_to :project validates_presence_of :project validates_associated :project end class Project < ActiveRecord::Base has_many :tasks validates_presence_of :name end
Then in the form for creating/editing a task, you might provide a drop-down box to select the associated project in the view, and display project error messages underneath it:
<p>Project: <%= collection_select :task, :project_id, Project.find(:all), :id, :name %></p> <%= error_message_on :task, :project %>
Now if you try to save a task, you'll get the appropriate error message if you fail to select a project; also, if an invalid project gets assigned (e.g. invalid URL or incomplete Project instance), you'll get a validation error. I think this in uncontroversial.
BUT
If an error occurs on the project associated with the task, although you get an error message in the view, the drop-down box isn't highlighted (as it is with other fields). This is because the highlighting is done by looking at the errors object associated with the task, finding errors for any fields in the form with the same name, then altering the HTML for that field. So if there is an error for description and there's a field called task[description], that field gets an extra HTML <div> wrapped round it, e.g.
<div class="fieldWithErrors"> <input id="task_description" name="task[description]" size="30" type="text" value="" /> </div>
The issue with my task and project is that the validation errors are added for the projectattribute on the Task instance; but there isn't an HTML form field called task[project]: the drop-down is actually called task[project_id]. I tried to create a select element called task[project] instead, but this doesn't work (ActiveRecord complains that it is expecting a Project and you supplied a String).
My solution is to copy any project error for the Task to an error for project_id. I did this in the model, in the after_validation callback, like this:
class Task < ActiveRecord::Base
belongs_to :project
validates_presence_of :project
validates_associate :project
def after_validation
project_error = errors.on(:project)
errors.add(:project_id, project_error) if project_error
end
end
This works fine: now any errors for the project are reflected onto project_id, so the task[project_id] drop-down box gets the error <div> (with class attribute set to "fieldWithErrors") wrapped around it. However, this doesn't feel quite right, and it makes me think I'm missing something. Is there some Rails voodoo I've overlooked which can be used to do this more cleanly?
Comments
Still no better solution?
I'm stuck with the same issue here. Is there still no better way to achieve this?
Thanks
This seems to do the trick, has this not been fixed yet in Rails 2? A google search about this problem yields very little information, i thought it would be a very common problem. Thats for the info.
Use project_id
Why don't you use directly:
?
Thanks for the suggestion. I
Thanks for the suggestion. I think I used to do this. Presumably you're suggesting:
The issue here is if you create a new project (without saving it), assign the project to the task, then try to save the task. Even if the task is valid, and the project is valid, the project doesn't have an ID yet; so project_id isn't set on the task; so the task won't save. If you do validates_presence_of :project instead, you can create a new project then assign it to a new task without having to save the project; then save the whole lot together; the ID of the project automatically gets assigned to the task's project_id.
It doesn't make much difference when you're working through forms, but can make a difference if you're working from a script or the command line; or if you want to add a new project and associated task simultaneously from one form, but don't want to have to save the project before you can save the task. It makes validation simpler in the long run, as you can leave it to the validates_* methods, rather than having to save the project, check it's valid, then save the task only if it's valid; and if the task isn't valid, and you shouldn't have saved it, roll back the project save. (This happened to me when working on a system which added one record to each of two tables from a single form, which is where I started noticing this behaviour.)