I've been chatting to a few people at RailsConfEuropoe about resources_controller, so I thought I'd say a few words about waht it's key features are, and about RC at RailsConfEuropoe in general.

Key features

There's a few plugins out there that try and solve the same sort of problem - DRY up RESTful controllers. I believe that RC's standout features can be seen when considering how to write controller for a polymorhpic has_many relationship.

Polymorphic Tags

So you want to tag a bunch of models, and so you sure the :polymorphic has_many assoc, and make a Tag model with belongs_to :taggable, :polymorphic => true.

You want tags to be nested under a bunch of different resources like this:

  map.resources :users do |user|
   user.resources :tags, :controller => 'user_tags'
  end

  map.resources :posts do |post|
   post.resources :tags, :controller => 'post_tags'
  end

Standardly, you'd then have to write two controllers: UserTagsController, and PostTagsController, and map the above two nested routes to those different controllers. These would be essentially the same functionality except:

  • they would load different models in before filters: @user in one and @post in the other,
  • they get the post from different collections (@tag = @user.tags.find(params[:tags_id]) vs @tag = @post.tags.find(params[:tags_id])),
  • they redirect to different routes on completion of certain actions user_tags_path and post_tags_path in the other.

To do this, even with plugins to dry everything up, you still need to create two (or three, or four) controllers for tags - all doing essentially the same thing.

it gets worse. You'll need a bunch of different views - because they all need to link to urls relative to the enclosing resource. Suddenly you've got a lot of really similar code - or some really ungly hacks in your views.

(and all of this gets much worse if you have deeply nested routes)

Polymorphic tags with resources_controller

resources_controller (used in the default way) inspects the route that was used to invoke the controller. From this, it:

  • loads all of the enclosing resources,
  • uses the immediately enclosing resource as the resource service (the object that we send find and new to to - in the case of /posts it would be the Post class, in the case of /post/1/tags it would be the @post.tags association),
  • does some method missing magic so that you can refer to all named routes relative to the current resource

All this means you just need to write one controller, and one set of views for Tags

Here's some sample code

  class TagsController < ApplicationCntroller
    resources_controller_for :tags
  end

in show.html.erb:

  <%= link_to 'tags', resources_path %>

The above will be user_tags_path(@user) in one case and post_tags_path(@post) in another.

It gets better, you can refer to the enclosing resource as well:

  <%= link_to "back to #{enclosing_resource_name.humanize}", enclosing_resource_path %>

And if you have routes like /users/1/posts/2/tags, and /posts/1/tags, the you can use the same view for posts:

  <%= link_to 'tags', resource_tags_path %> # in /users/1/posts/2/tags will be:
                                            # user_post_tags_path(@user, @post)
                                            
  <%= link_to 'tags', resource_tags_path %> # in /posts/2/tags will be:
                                            # post_tags_path(@post)

That's just some of the features, I'd love to get feedback, patches, bug reports, etc. There are links to RC via svn, and rdoc on our plugins page

BoF and RejectConf

Man, I've got a lot to learn about presentations...

I gave a BoF at RailsConfEurope07 session on resources_controller - I was expecting about 10 people and a round table discussion on taking the pain out of RESTful controllers. About 50 or 60 people turned up so we ended up all crowded round a couple of laptops. But everyone was friendly and there was no heckling. Paolo, who gave the incredibly entertaining talk on widgets, took some photos

There was, however, plenty of heckling at the RejectConf talk. The format was 5 minutes of slides, 20 seconds automatic countdown for each one. I wrote the presentation during the day, and ran through it with my wife before hand. It sucked - way too much info. So I cut it down.

However, I was first up, and gave the audience the choice of the insane, or sane, talk, and they chose insane. I tried to get across about 1/2 an hour's worth of stuff in 5 minutes, and the audience looked as though they were in a wind tunnel. It was a great icebreaker for the other speakers though. I'll post the slides here in due course.

RejectConf was simply awesome by the way. The berlin ruby user group rocks.

On the 'glories' of spam

September 14th, 2007

It’s hardly a controversial position, but I don’t like spam. I really hate it, actually. Past email addresses eventually got so clogged that I had shut shut them down & create news just to regain sanity in my life. This approach is perhaps best called the “Slash and Burn” method.

But… you can’t deny that spam has come up with some wonderful things. Well, specifically, one: the spurious names that are appended to the “from” header. They consist of a combination of a couple of words taken randomly from the dictionary & a “middle initial”, all intended to bypass your spam filters. These random couplings sometimes beget glorious results:

  • Double O Tedious (Irish, perhaps?)
  • Urinate G. Coordinator (this almost sounds like a job title)
  • Omens H. Absolutism
  • Gunshots I. Senatorial (I’ve received this one many times over the years—perhaps these aren’t as random as I thought)
  • Religiously H. Panacea (interesting combination there)
  • Stultifies H. Putrescence
  • Chuvash B. Residue
  • Powering H. Kahlua (for the adolescent alcoholic)

Tracking our time spent on projects has been a major pain for us. We've tried a few of the OS X solutions out there, and they all have some of the following problems:

  • B L O A T E D - so many useless features
  • You have to either turn the tracker on and off manually, which we invariably forget to do, or
  • You can associate one application with turning the tracker on - which is not fine grained enough

So we rolled our own. matewatch simply checks what TextMate documents you're editing and records the intervals of time that those documents are frontmost in TextMate.

Howto

It's written in ruby (of course), and you need to grab rubygems, and install rubyosa and activesupport. (See the bottom of this post for instructions on this.)

Then install matewatch into a directory in your $PATH:

  svn export http://svn.ardes.com/ardes/matewatch/matewatch

Fire it up (in verbose mode to see what its doing)

  matewatch start -v

in another terminal, add a project, then do some work

  matewatch add rails_plugins ~/Development/rails_plugins

This is what the output looks like

  +++ Textmate active at Fri Sep 14 14:45:17 +0100 2007
  [#] plugins started at Fri Sep 14 14:45:27 +0100 2007
  --- Textmate inactive at Fri Sep 14 14:45:37 +0100 2007
  [ ] plugins stopped at Fri Sep 14 14:45:37 +0100 2007

Then, when you want to see how much time you've been hacking away

  matewatch report rails_plugins
  
  ======================================
  rails_plugins: report => Fri 14/Sep/07
  ======================================
   - 11/Sep/07:   0:00:35
   - 12/Sep/07:   4:40:01
   - 13/Sep/07:   2:35:16
   - 14/Sep/07:   0:00:10
   ----- TOTAL:   7:16:03 [7 hours, 30 minutes (15 min chunks)]

You can get a variety of reports. just do matewatch help to see what's available.

Sleepwatcher

If you install sleepwatcher (just the daemon, you don't need to install the startup item) then matewatch will interrupt sessions when the computer goes to sleep.

Happy tracking

We've just completed a small project that we estimated would take 14 hours - with matewatch we learnt that it took us closer to double that. It's turning out to be be a very useful little script.

If you find a bug, or have a feature request, let us know.

Installing dependencies

Ruby

See this guide for getting a suitable ruby on OS X

Rubygems

Get rubygems extract it, and set it up with

  sudo ruby setup.rb
gems

Now install the required gems

  sudo gem install activesupport
  sudo gem install rubyosa

All the options

This is the output of matewatch help


matewatch watches textmate and logs how long you are working on files in spec-
ified directories

USAGE

  matewatch COMMAND [OPTIONS]
  
  matewatch start
    Starts the project watcher
    
    --verbose               -v            show output
    --poll=<n>              -p <n>        poll every (n) seconds
    --require-frontmost     -r            require that textmate be frontmost
                                          application to log time data
  
  matewatch report [<name>]
    Show brief report of hours/minutes per day

    --hourly  OR  --session               show hourly/session report
    --from=<date>           -f <date>     from specified date
    --to=<date>             -t <date>     to specified date
    --day=<date>            -d <date>     for specified date

  matewatch list
    List projects being watched
    
  matewatch add <name> <path> [<position>]
    Add a project to the watch list
    
  matewatch remove <name>
    Remove a project from the watch list.  Copies the project data to
    a timestamped backup in /Users/ian/.matewatch
    
  matewatch move <name> <position>
    Move a project up or down the list

  matewatch pause
    Will pause the current matewatcher, 'matewatch start' will restart it

DATA

  If you want to get at the session data for your projects, you'll find them
  in /Users/ian/.matewatch.
  
  The files are YAML format, and so are easily editable/exportable.

    by Argument from Design (c) 2007 <http://www.ardes.com> (MIT License)


resources_controller - update

September 5th, 2007

It's been a while, but some major improvements to resources_controller have just been checked in.

The test coverage has slipped just a bit - that's my next task. However, it's still pretty good. Thanks to the RC group (in particluar Chris Hapgood) and the Guys at Greenvoice for nudging me towards singular resources, and away from lots of inherited controllers.

Major headlines

  • Singular resources are fully supported
  • RC loads enclosing resources by default, which means you pretty much just have to write one line to have all your routes taken care of (more on this below).
  • Cleaner code

Go get get it from the ArDes plugins page.

What follows is from the rdoc

With resources_controller (http://svn.ardes.com/rails_plugins/resources_controller) you can quickly add an ActiveResource compliant controller for your your RESTful models.

Examples

Here are some examples - for more on how to use RC go to the Usage section at the bottom, for syntax head to resources_controller_for

Example 1: Super simple usage

Here's a simple example of how it works with a Forums has many Posts model:

  class ForumsController < ApplicationController
    resources_controller_for :forums
  end

Your controller will get the standard CRUD actions, @forum will be set in member actions, @forums in index.

Example 2: Specifying enclosing resources

  class PostsController < ApplicationController
    resources_controller_for :posts, :in => :forum
  end
As above, but the controller will load @forum on every action, and use @forum to find and create @posts

Wildcard enclosing resources

All of the above examples will work for any routes that match what it specified

  
              PATH                     RESOURCES CONTROLLER WILL DO:

  Example 1  /forums                   @forums = Forum.find(:all)

             /users/2/forums           @user = User.find(2)
                                       @forums = @user.forums.find(:all)

  Example 2  /posts                    @posts = Post.find(:all)

             /forums/2/posts           @forum = Forum.find(2)
                                       @posts = @forum.posts.find(:all)

             /sites/4/forums/3/posts   @site = Site.find(4)
                                       @forum = @site.forums.find(3)
                                       @posts = @forum.posts.find(:all)

             /users/2/posts/1          This won't work as the controller specified
                                       that :posts are :in => :forum

It is up to you which routes to open to the controller (in config/routes.rb). When you do, RC will use the route segments to drill down to the specified resource. This means that if User 3 does not have Post 5, then /users/3/posts/5 will raise a RecordNotFound Error. You dont' have to write any extra code to do this oft repeated controller pattern.

With RC, your route specification flows through to the controller - no need to repeat yourself.

If you don't want to have RC match wildcard resources just pass :load_enclosing => false

  resources_controller_for :posts, :in => :forum, :load_enclosing => 'false'

Example 3: Singleton resource

Here's an example of a singleton, the account pattern that is so common.

  class AccountController < ApplicationController
    resources_controller_for :account, :class => User, :singleton => true do
      @current_user
    end
  end

Your controller will use the block to find the resource. The @account will be assigned to @current_user

Example 4: Allowing PostsController to be used all over

First thing to do is remove :in => :forum

  class PostsController < ApplicationController
    resources_controller_for :posts
  end

This will now work for /users/2/posts.

Example 4 and a bit: Mapping non standard resources

How about /account/posts? The account is found in a non standard way - RC won't be able to figure out how tofind it if it appears in the route. So we give it some help.

(in PostsController)

  map_resource :account, :singleton => true, :class => User, :find => :current_user

Now, if :account apears in any part of a route (for PostsController) it will be mapped to (in this case) the current_user method of teh PostsController.

To make the :account mapping available to all, just chuck it in ApplicationController

This will work for any resource which can't be inferred from its route segment name

  map_resource :peeps, :source => :users
  map_resource :posts, :class => BadlyNamedPostClass

Example 5: Singleton association

Here's another singleton example - one where it corresponds to a has_one or belongs_to association

  class ImageController < ApplicationController
    resources_controller_for :image, :singleton => true
  end

When invoked with /users/3/image RC will find @user, and use @user.image to find the resource, and @user.build_image, to create a new resource.

Putting it all together

An exmaple app

config/routes.rb:

 map.resource :account do |account|
   account.resource :image
   account.resources :posts
 end

 map.resources :users do |user|
   user.resource :image
   user.resources :posts
 end

 map.resources :forums do |forum|
   forum.resources :posts
   forum.resource :image
 end

app/controllers:

 class ApplicationController < ActionController::Base
   map_resource :account, :singleton => true, :find => :current_user

   def current_user # get it from session or whatnot
 end

 class ForumsController < AplicationController
   resources_controller_for :forums
 end
   
 class PostsController < AplicationController
   resources_controller_for :posts
 end

 class UsersController < AplicationController
   resources_controller_for :users
 end

 class ImageController < AplicationController
   resources_controller_for :image, :singleton => true
 end

 class AccountController < ApplicationController
   resources_controller_for :account, :singleton => true, :find => :current_user
 end

This is how the app will handle the following routes:

 PATH                   CONTROLLER    WHICH WILL DO:
 
 /forums                forums        @forums = Forum.find(:all)
 
 /forums/2/posts        posts         @forum = Forum.find(2)
                                      @posts = @forum.forums.find(:all)

 /forums/2/image        image         @forum = Forum.find(2)
                                      @image = @forum.image   
 
 /image                       no route

 /posts                       no route

 /users/2/posts/3       posts         @user = User.find(2)
                                      @post = @user.posts.find(3)
 
 /users/2/image POST    image         @user = User.find(2)
                                      @image = @user.build_image(params[:image])

 /account               account       @account = self.current_user

 /account/image         image         @account = self.current_user
                                      @image = @account.image

 /account/posts/3 PUT   posts         @account = self.current_user
                                      @post = @account.posts.find(3)
                                      @post.update_attributes(params[:post])

Views

Ok - so how do I write the views?

For most cases, just in exactly the way you would expect to. RC sets the instance variables to what they should be.

But, in some cases, you are going to have different variables set - for example

  /users/1/posts    =>  @user, @posts
  /forums/2/posts   =>  @forum, @posts

Here are some options (all are appropriate for different circumstances):

  • test for the existence of @user or @forum in the view, and display it differently
  • have two different controllers UserPostsController and ForumPostsController, with different views (and direct the routes to them in routes.rb)
  • use enclosing_resource - which always refers to the... immediately enclosing resource.

Using the last technique, you might write your posts index as follows (here assuming that both Forum and User have .name)

  <h1>Posts for <%= link_to enclosing_resource_path, "#{enclosing_resource_name.humanize}: #{enclosing_resource.name}" %></h1>

  <%= render :partial => 'post', :collection => @posts %>

Notice enclosing_resource_name - this will be something like 'user', or 'post'. Also enclosing_resource_path - in RC you get all of the named route helpers relativised to the current resource and enclosing_resource. See NamedRouteHelper for more details.

This can useful when writing the _post partial:

  <p>
    <%= post.name %>
    <%= link_to 'edit', edit_resource_path(tag) %>
    <%= link_to 'destroy', resource_path(tag), :method => :delete %>
  </p>

when viewed at /users/1/posts it will show

 <p>
   Cool post
   <a href="/users/1/posts/1/edit">edit</a>
   <a href="js nightmare with /users/1/posts/1">delete</a>
 </p>
 ...

when viewd at /forums/1/posts it will show

 <p>
   Cool post
   <a href="/forums/1/posts/3/edit">edit</a>
   <a href="js nightmare with /forums/1/posts/3">delete</a>
 </p>
 ...

This is like polymorphic urls, except that RC will just use whatever enclosing resources are currently loaded to generate the urls/paths.

General Usage

To use RC, there are just three class methods on controller to learn.

  resources_controller_for name, options, &block
 
  nested_in name, options, &block

  map_resource name, options, &block

Customising finding and creating

If you want to implement something like query params you can override find_resources. If you want to change the way your new resources are created you can override new_resource.

  class PostsController < ApplicationController
    resources_controller_for :posts

    def find_resources
      resource_service.find :all, :order => params[:sort_by]
    end

    def new_resource
      returning resource_service.new(params[resource_name]) do |post|
        post.ip_address = request.remote_ip
      end
    end
  end

In the same way, you can override find_resource.

Writing controller actions

You can make use of RC internals to simplify your actions.

Here's an example where you want to re-order an acts_as_list model. You define a class method on the model (say *order_by_ids* which takes and array of ids). You can then make use of *resource_service* (which makes use of awesome rails magic) to send correctly scoped messages to your models.

Here's how to write an order action

  def order
    resource_service.order_by_ids["things_order"]
  end

the route

  map.resources :things, :collection => {:order => :put}

and the view can conatin a scriptaculous drag and drop with param name 'things_order'

When this controller is invoked of /things the :order_by_ids message will be sent to the Thing class, when it's invoked by /foos/1/things, then :order_by_ids message will be send to Foo.find(1).things association

Liquid error: undefined method `current_page' for #