This guide covers customisation of a controller in Hobo. As you've probably seen, your controllers in a Hobo app often to have no code at all in them (just a declaration or two). That can make it hard to know what to do when you need to customise the behaviour. The controllers are highly customisable though. Read on to find out how.
IMPORTANT: Hobo's automatic routing only takes place at application start-up, even if you're running in development mode. Whenever you add or remove an action to a controller, you need to restart the server in order to get the new routes.
Here's a typical controller in a Hobo app. In fact this unchanged from the code generated by the hobo_model_controller
generator:
class AdvertsController < ActiveRecord::Base
hobo_model_controller
auto_actions :all
end
{: .ruby}
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.
Hobo provides working implementations of the full set of standard REST actions that are familiar from Rails:
index
show
new
create
edit
update
destroy
Hobo can also provide actions for any has_many
associations that your model has. For example, say our advert model declares has_many :comments
, Hobo can provide:
comments
(routed to /advert/:id/comments)new_comment
(routed to /advert/:id/comments/new)
(Note: if your has_many
has any conditions declared, new_comment
is not available.)
A controller that declares
auto_actions :all
{: .ruby}
Will have all of the above actions, including the association actions for every has_many
.
You can customise this either by listing the actions you want:
auto_actions :new, :create, :show
{: .ruby}
Or by listing the actions you don't want:
auto_actions :all, :except => [ :index, :destroy ]
{: .ruby}
As a convenience, you can disable all of the actions for has_many
associations in one go:
auto_actions :all, :except => :collections
{: .ruby}
(Note: 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
, :show
and the names of the has_many
collections, and :write_only
is a shorthand for :create
, :update
and :destroy
. These shorthands must be the first argument, and you can still list other actions and the :except
option:
auto_actions :write_only, :show
{: .ruby}
Or
auto_actions :read_only, :except => :collections
{: .ruby}
Note that Hobo's automatic routing inspects your controllers and only creates routes for actions that are available.
It's common to want actions beyond the basic REST defaults. We can add these in such a away that they are also routed automatically.
There's absolutely nothing stopping you from adding actions in the normal Rails way - simply by defining public instance methods in the controller, but you'll probably end up needing to manually add routes for these.
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
{: .ruby}
This gets routed to /adverts/:id/detail
.
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
{: .ruby}
This gets routed to /adverts/table
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.
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. This is a good article:
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:
class AdvertsController < ActiveRecord::Base
hobo_model_controller
auto_actions :all
def index
@adverts = Advert.published.all
end
end
{: .ruby}
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
or @adverts
as the initial context for a DRYML page.
(Note: In the above example, we've asked for the default index
action and then just overridden it. It might have been neater to say "auto_actions :all, :except => :index
" but it really doesn't matter.)
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
{: .ruby}
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 before / after actions:
def show
@foo = "bar"
hobo_show
logger.info "Done show!"
end
{: .ruby}
(Note: assigning to instance variables to make data available to the views work exactly as it normally would in Rails)
Switching to the update
action, you might think you can do
def update
hobo_update
redirect_to my_special_place # BREAKS!
end
{: .ruby}
But 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.
The correct solution is to pass hobo_update
a block. All the hobo_*
actions take a block and yield to the block just before doing 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
{: .ruby}
The problem this time is that we almost certainly don't want to do that redirect if there were validation errors during the update. Hobo provides a method valid?
for these situations:
def update
hobo_update do
redirect_to my_special_place if valid?
end
end
{: .ruby}
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
.
There's one more convenience to the block. If you give your block a single argument, it will be passed the "wants" object from Rails' repsond_to
, e.g.:
def update
hobo_update do |wants|
wants.js { ... }
wants.html { ... }
end
end
{: .ruby}
Here's another example of tweaking one of the automatic actions. The hobo_*
methods all take a block as seen above. They also take various parameters. Here's a simple example: changing the page size on an index page:
def index
hobo_index :per_page => 10
end
{: .ruby}
That's pretty much all there is to it: define the action yourself, call the appropriate hobo_*
method, give it parameters and/or a block. The remainder of this guide will cover the parameters available to each of the hobo_*
methods.
All of the "read only" actions (index, show etc.) do one thing only: load the record(s) to be displayed. So if your customisation is that you want to find the record(s) with your own code, don't use the hobo_*
action at all.
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.
Any options you pass are simply forwarded to the find method. This is particularly useful with the :include
option to avoid the dreaded N+1 query problem.
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, 1.
Options to hobo_show
are forwarded to the method find_instance
which does:
model.user_find(current_user, params[:id], options)
{: .ruby}
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
will take either instantiate the model for you, or take the first argument (if you provide one) as the new record. It then sets the creator (if one is defined in your model) to the current user and performs a create permission check.
hobo_create
will instantiate the model, 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 like this:
this.user_save_changes(current_user, attributes)
This method sets the creator (if the model has one) and performs a permission check before saving the model.
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 ajax-part updates as required
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 Sciptaculous.
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)
{: .ruby}
which performs a permission check first.
The response is either a redirect or an ajax-part update as appropriate.
To customise an action that displays an association, use hobo_show_collection
and pass the name of the association:
def comments
hobo_show_collection :comments
end
{: .ruby}
Or you can pass a finder, e.g. if you've got moderated comments you might do:
def comments
hobo_show_collection find_instance.comments.published
end
{: .ruby}
The options are the same as for hobo_index
To customise a "new in collection" page such as /adverts/1/comments/new
, use hobo_new_in_collection
and pass the association name:
def new_comment
hobo_new_in_collection :comments
end
{: .ruby}
If you wish to instantiate the new record yourself, you can pass it as the optional second argument:
def new_comment
comment = ...
hobo_new_in_collection :comments, comment
end
{: .ruby}
The create, update and 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.
The create, update and destroy actions all perform a redirect on success. 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 (note that the<after-submit>
tag in Rapid sets this), else - Go to the records
show
page if there is one, else - Go to the show page of the objects
owner
if there is one (For example, this might take you to the blog post after editing a comment), else - Go to the index page for this model if there is one, else
- Give up trying to be clever and go to the home-page (the root URL, or override by implementing
home_page
in ApplicationController)
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.