unwwwritten
Taking a rest and making friends
Posted August 9th, 2008 at 11:23 am PDT by S. Brent Faulkner — View Comments
So, what's next on the list?
- each blog entry will be referenced by a friendly link (rather than its id)
In my last post, I mentioned that I had considered using a single BlogController to handle all requests for my blog... I didn't. First off, the blog consists of Posts and Comments. Now, from a data (or model) point of view these may be the same, but as far as usage goes they get handled a bit differently. Plus, since I ultimately want to have some sort of RESTful API for the blog, and possibly the site in general, I think it makes more sense to divide the work up into two separate controllers (with a nested route).
So, now I've got a PostsController and a CommentsController with the following route(s)...
map.resources :posts do |post| post.resources :comments end
For anyone who hasn't seen a nested route before, they let you access nice RESTful resources like /posts/1/comments. Now, I don't like the default implementation of some of the nesting, but I'll talk about that in another post (if you just can't wait, you can check out my selective_mapper project on github).
To access a specific post, the generated show, edit, update and delete actions for the PostsController all reference the posts by id... not the friendliest way of linking to a post. Luckily for us, it's really easy change that behaviour.
First off, is the link. We'll add a permalink column to our posts table and we'll fill it in from our post's title (replacing characters where appropriate). There are a number of plugins out there that will take care of this for you (for example, permalink_fu by Rick Olson aka technoweenie).
Just so that we can explore a few more things in this post, I'll look at the basic method of doing this yourself. In the post model, we'll add a before_validation filter to set the permalink attribute from the title.
class Post < ActiveRecord::Base before_validation :update_permalink protected def update_permalink self.permalink = title if permalink.blank? self.permalink = permalink.gsub(/\W/, '_').downcase end end
This filter is pretty simple... set the link from the title (if not already set), replace anything that is not a word character (alphanumeric or underscore) with an underscore and then slam it all to lower case. We do this before validation so that we can add appropriate validations for the permalink and those validations occur on the cleaned up permalink...
class Post < ActiveRecord::Base validates_format_of :permalink, :with => /^\w+$/ validates_uniqueness_of :permalink end
Once we have a unique permalink, Rails makes it really easy to use the link as a parameter instead of the id. All you need to do is override the to_param method of your model class...
class Post < ActiveRecord::Base def to_param permalink end end
... and as Emeril would say, BAM!
Now, as I implemented my Project model later on, I realized that I wanted the same sort of permalink ability for that model. I wanted to convert my project name into a friendly link so that I could reference the projects/warp_core page instead of projects/3. Since I didn't think the world wanted YAPP (yet-another-permalink-plugin), I just added a has_permalink module in my lib directory and included it in my environment.rb...
module ActiveRecord class Base class << self def has_permalink(options = nil) options ||= { :permalink => :title } link_attr = options.keys.first title_attr = options.values.first before_validation :update_permalink validates_presence_of title_attr, link_attr validates_format_of link_attr, :with => /^\w+$/ validates_uniqueness_of link_attr self.class_eval "def to_param;#{link_attr};end" self.class_eval "def update_permalink;self.#{link_attr}=#{title_attr} if #{link_attr}.blank?;self.#{link_attr}=#{link_attr}.gsub(/\\W/,'_').downcase;end" end end end end
Then, in each of my models I was able to simply call my new has_permalink method.
class Post < ActiveRecord::Base has_permalink end class Project < ActiveRecord::Base has_permalink :link => :name end
If I need this functionality again in this project, it's all there and ready. If (or when) I need it in another project, I may move it into a plugin -- more likely, I'll use someone else's plugin since this was just an exercise in how it could be done. One reason I may build my own plugin is that most of the other plugins out there seem to use dashes instead of underscores in permalinks... I use underscores to maintain backward compatibility with my urls for my historic blog posts.
Anyway, friendly links for restful resources without custom routes... simple, eh?
Cheers.
blog comments powered by Disqus