Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to display a form for a subset of associated records, some of which don't exist yet?

I have Tasks and Users. When a user completes a task, I create a Completion which has a field for the user to indicate how long they spent. I need a form that shows all the tasks with their completion status and time_spent attribute. On submit, completions that existed should be updated and new ones should be created. I'd like to do this in Formtastic if possible but I'll be happy with a basic Rails 3 solution.

class Completion < ActiveRecord::Base
  belongs_to :task
  belongs_to :user

  # attribute time_spent
end

class User < ActiveRecord::Base
  has_many :completions
  has_many :tasks, :through => :completions
end    

class Task < ActiveRecord::Base
  belongs_to :milestone
  has_many :completions
  has_many :users, :through => :completions
end

An extra aspect is that I want to show just a certain set of tasks, such as those belonging to a Milestone. Should I have a form on the Milestone controller that posts to the Completions controller?

class Milestone < ActiveRecord::Base
  has_many :tasks
  has_many :completions, :through => :tasks
end

UPDATE I've looked for days now and I've found many dead ends. This Multiple objects in a Rails form is close, but it requires that all the linking objects already exist.

What sets this question apart is that some of the links don't exist yet and there is no single model for the links to be nested in. E.g. With Ryan Daigle's Nested Object Forms post) I've made this work in a form to edit all possible Completions for a user, but I need to edit a subset of possible Completions in one form. Do I need to make a redundant object MilestoneCompletions that has_many Completions and belongs_to User? Can an ActiveModel has_many?

like image 984
Turadg Avatar asked Feb 02 '11 15:02

Turadg


1 Answers

I finally solved this. One key is the collection argument to fields_for. The other is to generate the collection with a mix of existing and new records.

So in the view, something like:

<%= form_for @user do |f| %>
  <table>
    <tr><th>Completed</th><th>Time spent</th><th>Task</th></tr>

    <%= f.fields_for :completions, available_completions_for_milestone(@user, @milestone) do |cf| %>
      <tr>
        <td><%= cf.check_box :status, {disabled: cf.object.persisted?}, "done", "unreported" %></td>
        <td><%= cf.text_field :time_spent_text %></td>
        <td><%= cf.object.task.description %></td>
      </tr>
      <%= cf.hidden_field :task_id %>
    <% end -%>

With a helper method:

def available_completions_for_milestone(user, milestone)
  user_completions = user.completions.in_milestone(milestone)    
  available = []
  milestone.tasks.each do |t|
    c = user_completions.select{|c| c.task_id == t.id}.first
    if !c then # make it
      c = user.completions.build( task: t )
    end
    available << c
  end
  available
end

Notice in the view that completions already in the DB are checked and disabled so they can't be unchecked. The unchecked state gets the value "unreported" and the User model can filter out those records so they don't go in the DB:

has_many :completions
accepts_nested_attributes_for :completions, :reject_if => proc { |attrs| attrs['status'] == 'unreported' }

I also had to make completions_attributes attr_accessible on the User model. If you make task_ids accessible then update will delete completions that were left out of the PUT.

like image 98
Turadg Avatar answered Oct 14 '22 13:10

Turadg