Hobo Cookbook

View Source

Manual

Last updated: January 19, 2010

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:

  • index
  • show
  • new
  • create
  • edit
  • update
  • destroy

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_author is routed as /users/:author_id/products for GET requests
  • new_for_author is routed as /users/:author_id/products/new for GET requests
  • create_for_author is routed as /users/:author_id/products for 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 params and 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_for
  • hobo_new_for
  • hobo_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_url is 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 show page if there is one, or
  • Go to the show page of the object’s owner if 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_page in 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 empty action with the route /carts/:id/empty for 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.