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
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 model 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.
At this time it is not possible to customise Hobo’s routes beyond turning them on or off (by adding or removing controller actions). This will be addressed in the future. However, like most things in Hobo, it’s important to understand that it’s just Rails underneath. There’s nothing to stop you defining your own routes in addition to Hobo’s. You could even remove Hobo’s routes altogether, and define them all yourself. To do that, simply remove the call to Hobo.add_routes that Hobo adds to your routes.rb file.
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. 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
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. It might have been neater to say ”auto_actions :all, :except => :index” but it really doesn’t matter.)
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 above redirect will happen. 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.
Handling different formats
By default, the response block is only called if an HTML response is required. If you want to handle other response types, declare a block with a single argument. The “format” object from Rails’ respond_to will be passed. The typical usage would be:
def update
hobo_update do |format|
format.html { ... }
format.js { ... }
end
end
Passing options
Here’s another example of tweaking one of the automatic actions. The hobo_* methods can all be passed a range of options. Here’s a simple example: changing the page size on an index page:
def index
hobo_index :per_page => 10
end
That’s pretty much all there is to customizing Hobo’s automatic actions: define the action as a public method in which you call the appropriate hobo_* method, passing it parameters and/or a block.
The remainder of this guide will cover the parameters available to each of the hobo_* methods.
Note that you can also pass these options directly to the index_action and show_action declarations, e.g.:
index_action :table, :per_page => 10
The default actions
In this section we’ll go through each of the action implementations that Hobo provides.
hobo_index
hobo_index takes a “finder” as an optional first argument, and then options. A finder is any object that supports the find and / or paginate methods, such as an ActiveRecord model class, a has_many association, or a scope.
Find options
Any of the standard ActiveRecord find options you pass are forwarded to the find method. This is particularly useful with the :include option to avoid the dreaded N+1 query problem.
Pagination
Turn pagination on or off by passing true/false to the :paginate option. If not specified Hobo will guess based on the value of request.format. It’s normally on, but won’t be for things like XML and CSV. When pagination is on, any other options to hobo_index are forwarded to the paginate method from will-paginate, so you can pass things like :page and :per_page. If you don’t specify :page it defaults to params[:page] or if that’s not given, the first page.
Scope
The finder may be filtered via a scope in a fashion similar to how an association is scoped.
hobo_show
Options to hobo_show are forwarded to the method find_instance which does:
model.user_find(current_user, params[:id], options)
user_find is a method added to your model by Hobo which combines a normal find with a check for view permission.
As with hobo_index, a typical use would be to pass :include to do eager loading.
hobo_new
hobo_new will either instantiate the model for you using the user_new method from Hobo’s permission system, or will use the first argument (if you provide one) as the new record.
hobo_create
hobo_create will instantiate the model (using user_new), or take the first argument if you provide one.
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 as the first argument if you want to find it yourself.
The response is also essentially the same as hobo_create, with some extra smarts to support the in-place-editor from Script.aculo.us.
hobo_destroy
The record to destroy is found using the find_instance method, unless you provide it as the first argument.
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.
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.
Block Response
If you supply a block to the hobo_* action, no redirection is done so that it may be performed by the block:
def update
hobo_update do
redirect_to my_special_place if valid?
end
end
the :redirect parameter
If you supply a block to the hobo_* action, you must redirect or render all potential formats. But what if you want to supply a redirect for HTML requests, but let Hobo handle AJAX requests? In this case you can supply the :redirect option to hobo_*:
def update
hobo_update :redirect => my_special_place
end
:redirect is only used for valid HTML requests.
The :redirect: option may be one of:
- Symbol: redirects to that action using the current controller and model. (Must be a show action).
- Hash or String: redirect\to from Rails is 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.