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 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 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
    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.

If you add a parameter but you don’t add at least one format.xxx you will probably get “Completed 406 Not Acceptable” errors.

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_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.

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 :detail in 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 routes lists available paths that can be 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.


Edit this page