Archive for the ‘work’ Category

And Now For Something Completely Useless

Tuesday, September 23rd, 2008
class Model < ActiveRecord::Base
  has_many (Dir.entries("#{RAILS_ROOT}/app/models")-['.','..',"#{self.to_s.downcase}.rb"]).delete_if{|a|
File.directory?("#{RAILS_ROOT/app/models/#{a}")}.rand.gsub(".rb","").pluralize.to_sym
end

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.

My Last Day

Monday, February 4th, 2008

It’s been a while, but two Fridays ago was my last day working for SeaLink, well, for this contract anyway. You never know if I might get a new contract with them further down the road and I would love to come back and work with those amazing people.

I completed their online bookings “module” for their site as the last thing I did before I left, and it seemed they really appreciate it. I even got a better card than what Coles gave me, this one was actually bought from a shop instead of written hastily on receipt paper. Written in it was:

“Dear Ryan, Thank you for all your help & friendship and best of luck with the next exciting chapter of your life. Keep in touch & we all look forward to hear from you. All the best Ali.”

“Dear Ryan, It was good working with you at your blistering pace :) Have fun, and I’ll see you at next Ruby meetup. Anuj.”

“Dear Ryan, It has been a pleasure working with you. Thank you for your hard work & best wishes for the future. Michael”

“Dear Ryan, It was great to work with you. hope you do great things in future… Good luck with your next assignment! Have fun!!! Keep in touch. Vish”

“Dear Ryan Best of Luck. Who will Elise and I argue with no? Keep in touch to let us know what’s next. Ems (Emma)”

“Dear Ryan, Wishing you all the very best with your next adventure! It was lovely to work with you. Now put your shoes back on! Elise”

I look forward to catching up with these guys again in the coming weeks.

P.S. The shoes reference is I used to take my shoes off when it got really hot as I feel I work better without wearing sweaty shoes. I also used to walk 2km to work every day.

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.