Posts Tagged ‘tutorial’

Pretty-ifying URLs

Saturday, April 18th, 2009

In today’s modern world we are not limited (cough, FORTRAN) to storing information in just a couple of bits. Some say that having pretty URLs is good SEO. I say it’s just common sense. I want the URL to tell a story, not give me some number that is only important to the system it’s coming from.

This is where to_param comes in.

By default this is defined like this:

def to_param
  id
end

Pretty high-tech stuff there. I won’t explain it to you. Give it a moment to sink it. et-cetera. This method is called on an object when it is passed to a url helper such as link_to or redirect_to. It is important to note that you should NEVER call @object.id as that will give the id of the object, not the to_param version of that name.

You can override this in your model in order to give you pretty urls! The way you do this is:

def to_param
  "#{id}-#{name.parameterize}"
end

Where id is the id of the model and name is the text you want into the URL. We call parameterize on this text in order to make it URL friendly.

The reason why we put in the id in the URL is two-fold. Firstly, (/forums/1-best-forum-ever) is so that when it gets passed to our controller as params[:id] or similar, we’re able to pass it to a finder:

Forum.find(params[:id])
# will be passed in as...
Forum.find("1-best-forum-ever")

Now you may go “Hey… wait a second! I don’t have an ID in my database that says ’1-best-forum-ever’!” and you’d be right, you don’t. But you do have an ID of 1 for a record somewhere. Rails will call to_i on this value and convert it simply down to 1 and you’ll be able to treat the pretty URL as if it were a real ID.

Secondly, if two objects in your database have the same name they won’t clash for the parameterized versions, since the id will always be unique.

But I don’t want my ID in my URL

Hey, that’s cool too! Just a little tougher… Before you save your records you’ll want to write out the permalink as a new field, so define a field called permalink in your table if you want to take this route. Then you do a before_save and define to_param like this:

before_create :set_permalink

def set_permalink
  self.permalink = name.parameterize
end

def to_param
  permalink
end

Now when you call the finder you’re going to need to find_by_permalink! instead of just find:

Forum.find_by_permalink!(params[:id])

We have to use the bang version of find_by_permalink because this will raise an ActiveRecord::RecordNotFound exception if the record is not found, just like find. (Thanks to Yarsolav Markin for mentioning this)

So there you have it. to_param overriding for pretty URLs.

Testing Gems with multiruby

Thursday, April 2nd, 2009

A couple of days ago I was requested politely and with acknowledgement of my humanity, by Dr Nic to write a post about testing gems on 1.9 and because he’s my boss I’m obliged to do the things he tells me to do. He told me to read his post about Future-proofing Your Ruby Code and in particular the instructions about installing multiruby.

How to install multiruby

You can install multiruby, different versions of ruby, rubygems and useful gems using these commands:

sudo gem install ZenTest
multiruby_setup mri:svn:tag:v1_8_6_114
multiruby_setup mri:svn:tag:v1_8_7_153
multiruby_setup mri:svn:tag:v1_9_1_0

multiruby_setup update:rubygems
multiruby -S gem install --no-ri --no-rdoc --development test-unit

This will:

  1. Install ZenTest (or the latest version)
  2. Install Ruby 1.8.6, build 114
  3. Install Ruby 1.8.7, build 153
  4. Install Ruby 1.9.1, build 0
  5. Setup Rubygems for all three versions and;
  6. install test-unit for all three versions!

You may also want to multiruby -S gem install –no-ri –no-rdoc –development rails mocha rspec.

multiruby is awesome because it sticks this all in your home directory in a folder called .multiruby, so don’t worry about it clashing with your existing version(s) of ruby. Different versions go in different directories inside .multiruby/versions.

numero!

Today we’re going to be testing a gem that I recently made use of on this site. For those of you that visited the site yesterday you would’ve seen something like this:

  112.117.116.115.32.34.104.101.108.108.111.32.119.111.114.108.100.34

This is numero! (exclamation mark compulsory). I don’t know where the idea came from. According to the MSN logs I keep, at 8:03am Friday morning I went and made breakfast and then at 8:13am I had the idea. But it’s awesome. The “language” uses the ASCII representation of all your favourite characters and separates them using a dot. This is numero! script. You run it using the numero! gem. This is how you get it:

sudo gem install radar-numero --source http://gems.github.com

Test it out by running it on your favourite ruby script!

numero favourite.rb

This will generate a file called favourite.numero and then evaluate the newly generated numero! code. Now you can throw away your ugly ruby file and bathe in the glory that is numero by then running:

numero favourite.numero

So it appears we’ve gone off on a tangent here! Fear not! For I want you to test my gem on your new multiruby setup:

git clone git://github.com/radar/numero
cd numero
git checkout -b broken origin/broken

Now this sensible naming schema will probably dissolve the mystery surrounding what’s going to happen next. You’re going to be testing my gem, and it’s going to break.

Testing numero!

When I wrote this particular branch of numero!, I used “a”[0] to get the ASCII representation (97) of the letter a. Generally, I would’ve used use a variable rather than “a”, but this is just an example. The problem is that this doesn’t work the same way in Ruby 1.8.* and Ruby 1.9. If we run multiruby -S test/test_numero.rb we see that it passes on both versions of Ruby 1.8, but fails on Ruby 1.9!

Ruby 1.8.*

Loaded suite test/test_numero
Started
.
Finished in 0.001186 seconds.

1 tests, 1 assertions, 0 failures, 0 errors

Ruby 1.9 Loaded suite test/test_numero Started F

Finished in 0.025228 seconds.

1) Failure: testnumerocompiles(TestNumero) [test/test_numero.rb:9]: <”[\"p.u.t.s. .\\".h.e.l.l.o. .w.o.r.l.d.\\"\"]“> expected but was <”112.117.116.115.32.34.104.101.108.108.111.32.119.111.114.108.100.34″>.

1 tests, 1 assertions, 1 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications

Not good! This is something I wouldn't have found if I didn't test the functionality on multiple Rubies. Using multiruby is essential to ensure that your code works for everybody. We don't want to end up like Internet Explorer.

The solution, of course, is to use str.unpack("C").first instead. You have two options: 1) change the code yourself in lib/numero.rb to use the afore-mentioned example or 2) git checkout master. Now we run the tests again and now Ruby 1.9 passes:

Ruby 1.8.7

Finished in 0.001109 seconds.

1 tests, 1 assertions, 0 failures, 0 errors

Ruby 1.9.1

Finished in 0.001353 seconds.

1 tests, 1 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications

Lovely!

Single Table Inheritance

Sunday, September 28th, 2008

This tutorial was written using Ruby 1.8.6, Rails 2.1.

A lot of the time I see people asking how they can do something like access levels for their Rails applications and this usually boils down to some STI (single table inheritance) love. You’ll need to have restful_authentication installed.

What is Single Table Inhertiance?

Single Table Inheritance is where you have multiple models that inherit from a single table, hence the name. It’s great for situations like this where we want multiple user access levels, but we don’t want to create multiple tables for the different kinds of users of our application.

Okay.. so how do I use it?

Firstly you start off with one model that inherits from ActiveRecord::Base, and I would advise running script/generate authenticated person session to get this:

app/models/person.rb

class Person < ActiveRecord::Base

end

And then you subclass some other classes from Person app/models/student.rb
class Student < Person

end

app/models/teacher.rb
class Teacher < Person

end

app/models/admin.rb
class Admin < Person

end

The great thing about splitting these into individual files is because they’re more easily managed and closely follows the convention of one model per file.

Now you have a person, student and teacher model and you should have a brand new migration for your people table in db/migrate so go on and open that up and you’ll see something like this:

class CreatePeople < ActiveRecord::Migration
  def self.up
    createtable "people", :force => true do |t|
      t.column :login,                     :string, :limit => 40
      t.column :name,                      :string, :limit => 100, :default => '', :null => true
      t.column :email,                     :string, :limit => 100
      t.column :cryptedpassword,          :string, :limit => 40
      t.column :salt,                      :string, :limit => 40
      t.column :createdat,                :datetime
      t.column :updatedat,                :datetime
      t.column :remembertoken,            :string, :limit => 40
      t.column :remembertokenexpiresat, :datetime

end
add_index :people, :login, :unique => true

end

def self.down droptable "people" end end

To enable STI for this table, just add:
t.column :type, :string
into the create
table block, just after t.column :remembertokenexpires_at, :datetime. This type column will be set to whatever class the record is, so if we create a new Student object, this type column will be set to “Student”.

Controllers

To create new objects for these subclasses I would recommend building a people controller and using the form partial from that to base your form off from the other controllers, such as StudentsController and TeachersController. These controllers should NOT subclass from PeopleController, they should be their own independent controllers because Teachers, People and Students are all individual resource types.

Associated Models

If you have associated models (e.g. Person hasmany :enrolments), any subclass of Person will inherit this relationship. For the relationship, this will be referenced via personid still in the enrolments table.

A bit of refactoring love

Friday, June 13th, 2008

Find, Find, Find, Find, I don’t think so…

As explained in previous posts, Rails controllers have 7 default actions (index, new, create, show, edit, update, destroy). Four of these seven actions make the same find call, Model.find(params[:id]) and this tutorial is to tidy that up so you’re not repeating yourself over four different actions. To clean this up we’ll just call a before filter:

class ForumsController < ApplicationController
  before_filter :find_forum

  # Actions go here
  
  private
    def find_forum 
      @forum = Forum.find(params[:id])
    end
end

Now you may be thinking, "Why are we doing that? That's 5 lines!". Think about if you wanted to change the find statement, and now you'll begin to picture why. Changing one line is much easier than changing four. For example, if I wanted to find forums by their slugs instead of an ID I would simply change @forum = Forum.find(params[:id]) to @forum = Forum.findbyslug(params[:id]). Of course, for this to work with the restful routes helpers the way we expect it to (e.g. forumpath(@forum) -> /forums/the-first-forum), we'll need to re-define #toparam in our model:

class Forum
  def to_param
    slug
  end
end

Common Lookups

Sometimes you'll have data initialised for your forms and you'll want to initialise this data multiple times. Instead of repeating yourself like this:

class ForumsController

  def new
    @forum = Forum.new
    @something_special = SomethingSpecial.find(:all, :order => "id DESC")
  end
 
  def create
    @forum = Forum.new(params[:forum])
    if @forum.save
      flash[:success] = "A forum has been created."
      redirect_to @forum
    else
      flash[:failure] = "A forum could not be created."
      @something_special = SomethingSpecial.find(:all, :order => "id DESC")
      render :action => "new"
    end
  end

end

You could instead have:

class ForumsController

  def new
    @forum = Forum.new
    common_lookups
  end
 
  def create
    @forum = Forum.new(params[:forum])
    if @forum.save
      flash[:success] = "A forum has been created."
      redirect_to @forum
    else
      flash[:failure] = "A forum could not be created."
      common_lookups
      render :action => "new"
    end
  end

  private
    def common_lookups
      @something_special = SomethingSpecial.find(:all, :order => "id DESC")
    end
end

Shorter Routing

One last thing that I'd like to show you is shorter routing. Ever since the restful routing helpers were added, routing to specific controllers and their actions has become easier and easier. Rails 2.0 makes it extremely easy, but first we'll see how far we've come:

  1. <%= link_to @forum, { :controller => "forums", :action => "show", :id => @forum.id } %>
  2. <%= link_to @forum, forum_path(@forum) %>
  3. <%= link_to @forum, forum_path %>
  4. <%= link_to @forum, @forum %>
  5. <%= link_to @forum %>

As long as there's a #tos method in the Forum model it will insert that as the phrase shown to the user for the link. All of the above should produce the same URL, with the exception of the first which will produce /forums/show/1, and going down the list they're just shorter ways of writing the same thing. If you had nested routes such as forumtopicpath(@forum, @topic) you could do <%= link_to @topic, [@forum, @topic] %> as the extremely short version of it. The reason why we can't do just <%= link_to [@forum, @topic] %> is because this will show the tos version of @forum, followed immediately by the to_s version of @topic.

Updating a select box based on another

Tuesday, May 20th, 2008

In two projects now I’ve had to use code where I update a select box (machines) in regards to what’s selected in the first (customers). mark[oz] from #rubyonrails asked how to do this last week and I told him I would write a blog post the following night. 5 days later, here it is.

I use this when I’m creating a new call and the user needs to select which customer and then which machine. I have the following in my CallsController:

def new
  @call = Call.new
  @customers = Customer.find(:all, :order => "name ASC")
end
Relatively simple. We instantiate a new call object so the form knows where to go, and we define @customers so the first select box has some information.

Of course in our Customer model we have:

class Customer < ActiveRecord::Base
  hasmany :machines
end
And the machine model:
class Machine < ActiveRecord::Base
  belongsto :customer
end
And the call model:
class Call < ActiveRecord::Base
  belongsto :machine
  belongsto :customer

These define the relationships used within the system.

Next is the new.html.erb page itself.

New Call
<% form_for @call do |f| %>
  <%= render :partial => "form" %>
  <%= submit_tag "Create" %>
<% end %>
And a stripped-down version of the form partial:
<%= f.label "customer_id" %>
<%= f.select "customer_id", @customers.map { |c| [c.name, c.id] }%>
<%= observe_field "call_customer_id", :url => by_customer_machines_path, :with => "customer_id" %>
<%= f.label "machine_id" %>
<%= f.select "machine_id", "Please select a customer" %>
Right now that we have the form all set up, lets set up our machines controller
class MachinesController < ApplicationController

CRUD operations go here

def bycustomer @machines = Machine.findallbycustomerid(params[:customerid]) end end

And our bycustomer.rjs:
page.replacehtml 'callmachineid', optionsfromcollectionforselect(@machines, "id", "name")
And finally the config/routes.rb:
map.resources :customers do |customer|
  customer.resources :machines
end

map.resources :machines, :collection => { :by_customer => :post }

Now when we select a customer from the top drop down, the machines drop down should be populated too.

ActiveResource & ZenDesk

Wednesday, May 14th, 2008

With many, many thanks to Frederick Cheung, who without this would’ve been way more painful and time-consuming. My original question and our discussion can be found here.

At my new job we signed up with ZenDesk, which acts as our helpdesk/ticketing system for our clients who sign up to our site and buy our product. Because ZenDesk uses emails instead of plain ol’ usernames for authentication, Ruby chucked a fit when we tried doing stuff like:

class User < ActiveResource::Base
self.site = "http://ouremail@ourwebsite.com:ourpassword@ourplace.zendesk.com"
end
Ruby’s URI class just didn’t like that first @ sign in there! So Fred originally recommended we try to encode it, %40. That didn’t work. Then the next morning Fred suggests doing this:
class User < ActiveResource::Base
self.site = "http://ourplace.zendesk.com"

def (self.site).user "ouremail@ourwebsite.com" end

def (self.site).password "ourpassword" end

end

And ‘lo and behold the thing worked!

So for all you savvy kids using Rails’ ActiveResource and trying to make it play nice with ZenDesk, that’s how we did it.

Then I went a little further with the refactoring and just made a ZenDesk model:

class ZenDesk < ActiveResource::Base

self.site = "http://ourplace.zendesk.com"

def (self.site).user "ouremail@ourwebsite.com" end

def (self.site).password "ourpassword" end

end end

And then got the models I wanted to inherit from that.
class User < ZenDesk
end
Brilliant!

Also one more note. If you’re going through and testing out creating Organisations/Users in ZenDesk through ActiveResource, don’t forget to delete them as you go! It’s time-consuming clicking edit, and then the delete button for 50 odd objects… but of course you could do this:

for i in originalobjectid..lastobjectid
User.find(i).destroy
end
Which, if you did it, say, all in the same day like I did, should delete all the users within that range.

Self-Referential Relationships

Tuesday, April 29th, 2008

I’ve seen this question asked time and time again, so I’m going to write a short tutorial about how to do it. The question is self-refferential relationships for a model, often the User model to determine the relationship between two different users. I’ll assume that you’ve already got a Rails application and at least a User model for this. We’ll use a has_many :through relationship to define which users are related to who.

Let’s generate a model for the relationship: script/generate model relationship. This will generate a migration which we’ll create our relationships table with.

db/migrate/xxxcreaterelationships.rb

class CreateRelationships < ActiveRecord::Migration
  def self.up
    createtable :relationships do |t|
      t.integer :userid, :friendid
      t.string :relationshiptype
    end
  end

def self.down drop_table :relationships end end

And that should do us. Run rake db:migrate to add the table in.

Now we go into our User model and we add in the following:

app/models/user.rb

class User < ActiveRecord::Base
  hasmany :relationships
  hasmany :friends, :through => :relationships
end
And in our relationship.rb model:

app/models/user.rb

class Relationship < ActiveRecord::Base
  belongsto :friend, :classname => "User"
  belongs_to :user
end
And now we see if it works:

script/console

>> u = User.findbyname("Ryan")
=> #

u.friends << User.findbyname("Charlie") => # u.save => true

Now what if we want to change that relationship field? We’ll add in two methods in to the user model to find the relationship for a specific user.

app/models/user.rb

def to_i
  id
end

def findrelationshipwith(user) Relationship.findbyfriendid(user.toi) end

The first method, toi, will return just the id for the user. The reason why we do this is because in the next method, findrelationshipwith we pass in a single argument, user. Now because we’ve defined the toi method on our User model, this means we can either pass in a user id or a user object to this method, and it will call to_i on whatever we pass in, ending up with an id. When the method’s done, it will return a relationship object which you can then modify.

Best of luck.

Administration Namespacing

Sunday, March 16th, 2008

A while after completing work at SeaLink, Tom asked me about my forum hobby project and why it wasn’t working. This led to me working on it for a few days and getting it all up to scratch again, and this involved moving it over to Rails 2.0 (that’s how long I hadn’t worked on it for), and using the awesomeness that is namespacing.

Namespacing is where you have some controllers in a separate area of your site. In my example, I have an admin folder in app/controllers which contains all the controllers for the administration section of my site, and all the relevant actions. Just inside the app/controllers directory, I have the other controllers which do all the basic stuff such as showing forums, basically anything a standard user can do.

It seems to be a fairly common question asked in places, so I figured if I sat down and wrote this, I would have something to send to people, much like my Restful Routes tutorial, which ideally should’ve covered namespacing too.

First of all we’re going to create our namespace. To do that, we open up config/routes.rb and specify our namespace:

map.namespace(:admin) do |admin|
admin.resources :forums, :topics, :posts
end
Now what this does is defines some routes for us. If you’ve seen map.resources you’ll know that this defines routes for us, for example doing map.resources :forums will define methods such as forumspath which is the same as { :controller => “forums”, :action => “index” } and forumpath(@forum) which is the same as { :controller => “forums”, :action => “show”, :id => @forum.id }. These methods really are lifesavers and save you a hell of a lot of typing. The routes defined by this namespace method however are prefixed with whatever argument you pass it, in this case we’ve passed it :admin so it’s going to give us routes like adminforumspath, which is the same as { :controller = > “admin/forums”, :action => “index” }, and as you can see again saves us a lot of typing.

Now that we have our namespace, we can create our subfolders. These subfolders are placed in app/controllers and app/views and are given the same name as the namespace, admin. So go ahead and do that now. In app/controllers/admin is where we place our controllers. As an example, here’s what my forums controller’s edit and update actions look like:

class Admin::ForumsController < Admin::ApplicationController
  beforefilter :storelocation, :only => [:index, :show]
  def edit
    @forum = Forum.find(params[:id]) 
    @forums = Forum.find(:all, :order => "title") - [@forum] - @forum.descendants
  end

def update @forum = Forum.find(params[:id]) if @forum.updateattributes(params[:forum]) flash[:notice] = "Forum has been updated." redirect else flash[:notice] = "Forum has not been updated." render :action => "edit" end end end

What I really want to show you in here is only the first line the class is defined as Admin::ForumsController, which shows that we’re namespacing it. We don’t have to define the Admin prefix anywhere. What we do have to define however is our non-existant Admin::ApplicationController. In my code, I’ve defined my own Admin::ApplicationController as a means of calling methods that should be called before all admin actions, such as my nonadminredirect method, which is defined in lib/authenticatedsystem.rb and goes something like this:
  def nonadminredirect
    if !isadmin?
      flash[:notice] = "You need to be an admin to do that."
      redirectbackordefault(:controller => "/accounts", :action => "login")
    end
  end
To define your Admin::ApplicationController, make a file in app/controllers/admin called applicationcontroller.rb. Even though the main application controller is defined as application.rb in app/controllers, that file is automatically loaded by Rails. If we named our applicationcontroller to just application.rb, it would not be automatically loaded because Rails only looks for application.rb and files ending in controller.rb in the app/controllers directory, so we name ours applicationcontroller.rb so it plays nice with Rails.

In here we define our class, layout and helper:

class Admin::ApplicationController < ApplicationController
  layout "admin"
  helper "admin"
  beforefilter :nonadminredirect
end
I’ve defined a new layout here because my admin layout is different to my main layout, but still includes some elements from it (thanks to nestedlayouts)

The before_filter is triggered before every action in the admin controller to make sure it’s an admin doing the action rather than a standard user.

And that’s all there is to it, really. It’s all pretty simple. Now all you’ve gotta do is generate your views. Remember to place them in app/views/admin/thecontroller’sname, otherwise you’ll run into problems.

It seems I forgot to mention how it’s supposed to work when you’re calling the method to go to the namespaced path, well that’s simple. If you have a forum you would like to edit, the correct method is editadminforumpath(forumobject), because you want to edit, in the namespace of admin, a certain forum. For paths not requiring a prefix, such as the show and index actions, they are adminforumpath(forumobject) and adminforums_path respectively.

For an action such as an update action, it would be adminforumpath(forumobject) with a :method => :put option specified in whatever you’re using. Usually you won’t have to do this, because the formfor helper would do it for you, but in some cases you might have to.

Restful Routing: An Overview

Sunday, January 6th, 2008

Here’s something I posted on rubyonrails talk:

In truth, restful routing is plain and simple. It’s like those books you wrote when you were a kid in kindergarten that if a book critic were to read them he would jab his eyes out with a pen.

For this example I’m going to use a personal favourite: a forum system. It’s small enough to not be overwhelming, yet large enough to explain how restful routing should work (and generally why you should use it).

It all starts with the good ‘ol config/routes.rb file. In here is where all the nice little routes live, from map.root all the way down to the map.connect ‘:controller/service.wsdl’, :action => ‘wsdl’ that many people still leave in their routes file, thinking that if they remove it the entire world would collapse upon itself into one small quantum singularity. In here you’d place something similar to:

map.resources :forums, :hasmany => :topics
map.resources :topics, :hasmany => :posts
map.resources :posts
Not running on Rails 2.0? Then this is the code you want:
map.resources :forums do |forum|
  forum.resources :topics, :nameprefix => "forum"
end

map.resources :topics do |topic| topic.resources :posts, :nameprefix => "topic" end

map.resources :posts

This defines routes like:
/forums/1
- Show a forum
/forums/1/topics
- Index action for a single forum
/forums/1/topics/1
- showing a single topic
/topics/1
- Same thing
/topics/1/posts/
- I would imagine this would do a similar thing to /topics/1
/topics/1/posts/1/edit
- Allows you to edit a single post
/posts/1/edit
- Same thing

Now to define something like this without the magic of restful routing, one would have to be clinically insane:

map.connect "/forums/:id", :controller => "forums", :action => "show"
map.connect "/forums/:forumid/topics/", :controller => "topics", :action => "index"
map.connect "/forums/:forumid/topics/:id", :controller => "topics", :action => "show"
map.connect "/topics/:id", :controller => "topics", :action => "show" <- Does this look familar to /forums/:id?
map.connect "/topics/:topic_id/posts", :controller => "posts", :action => "index"
map.connect "/topics/:topic_id/posts/:id/edit", :controller => "posts", :action => "edit"
map.connect "/posts/:id/edit", :controller => "posts", :action => "edit"
And this is only the tip of the iceberg!

Seeing a pattern here? Restful routing gives you a whole heap of cool stuff, namely the 7 core methods that I’ll cover right after the models.

A forum system has the following tables: forums, topics, posts and users, and the models would look something like the following:

class Forum < ActiveRecord::Base
  hasmany :topics
  hasmany :posts, :through => :topics
end

class Topic < ActiveRecord::Base
  hasmany :posts
  belongsto :forum
  belongs_to :user
end


class Post < ActiveRecord::Base
  belongsto :topic
  belongsto :user
end


class User < ActiveRecord::Base
  hasmany :topics
  hasmany :posts
  def to_s
    login
  end
end
The fields don’t matter, but throughout the tutorial I make reference to @forum.name or something similar, so we’ll assume forums has at least a name field. We’ll assume post has a text field and users has a login field.

That’ll give you some idea of how the system works: Forum -> Topics -> Posts.

In restful routing there are seven “core” methods (actions) that you’re given for the controllers: index, show, new, create, edit, update, destroy. Each of these have a set request method on them, for example you can’t GET to the create, update and destroy actions and you can’t post to the index, new or edit actions. These actions work with these request methods:

GET: index, show, new, edit POST: create PUT: update DELETE: destroy

“What the?! PUT & DELETE, where did they come from?”, I hear you cry! These are hacked into the calls for the appropriate action using javascript, it passes in one more parameter (_method) which is then handled by the rails code and depending on what method you called you will get the page you were looking for, or a routing error.

The forums controller could look like this:

class ForumsController < ApplicationController
  def index
    @forums = Forum.find(:all)
  end

def show @forum = Forum.find(params[:id]) end

def new @forum = Forum.new end

def create @forum = Forum.new(params[:forum]) if @forum.save flash[:notice] = "You have created a forum!" redirectto forumspath else render :action => "new" end end

def edit @forum = Forum.find(params[:id]) end

def update @forum = Forum.find(params[:id]) if @forum.updateattributes(params[:forum]) flash[:notice] = "You have updated #{@forum.name}" redirectto forum_path else flash[:error] = "This forum could not be updated." render :action => "new" end end

def destroy @forum = Forum.find(params[:id]) @forum.destroy flash[:notice] = "You have deleted #{@forum.name}" redirectto forumspath end

end

You’ll see here that I’ve twice made a call to redirectto using the argument of forumspath. Because we’ve defined map.resources :forums in our config/routes.rb file, it knows that we want to go to { :controller => “forums”, :action => “index” } and the best part is that we don’t have to keep trying { :controller => “forums”, :action => “index” } every time we want to go to that specific action, but instead we type forums_path.

I’ve also made a single call to forum_path, and I haven’t specified an argument for it, so how does Rails know that I want to go to the forum that I just updated?

Rails will see that there’s an argument mission from the forum_path and will go looking for the @forum instance variable you’ve defined in your controller. If you never defined one or defined it as something other than @forum, it will mention something about ambiguous routes and you’ll have to specify the variable.

Now what if you wanted to go to the new or edit action? Simple: newforumpath and editforumpath(@forum) will take you to the corresponding actions. Remember that you don’t need to specify an argument for the editforumpath if @forum is defined. Inside these actions you’ll want to go further, you’ll want to create a new forum and update a forum.

For the create action you could specify this for your form: Rails 2.0:

<% form_for @forum do |f|%>
Rails 2.0 will see that @forum is a new record and link you the create action.

Pre Rails 2.0:

<% form_for :forum, @forum, :url => forums_path do |f|%>
Prior to Rails 2.0 that checking wasn’t in, you’ll have to define your own link.

“B-b-b-ut”, you stammer, “you’ve linked to the forums index, right? Isn’t that what forums_path is?”

Well, yes and no. This has everything to do with the four request methods mentioned previously, because the form’s method attribute is “post”, Rails knows that if you’re posting to forums_path, you mean the create action. And now for the update action!

Here the form_for’s a little different, but only for pre-Rails 2.0:

Rails 2.0:

<% form_for @forum do |f| %>
Again the same deal applies: Rails 2.0 knows that @forum is not a new record, so it’ll link you to to the update action because it’s included in a form. This automatically specifies :html => { :method => :put } for you.

Pre Rails 2.0

<% form_for :forum, @forum, :url => forum_path(@forum), :html => { :method => "put" } do |f| %>
It knows to link you to the update action because of the method => “put” we’ve specified.

Now lets escape from the confines of a single controller and bring the topics controller into the mix. In the forum show action is where you would generally show all the topics for that forum, but for the purposes of this tutorial I will do it in the topics controller instead. This will have something similar defined to the forums controller but personally I would define this for the controller:

class TopicsController < ApplicationController
  beforefilter :getforum

def index @topics = @forum.topics end

#other actions private def getforum @forum = Forum.find(params[:forumid]) if params[:forum_id] end end

The private call makes any method after it private, that means that if you were to try and access this method (without restful routing), it would play dumb. Personally, I think something like this should be in-built to Rails, if you’re accessing a child object (topics) from a parent (forum) it should automatically define @forum for you.

Because we’ve already defined the :hasmany topics on map.resources :forums, the topic routes are already defined for us, so to view all the topics for a forum, before you would have to do define a route like this:

map.connect "/forums/:forumid/topics/", :controller => "topics", :action => "index"
and then call it like this:
{ :controller => "topics", :action => "index", :forumid => @forum.id }
Instead you’ve already defined :hasmany => :topics, so instantly you’ll gain access to forumtopicspath. Again the wonderful Rails will realise that you want all topics for the @forum object and then direct you to “/forums/1/topics/” through forumtopicpath. To edit a single topic, you could do editforumtopicpath as of Rails 2.0, or forumedittopicpath prior to Rails 2.0. The first reads more like “edit this topic belonging to this forum” where the second reads like “in this forum, edit this topic”. Alternatively you could ditch the whole forum part out of the method call and just do edittopicpath because we’ve defined map.resources :topics.

Throwing one more controller into the mix now, called postscontroller. This would be very similar to the topics controller but instead of getforum it would have get_topic, modified correctly.

Now what if you wanted to add a custom action to posts_controller, called quote? This action would bring up a form with the post you were quoting which would then send the information from the form into posts/new to create a new post:

config/routes.rb

map.resources :posts, :member => { :quote => :get }
The extra argument of :member indicates a hash of any further actions and their request methods you would like to be added onto singular posts. You can call these like all the other singular methods: quotepostpath(@post), for example. The request methods can be in string or symbol format, it doesn’t matter.
def quote
  @oldpost = Post.find(params[:id])
  @post = Post.new
  @post.text = "[quote='#{@oldpost.user}']#{@old_post.text}[/quote]"
  render :action => "new"
end
Here I’ve defined the old post only to get the text and the user’s name from it, and then we’re rendering the new view so we have the form. Everything from there on is taken care by Rails.

Now what if you want to define a new action to work with a group of posts? Well you define it like this: routes.rb

map.resources :posts, :member => { :quote => :get }, :collection => { :destroyall => :delete }
Now if you wanted to destroy all posts for a topic, bar the first one, with an action like this (remembering @topic is defined in gettopic): postscontroller.rb
def destroyall
  @posts = @topic.posts  - @topic.posts.first
  @posts.each { |post| post.destroy }
  flash[:notice] = "All posts have been deleted.
end
You would linkto it like this:
<%= link_to "Delete all posts", destroy_all_topic_posts_path(@topic), :method => "delete", 
:confirm => "Are you sure you want to delete all posts from this topic?" %>
The :method => “delete” corresponds with the :delete
all => :delete we specified in config/routes.rb.

One last thing is nested form_fors. Say you want to edit a post within a topic. To do this you would use this code:

<% form_for [@topic, @post] do |f| %>
  <%= render :partial => "form", :locals => { :f => f } %>
  <%= submit_tag "Update" %>
<% end %>

Here you pass one argument still to form_for, an array. It passes the array and will give you a url like /topics/1/posts/1 if you were editing an existing post, or /topics/1/posts if you were creating a new post.