Sunday, July 31, 2011

More Routing Issues in Rails

I've made some progress with Manticore this weekend, but I've hit another wall. Surprise!

Thanks to @unixmonkey and @jqr, I can now add, view and destroy items, linking to the items index page from a menu partial that is called from the character's show page. Success! However, I thought it might be handy to be able to edit items. And that's where my problem comes in. For a while, I was getting a routing error. Then I edited the code and now I'm getting this old chestnut again:

undefined method `item' for #<Character:0x103f0ce50>

What gives? I thought I'd gotten rid of this guy when I defined the character in the items controller.

Here's my edit method from the items controller:

  def edit
   @character = Character.find(params[:character_id])
   @item = @character.item
  end

  def update
    @character = Character.find(params[:character_id])
    @item = @character.statistic
    if @item.update_attributes(params[:item])
      redirect_to character_items_path, :notice => 'Item was successfully updated.'
    else
      render :action => "edit"
    end
  end

And here's how I'm calling it from the partial I created for creating and editing items:

<%= link_to 'Edit', edit_character_item_path(@character, item) %>

Any more tips? I'm pretty sure I'm just not writing this correctly. This code is based on what I've got for my other models (statistics, armor class, etc) but I wonder if it might be different based on a couple of things. For one thing, those other models have a has_one relationship with Character, while Items has a has_many. For another, the has_one models are all displayed in the Character show page, while Items is being created from the Items index page. Am I on the right track here? Any advice as to how to solve this issue?

On a less begging for help note, I've spent a good 5 hours working on Manticore this weekend. I notice when I'm working on a personal project and not a tutorial, I can focus for longer periods of time and I find the work more interesting. When doing a tutorial, I feel like it's a valuable use of my time but it's not an interesting use of my time. Does that make sense? Regardless, I absolutely lose track of time when working on my own projects, even if it's just repeatedly butting my head up against trying to get a goddamn edit method to work.

12 comments:

  1. Looking through the project on GitHub, I can see that you have this in character.rb:

    has_many :items, :dependent => :destroy

    Which means you're defining the plural association, not singular. You need to tell Rails which one of the items you want to edit, probably like so:

    @character.items.find(X)


    Or change it to a has_one association.

    ReplyDelete
  2. Also, this has nothing to do with routing :)

    ReplyDelete
  3. Whoops, I don't know why I titled it that way. I think my previous problem was spilling over into this one.

    A character can have any number of different items, which is why I made it a has_many association. I'm modeling this after the basic blog tutorial, substituting character for post and item for comments. Then once I get this hammered out, I can use that same method to create other models that belong_to character.

    So I'm still having trouble understanding what you mean. When you say:

    @character.items.find(X)

    What should X be? Here's what I've got now:

    link_to 'Edit', edit_character_item_path(@character.items.find(item))

    but then I get Rails complaining that no route matches.

    Switching that code to this:

    link_to 'Edit', edit_character_item_path(@character.items.find(@item))

    Gives me another, more interesting error:

    Couldn't find item without an ID

    So I'm guessing this is closer to what I need, but I'm not declaring the id? Switching @item for :character_id, :item_id and :id don't work either.

    @_@

    ReplyDelete
  4. Because this is a nested resource you need to pass in both the character and item in your url helpers, I see you are only passing in one item and it is expecting two. Try this:

    link_to 'Edit', edit_character_item_path(@character, @item)

    The "x" is the param you are finding the item with. Since this is the items_controller, params[:id] should be the item id sent when requesting to edit. It is also a good idea to scope it to find off of the character, so someone can't go messing with the item of a different character:

    def edit
    @character = Character.find(params[:character_id])
    @item = @character.items.find(params[:id])
    end

    ReplyDelete
  5. @David I'm still getting an error with this updated code. I'm not sure if I've got something wrong in the items controller or what, but now I get this:

    No route matches {:controller=>"items", :action=>"edit", :id=>nil, :character_id=>#}

    And it only happens when I include a link_to the edit action. Any ideas? Here's a link to the items controller code:

    https://github.com/illbzo1/Manticore/blob/master/app/controllers/items_controller.rb

    ReplyDelete
  6. Line 13 of app/views/items/_items.html.erb should have:

    link_to 'Edit', edit_character_item_path(@character, item)

    (no @symbol on item because you are looping over character items and item is the loop variable).

    ReplyDelete
  7. Awesome, this works! I'm still having an issue, though. While the link_to 'Edit' works, it simply brings up the form for creating a new item. This is my current form:

    <%= form_for ([@character, @character.items.build]) do |f| %>
    ...
    <% end %>

    and I thought it might have something to do with @character.items.build, but when I change it to this:

    form_for ([@character, @item])

    I get this error:

    undefined method `model_name' for NilClass:Class

    Here's the edit method from items_controller:

    def edit
    @character = Character.find(params[:character_id])
    @item = @character.items.find(params[:id])
    end

    Also, do I need to define @item in characters_controller? I have @initiative and others defined under the show method, but I'm assuming that's only because I'm listing them in the characters show page, so defining item there won't be necessary.

    ReplyDelete
  8. I see. You've coded yourself into a bit of a corner, but you can still get out; but you're still going to leave to footsteps in the paint.

    So, going to /characters/1/items blows up "undefined method 'model_name' for nil":
    That's because app/views/items/index.html.erb loads the _form partial, and the _form partial is doing form_for([@character, @item]).

    You never defined @item in items_controller#index, so lets do that:

    def index
    @character = Character.find(params[:character_id])
    @item = @character.items.build
    end

    Refresh that, and now it blows up with "no route matches blah blah..",
    that's because app/views/items/_items.html.erb is doing this:

    @character.items.each do |item|
    ...
    link_to 'Edit', edit_character_item_path(@character, item)

    because we did @character.items.build in the controller, now we have an extra item with no attributes, and cannot be linked to, so the link_to fails.

    You can get around this by skipping that one with something like this on line 4 of app/views/items/_items.html.erb

    @character.items.reject{|item| item.new_record? }.each do |item|

    This omits the "built" item from the list you are looping over, but still allowing it to exist for use in the form.

    There are a lot of cleaner ways you could tackle this, like passing locals into your form partial, since you are using it on (at least) index/new/edit and it has to be set up just so whenever it is on the page.

    ReplyDelete
  9. Awesome! Finally, a working edit method! Even if it IS a bit messy. Messy code is what refactoring is for, right?

    I did run into another problem with the update method for items.

    I got this error:

    undefined method `item' for #

    Taking a look at the update method, I saw that I had this:

    @character = Character.find(params[:character_id])
    @item = @character.item

    So it was pretty clear what was throwing the error. I simply needed to change it to:

    @item = @character.items.find(params[:id])

    And bingo! Thanks for all your help on this problem. Now that I get how it works, I can extrapolate this method and flesh this application out a bit more.

    ReplyDelete
  10. One weird thing though: now the form isn't saving item.type, either when creating or editing an item. Any idea why a single field wouldn't be saving? The others are working correctly.

    ReplyDelete
  11. .type is a method already defined by Rails (for use with single table inheritance) so using it as a column name is a bad choice. I typically name my type columns 'somthing_type' or 'kind'.

    Either rename the column, or if you want to get hacky you can work around it by explicitly setting it like this
    @foo[:type] = 'awesome'
    @foo.save

    But you'll have to do this everywhere, so its advisable to create a migration to rename_column :table_name, :type, :thing_type
    then go back and change your forms and partials.

    ReplyDelete
  12. I figured type was a reserved term, but I didn't see it when I looked for a list of them. Changing it won't be a big deal, and I think writing a migration is a better solution than hacking around it. Thanks for the heads up!

    ReplyDelete