A DRY and RESTful Blog
One of the main ideas behind writing my own blog application, was to explore the implementation of the Simply Restful plugin in Edge Rails.
In practice Simply Restful adds some relatively benign features to Rails, to support some less benign changes in how we structure our applications. Lots have been said about this since David Hannson’s keynote presentation at RailsConf 2006, but let us have a look at what the changes mean for a blog such as this one.
Routing Resources
The main feature of Simply Restful is the new way of specifying routes. Here is the line from routes.rb, that does almost all of the routing needed for Biilmann Blog:
map.resources :users, :sessions, :pages, :search, :menu_categories, :menu_entries
This single line of code defines a whole lot of routes. Let us take the pages controller as an example. Because of the inclusion in the list of resources, the following requests will now be routed to it:
GET /pages => PagesController.index
GET /pages/:id => PagesController.show
GET /pages/new => PagesController.new
GET /pages /:id;edit => PagesController.edit
POST /pages=> PagesController.create
PUT /pages/:id => PagesController.update
DELETE /pages/:id => PagesController.destroy
The idea behind this is that all controllers should present a uniform interface that responds, in a logical way, to the basic CRUD operations, expressed through the HTTP methods: POST, GET, PUT and DELETE.
Restful Users
Biilmann Blog uses the popular acts_as_authenticated plugin for user authentication, but the users controller generated by the plugin doesn’t present the uniform interface that we strive to make all our restful controllers adhere to. The controller comes with the following methods:
- index
- login
- signup
- logout
Index is fine and signup can just be renamed to create, but login and logout doesn’t correspond to any CRUD operation on the user model. After all we are not creating or destroying users just because they log in and out. What we are destroying and creating is authenticated sessions. So in Biilmann blog the user controller is split into UsersController and SessionsController – each relying only on CRUD operations.
Associated Comments
An important feature of any half-decent blog is comments, and of course this one has them. The comments always belongs to a blogpost, so just calling CommentsController.index would hardly make any sense. We need to supply the controller with a blogpost id.
Simply Restful makes this easy. The following lines set up the routing for blogposts and comments:
map.resources :blogposts do |posts|
posts.resources :comments
end
Now blogposts gets the routes we already saw in the pages controller example, while comments gets some slightly different ones:
GET /blogposts/:blogpost_id/comments => CommentsController.index
GET /blogposts/:blogpost_id/comments/:id => CommentsController.show
GET /blogposts/:blogpost_id/comments/new => CommentsController.new
GET /blogposts/:blogpost_id/comments/:id;edit => CommentsController.edit
PUT /blogposts/:blogpost_id/comments/:id => CommentsController.update
POST /blogposts/:blogpost_id/comments => CommentsController.create
DELETE /blogposts/:blogpost_id/comments/:id => CommentsController.destroy
To view a blogpost with id 5, you would go to the url /blogposts/5. To create a comment for this blogpost you would send a HTTP POST request to /blogposts/5/comments.
What About the Views?
Making Rails apps the Simply Restful way requires us to change the way we construct our dynamic URLs a little. With the new routings we get some very handy named routes for free, so we better use them.
To create a link to the index action on our blogposts controller this is all we have to do:
<%= link_to blogposts_url %>
In the form we use when posting to the blog, the URL is the same:
<%= start_form_tag blogposts_url %>
The only difference is that the form will send a POST request, the link a GET request. Linking to the post itself can be done like this:
<%= link_to blogpost_url(:id => 5) %>
Notice the singularization of the controller name here. A delete link would use the same URL, but with an addition:
<%= link_to blogpost_url(:id => 5), :method => 'delete' %>
By specifying the method we tell the controller what to do with the specified post. Update works the same way.
What about the comments? How do we construct the /blogposts/5/comments URL?
In much the same way. Lets look at the form for a new comment. It could look like this:
<%= start_form_tag comments_url(:blogpost_id => 5) %>
This form will send a POST request to /blogposts/5/comments.
To get a link to the edit action for a comment we can do this:
<%= link_to edit_comment_url(:blogpost_id => 5, :id => 2) %>
This creates a link to /blogposts/5/comments/2;edit
Refactoring the CRUD Controllers
Earlier we had a look at the routes for the pages controller. Lets have a look at the first implementation of the create and destroy actions in PagesController.
def create
@page = Page.new(params[:page])
if params[:preview] > " "
render :action => 'new'
return
elsif @page.save
flash[:notice] = "New page created.'"
redirect_to :action => 'show'
return
end
render :action => 'new'
end
def destroy
@page = Page.find(params[:id])
if Page.destroy(params[:id])
flash[:notice] = "The page was deleted"
else
flash[:notice] = "Could not delete the page"
end
redirect_to :action => index
end
This is very basic and works just fine, but lets have a look at the same two actions from the blogposts:
def create
@blogpost = Blogpost.new(params[:blogpost])
@blogpost.user = current_user
if params[:preview] > "
render :action => 'new'
return
elsif @blogpost.save
flash[:notice] = "New entry posted"
redirect_to :action => 'show', :id => @blogpost.id
return
end
render :action => 'new'
end
def destroy
if Blogpost.destroy(params[:id])
flash[:notice] = "The entry was deleted"
redirect_to :action => 'index'
return
else
flash[:notice] = "The blogpost could not be deleted"
redirect_to :action => 'index'
return
end
end
One of the basic principles of coding that we should always strive towards, is the DRY one: Don’t Repeat Yourself! It doesn’t take more than a quick glance at these four methods to note that the code is soaking wet. And it only gets worse if we look through all of the application’s controllers. They all implement some of the methods:
- index
- show
- new
- create
- edit
- update
- destroy
And that’s it – there’s no other methods in any of the controllers, and apart from those in the sessions controller, the implementations of these methods all look very alike. This is a prime target for refactoring.
Mixing in a Module
Ruby offers us an obvious route to refactor a bunch of classes with similar methods: mixin a module. This is exactly what we are going to do. Here is the full CrudBase module I created for this blog:
module CrudBase
# CRUD Mixin - include in a controller to get the following actions
# index, show, new, create, edit, update, destroy.
# The module is has a bunch of hook methods to overwrite for a more specific behaviour
# after creation, updating or deletion.
# The module will try to infer the name of the instance variable set for use in the view,
# the parameters recieved from the forms and the model to operate on, from the name
# of the controller.
def create
set_instance_var model.new(params[obj_name])
create_associations
if params[:preview] && params[:preview] > "
preview_new
return
elsif instance_var.save
created
return
end
create_failed
end
def update
set_instance_var model.find(params[:id])
instance_var.attributes = params[obj_name]
if params[:preview] && params[:preview]> "
preview_edit
return
elsif instance_var.save
updated
return
end
edit_failed
end
def destroy
set_instance_var model.find(params[:id])
if model.destroy(params[:id])
destroyed
return
else
destroy_failed
return
end
end
def show
set_instance_var model.find(params[:id])
end
def new;
set_instance_var model.new
end
def edit
set_instance_var model.find(params[:id])
end
def index
instance_variable_set("@#{list_name}", model.find(:all, :conditions => "#{association_conditions}"))
end
protected
# these methods try to infer the name of the instance variables and models
def model
obj_name.camelize.constantize
end
def set_instance_var(value)
instance_variable_set("@#{obj_name}", value)
end
def instance_var
instance_variable_get("@#{obj_name}")
end
def list_name
controller_name
end
def obj_name
list_name.singularize
end
# Owerwrite in controller if more associations needs to be handled here
# This method should also be handled by the controller if you have
# protected attributes besides from the user_id that you dont want
# the user to be able to set through the post params
def create_associations
model.reflect_on_all_associations(:belongs_to).each do |association|
# if the object belongs to a user, set it to the current_user
if association.class_name == current_user.class.to_s
instance_var[association.primary_key_name] = current_user.id
else
instance_var[association.primary_key_name] = params[association.primary_key_name]
end
end
end
# Created the conditions for find. If an id of a belongs_to associations is specified in
# the params the find will be limited to objects belonging to that id
def association_conditions
conditions = "
model.reflect_on_all_associations(:belongs_to).each do |association|
if params[association.primary_key_name].to_i
conditions << " AND " if conditions > "
conditions << "#{association.primary_key_name} = #{params[association.primary_key_name].to_i}"
end
end
conditions = "1" if conditions == "
conditions
end
# the following methods is called after success or failure of the CRUD methods
# overwrite in the model for different behaviour
def created
flash[:notice] = "New #{obj_name} added"
redirect_to :action => 'show', :id => instance_var.id
end
def updated
flash[:notice] = "#{obj_name.capitalize} updated"
redirect_to :action => 'show', :id => instance_var.id
end
def destroyed
flash[:notice] = "#{obj_name.capitalize} deleted"
redirect_to :action => 'index'
end
def create_failed
render :action => 'new'
end
def edit_failed
render :action => 'edit'
end
def destroy_failed
flash[:notice] = "The #{obj_name} could not be deleted"
render :action => 'show', :id => instance_var.id
end
def preview_new
render :action => 'new'
end
def preview_edit
render :action => 'edit'
end
end
A Sign of Things to Come?
In a long post about Opinionated Controllers, Christian Anderson calls for something like the ActiveResource component that David Hannson proposed in his keynote, but on the end that is providing the resource.
He wants to be able to do something like:
controls :memberships
To get a CRUD controller for memberships.
What I have done here is hardly as elegant or generalized – it is really just a refactoring of the specific code in this blog app – but I like to think that it is at least a proof of the concept: showing that the creation of a convention based component for CRUD controllers is probably not too tough a nut to crack. Lets hope we get to see something like it in a future version of Rails.
RDoc widget
Stumbled across this wonderful RDoc Dashboard widget today.
With this widget all of the Rails and Ruby APIs is just a tap on F8 away. Incredibly nice for an API addict as me!
The widget really suffers from the lack of one feature, though: You cannot copy and paste anything from the RDoc documents.
I fiddled a bit with the source and figured out how to fix that. Here is the recipe:
Browse to the widget in the ~/Library/Widgets folder and select ‘Show Package Contents’ from the context menu.
Open the file RDoc.css in your favorite text-editor.
Under #docoDiv uncomment the line:
-apple-dashboard-region: dashboard-region(control rectangle);Add the line:
-khtml-user-select:text;
Save the file, and enjoy copying and pasting from the APIs!
Ruby on Rails and the X in front of HTML
Ruby on Rails seems to do its best to push developers towards XHTML. Every generated scaffold is marked up XHTML-style and all the useful helper-functions produce valid XHTML. But in spite of this, I think it is worth thinking twice about which doctype to use.
It is still not a very well known fact, but Internet Explorer has no real support for XHTML. It will happily display pages with XHTML doctypes, as long as they are fairly compatible with HTML, but it will use its ordinary rendering engine and just think about XHTML as a slightly weird form of HTML.
Firefox , Opera and Safari will think the same about the XHTML documents sent from a standard Rails application, but in contrast to IE these two browser actually have real support for XHTML documents, you just need to tell them to use it. The way you tell them that, is by setting the content-type in the HTTP-headers to 'application/xhtml+xml'. Do this, and suddenly XTHML supporting browsers will no longer think about your website as a big soup of tags, but as XML that better be well-formed and computer-readable.
XHTML Problems
As long as you keep serving XHTML as ‘text/xhtml’, it easily seems like the only difference between good old HTML and the doctypes with a X in front, is just the required lowercase and the closing of all tags. But start pushing XHTML supporting browser to use their XML rendering engines, and the differences will suddenly become far more visible.
Roger Johansson has written about the differences here
One important difference, that will be a showstopper for some, is that those wonderful javascript libraries which we Rails developers have come to love and trust, won’t work without modifications, when content is actually served as XHTML.
It shouldn’t come as a surprise, but in the XHTML DOM, there is no such thing as .innerHTML – an attribute that prototype.js depends heavily on.
And a solution
Of course you can always go on serving XHTML as text/html, but it seems backwards to specify one doctype in your documents, and ask the browsers to use the render for another doctype in your HTTP-headers.
This is the reason that this site is done in HTML 4.01, but this brings me back to Ruby on Rails helper functions. All these generate valid XHTML, but sometimes that means invalid HTML. I browsed the Rails source for a while, and found a nice solution for that. In your application helper add this:
def tag(name, options = nil, open = true)
"<#{name}#{tag_options(options.stringify_keys) if options}" + (open ? ">" : " />")
end
This overrides the tag function, and for me these 3 lines have been enough to solve all HTML 4.01 validation problems with Rails.
[Update – I’ve since changed the site from HTML to Transitional XHTML after all, in order to play around with MicroFormats a bit]
Biilmann Blog
Welcome to my new home planet, circling its lonely orbit in the void of a sunless cyberspace.
The inhabitant of this planet is a Danish musicologist, student of modern culture and music reviewer, turned computer programmer.
This web-hangout is a Ruby on Rails application made in a few days at the beginning of August 2006. Feel free to browse the sourcecode in the SVN repository here (UPDATE - this blog is now made entirely in the Webpop.com framework, and the old rails code is no longer online)
Enjoy your stay!
