0

I have researched similar questions however I don't feel link they have addressed my particular issue:
Rails form_for results in POST instead of PUT when trying to edit

form_for with nested resources

I'm a novice with Rails (using Rails 4.2.5) an am attempting my first application. My issue is two fold: (1) When a user goes to edit a user story the fields of the form do not populate with previously inputted data (2) When the form is resubmitted, a new entry is created, opposed to editing the old data.

I have a feeling that my form_for for user_stories/edit.html.erb is the issue. When I take out the .build method from the form I get the following error message:

undefined method `to_key' for #UserStory::ActiveRecord_Associations_CollectionProxy:0x007f456a759138>

The projects/_form.html.erb for my project's view does not have the .build method and functions correctly. However the only way I can get the `user_stories/_form.html.erb form to work is if I attach the build method.

Here is my code:

user_story.rb

class UserStory < ActiveRecord::Base
  belongs_to :project
  belongs_to :user

  include RankedModel
  ranks :row_order
end

project.rb

class Project < ActiveRecord::Base
  has_many :user_stories 
  belongs_to :user
end

routes.rb

Rails.application.routes.draw do
  devise_for :users
    resources :projects do
      resources :user_stories
    end
  end

  resources :user_stories do
    post :update_row_order, on: :collection
  end

  root 'welcome#index'
end

user_stories/_form.html.erb

<%= form_for([@project, @user_story.build]) do |f| %>
    <div class="form-group">
      <p>As a ...</p>
      <%= f.text_field :param1, placeholder: "type of user", class: "form-control"     %>
    </div>
    <div class="form-group">
      <p>I want ...</p>
      <%= f.text_field :param2, placeholder: "desired functionality", class: "form-control" %>
    </div>
    <div class="form-group">
      <p>so that...</p>
      <%= f.text_field :param3, placeholder: "reason for desired functionality", class: "form-control" %>
    </div>
    <div class="actions">
      <%= f.submit class: "btn btn-primary" %>
    </div>
<% end %>

user_stories_controller.rb

class UserStoriesController < ApplicationController

  before_action :set_project
  before_action :set_user_story, except: [:create]


  def index
    @user_story = @project.user_stories.rank(:row_order).all
  end

  def update_row_order
    @user_story.row_order_position = user_story_params[:row_order_position]
    @user_story.save

    render nothing:true # this is a POST action, updates sent via AJAX, no view   rendered

  end

  def create
    @user_story = @project.user_stories.create(user_story_params)
    redirect_to @project
  end

  def new
  end 

  def destroy
    if @user_story.destroy
      flash[:success] = "User story deleted"
    else
      flash[:error] = "User story could not be deletd"
    end
    redirect_to @project
  end

  def complete
    user_story.update_attribute(completed_at, Time.now)
    redirect_to @project, notice: "User story completed functionality complete"
  end

  def update
    respond_to do |format|
      if @project.user_stories.update(@project, user_story_params)
        format.html { redirect_to project_path(@project), notice: 'User story was successfully updated.' }
        format.json { render :show, status: :ok, location: @user_story }

      else
        format.html { render :edit }
        format.json { render json: @user_story.errors, status: :unprocessable_entity }
      end
    end
  end

  def edit
    @project = Project.find(params[:project_id])
    @user_story = @project.user_stories(params[:id])
  end

  def show
  end

private

  def set_project
    @project = Project.find(params[:project_id])
  end

  def set_user_story
    @user_story = @project.user_stories(params[:id])
  end

  def user_story_params
    params[:user_story].permit(:param1, :param2, :param3, :row_order_position)
  end
end
Community
  • 1
  • 1

1 Answers1

0

There are just a few changes needed (tweaks, really), and I'll go through them top-to-bottom.

1) before_action :set_user_story

This will use the param[:id] to find the proper @user_story model object and automatically make it available to the proper methods. In this case it's being excepted for :create, but should also exclude other methods that don't have an :id in the route. Use this instead:

before_action :set_user_story, except: [:index, :new, :create]

This will solve (or prevent) some annoying and persistent ActiveRecord failures.

2) The index action

In this method, the name of the variable is non-standard by Rails naming conventions. The variable is currently singular, but represents a list of UserAction model object, which typically uses a plural name. Use this, instead:

def index
  @user_stories = @project.user_stories.rank(:row_order).all
end

This change will cause a break in the app/views/user_stories/index.html.erb view, where any use of the @user_story variable would need to be changed to @user_stories. Keeping with naming conventions has many immediate and long-term benefits, so it's worth making the extra effort to change this to be consistent.

Note: the index action typically doesn't have a singular model object to work with, as this action is used to provide a list of the model objects.

3) The new action

The new action is used to create and initialize a new model object for editing. As the before_action :set_user_story is no longer being called for the new action, the @user_story model object has to be created here. This code will do that correctly:

def new
  @user_story = UserStory.new
  @user_story.project = @project
  # Set other important default values for display now
end

And at this point, you should be able to successfully create a new UserStory model object, ready to be edited by the user.

4) The edit action

As the before_action :set_user_story handler is already being called for the edit action, there's no need to query for @user_story from within the body of the edit action; that line can be removed:

def edit
  @project = Project.find(params[:project_id])
end

This will actually fix the original issue that was reported, as this form of find will (unfortunately for this situation) return multiple records, which means that you get a collection back, and not a single record. This is the actual cause of this error message:

undefined method `to_key' for #UserStory::ActiveRecord_Associations_CollectionProxy:0x007f456a759138>

Assigning the @user_story within the edit action overwrote the value that had previously been assigned from the before_action handler, and replaced it with an improper query result.

5) The complete action

The complete action is a custom member action, which means that it depends on the :id, just like many of the other actions. The code is almost correct, except that the user_story variable used within the body of the method is actually missing the @; this is originally retrieved by the before_action handler.

def complete
  @user_story.update_attribute(completed_at, Time.now)
  redirect_to @project, notice: "User story completed functionality complete"
end

It's likely that this method had not been called yet during testing, as the edit action was an upstream test that failed. This should work when you get to testing this method.

6) Teh codez

Changing those few details will finalize the UserStoriesController, which was in pretty great shape to begin with. Adding in those changes, this is the final controller code:

class UserStoriesController < ApplicationController

  before_action :set_project
  before_action :set_user_story, except: [:index, :new, :create]

  def index
    @user_stories = @project.user_stories.rank(:row_order).all
  end

  def update_row_order
    @user_story.row_order_position = user_story_params[:row_order_position]
    @user_story.save

    render nothing:true # this is a POST action, updates sent via AJAX, no view rendered
  end

  def create
    @user_story = @project.user_stories.create(user_story_params)
    redirect_to @project
  end

  def new
    @user_story = UserStory.new
    @user_story.project = @project
    # Set other important default values for display now
  end 

  def destroy
    if @user_story.destroy
      flash[:success] = "User story deleted"
    else
      flash[:error] = "User story could not be deleted"
    end
    redirect_to @project
  end

  def complete
    @user_story.update_attribute(completed_at, Time.now)
    redirect_to @project, notice: "User story completed functionality complete"
  end

  def update
    respond_to do |format|
      if @project.user_stories.update(@project, user_story_params)
        format.html { redirect_to project_path(@project), notice: 'User story was successfully updated.' }
        format.json { render :show, status: :ok, location: @user_story }
      else
        format.html { render :edit }
        format.json { render json: @user_story.errors, status: :unprocessable_entity }
      end
    end
  end

  def edit
    @project = Project.find(params[:project_id])
  end

  def show
  end

 private

  def set_project
    @project = Project.find(params[:project_id])
  end

  def set_user_story
    @user_story = @project.user_stories(params[:id])
  end

  def user_story_params
    params[:user_story].permit(:param1, :param2, :param3, :row_order_position)
  end
end
Michael Gaskill
  • 7,913
  • 10
  • 38
  • 43