resource_controller plugin
For those of you learning or using Ruby on Rails, here are my notes on Fabio Akita’s excellent screencast tutorial for James Golick’s resource_controller plugin. I find it to be useful to be able to refer to a written document quickly instead of having to watch the entire hour presentation if you forget something. Feel free to report errors or inaccuracies in my notes. I hope you find the notes to be handy too.
Notes on Fabio Akita’s screencast: http://www.akitaonrails.com/2008/1/25/easy-restful-rails-screencast
Topic: James Golick’s resource_controller plugin for Ruby on Rails: http://jamesgolick.com/resource_controller
Problem #1: RESTful controllers almost always have 7 methods that look nearly the same. There is a lot of repetitive code. This is not DRY. Instead, wouldn’t it be great if you could inherit from a standard RESTful superclass and just configure your controller if it needed.
Problem #2: When you have a has_many relationship between two models, and want to nest your routes. e.g. http://yoursite.com/posts/42/comments/3, then you have to change all your links in all your sub-model’s views. This is kind of a pain.
Solution: resource_controller plugin provides that RESTful superclass and generators to replace all the repetitive code and makes your controllers skinny like models. Standard models are customizeable through a DSL-like API provided by the plugin. Generic helper functions for views are also provided so that helper links are generic so you don’t have the problem of customizing links when making nested routes (is this true?).
This tutorial describes how to create blog of postings with a Post model and a Comment model.
The Comment model is polymorphic which means that it can be used to place comments not just on the Post model but any other model in the application.
Also, the Comment model will be using nested routes.
How to install:
In your rails_root application directory type:svn export http://svn.jamesgolick.com/resource_controller/tags/stable vendor/plugins/resource_controller
How to use the generator
ruby script/generate scaffold_resource Post title:string body:text
How to create a polymorphic has_many submodel
ruby script/generate scaffold_resource Comment commentary:references comment:text
Note: you need to make your own css and application layouts:
public/stylesheets/scaffold.css app/views/layouts/application.html.erb
CONTROLLERS
Now your controllers are skinny:
# /app/controllers/posts_controller.rb
class PostsController < ResourceController::Base
end
/app/controllers/comments_controller.rb
class CommentsController < ResourceController::Base
end
ResourceController::Base inherits from the usual ApplicationController
MODELS
Now modify your Post model to express the has_many relationship with the Comment model.
Ie. A post have many comments.
# /app/models/post.rb
class Post < ActiveRecord::Base
has_many :comments, :as => :commentary
end
Note: For non-polymorphic relationships you don’t need :as => commentary.
Modify your Comment model to have the polymorphic belongs_to relationship.
# /app/models/comment.rb
class Comment < ActiveRecord::Base
belongs_to :commentary, :polymorphic => true
end
How polymorphic tables works: Your comments table has two columns (in addition to the text field that stores your comment): commentary_id and commentary_type. commentary_type is the name of the table you want to reference. commentary_id is the row in the table that you’re referencing. In this way you can associate a comment to any row in any table in your database. You can comment on anything in your application.
CREATING A POST HAS_MANY COMMENT RELATIONSHIP, WHERE COMMENT IS POLYMORPHIC.
MIGRATIONS You need to modify your 002_create_comments.rb migration to make it polymorphic.
# /db/migrate/002_create_comments.rb
class CreateComments < ActiveRecord::Migration
def self.up
create_table :comments, :force => true do |t|
t.references :commentary, polymorphic => true
t.text :comment
t.timestamps
end # do
end # def self.up
def self.down
drop_table :comments
end
end # class
Now run your migration.
rake db:migrate
Now you need to add “belongs_to :post” to your controller.
This will make it so that you don’t have to load the parent resource in your controller like this
@post = Post.find(param[:post_id])
“belongs_to :post” will automatically load your parent resource. This reduces a lot of repetitive coding.
# /app/controllers/comments_controller.rb
class CommentsController < ResourceController::Base
belongs_to :post
end
Modify your routes.rb file to enable nested routing.
# /config/routes.rb file
ActionController::Routing::Routes.draw do |map|
map.resources :posts, :has_many => :comments
end
VIEWS In your /app/views/comments/_form.html.erb file, take out the commentary field, cuz that’s not something that is user-facing. It’s an internal field used to implement polymorphism.
Start up your server
script/server
Go to
http://localhost:3000/posts/new
Create dummy data in your database by filling out the form.
Now, you should be able to add a comment to that posting. (Assum your posting has an id of 1.) Go tohttp://localhost:3000/posts/1/comments/new
Fill out the form. It should have created a new comment linked to your post record with id 1.
Check to see if the comment has been associated with your post using the console: script/console
>> Post.find(1)
=> #<Post id: 1, title: "My first post", body: "restful post", .......>
>> Post.find(1).comments
=> [#<Comment id: 1, commentary_id: 1, commentary_type: "Post", comment: "This is a new comment." ......>]
>> Comment.count
=> 1
Where is the magic happening? Let’s look at the edit comment view.
Normally, without using the resource_controller plug, with nested resources, you would have to refer to the parent resource as post_comment_url(@post, @comment). With resource_controller, all you have to do is use: “object_url”. Ie. in the old way, the helper method names are coupled to the names of your models. In the new way, the information of the relationships are taken from the controller, and helper method names are independent of the names of the models. (Coupling is generally bad, because it means your code is brittle and linked dependent on other parts of your code. Independence is good because it makes your code more modular.)
# /app/views/comments/edit.html.erb
....
<%= link_to 'Show', object_url %> |
<%= link_to 'Back', collection_url %>
ADDING ANOTHER COMMENTABLE MODEL CALLED “ARTICLE”
To see the value of a polymorphism, let’s create another model called Article and use the same comments table to store comments on articles. Use the resource_controller generator to create articles:
ruby script/generate scaffold_resource Article title:string
author:string article:text
Create the database table.
rake db:migrate
Configure the models
Add the has_many clause.
# /app/models/article.rb
class Article <ActiveRecord::Base
has_many :comments, :as => :commentary
end
Configure the routes.rb file
# /config/routes.rb
ActionController::Routing::Routes.draw do |map|
map.resources :articles, :has_many => :comments
map.resources :comments
map.resources :posts, :has_many => :comments
end
Finally, add :article to the belongs_to method call in CommentsController
# /app/controllers/comments_controller.rb
class CommentsController < ResourceController::Base
belongs_to :post, :article
end
Now it should work.
Let’s say you already input an article with id of 1.
Then add a comment to your article
http://localhost/articles/1/comments/new
CREATING AN ADMIN NAMESPACE
Let’s say you want only the admin to create, edit, or delete Post instances.
Make an admin folder in in /app/controllers and /app/views.
mkdir app/controllers/admin
mkdir app/views/admin
mkdir app/views/admin/posts
Copy your your Post views into /app/views/admin/posts
Note the all the post views still exist in the normal directory location at this point. They are still accessible publicly at this point.
cp app/views/posts/* app/views/admin/posts/
Copy your post controlelr into /app/controllers/admin
Note: there are going to be 2 copies of the controller, one for the public and one for the admin. See how we configure each later.
cp app/controllers/posts_controller.rb app/controllers/admin
Modify your posts controller. Just add “Admin::” to the front of your PostsController.
# /app/controllers/admin/posts_controller.rb
class Admin::PostsController < ResourceController::Base
end
To restrict editing ability of posts from the public, delete the following files _form, edit, and new.html.erb in /app/views/posts/.
rm app/views/posts/_form.html.erb
rm app/views/posts/new.html.erb
rm app/views/posts/edit.html.erb
Take out the links to edit functionality in your remaining views:
app/views/posts/index.html.erb
remove these lines:
<%= link_to 'Edit', object_url(post) %>
<%= link_to 'Destroy', object_url(post), :confirm => ......... %>
<%= link_to 'New Post', new_object_url %>
Remove similar links in the show page: app/views/posts/show.html.erb
Configure the public PostsController:
# /app/controllers/posts_controller.rb
class PostsController < ResourceController::Base
actions :all, :except => [:new, :edit, :create, :update, :destroy]
end
Create the namespace routes in routes.rb. Add the following link at the bottom of your routes list in routes.rb
# /config/routes.rb
ActionController::Routing::Routes.draw do |map|
...
map.namespace :admin do |admin|
admin.resources :posts
end
end
If necessary restart your web server.
Now you can access:
http://localhost:3000/admin/posts
Note: Your admin section will have access to new, edit, destroy, etc..
And your public access is here:
http://localhost:3000/posts
NOW LET’S ADD PAGINATION BY CUSTOMIZING THE WAY THE POST CONTROLLER BUILDS COLLECTIONS OF POSTINGS. A COLLECTION IS A VARIABLE USED WHEN YOU WANT TO DISPLAY A LIST OF ALL YOUR RECORDS.
First, install the will_paginate plugin. By the way, there is a free screencast on will_paginate here: http://railscasts.com/episodes/51 Here is the command to install the plugin:
script/plugin install svn://errtheblog.com/svn/plugins/will_paginate
Without customization the PostsController builds collections like this: @collection = Post.find(:all) We want to customize this. Define the collection method to overwrite the way collections are built. end_of_association_chain is the way resource_controller plugin refers to the name of the model in question. In this case it will return the Post controller. If we were in the CommentsController, it would return the Comment model associated with params[:post_id].
private
def collection
@collection ||= end_of_association_chain.paginate :page => params[:page], :per_page => 5
end
Note: The paginate function comes from the will_paginate controller.
# /app/views/posts/index.html.erb # Add pagination links at the bottom.
....
<%= will_paginate @posts %>
OTHER METHODS FROM RESOURCE_CONTROLLER
For more documentation and helper methods, take a look at the resource_controller README file:
/vendor/plugins/resource_controller/README
For example,
To customize the create method. Place this inside your controller:
create do
# Sample customization code and methods:
flash "Object was created successfully! Right on!"
wants.js {render :template => "show.rjs"}
failure.wants.js {render :template => "display_errors.rjs"}
end
To create a before filter before your controller’s new method, place this in your controller:
new_action.before do
3.times { object.tasks.build } # This is just an example of a customization.
end
To customize the way your controller builds an individual object for the view, overwrite the build_object method.
private
def build_object
@object ||= end_of_association_chain.build_my_object_some_funky_way object_params
end
If that was your PostsController, then in your posts views you would be able to access your @post variable like normal, but it would be built in the funky way as prescribed by the way you defined build_my_object_some_funky_way method.
See the README for many other functions.
Posted by David Beckwith on Monday, March 24, 2008