Controllers and Routing
This chapter of the Hobo Manual describes Hobo’s Model Controller and automatic routing. In a very simple Hobo app, you hardly need to touch the automatically generated controllers, or even think about routing. As an app gets more interesting though, you’ll quickly need to know how to customise things. The down-side of having almost no code at all in the controllers is that there’s nothing there to tweak. Don’t worry though, Hobo’s controllers have been built with customisation in mind. The things you will tweak commonly are extremely easy, and full customisation is not hard at all.
Contents
- Introduction
- Selecting the automatic actions
- Owner actions
- Instance Variables
- Automatic Routes
- Adding extra actions
- Changing action behaviour
- The default actions
- Automatic redirection
- Web methods
- Autocompleters
- Drag and drop reordering
- Permission and not-found errors
- Lifecycles
Introduction
Here’s a typical controller in a Hobo app. In fact this is unchanged from the code generated by the hobo_model_controller generator:
class AdvertsController < ActiveRecord::Base
hobo_model_controller
auto_actions :all
end
The hobo_model_controller declaration just does include Hobo::ModelController, and gives you a chance to indicate which model this controller looks after. E.g., you can do
hobo_model_controller Advert
By default the model to use is inferred from the name of the controller.
Selecting the automatic actions
Hobo provides working implementations of the full set of standard REST actions that are familiar from Rails:
indexshownewcreateeditupdatedestroy
A controller that declares
auto_actions :all
Will have all of the above actions.
You can customise this either by listing the actions you want:
auto_actions :new, :create, :show
Or by listing the actions you don’t want:
auto_actions :all, :except => [ :index, :destroy ]
The :except option can be set to either a single symbol or an array
There are two more conveniences: :read_only and :write_only. :read_only is a shorthand for :index and :show, and :write_only is a shorthand for :create, :update and :destroy. Either of these shorthands must be the first argument to auto_actions, after which you can still list other actions and the :except option:
auto_actions :write_only, :show
Owner actions
Hobo’s model controller can also provide three special actions that take into account the relationships between your records. Specifically, these are the “owner” versions of new, index and create. To understand how these compare to the usual actions, consider a recipe model which belongs_to :author, :class_name => "User". The three special actions are:
- An index page that only lists the recipes by a specific author
- A “New Recipe” page specific to that user (i.e. to create a new recipe by that author)
- A create action which is specific to that “New Recipe” page
These are all part of the RecipesController and can be added with the auto_actions_for declaration, like this:
auto_actions_for :author, [ :index, :new, :create ]
If you only want one, you can omit the brackets:
auto_actions_for :author, :index
Action names and routes
The action names and routes for these actions are as follows:
index_for_authoris routed as/users/:author_id/productsfor GET requestsnew_for_authoris routed as/users/:author_id/products/newfor GET requestscreate_for_authoris routed as/users/:author_id/productsfor POST requests
It’s common for the association name and the class name of the target to be the same (e.g. in an association like belongs_to :category). We’ve deliberately chosen an example where they are different (“author” and “user”) in order to show where the two names are used. The association name (“author”) is used everywhere except in the /users at the beginning of the route.
Instance Variables
As well as setting the default DRYML context, the default actions all make the record, or collection of records, available to the view in an instance variable that follows Rails conventions. E.g. for a ‘product’ model, the product will be available as @product and the collection of products on an index page will be available as @products
Owner actions
For owner actions, the owner record is made available as @<association-name>. E.g. @author in our above example.
Automatic Routes
Hobo’s router will automatically create standard RESTful routes for each of your models. The router inspects your controllers: any action that is not defined will not be routed.
The routes are generated by hobo when needed, and written in a file for easy user inspection. You can eventually override the file path by setting the configuration variable (routes_path) in the application block in config.application.rb.
You can override the hobo routes in the regular application config/routes.rb file.
Notice: The generation of the hobo_routes.rb file at startup is skipped when the read_only_file_system config variable is true (which is when it runs on Heroku)
Adding extra actions
It’s common to want actions beyond the basic REST defaults. In Rails a controller action is simply a public method. That doesn’t change in Hobo. You can define public methods and add routes for them just as you would in a regular Rails app. However, you probably want your new actions to be routed automatically, and even implemented automatically, just like the basic actions. For this to happen you have to tell Hobo about them as explained in this section.
Show actions
Suppose we want a normal view and a “detailed” view of our advert. In REST terms we want a new ‘show’ action called ‘detail’. We can add this like this:
class AdvertsController < ActiveRecord::Base
hobo_model_controller
auto_actions :all
show_action :detail
end
This action will be routed to /adverts/:id/detail. Hobo will provide a default implementation. You can override this simply by defining the method yourself:
show_action :detail
def detail
...
end
Or, as a shorthand for the same, give a block to show_action:
show_action :detail do
...
end
Index actions
In the same way, we might want an alternative listing (index) of our adverts. Perhaps one that gives a tabular view of the adverts:
class AdvertsController < ActiveRecord::Base
hobo_model_controller
auto_actions :all
index_action :table
end
This gets routed to /adverts/table. As with show_action, if you want your own implementation, you can either define the method as normal, or pass a block to index_action.
Changing action behaviour
Sometimes the implementations Hobo provide aren’t what you want. They might be close, or they might be completely out. Not a problem - you can change things as needed.
A cautionary note concerning controller methods
Always start by asking: should this go in the model? It’s a very, very, very common mistake to put code in the controller that belongs in the model. Want to send an email in the create action? Don’t! Send it from an after_create callback in the model, or even better: use a lifecycle. Want to check something about the current user before allowing a destroy to proceed? Use Hobo’s Permission System.
Typically, valid reasons to add custom controller code are things like:
- Provide a custom flash message
- Change the redirect after a create / update / destroy
- Extract parameters from
paramsand pass them to the model (e.g. for searching / filtering) - Provide special responses for different formats or requested mime-types
A good test is to ask: is this related to http? No? Then it probably shouldn’t be in the controller. I tend to think of controllers as a way to publish objects via http, so they shouldn’t really be dealing with anything else.
A lot has been written about this elsewhere, so there’s no need to repeat it all here. Perhaps the original article on this issue would be:
Writing an action from scratch
The simplest way to customise an action is to write it yourself. Say your advert has a boolean field published and you only want published adverts to appear on the index page. Using one of Hobo’s automatic scopes, you could write:
class AdvertsController < ActiveRecord::Base
hobo_model_controller
auto_actions :all
def index
@adverts = Advert.published.all
respond_with(@adverts)
end
end
In other words you don’t need to do anything different than you would in a normal Rails action. Hobo will look for either @advert (for actions which expect an ID) or @adverts (for index actions) as the initial context for a DRYML page.
Note: In the above example, we’ve asked for the default index action and then overwrote it. That way Hobo also creates the route for us.
The above example won’t perform Hobo’s part AJAX. If you need this, you can use
def index
@adverts = Advert.published.all
if request.xhr?
hobo_ajax_response
else
respond_with(@adverts)
end
end
Customising Hobo’s implementation
Often you do want the automatic action, but you want to customise it in some way. The way you do this varies slightly for the different kinds of actions, but they all follow the same pattern. We’ll start with show as an example.
The default show provided by Hobo is simply:
def show
hobo_show
end
All the magic (and in the case of show there really isn’t much) takes place in hobo_show. So immediately we can see that it’s easy to add code before or after the default behaviour:
def show
@foo = "bar"
hobo_show
logger.info "Done show!"
end
Note: assigning to instance variables to make data available to the views work exactly as it normally would in Rails.
There is a similar hobo_* method for each of the basic actions: hobo_new, hobo_index, etc.
Switching to the update action, you might think you can do:
def update
hobo_update
redirect_to my_special_place # DON'T DO THIS!
end
That will give you an error: actions can only respond by doing a single redirect or render, and hobo_update has already done a redirect. Read on for the simple solution…
The block
The correct place to perform a redirect is in a block passed to hobo_update. All the hobo_* actions take a block and yield to the block just before their response. If your block performed a response, Hobo will leave it at that. So:
def update
hobo_update do
redirect_to my_special_place # better but still problematic
end
end
The problem this time is that we almost certainly don’t want to do that redirect if there were validation errors during the update. As with the typical Rails pattern, validation errors are handled by re-rendering the form (along with the error messages). Hobo provides a method valid? for these situations:
def update
hobo_update do
redirect_to my_special_place if valid?
end
end
If the update was valid, the redirect will proceed. If it wasn’t, the block won’t respond so Hobo’s response will kick in and re-render the form. Perfect!
If you want access to the object either in the block or after the call to hobo_update, it’s available either as this or in the conventional Rails instance variable, in this case @advert.
Passing options
In previous versions of Hobo, options to hobo controller actions were documented in this manual chapter. They are still available for backwards compatibility reasons, but are not recommended for use in new applications.
Using options, you could pass a finder, options for will_paginate, and options for find.
To adjust the finder and/or adjust will_paginate options, simply assign to self.this before calling the controller action.
def index
self.this = Foo.paginate(:page => params[:page] || 1, :per_page => 10)
hobo_index
end
Note that self.this = ... is equivalent to @foos = ... in hobo controllers. Both @foos and this will be assigned no matter which form you use.
In Rails 3.0 and greater, the use of find is discouraged. The use of scopes and where provides more idiomatic Rails 3.0 code. So rather than passing :order_by => "name asc", :include => :foos as an option to hobo_index, you can use scopes on self.this
def index
hobo_index do
self.this = this.order_by("name asc").includes(:foos)
end
end
Alternate Response Formats
Hobo 2.0 uses a respond_with block to render its responses. Therefore, you can add a respond_to declaration to your class and Hobo will also render those types.
respond_to :html, :json
The default is respond_to :html. Hobo controller actions also respond to part AJAX, although that is done outside of the respond_with block, so you do not have to add :js to your respond_to declaration for part AJAX.
The default respond_with handlers return the context. If you need a custom response you can render it before the hobo action or inside of a block passed to the hobo action:
hobo_show do
render foo if request.format.pdf?
end
If your block takes a parameter (aka has an arity of 1), Hobo will open up a respond_to block for you:
hobo_show do |format|
format.pdf { render foo }
end
Note that this is a respond_to block, not a respond_with block.
The hobo action will only render or redirect if you do not render or redirect before the action or inside the block.
Note that the JSON and XML interfaces will only use coarse grained model-level permission checking rather than fine grained attribute level permission checking.
The default actions
In this section we’ll go through each of the action implementations that Hobo provides.
hobo_index and index_response
hobo_index does a paginated query if self.this is not yet set, and then calls any supplied block. If the block has not rendered or redirected, it calls index_response. index_response calls either hobo_ajax_response or respond_with as appropriate.
hobo_show, find_instance and show_response
hobo_index calls find_instance if self.this is not yet set, and then calls any supplied block. If the block has not rendered or redirected, it calls show_response. show_response calls either hobo_ajax_response or respond_with as appropriate.
find_instance essentially does
model.user_find(current_user, params[:id])
user_find is a method added to your model by Hobo which combines a normal find with a check for view permission.
hobo_new
hobo_new will either instantiate the model for you using the user_new method from Hobo’s permission system if you have not already supplied it in self.this. If you do not redirect or render in a supplied block, show_response is then called.
hobo_create
hobo_create will instantiate the model (using user_new) if you have not already supplied it in self.this
The attributes hash for this new record are found either from the option :attributes if you passed one, or from the conventional parameter that matches the model name (e.g. params[:advert]).
The update to the new record with these attributes is performed using the user_update_attributes method, in order to respect the model’s permissions.
The response (assuming you didn’t respond in the block) will handle
- redirection if the create was valid (see below for details)
- re-rendering the form if not (or sending textual validation errors back to an ajax caller)
- performing Hobo’s part updates as required for ajax requests
hobo_update
hobo_update has the same behaviour as hobo_create except that the record is found rather than created. You can pass the record in self.this if you want to find it yourself.
hobo_destroy
The record to destroy is found using the find_instance method, unless you provide it in self.this.
The actual destroy is performed with:
this.user_destroy(current_user)
which performs a permission check first.
The response is either a redirect or an ajax part update as appropriate unless you have rendered or redirected in a supplied block.
Owner actions
For the “owner” versions of the index, new and create actions, Hobo provides:
hobo_index_forhobo_new_forhobo_create_for
These are pretty much the same as the regular hobo_index, hobo_new and hobo_create except they take an additional first argument – the name of the association. For example, the default implementation of, say, index_for_author would be:
def index_for_author
hobo_index_for :author
end
Flash messages
The hobo_create, hobo_update and hobo_destroy actions all set reasonable flash messages in flash[:notice]. They do this before your block is called so you can simply overwrite this message with your own if need be.
Automatic redirection
The hobo_create, hobo_create_for, hobo_update and hobo_destroy actions all perform a redirect on success.
the :redirect parameter
The :redirect option may be one of:
- Symbol: redirects to that action using the current controller and model. If you have
show_action :detailin your controller, then you could use:redirect => :detail. - Hash or String: redirect_to from Rails is used. Examples:
:redirect => {:controller => "sites", :action => :admin, :id => this.site}and:redirect => sites_admin_path(this.site).rake routeslists available paths that can be used. - Array:
object_urlis used.
Automatic redirects
If neither a response block nor :redirect are passed to hobo_*, the destination of this redirect is determined by calling the destination_after_submit method. Here’s how it works:
- If the parameter “
after_submit” is present, go to that URL (See the <after-submit> tag in Rapid for an easy way to provide this parameter), or - Go to the record’s
showpage if there is one, or - Go to the show page of the object’s
ownerif there is one (For example, this might take you to the blog post after editing a comment), or - Go to the index page for this model if there is one, or
- Give up trying to be clever and go to the home-page (the root URL, or override by implementing
home_pagein ApplicationController)
Web methods
Web methods provide a simple mechanism for adding a side-effecting action to your controller, routed as an HTTP POST. In keeping with good Rails and Hobo style, a web method is a thin wrapper on top of a method on your model. In other words, a web method is a way to publish an instance method from your model, via the web.
When to use web methods
When Rails made the shift to the RESTful style, DHH made the comment that REST was an aspiration rather than a law - sometimes we’ll have to fall back on plain old “remote procedure calls”. In other words, sometimes you need to provide a service that can’t be expressed as creating, reading, updating, or deleting a resource.
Hobo provides two mechanisms for supporting these situations. The first is Lifecycles. Whenever you need some operation that falls outside of the REST paradigm, the first thing you should ask yourself is: do I need to define a lifecycle here? Like REST, lifecycles provide a high-level structure, that, if it fits what you are trying to do, will make your app easier to understand and to change in the future. If the neither REST nor Lifecycles are a good fit for what you are trying to do, web methods provide a low-level way to add a service to your application.
A cautionary note from Tom concerning web methods
Web methods pre-dated Lifecycles, and since the addition of lifecycles I have never used a web method in any of my applications. Indeed, I can’t even think of a good example for the manual – the ‘empty shopping cart’ example presented below would be better done as a lifecycle. It’s possible that good examples of the need for web methods will be found, but it’s also possible that we’ll remove web methods from Hobo altogether.
Usage
To add a web method to your controller, use the web_method declaration. The simplest usage is:
class CartController < ApplicationController
...
web_method :empty
...
end
This declaration does the following:
- Adds an
emptyaction with the route/carts/:id/emptyfor HTTP POST requests - Implements the action to: find the record using the passed ID,
- Checks if the current user has permission by calling
@cart.method_callable_by?(:empty, current_user)(see Permissions) - Calls
@cart.empty
As long as Cart#empty and Cart#empty_permitted? are defined on your model, that’s all there is to it. You can go ahead and add a form to your view (in this case, just a simple button) to invoke the web method. (TO DO: Link to the relevant part of the Rapid chapter)
The default response, if the request was an ajax request, is to perform an ajax part update. For a non-ajax request, the default is to render a template with the same name as the web method (in other words, the regular Rails default).
Publishing a different name
If you want to make the web-visible name of the method different than the actual method name on the object, you can do so with the :method option:
web_method :empty, :method => :remove_all
This will create a web method called empty that calls the remove_all method on your model.
Passing parameters or customising the response.
The previous example used a web method with no parameters, and did nothing special with the response. If you need to do either of these, you can do so by giving a block to web_method. When you provide a block, you are expected to call the method yourself. Hobo will find the record, check the current user has permission and leave the rest to you. For example:
web_method :increase_prices do
@category.increase_prices params[:by]
redirect_to this
end
The block you provide should always call the method on your model. If it doesn’t, nothing will go wrong, but this is a misuse of the web method feature.
Full customisation
To have complete control over the action, simply redefine the method yourself. In this case, Hobo is doing nothing except providing the route:
web_method :increase_prices
def increase_prices
...
end
Autocompleters
Hobo makes it easy to build auto-completing text fields in your user interface; the Rapid tag library provides support for them in the view layer, and the controller provides an easy way to add the action that looks up the completions.
The simplest form for creating an auto-completing field is just a single declaration:
class UsersController < ApplicationController
...
autocomplete
...
end
Because Hobo allows you to specify which field of a model is the name (using :name => true in the model’s field declaration block), you don’t need to tell autocomplete which field to complete on if it is autocompleting the “name” field. To create an autocompleter for a different field, pass the field as a symbol:
autocomplete :email_address
The autocomplete declaration will create an action named according to the field, e.g. complete_email_address routed as, in this case, /users/complete_email_address for GET requests.
Options
The autocomplete behaviour can be customised with the following options:
:field– specify a field to complete on. Defaults to the name (first argument) of the autocompleter.:limit– maximum number of completions. Defaults to 10.:param– name of the parameter in which to expect the user’s- input. Defaults to
:query :query_scope– a named scope used to do the database query. Change this to control things such as handling of multiple words, case sensitivity, etc. For our example this would be:email_address_contains. Note that this is one of Hobo’s automatic scopes. Instead of a single named scope, you may instead pass a list of named scopes.
Further customisation
The autocomplete action follows the same pattern for customisation as the regular actions. That is, the implementation given to you is a simple call to the underlying method that does the actual work, and you can call this underlying method directly. To illustrate, say, on a UsersController in which you declare autocomplete :email_address, the generated method looks like:
def complete_email_address
hobo_completions :email_address, User
end
To gain extra control, you can call hobo_completions yourself by passing a block to autocomplete:
autocomplete :email_address do
hobo_completions ...
end
The parameters to hobo_completions are:
- Name of the attribute
- A finder, i.e. a model class, association, or a scope.
- Options (the same as described above)
You can see an example of autocompleter customization in a recipe and in the manual for name-one.
Drag and drop reordering
The controller has the server-side support for drag-and-drop reordering of models that declare acts_as_list. Say, for example, your Task model uses acts_as_list, then Hobo will add a reorder action routed as /tasks/reorder that looks like:
def reorder
hobo_reorder
end
This action expects an array of IDs in params[:task_ordering], and will reorder the records in the order that the IDs are given.
The action can be removed in the normal ways (e.g., blacklisting):
auto_actions :all, :except => :reorder
The action will raise a PermissionDeniedError if the current user does not have permission to change the ordering.
Permission and not-found errors
Any permission errors that happen are handled by the permission_denied controller method, which renders the DRYML tag <permission-denied-page> or just a text message if that doesn’t exist.
Not-found errors are handled in a similar way by the not_found method, which tries to render <not-found-page>
Both permission_denied and not_found can be overridden either in an individual controller or site-wide in ApplicationController.
Lifecycles
Hobo’s model controller has extensive support for lifecycles. This is described in the Lifecycles chapter of the manual.
Edit this page