Module: ResourcesController

Defined in:
lib/resources_controller.rb,
lib/resources_controller/helper.rb,
lib/resources_controller/actions.rb,
lib/resources_controller/railtie.rb,
lib/resources_controller/version.rb,
lib/resources_controller/specification.rb,
lib/resources_controller/include_actions.rb,
lib/resources_controller/resource_methods.rb,
lib/resources_controller/singleton_actions.rb,
lib/resources_controller/named_route_helper.rb,
lib/resources_controller/active_record/saved.rb,
lib/resources_controller/request_path_introspection.rb

Overview

With resources_controller you can quickly add an ActiveResource compliant controller for your your RESTful models.

Examples

Here are some examples - for more on how to use RC go to the Usage section at the bottom, for syntax head to resources_controller_for

Example 1: Super simple usage

Here's a simple example of how it works with a Forums has many Posts model:

class ForumsController < ApplicationController
  resources_controller_for :forums
end

Your controller will get the standard CRUD actions, @forum will be set in member actions, @forums in index.

Example 2: Specifying enclosing resources

class PostsController < ApplicationController
  resources_controller_for :posts, :in => :forum
end

As above, but the controller will load @forum on every action, and use @forum to find and create @posts

Wildcard enclosing resources

All of the above examples will work for any routes that match what it specified

            PATH                     RESOURCES CONTROLLER WILL DO:

Example 1  /forums                   @forums = Forum.find(:all)

           /users/2/forums           @user = User.find(2)
                                     @forums = @user.forums.find(:all)

Example 2  /posts                    This won't work as the controller specified
                                     that :posts are :in => :forum

           /forums/2/posts           @forum = Forum.find(2)
                                     @posts = @forum.posts.find(:all)

           /sites/4/forums/3/posts   @site = Site.find(4)
                                     @forum = @site.forums.find(3)
                                     @posts = @forum.posts.find(:all)

           /users/2/posts/1          This won't work as the controller specified
                                     that :posts are :in => :forum

It is up to you which routes to open to the controller (in config/routes.rb). When you do, RC will use the route segments to drill down to the specified resource. This means that if User 3 does not have Post 5, then /users/3/posts/5 will raise a RecordNotFound Error. You dont' have to write any extra code to do this oft repeated controller pattern.

With RC, your route specification flows through to the controller - no need to repeat yourself.

If you don't want to have RC match wildcard resources just pass :load_enclosing => false

resources_controller_for :posts, :in => :forum, :load_enclosing => false

Example 3: Singleton resource

Here's an example of a singleton, the account pattern that is so common.

class AccountController < ApplicationController
  resources_controller_for :account, :class => User, :singleton => true do
    @current_user
  end
end

Your controller will use the block to find the resource. The @account will be assigned to @current_user

Example 4: Allowing PostsController to be used all over

First thing to do is remove :in => :forum

class PostsController < ApplicationController
  resources_controller_for :posts
end

This will now work for /users/2/posts.

Example 4 and a bit: Mapping non standard resources

How about /account/posts? The account is found in a non standard way - RC won't be able to figure out how tofind it if it appears in the route. So we give it some help.

(in PostsController)

map_enclosing_resource :account, :singleton => true, :class => User, :find => :current_user

Now, if :account apears in any part of a route (for PostsController) it will be mapped to (in this case) the current_user method of teh PostsController.

To make the :account mapping available to all, just chuck it in ApplicationController

This will work for any resource which can't be inferred from its route segment name

map_enclosing_resource :users, :segment => :peeps, :key => 'peep_id'
map_enclosing_resource :posts, :class => OddlyNamedPostClass

Example 5: Singleton association

Here's another singleton example - one where it corresponds to a has_one or belongs_to association

class ImageController < ApplicationController
  resources_controller_for :image, :singleton => true
end

When invoked with /users/3/image RC will find @user, and use @user.image to find the resource, and @user.build_image, to create a new resource.

Example 6: :resource_path (equivalent resource path): aliasing a named route to a RESTful route

You may have a named route that maps a url to a particular controller and action, this causes resources_controller problems as it relies on the route to load the resources. You can get around this by specifying :resource_path as a param in routes.rb

map.root :controller => :forums, :action => :index, :resource_path => '/forums'

When the controller is invoked via the '' url, rc will use :resource_path to recognize the route.

This is only necessary if you have wildcard enclosing resources enabled (the default)

Putting it all together

An exmaple app

config/routes.rb:

map.resource :account do ||
  .resource :image
  .resources :posts
end

map.resources :users do |user|
  user.resource :image
  user.resources :posts
end

map.resources :forums do |forum|
  forum.resources :posts
  forum.resource :image
end

map.root :controller => :forums, :action => :index, :resource_path => '/forums'

app/controllers:

class ApplicationController < ActionController::Base
  map_enclosing_resource :account, :singleton => true, :find => :current_user

  def current_user # get it from session or whatnot
end

class ForumsController < AplicationController
  resources_controller_for :forums
end

class PostsController < AplicationController
  resources_controller_for :posts
end

class UsersController < AplicationController
  resources_controller_for :users
end

class ImageController < AplicationController
  resources_controller_for :image, :singleton => true
end

class AccountController < ApplicationController
  resources_controller_for :account, :singleton => true, :find => :current_user
end

This is how the app will handle the following routes:

PATH                   CONTROLLER    WHICH WILL DO:

/forums                forums        @forums = Forum.find(:all)

/forums/2/posts        posts         @forum = Forum.find(2)
                                     @posts = @forum.forums.find(:all)

/forums/2/image        image         @forum = Forum.find(2)
                                     @image = @forum.image   

/image                       <no route>

/posts                       <no route>

/users/2/posts/3       posts         @user = User.find(2)
                                     @post = @user.posts.find(3)

/users/2/image POST    image         @user = User.find(2)
                                     @image = @user.build_image(params[:image])

/account               account       @account = self.current_user

/account/image         image         @account = self.current_user
                                     @image = @account.image

/account/posts/3 PUT   posts         @account = self.current_user
                                     @post = @account.posts.find(3)
                                     @post.update_attributes(params[:post])

Views

Ok - so how do I write the views?

For most cases, just in exactly the way you would expect to. RC sets the instance variables to what they should be.

But, in some cases, you are going to have different variables set - for example

/users/1/posts    =>  @user, @posts
/forums/2/posts   =>  @forum, @posts

Here are some options (all are appropriate for different circumstances):

  • test for the existence of @user or @forum in the view, and display it differently

  • have two different controllers UserPostsController and ForumPostsController, with different views (and direct the routes to them in routes.rb)

  • use enclosing_resource - which always refers to theā€¦ immediately enclosing resource.

Using the last technique, you might write your posts index as follows (here assuming that both Forum and User have .name)

<h1>Posts for <%= link_to enclosing_resource_path, "#{enclosing_resource_name.humanize}: #{enclosing_resource.name}" %></h1>

<%= render :partial => 'post', :collection => @posts %>

Notice enclosing_resource_name - this will be something like 'user', or 'post'. Also enclosing_resource_path - in RC you get all of the named route helpers relativised to the current resource and enclosing_resource. See NamedRouteHelper for more details.

This can useful when writing the _post partial:

<p>
  <%= post.name %>
  <%= link_to 'edit', edit_resource_path(tag) %>
  <%= link_to 'destroy', resource_path(tag), :method => :delete %>
</p>

when viewed at /users/1/posts it will show

<p>
  Cool post
  <a href="/users/1/posts/1/edit">edit</a>
  <a href="js nightmare with /users/1/posts/1">delete</a>
</p>
...

when viewd at /forums/1/posts it will show

<p>
  Other post
  <a href="/forums/1/posts/3/edit">edit</a>
  <a href="js nightmare with /forums/1/posts/3">delete</a>
</p>
...

This is like polymorphic urls, except that RC will just use whatever enclosing resources are loaded to generate the urls/paths.

Usage

To use RC, there are just three class methods on controller to learn.

resources_controller_for <name>, <options>, <&block>

ClassMethods#nested_in <name>, <options>, <&block>

map_enclosing_resource <name>, <options>, <&block>

Customising finding and creating

If you want to implement something like query params you can override find_resources. If you want to change the way your new resources are created you can override new_resource.

class PostsController < ApplicationController
  resources_controller_for :posts

  def find_resources
    resource_service.find :all, :order => params[:sort_by]
  end

  # you can call super to help yourself to the existing implementation
  def new_resource
    super.tap {|r| r.ip_address = request.ip_address }
  end

In the same way, you can override find_resource.

Writing controller actions

You can make use of RC internals to simplify your actions.

Here's an example where you want to re-order an acts_as_list model. You define a class method on the model (say order_by_ids which takes and array of ids). You can then make use of resource_service (which makes use of awesome rails magic) to send correctly scoped messages to your models.

Here's how to write an order action

def order
  resource_service.order_by_ids["things_order"]
end

the route

map.resources :things, :collection => {:order => :put}

and the view can conatin a scriptaculous drag and drop with param name 'things_order'

When this controller is invoked of /things the :order_by_ids message will be sent to the Thing class, when it's invoked by /foos/1/things, then :order_by_ids message will be send to Foo.find(1).things association

using non standard ids

Lets say you want to set to_param to login, and use find_by_login for your users in your URLs, with routes as follows:

map.reosurces :users do |user|
  user.resources :addresses
end

First, the users controller needs to find reosurces using find_by_login

class UsersController < ApplicationController
  resources_controller_for :users

protected
  def find_resource(id = params[:id])
    resource_service.(id)
  end
end

This controller will find users (for editing, showing, and destroying) as directed. (this controller will work for any route where user is the last resource, including the /users/dave route)

Now you need to specify that the user as enclosing resource needs to be found with find_by_login. For the addresses case above, you would do this:

class AddressesController < ApplicationController
  resources_controller_for :addresses
  nested_in :user do
    User.(params[:user_id])
  end
end

If you wanted to open up more nested resources under user, you could repeat this specification in all such controllers, alternatively, you could map the resource in the ApplicationController, which would be usable by any controller

If you know that user is never nested (i.e. /users/dave/addresses), then do this:

class ApplicationController < ActionController::Base
  map_enclosing_resource :user do
    User.find(params[:user_id])
  end
end

or, if user is sometimes nested (i.e. /forums/1/users/dave/addresses), do this:

map_enclosing_resource :user do
  ((enclosing_resource && enclosing_resource.users) || User).find(params[:user_id])
end

Your Addresses controller will now be the very simple one, and the resource map will load user as specified when it is hit by a route /users/dave/addresses.

class AddressesController < ApplicationController
  resources_controller_for :addresses
end

Defined Under Namespace

Modules: Actions, ActiveRecord, ClassMethods, Helper, IncludeActions, InstanceMethods, NamedRouteHelper, RequestPathIntrospection, ResourceMethods, SingletonActions Classes: CantFindSingleton, CantMapRoute, Railtie, ResourceMismatch, ResourceService, SingletonResourceService, SingletonSpecification, Specification

Constant Summary collapse

VERSION =
"2.1.1"

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.extended(base) ⇒ Object


386
387
388
389
390
391
# File 'lib/resources_controller.rb', line 386

def self.extended(base)
  base.class_eval do
    class_attribute :resource_specification_map
    self.resource_specification_map = {}
  end
end

.raise_cant_find_singleton(name, klass) ⇒ Object

:nodoc:

Raises:


817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
# File 'lib/resources_controller.rb', line 817

def raise_cant_find_singleton(name, klass) #:nodoc:
  raise CantFindSingleton, <<-end_str
Can't get singleton resource from class #{klass.name}. You have have probably done something like:

nested_in :#{name}, :singleton => true  # <= where this is the first nested_in

You should tell resources_controller how to find the singleton resource like this:

nested_in :#{name}, :singleton => true do
  #{klass.name}.find(<.. your find args here ..>)
end

Or: 
nested_in :#{name}, :singleton => true, :find => <.. method name or lambda ..>

Or, you may be relying on the route to load the resource, in which case you need to give RC some
help.  Do this by mapping the route segment to a resource in the controller, or a parent or mixin

map_enclosing_resource :#{name}, :segment => ..., :singleton => true <.. as above ..>
end_str
end

.raise_resource_mismatch(controller) ⇒ Object

:nodoc:

Raises:


839
840
841
842
843
844
845
846
847
# File 'lib/resources_controller.rb', line 839

def raise_resource_mismatch(controller) #:nodoc:
  raise ResourceMismatch, <<-end_str
resources_controller can't match the route to the resource specification
path:         #{controller.send(:request_path)}
specification: enclosing: [#{controller.specifications.collect{|s| s.is_a?(Specification) ? ":#{s.segment}" : s}.join(', ')}], resource :#{controller.resource_specification.segment}

the successfully loaded enclosing resources are: #{controller.enclosing_resources.join(', ')}
  end_str
end

Instance Method Details

#include_actions(mixin, options = {}) ⇒ Object

Include the specified module, optionally specifying which public methods to include, for example:

include_actions ActionMixin, :only => :index
include_actions ActionMixin, :except => [:create, :new]

503
504
505
506
# File 'lib/resources_controller.rb', line 503

def include_actions(mixin, options = {})
  mixin.extend(IncludeActions) unless mixin.respond_to?(:include_actions)
  mixin.include_actions(self, options)
end

#map_enclosing_resource(name, options = {}, &block) ⇒ Object

Creates a resource specification mapping. Use this to specify how to find an enclosing resource that does not obey usual rails conventions. Most commonly this would be a singleton resource.

See Specification#new for details of how to call this


490
491
492
493
# File 'lib/resources_controller.rb', line 490

def map_enclosing_resource(name, options = {}, &block)
  spec = Specification.new(name, options, &block)
  self.resource_specification_map = resource_specification_map.merge spec.segment => spec
end

#map_resource(*args, &block) ⇒ Object

this will be deprecated soon as it's badly named - use map_enclosing_resource


496
497
498
# File 'lib/resources_controller.rb', line 496

def map_resource(*args, &block)
  map_enclosing_resource(*args, &block)
end

#resources_controller_for(name, options = {}, &block) ⇒ Object

Specifies that this controller is a REST style controller for the named resource

Enclosing resources are loaded automatically by default, you can turn this off with :load_enclosing (see options below)

resources_controller_for <name>, <options>, <&block>

Options:

  • :singleton: (default false) set this to true if the resource is a Singleton

  • :find: (default null) set this to a symbol or Proc to specify how to find the resource. Use this if the resource is found in an unconventional way. Passing a block has the same effect as setting :find => a Proc

  • :in: specify the enclosing resources, by name. ClassMethods#nested_in can be used to specify this more fully.

  • :load_enclosing: (default true) loads enclosing resources automatically.

  • :actions: (default nil) set this to false if you don't want the default RC actions. Set this to a module to use that module for your own actions.

  • :only: only include the specified actions.

  • :except: include all actions except the specified actions.

Options for unconvential use

(otherwise these are all inferred from the name)

  • :route: the route name (without name_prefix) if it can't be inferred from name. For a collection resource this should be plural, for a singleton it should be singular.

  • :source: a string or symbol (e.g. :users, or :user). This is used to find the class or association name

  • :class: a Class. This is the class of the resource (if it can't be inferred from name or :source)

  • :segment: (e.g. 'users') the segment name in the route that is matched

The :in option

The default behavior is to set up before filters that load the enclosing resource, and to use associations on that model to find and create the resources. See ClassMethods#nested_in for more details on this, and customising the default behaviour.

load_enclosing_resources

By default, a before_filter is added by resources_controller called :load_enclosing_resources - which does all the work of loading the enclosing resources. You can use ActionControllers standard filter mechanisms to control when this filter is invoked. For example - you can choose not to load resources on an action

resources_controller_for :foos
skip_before_filter :load_enclosing_resources, :only => :static_page

Or, you can change the order of when the filter is invoked by adding the filter call yourself (rc will only add the filter if it doesn't exist)

before_filter :do_something
prepend_before_filter :load_enclosing_resources
resources_controller_for :foos
before_filter :do_something_else     # chain => [:load_enclosing_resources, :do_something, :do_something_else]

Default actions module

If you have your own actions module you prefer to use other than the standard resources_controller ones you can set ResourcesController.actions to that module to have this be included by default

ResourcesController.actions = MyAwesomeActions
ResourcesController.singleton_actions = MyAweseomeSingletonActions

class AwesomenessController < ApplicationController
  resources_controller_for :awesomenesses # includes MyAwesomeActions by default
end

453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
# File 'lib/resources_controller.rb', line 453

def resources_controller_for(name, options = {}, &block)
  options.assert_valid_keys(:class, :source, :singleton, :actions, :in, :find, :load_enclosing, :route, :segment, :as, :only, :except, :resource_methods)
  when_options = {:only => options.delete(:only), :except => options.delete(:except)}
  
  unless included_modules.include? ResourcesController::InstanceMethods
    class_attribute :specifications, :route_name
    hide_action :specifications, :route_name
    extend  ResourcesController::ClassMethods
    helper  ResourcesController::Helper
    include ResourcesController::InstanceMethods, ResourcesController::NamedRouteHelper
    include ResourcesController::ResourceMethods unless options.delete(:resource_methods) == false || included_modules.include?(ResourcesController::ResourceMethods) 
  end

  before_filter(:load_enclosing_resources, when_options.dup) unless load_enclosing_resources_filter_exists?
  
  self.specifications = []
  specifications << '*' unless options.delete(:load_enclosing) == false
  
  unless (actions = options.delete(:actions)) == false
    actions ||= options[:singleton] ? ResourcesController.singleton_actions : ResourcesController.actions
    include_actions actions, when_options
  end
  
  route = (options.delete(:route) || name).to_s
  name = options[:singleton] ? name.to_s : name.to_s.singularize
  self.route_name = options[:singleton] ? route : route.singularize
  
  nested_in(*options.delete(:in)) if options[:in]
  
  class_attribute :resource_specification, :instance_writer => false
  self.resource_specification = Specification.new(name, options, &block)
end