undefined method `to_key' for #<Class:0x17a6408> -rails-3

13,798

From what I can see, your form_for should be something along the lines of

<%= form_for [@parent, @upload], :html => { :multipart => true } do |f| %>

as I'm assuming your upload object is nested within another object, similar to the following:

resources :posts do
  resources :uploads
end

What form_for does when passed an array like this is construct the relevant path based on the class of the given objects and whether they are new records.

In your case, you create a new upload object in the new action of your controller, so form_for will inspect the array, get the class and id of @parent, then get the class and id of @upload. However, because @upload has no id, it will POST to /parent_class/parent_id/upload instead of PUTting to parent_class/parent_id/upload/upload_id.

Let me know if that doesn't work and we'll figure it out further :)

-- EDIT - after comments --

This means that one of @parent or @upload is nil. To check, you can put the following in your view

<%= debug @parent %>

and the same for @upload and see which is nil. However, I'm guessing that @upload is nil, because of this line in your controller:

# UploadsController#new
@upload = @parent.uploads.new unless @uploads.blank?

specifically the unless @uploads.blank? part. Unless you initialize it in the ApplicationController, @uploads is always nil, which means @uploads.blank? will always be true, which in turn means @upload will never be initialized. Change the line to read

@upload = @parent.uploads.new

and the problem will hopefully be resolved. The same is true of the other methods where you have used unless @uploads.blank?.

On a semi-related note, in UploadsController#find_parent, you have this line

classes ||= []

because the variable is local to the find_parent method, you can be assured that it is not initialized, and should rather write classes = [].

Also, you have this line of code

return unless classes.blank?

right before the end of the method. Did you add that in so that you return from the method once @parent has been initialized? If so, that line should be inside the each block.

Further, since classes isn't used outside of the method, why define it at all? The code could read as follows and still have the same behaviour

def find_parent
  params.each do |name ,value|
    @parent = $1.pluralize.classify.constantize.find(value) if name =~ /(.*?)_id/
    return if @parent
  end
end

Amongst other things, you'll see that this does a few things:

  1. Avoids initializing a variable that is not needed.
  2. Inlines the if statement, which helps readability for single line conditionals
  3. Changes use of unless variable.blank to if variable. Unless your variable is a boolean, this accomplishes the same thing, but reduces the cognitive load, as the former is essentially a double negative which your brain has to parse.

-- EDIT - from email exchange about the issue --

You are correct - if @parent will return true if parent is initialized. As I mentioned on SO however, the exception to this is if @parent is initialized and set to false. Essentially what it means is that in Ruby, all values except nil and false are considered true. When an instance variable has not been initialized, it's default value is nil, which is why that line of code works. Does that make sense?

In terms of setting @parent in each action that renders form in the UsersController, which of these is the correct way to do this on the index action. I have tried all 3 but got errors

Remember that both @parent and @upload must be instances of ActiveRecord (AR) objects. In the first case, you set @parent to User.all, which is an array of AR objects, which will not work. Also, you try to call @parent.uploads before @parent is initialized, which will give a no method error. However, even if you were to swap the two lines around, you are calling @parent.uploads when parent is an array. Remember that the uploads method is defined on individual AR objects, and not on an array of them. Since all three of your implementations of index do similar things, the above caveats apply to all of them in various forms.

users_controller.rb

def index @upload = @parent.uploads @parent = @user = User.all end

  or

def index # @user = @parent.user.all @parent = @user = User.all end

  or

def index @parent = @upload = @parent.uploads @users = User.all
end

I'll quickly walk you through the changes I made. Before I start, I should explain that this

<%= render "partial_name", :variable1 => a_variable, :variable2 => another_variable %>

is equivalent to doing this

<%= render :partial => "partial_name", :locals => {:variable1 => a_variable, :variable2 => another_variable} %>

and is just a shorter (and somewhat cleaner) way of rendering. Likewise, in a controller, you can do

render "new"

instead of

render :action => "new"

You can read more about this at http://guides.rubyonrails.org/layouts_and_rendering.html Now on to the code.

#app/views/users/_form.html.erb
<%= render :partial => "uploads/uploadify" %>

<%= form_for [parent, upload], :html => { :multipart => true } do |f|  %>


 <div class="field">
    <%= f.label :document %><br />
    <%= f.file_field :document %>
  </div>

  <div class="actions">
    <%= f.submit "Upload"%>
  </div>
<%end%>

On the uploads form, you'll see that I changed @parent and @upload to parent and upload. This means you need to pass the variables in when you render the form instead of the form looking for instance variable set by the controller. You'll see that this allows us to do the following:

#app/views/users/index.html.erb
<h1>Users</h1>
<table>
  <% @users.each do |user| %>
    <tr>
      <td><%= link_to user.email %></td>
      <td><%= render 'uploads/form', :parent => user, :upload => user.uploads.new %></td>
    </tr>
  <% end %>
</table>

Add an upload form for each user in UsersController#index. You'll notice that because we now explicitly pass in parent and upload, we can have multiple upload forms on the same page. This is a much cleaner and more extensible approach to embedding partials, as it becomes immediately obvious what parent and upload are being set to. With the instance variable approach, people unfamiliar with the code base might struggle to determine where @parent and @upload are being set, etc.

#app/views/users/show.html.erb
<div>
  <% @user.email %>
  <h3 id="photos_count"><%= pluralize(@user.uploads.size, "Photo")%></h3>
  <div id="uploads">
    <%= image_tag @user.upload.document.url(:small)%>
    <em>on <%= @user.upload.created_at.strftime('%b %d, %Y at %H:%M') %></em>
  </div>

  <h3>Upload a Photo</h3>
  <%= render "upload/form", :parent => @user, :upload => user.uploads.new %>
</div>

This is similar to the changes above, where we pass in the parent and upload objects.

 #config/routes.rb
 Uploader::Application.routes.draw do
  resources :users do
    resources :uploads
  end

  devise_for :users

  resources :posts do
    resources :uploads
  end

  root :to => 'users#index'
end

You'll see that I removed uploads as a top level resources in the routes. This is because uploads requires a parent of some sort, and so cannot be top level.

#app/views/uploads/new.html.erb
<%= render 'form', :parent => @parent, :upload => @upload %>

I made the same changes as above, passing parent and upload through explicitly. You'll obviously need to do this wherever you render the form.

#app/controllers/users_controller.rb
class UsersController < ApplicationController
 respond_to :html, :js

  def index
    @users =  User.all
  end

  def show
    @user = User.find(params[:id])
  end

  def new
    @user = User.new
  end

  def create
    @user = User.new(params[:user])
    if @user.save
      redirect_to users_path
    else
      render :action => 'new'
    end
  end

  def update
    @user = User.find_by_id(params[:id])
    @user.update_attributes(params[:user])
    respond_with(@user)
  end

  def destroy
    @user = User.find_by_id(params[:id])
    @user.destroy
    respond_with(@user)
  end
end

I've removed any mention of @parent from the user controller, as we pass it through explicitly.

Hopefully that all makes sense. You can extrapolate from these examples and pass through the parent and upload object wherever you want to render an upload form.

Share:
13,798
brg
Author by

brg

Updated on June 05, 2022

Comments

  • brg
    brg almost 2 years

    I am experiencing problems with undefined method `to_key' for, on a form for polymorphic upload.

    This is the form partial:

    <%= form_for [@parent, Upload], :html => { :multipart => true } do |f|  %>
    
      <div class="field">
        <%= f.label :document %><br />
        <%= f.file_field :document %>
      </div>
    
      <div class="actions">
        <%= f.submit "Upload"%>
      </div>
    <% end %>
    

    This is the controller:

    class UploadsController < ApplicationController
      before_filter :find_parent
    
      respond_to :html, :js
    
      def index
        @uploads = @parent.uploads.all unless @uploads.blank?
        respond_with([@parent, @uploads])
      end
    
      def new
        @upload = @parent.uploads.new unless @uploads.blank?
      end
    
      def show
        @upload = @parent.upload.find(params[:upload_id])
      end
    
      def create
        # Associate the correct MIME type for the file since Flash will change it
        if  params[:Filedata]
          @upload.document = params[:Filedata]
          @upload.content_type = MIME::Types.type_for(@upload.original_filename).to_s
          @upload = @parent.uploads.build(params[:upload])
          if @upload.save
            flash[:notice] = "suceessfully saved upload"
            redirect_to [@parent, :uploads]
          else
            render :action => 'new'
          end
        end
      end
    
      def edit
        @upload = Upload.where(params[:id])
      end
      private
    
    
      def find_parent
        classes ||= []
        params.each do |name ,value|
          if name =~ /(.*?)_id/
            @parent =  classes << $1.pluralize.classify.constantize.find(value)
          end
        end
        return unless classes.blank?
      end
    end
    

    If i change

    <%= form_for [@parent, Upload], :html => { :multipart => true } do |f| %>
    

    to

    <%= form_for [parent, Upload], :html => { :multipart => true } do |f| %>
    

    I get a new error: undefined local variable or method `parent' for #<#:0x21a30e0>

    This is the error trace:

    ActionView::Template::Error (undefined method `to_key' for #<Class:0x2205e88>):
    1: <%= render :partial => "uploads/uploadify" %>
    2: 
    3: <%= form_for [@parent, Upload], :html => { :multipart => true } do |f|  %>
    4: 
    5: 
    6:  <div class="field">
    

    The "uploads/uploadify" partial is in this gist: https://gist.github.com/911635

    Any pointers will be helpful. Thanks