Skip to content

Latest commit

 

History

History
395 lines (221 loc) · 14.3 KB

controllers.markdown

File metadata and controls

395 lines (221 loc) · 14.3 KB

Hobo's Model Controller

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.

Introduction

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.

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

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.

Adding new actions

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.

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

{: .ruby}

This gets routed to /adverts/:id/detail.

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

{: .ruby}

This gets routed to /adverts/table

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

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:

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:

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

Changing the behaviour of an automatic action

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 block

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}

Passing options

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.

The automatic actions

General points

Read-only actions

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

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

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

hobo_show

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

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

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

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.

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)

{: .ruby}

which performs a permission check first.

The response is either a redirect or an ajax-part update as appropriate.

hobo_show_collection

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

hobo_new_in_collection

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}

Flash messages

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.

Automatic Redirection

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)

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.