resources_controller part II

February 14th, 2007

Update: check out resource s controller update

Update: point 3 below (broken in rails < edge) is now solved. (non-edge) Rails <= 1.2.2 users should also install this plugin.

(this is a followup to this entry)

There were some problems with resources_controller as released a couple of weeks ago, including:

  • No support for name_prefix, which crippled the url_helpers
  • Limited support for polymorphic resources
  • Broken in rails < Edge, due to the fact that scoped creates for associations are new.
  • Lack of test support to show me the above

The 1st, 2nd and 4th points have been thoroughly addressed in the latest release, and if you run the tests you'll find out if your version of rails is susceptible to the 3rd point. The solution to the 3rd point should be pretty trivial and will appear soon.

(Thanks to Igor Alexeiuk for pointing me in the right direction on the above, and for contributing some code for name_prefix stuff)

Here's an example of what you can do:

class TagsController < ApplicationController
  resources_controller_for :tags
  nested_in :taggable, :polymorphic => true, :load_enclosing => true
end

The above controller can be used to service all of your models that have tags, wherever they're located in the resource schema. The controller will load the enclosing resources (raising RecordNotFound if the relationships don't match) and the named routes will also work.

Take a look at the test app to see what the new syntax is like, and what the plugin is capable of here

9 Responses to “resources_controller part II”

  1. Jesse Grosjean Says:

    Thanks so much of this plugin, it's another piece in the puzzle helping me understand rails and rest. And now I've got a question. Lets say I have these to classes, the Forums model has a to_many to posts, and posts has a belongs_to to forum. In the controllers I've done this:

     class ForumsController < ApplicationController
                  resources_controller_for :forums
                
                class PostsController < ApplicationController
                  resources_controller_for :posts, :in => [:forum]

    The problem is that it doesn't seem like the forum_id column of the post is getting set when a new post is created. Before switching to resource_controller I had something like this in my create method for post

    def create
                    @post = Post.new(params[:post])
                    @forum.posts << @post # add post to forum, set post.forum_id as resut.
    ...

    So my question is what's the best way to ensure that new posts are added to there enclosing forum using your plugin. Thanks for any suggestions.

  2. Ian Says:

    Hi Jesse,

    You’re probably not on edge rails - if this is true, then you need to also add the plugin mention at the very top of this post (in the section marked ‘update’)

    The reason is that calls to new on associations are scoped in edge rails (at least that seems to be the reason). This means that a ‘new’ will automagically passed the association foreign_key. In rails less than edge you need to set this yourself (as in your example) or use ‘build’, which is what the plugin does.

    Let me know if it works for you,

    Cheers,

    Ian

  3. Dan Kubb Says:

    Hi Ian,

    Included is a small patch to your plugin resource controller that:

    • Fixes a small mistake in the flash message when the destroy action runs.

    • Removes the explicit usage of new and find in ResourceService and replaces them with the more generic method_missing method.

    The reason for the first change is obvious, but the reason for the second change isn’t.

    I have pagination methods that take an association object and use it to paginate through the records. I’d like to be able to just hand in the ResourceService object and have it work just like any other association object, which is why I use method_missing instead of just explicitly defining each of the methods it uses.

    Also I have one suggestion. In the module that defines all the base actions for the controler, it explicitly does one of the following at the top of each action:

    self.resources = find_resources  (index)
    self.resource  = find_resource   (show, edit, update, delete)
    self.resource  = new_resource    (new, create)
    

    When I override any of the supplied action methods, I have to remember to add those same lines of code to my actions. It would be DRYer if they were set up in before_filters, and in the rare case where I’d want to do something else, I could just use skipbeforefilter to turn off those filters, or override them by creating my own filters.

    So if there were 3 filters defined:

    def find_resources_filter
      self.resources = find_resources
    end
    
    def find_resource_filter
      self.resource = find_resource
    end
    
    def find_new_resource_filter
      self.resource = new_resource
    end
    

    And before_filters could set up like:

    before_filter :find_resources_filter,    :only => [ :index ]
    before_filter :find_resource_filter,     :only => [ :show, :edit, :update, :delete ]
    before_filter :find_new_resource_filter, :only => [ :new, :create ]
    

    This seems to cover the normal usage case that your plugin seeks to handle, while cutting out a bit of code in the overridden actions.

    What do you think?

    Thanks,

    Dan

  4. Ian Says:

    Hi Dan

    Thanks for the patch, and suggestions.

    I’ve applied your method_missing patch slightly differently. For performance reasons I’ve not replaced find and new, but simply added method_missing. This should do the trick for what you have in mind.

    I’ll think about the other stuff and get back to you (work time has finished for me today). What you propose seems like a good idea. I’ve been having similar thought wrt declaratively specifying the responds_to part of actions. The idea being that if you want to add, say, an atom feed to index, you’d do something like this:

    class MyController < SomeController
       respond_to :index do |format|
         format.atom { render_atom_feed(resources) }
       end
     end
     

    instead of rewriting your entire index method.

    Your idea and this idea seem to be of the same vein (splitting actions into smaller, overridable, units). And it also potentially applies to more than just resources_controller.

    Because of this, I think that it might be better in it’s own plugin.

    On a side note, you can achieve what you wanted right now with resources_controller by writing your own Actions module like so

    module DanKubbActions
       def self.included(base)
         base.class_eval do
          # the before filters you mention
         end
       end
    
       def index ...
     end
     

    and then

    class ForumsController < ActionController::Base
       resources_controller_for :forums, :actions_include => DanKubbActions
     end
     

    Finally, would you mind if I posted this conversation as comments on the blog, I think it might be interesting for others.

    Cheers, Ian

  5. Dan Kubb Says:

    On a side note, you can achieve what you wanted right now with resources_controller by writing your own Actions module like so

    Yup, I knew about this. I’d been using this since I don’t like the way the standard scaffolding works for create, update and destroy. Specifically I don’t like the idea of having to rely in cookies/sessions and then redirecting the user to another action to see the flash messages. Cookies used to store state that modifies the response is considered a REST anti-pattern. I try hard to avoid it when I can.

    If the same end-goal can be reached without cookies or redirects then I say why not.

    So, in my application.rb I do:

    session :off
     

    And then I use :actions_include to pull in the following module in my controllers:

    module Autopilot
       module Controller
         module Actions
           def create
             self.resource = new_resource
             resource.save!
             respond_to do |format|
               flash[:notice] = "#{resource_name.humanize} was successfully created."
               headers['Location'] = resource_url(resource)
               self.resources = find_resources
               format.html { render :action => :index, :status => :created }
               format.xml  { head :created }
             end
           end
    
           def update
             self.resource = find_resource
             resource.update_attributes!(params[resource_name])
             respond_to do |format|
               flash.now[:notice] = "#{resource_name.humanize} was successfully updated."
               format.html { render :action => :show }
               format.xml  { render :xml => resource.to_xml }
             end
           end
    
           def destroy
             self.resource = find_resource
             resource.destroy
             respond_to do |format|
               flash.now[:notice] = "#{resource_name.humanize} was successfully destroyed."
               self.resources = find_resources
               format.html { render :action => :index }
               format.xml  { head :ok }
             end
           end
         end
       end
     end
     

    I have a simple rescue_action method in application.rb that catches any of the normal AR exceptions like:

    ActiveRecord::StaleObjectError
     ActiveRecord::RecordInvalid
     ActiveRecord::RecordNotSaved
     

    .. and displays the correct status code (1409, 422 and 422 respectively.

    Finally, would you mind if I posted this conversation as comments on the blog, I think it might be interesting for others.

    Yeah, for sure.

    Thanks,

    Dan

  6. Jesse Grosjean Says:

    Ian,

    Thanks for your help. You were correct, I wasn’t on edge rails, and moving to edge rails solved the problem. Now I’ve got another quick question.

    Using my initial setup with Forums, Posts, Comments… Now that I’m on edge rails and using resources controller when I create a new comment it is assigned the correct postid. GOOD! But in my setup comments also have a userid, and I’d like to set that to the current user when a new comment is created.

    My original setup before moving to resources controller looked like this:

    def create
       @comment = Comment.new(params[:comment])
       @current_user.comments << @comment
       ...
     end
     

    Now that I’m using resources controller I’m trying to duplicate that same behavior like this:

    def new_resource
       returning resource_service.new(params[resource_name]) do |comment|
         @current_user.comments << comment
       end
     end
     

    But when I do that it seems to run a validation on the comment forum before the initial display. That means that when a user first creates a comment they will initially see lots of errors about missing attributes even though they haven’t yet had a chance to fill anything in yet. I’m sure this is a pretty basic rails thing, but so far I haven’t been able to figure it out. Could you point me in the right direction? Thanks.

  7. Ian Says:

    Hi Jesse,

    Glad you got the first problem sorted out. (Feel free to email me on ian dot w dot white at_ gmail _dot com by the way).

    As for your next problem, concat’ing the comment to @current_user.comments will try and save the comment, thus causing the validation errors you mention. Since the comment is under construction, perhaps you want to do this:

    def new_resource
      returning resource_service.new(params[resource_name]) do |comment|
        comment.user = @current_user
      end
    end
    

    Be sure that your Comment model contain the following:

      belongs_to :user
    

    Make sense? Does that work for you?

    Cheers, Ian

  8. Jesse Grosjean Says:

    Ian,

    Thanks yet again. That change did the trick.

    But I’m confused. Why is it that adding the comment to the current user’s comments collection forces a save, while assigning the comment’s user to the current user does not? Is this a ruby rails thing, or something to do with your plugin? If it’s a rails think where can I read more about the “hows” and “whys” of it?

  9. Ian Says:

    It’s a rails thing.

    But, like most rails things, it makes a lot of sense.

    When creating a comment, you’re specifying one of its attributes - that it has a certain user (comment.user = @current_user).

    When you’re adding a comment to the user’s comments, it needs to be the case that the comment exists (otherwise what comment are you adding?) That’s why rails tries to save the comment if it’s a new record. (In fact rails can delay the save, but only when the user you’re adding the comments to isn’t yet saved).

    Make sense?

Leave a Reply