2

Very new Rails 4 developer here. I've got a form where a user is creating Exercises. Exercises can have many Equipment, and Equipment can be optional( think push-up stands for doing push-ups ). I store this "optional" field on the join table exercise_equipment.

I cannot get the parameters to actually send through the values of the collection element that I pick. See below for the model, view, controller, and parameters.

Here are the attributes/relationships of my models:

# id        :integer
# name      :string
# is_public :boolean
Exercise
    has_many :exercise_equipment
    has_many :equipment, :through => :exercise_equipment
    accepts_nested_attributes_for :exercise_equipment


# id           :integer
# exercise_id  :integer
# equipment_id :integer
# optional     :boolean
ExerciseEquipment
    belongs_to :exercise
    belongs_to :equipment
    accepts_nested_attributes_for :equipment


# id   :integer
# name :string
Equipment
    has_many :exercise_equipment
    has_many :exercises, :through => :exercise_equipment

Here are some (maybe) relevant controller methods:

def new
  @exercise = Exercise.new
  @exercise.exercise_equipment.build
end

def create
  @exercise = Exercise.new( exercise_params )
  if @exercise.save
    redirect_to @exercises
  else
    render 'new'
  end
end

def edit
  @exercise = Exercise.find( params[:id] )
end

def update
  @exercise = Exercise.find( params[:id] )
  if @exercise.update_attributes( exercise_params )
    redirect_to @exercises
  else
    render 'edit'
  end
end

def exercise_params
  params.require( :exercise ).permit(
    :name,
    :is_public,
    exercise_equipment_attributes: [
      :id,
      :optional,
      equipment_attributes: [
        :id,
        :name
      ],
    ]
  )
end

This is my shot at creating a view to do what I want:

exercises/new.html.erb

<%= form_for @exercise do |f| %>
  <%= render 'form', f: f %>
  <%= f.submit "New Exercise" %>
<% end %>

exercises/_form.html.erb

<%= f.label :name %><br />
<%= f.text_field :name %>

<%= f.check_box :is_public %> Public

<%= f.fields_for( :exercise_equipment ) do |eef|
  <%= eef.fields_for( :equipment ) do |ef|
    ef.collection_select :id, Equipment.all, :id, :name %>
  <% end %>
  <%= eef.check_box :is_optional %> Optional
<% end %>

When I put all of this together and submit an update to an already-existing exercise, the values all go through the params hash, but aren't changed to the new values I've selected...

Parameters: {
  "utf8"=>"[checkbox]",
  "authenticity_token"=>"[token]",
  "exercise"=>{
    "name"=>"Test", 
    "is_public"=>"1", 
    "exercise_equipment_attributes"=>{
      "0"=>{
        "equipment_attributes"=>{
          "id"=>"1"
        }, 
        "optional"=>"1", 
        "id"=>"2" 
      } 
    } 
  },
  "commit"=>"Save Exercise",
  "id"=>"1" 
}

If you can help me out, I'd be super appreciative. Just let me know if you need any more information and I can provide it.

EDIT

Here is the state of the database before updating:

postgres@=>db=# select id, name, is_public from exercises;
 id | name | is_public 
----+------+-----------
  2 | Test | t
(1 row)

Time: 61.279 ms
postgres@=>db=# select id, exercise_id, equipment_id, optional from exercise_equipment;
 id | exercise_id | equipment_id | optional 
----+-------------+--------------+----------
  2 |           2 |            1 | t
(1 row)

Time: 58.819 ms
postgres@=>db=# select id, name from equipment where id = 1;
 id |    name     
----+-------------
  1 | Freeweights
(1 row)

I then go to the update route for that exercise, select a different equipment from the collection, and submit the form. I get the following Rails Console results:

Started PATCH "/exercises/system-test" for 127.0.0.1 at 2014-08-12 23:48:18 -0400
Processing by ExercisesController#update as HTML
  Parameters: {"utf8"=>"✓", "authenticity_token"=>"PsbbUPSCiIew2Fd22Swn+K4PmLjwNDCrDdwXf9YBcm8=", "exercise"=>{"name"=>"Test", "is_public"=>"1", "exercise_equipment_attributes"=>{"0"=>{"equipment_attributes"=>{"id"=>"1"}, "optional"=>"1", "id"=>"2"}}}, "commit"=>"Save Exercise", "id"=>"system-test"}
  Exercise Load (60.5ms)  SELECT  "exercises".* FROM "exercises"  WHERE "exercises"."slug" = 'system-test'  ORDER BY "exercises"."id" ASC LIMIT 1
   (57.3ms)  BEGIN
  ExerciseEquipment Load (76.2ms)  SELECT "exercise_equipment".* FROM "exercise_equipment"  WHERE "exercise_equipment"."exercise_id" = $1 AND "exercise_equipment"."id" IN (2)  [["exercise_id", 2]]
  Equipment Load (59.1ms)  SELECT  "equipment".* FROM "equipment"  WHERE "equipment"."id" = $1 LIMIT 1  [["id", 1]]
  User Load (60.3ms)  SELECT  "users".* FROM "users"  WHERE "users"."id" = $1 LIMIT 1  [["id", 10]]
  Exercise Exists (60.5ms)  SELECT  1 AS one FROM "exercises"  WHERE ("exercises"."name" = 'Test' AND "exercises"."id" != 2 AND "exercises"."user_id" = 10) LIMIT 1
   (64.8ms)  COMMIT
Redirected to http://localhost:3000/exercises/system-test
Completed 302 Found in 590ms (ActiveRecord: 580.0ms)


Started GET "/exercises/system-test" for 127.0.0.1 at 2014-08-12 23:48:19 -0400
Processing by ExercisesController#show as HTML
  Parameters: {"id"=>"system-test"}
  Exercise Load (64.1ms)  SELECT  "exercises".* FROM "exercises"  WHERE "exercises"."slug" = 'system-test'  ORDER BY "exercises"."id" ASC LIMIT 1
  Equipment Load (58.7ms)  SELECT "equipment".* FROM "equipment" INNER JOIN "exercise_equipment" ON "equipment"."id" = "exercise_equipment"."equipment_id" WHERE "exercise_equipment"."exercise_id" = $1  [["exercise_id", 2]]
  Rendered exercises/show.html.erb within layouts/application (122.7ms)
  User Load (60.1ms)  SELECT  "users".* FROM "users"  WHERE "users"."id" = 10  ORDER BY "users"."id" ASC LIMIT 1
  Rendered shared/_header.html.erb (61.9ms)
  Rendered shared/_alerts.html.erb (0.1ms)
Completed 200 OK in 264ms (Views: 21.3ms | ActiveRecord: 240.8ms)
s_dolan
  • 1,196
  • 1
  • 9
  • 21
  • Is that the only output from your web server? It should tell you what it's trying to do, i.e. SQL query, save, etc. – Beartech Aug 13 '14 at 03:40
  • I've included the full PATCH trace from my server logs in an edit, along with my initial database state. Thanks so much for taking a look! – s_dolan Aug 13 '14 at 03:50
  • In your nested form, its this |e_form|, but you call `ef.collection_select` - shouldn't it be `e_form.collection_select` ? – olive_tree Aug 13 '14 at 04:00
  • Hmmm. I do something similar in a repair-ticket/parts-used scenario. When I load a service ticket and change the parts used it does some `UPDATE` and `DELETE` statements to make the changes in the DB. I'm not seeing any here. I'm a little rusty, but I know with multiple checkboxes and updating you have to use some hidden fields in your form. – Beartech Aug 13 '14 at 04:00
  • @Beartech thanks, I suppose I'll have to look into hidden fields. – s_dolan Aug 13 '14 at 04:03
  • http://railscasts.com/episodes/17-habtm-checkboxes-revised – Beartech Aug 13 '14 at 04:06
  • @Beartech it's actually just the associations that don't update. The boolean fields update correctly. – s_dolan Aug 13 '14 at 04:07
  • ACK! Sorry, it's late. I misunderstood what you wrote. You want to generate a new relation by using the nested form, right? Triple nested form is going to get complex. I'm gonna delete my comments since they don't add to the discussion. – Beartech Aug 13 '14 at 04:29
  • 1
    Your form_for helper should have @exercise. Typo? – Tiago Farias Aug 13 '14 at 06:22
  • @TiagoFarias Just an error in translation, thanks! – s_dolan Aug 13 '14 at 11:24

1 Answers1

1

Firstly, you need to make sure you define your associations correctly.

Any has_many association should be defined with a plural name -

#app/models/exercise.rb
Class Exercise < ActiveRecord::Base
    has_many :exercise_equipments
    has_many :equipments, :through => :exercise_equipments

    accepts_nested_attributes_for :exercise_equipments
end

#app/models/exercise_equipment.rb
Class ExerciseEquipment < ActiveRecord::Base
   belongs_to :exercise
   belongs_to :equipment
end

#app/models/equipment.rb
Class Equipment < ActiveRecord::Base
   has_many :exercise_equipments
   has_many :exercises, through: :exercise_equipments
end

If you've already got it working, and are happy with what you've got, then I'd recommend keeping your current setup. However, you may wish to adopt the above for convention's sake

Edit I see from the deleted answer that Beartech investigated this, and turns out Rails treats Equipment / Equipments as the same. Will be worth ignoring the above, but I'll leave it for future reference


Params

I cannot get the parameters to actually send through the values of the collection element that I pick. See below for the model, view, controller, and parameters.

I think I get what you mean - you're looking to update the record, but it does not send through the updated parameters to your controller, hence preventing it from being updated.

Although I can't see any glaring problems, I would recommend the issue is that you're trying to populate the exercise_id of an Exercise object. You need to define it for the exercise_equipment object:

<%= f.fields_for :exercise_equipment do |eef| %>
   <%= eef.collection_select :equipment_id, Equipment.all, :id, :name %>
   <%= eef.check_box :is_optional %>
<% end %>

This will populate your exercise_equipment table as described here:

Time: 61.279 ms
postgres@=>db=# select id, exercise_id, equipment_id, optional from exercise_equipment;
 id | exercise_id | equipment_id | optional 
----+-------------+--------------+----------
  2 |           2 |            1 | t
(1 row)

Currently, you're populating the Equipment model with equipment_id - which won't work. Populating the model in that way will server to create a new record, not update the ones already created


Extra Field

I want to have a link to add an additional equipment field when it is clicked, similar to how Ryan Bates did it in this RailsCast, but the helper method he writes( see "Show Notes" tab if you're not subscribed to see the source ) seems to become substantially more complex when dealing with the nested views shown in my code below. Any help in dealing with this?

This a trickier mountain to overcome

Ryan uses quite an outdated method in this process (to pre-populate the link and then just let JS append the field). The "right" way is to build a new object & append the fields_for from ajax. Sounds tough? That's because it is :)

Here's how you do it:

#config/routes.rb
resources :exercises do
   collection do
       get :ajax_update #-> domain.com/exercises/ajax_update
   end
end

#app/models/exercise.rb
Class Exercise < ActiveRecord::Base
   def self.build
       exercise = self.new
       exercise.exercise_equipment.build
   end
end

#app/controllers/exercises_controller.rb
Class ExercisesController < ApplicationController
   def new
       @exercise = Exercise.build
   end

   def ajax_update
      @exercise = Exercise.build
      render "add_exercise", layout: false #> renders form with fields_for
   end
end

#app/views/exercises/add_exercise.html.erb
<%= form_for @exercise do |f| %>
   <%= render partial: "fields_for", locals: { form: f } %>
<% end %>

#app/views/exercises/_fields_for.html.erb
<%= f.fields_for :exercise_equipment, child_index: Time.now.to_i do |eef| %>
    <%= eef.collection_select :equipment_id, Equipment.all, :id, :name  %>
    <%= eef.check_box :is_optional %>
<% end %>

#app/views/exercises/edit.html.erb
<%= form_for @exercise do |f| %>
    <%= render partial: "fields_for", locals: { form: f } %>
    <%= link_to "Add Field", "#", id: "add_field" %>
<% end %>

#app/assets/javascripts/application.js
$(document).on("click", "#add_field", function() {
    $.ajax({
       url: "exercises/ajax_update",
       success: function(data) {
          el_to_add = $(data).html()
          $('#your_id').append(el_to_add)
       }
    });
 });
Richard Peck
  • 76,116
  • 9
  • 93
  • 147
  • Could you perhaps explain what's going on in the javascript that you wrote? Also, is there a way to move it out of the application.js file and into an exercise-specific coffescript file? – s_dolan Aug 13 '14 at 11:58