22

I have a projects resource that has many tasks. I want to ensure that every task has a project_id by adding validates_presence_of :project_id to the tasks model.

However, when creating a new project with tasks, the project_id won't be available until the record saves, therefore I can't use validates_presence_of :project_id.

So my question is, how do I validate presence of project_id in the task model? I want to ensure every task has a parent.

...

class Project < ActiveRecord::Base

  has_many :tasks, :dependent => :destroy
  accepts_nested_attributes_for :tasks, :allow_destroy => true

...

class Task < ActiveRecord::Base

 belongs_to :project
 validates_presence_of :project_id
deb
  • 11,136
  • 19
  • 62
  • 83
  • This question isn't making much sense to me. You want a task to belong to a project without having a project to begin with... how is it possible to get an id for something that doesn't exist? – porkeypop Jun 08 '10 at 05:19
  • 3
    Are you creating tasks through a nested form when you create the project? – Steve Weet Jun 08 '10 at 21:45

7 Answers7

18

Your code works:

  • If you validates_presence_of :project, then as long as the project is there, it will validate. But if your project is unsaved, you could still save the task.
  • If you validates_presence_of :project_id, then the integer must be there, indicating a saved value.

Here's rSpec that proves the point. If you validate :project_id, you can't save a task without saving the Project.

class Task < ActiveRecord::Base
  belongs_to :project
end

/specs/model_specs/task_spec.rb

require File.dirname(__FILE__) + '/../spec_helper'

describe Task do

  before(:each) do 
    @project = Project.new
  end

  it "should require a project_id, not just a project object" do
    task = Task.new
    task.project = @project
    Task.instance_eval("validates_presence_of :project_id")
    task.valid?.should == false
  end

  it "should not be valid without a project" do
    task = Task.new
    task.project = @project
    Task.instance_eval("validates_presence_of :project")
    task.valid?.should == false
    task.save.should == false
  end

end
Jesse Wolgamott
  • 39,811
  • 4
  • 78
  • 105
15

See here for the definitive answer :

class Project < ActiveRecord::Base

  has_many :tasks, :dependent => :destroy, :inverse_of => :project
  accepts_nested_attributes_for :tasks, :allow_destroy => true

class Task < ActiveRecord::Base

 belongs_to :project
 validates_presence_of :project

Not so elegant if you ask me... It should transparently validate.

gamov
  • 3,540
  • 1
  • 28
  • 26
3

Maybe I don't understand something, but it looks like you are trying to cheat rails. Why don't you just do like this:

class Task < ActiveRecord::Base
  belongs_to :project
  validate_presence_of :project
end
bsboris
  • 639
  • 5
  • 8
  • This is a super old question: But, the reason you can't do a simple association validation is because the task is in :fields_for...they are nested in the :project form. On save, the project_id will not be available yet for validation and the validation will fail. You can test this by simply removing the validation for :project. The :inverse_of ensures the association is in tact at save. https://viget.com/extend/exploring-the-inverse-of-option-on-rails-model-associations – hellion Jan 22 '16 at 15:37
2

Take a look at this:

https://rails.lighthouseapp.com/projects/8994/tickets/2815-nested-models-build-should-directly-assign-the-parent

One thing I have done in the past is add: validates_presence_of :parent_id, :on => :update. Not great but it helps tighten the net a little.

tsdbrown
  • 4,918
  • 2
  • 33
  • 39
  • This completely neglects the create action. Not good. Use :inverse_of to ensure your parent object is associated at save in nested fields. https://viget.com/extend/exploring-the-inverse-of-option-on-rails-model-associations – hellion Jan 22 '16 at 15:38
2

I think you're having the same issue I dealt with. I have two models, Account and User, and when the account is created the first user is created through a @account.users.build. The User model has a validates_presence_of :account validation.

To make the first user pass validation, I added the following code to my Account model:

  before_validation_on_create :initialize_users

  def initialize_users
    users.each { |u| u.account = self }
  end
1

In reality you need both:

validates_presence_of project
validates_presence_of project_id

That way the task will not be saved in either of the following cases assuming that you have only 2 valid projects in the database, i.e. project id 99 is invalid:

task.project_id = 99
task.save

task.project = Project.new
task.save

I hope this is of help to someone.

Tabrez
  • 3,072
  • 3
  • 23
  • 32
0

Your Project class must define

accepts_nested_attributes_for :tasks

See Nested Model Form on Railscasts for more details on how to make the form.


EDIT:

In your form you should have something like this:

_form.html.erb

<% form_for @project do |f| %> 
    # project fields...
    <% f.fields_for :tasks do |builder| %>
        <%= render 'task_fields', :f => builder %>
    <% end %>
    <p><%= link_to_add_fields "Add task", f, :tasks %></p>
    <%= f.submit %>
<% end %>

_task_fields.html.erb

<%= f.label :name, "Task name:" %>
<%= f.text_field :name %>
# task fields...
<%= link_to_remove_fields "Delete task", f, :tasks %>

link_to_add_fields and link_to_remove_fields are methods defined in application_helper to add/delete fields dynamically.

True Soft
  • 8,246
  • 6
  • 48
  • 77