Courtesy of Twitter

Posts Tagged ‘tutorial’

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.find_by_slug(params[:id]). Of course, for this to work with the restful routes helpers the way we expect it to (e.g. forum_path(@forum) -> /forums/the-first-forum), we’ll need to re-define #to_param 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 #to_s 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 forum_topic_path(@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 to_s 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
has_many :machines
end

And the machine model:

class Machine < ActiveRecord::Base
belongs_to :customer
end

These define the relationships used within the system.

Next is the new.html.erb page itself.

<strong>New Call</strong>
<% 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 by_customer
@machines = Machine.find_by_customer_id(params[:customer_id])
end
end

And our by_customer.html.erb:

<%= options_from_collection_for_select(@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 original_object_id..last_object_id
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/xxx_create_relationships.rb

class CreateRelationships < ActiveRecord::Migration
def self.up
 
create_table :relationships do |t|
t.integer :user_id, :friend_id
t.string :relationship_type
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
has_many :relationships
has_many :friends, :through => :relationships
end

And in our relationship.rb model:

app/models/user.rb

class Relationship < ActiveRecord::Base
belongs_to :friend, :class_name => "User"
belongs_to :user
end

And now we see if it works:

script/console

>> u = User.find_by_name("Ryan")
=> #
>> u.friends << User.find_by_name("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 find_relationship_with(user)
Relationship.find_by_friend_id(user.to_i)
end

The first method, to_i, will return just the id for the user. The reason why we do this is because in the next method, find_relationship_with we pass in a single argument, user. Now because we’ve defined the to_i 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 forums_path which is the same as { :controller => “forums”, :action => “index” } and forum_path(@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 admin_forums_path, 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
before_filter :store_location, :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.update_attributes(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 non_admin_redirect method, which is defined in lib/authenticated_system.rb and goes something like this:

  def non_admin_redirect
 
    if !is_admin?
 
      flash[:notice] = "You need to be an admin to do that."
 
      redirect_back_or_default(:controller => "/accounts", :action => "login")
 
    end
 
  end

To define your Admin::ApplicationController, make a file in app/controllers/admin called application_controller.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 application_controller 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 application_controller.rb so it plays nice with Rails.

In here we define our class, layout and helper:

class Admin::ApplicationController < ApplicationController
  layout "admin"
  helper "admin"
  before_filter :non_admin_redirect
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 nested_layouts)

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/the_controller’s_name, 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 edit_admin_forum_path(forum_object), 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 admin_forum_path(forum_object) and admin_forums_path respectively.

For an action such as an update action, it would be admin_forum_path(forum_object) with a :method => :put option specified in whatever you’re using. Usually you won’t have to do this, because the form_for 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, :has_many => :topics
map.resources :topics, :has_many => :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, :name_prefix => "forum_"
end
 
map.resources :topics do |topic|
topic.resources :posts, :name_prefix => "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/:forum_id/topics/", :controller => "topics", :action => "index"
map.connect "/forums/:forum_id/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
has_many :topics
has_many :posts, :through => :topics
end

class Topic < ActiveRecord::Base
has_many :posts
belongs_to :forum
belongs_to :user
end

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

class User < ActiveRecord::Base
has_many :topics
has_many :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.create(params[:forum])
flash[:notice] = "You have created a forum!"
redirect_to forums_path
end
 
def edit
@forum = Forum.find (params[:id])
end
 
def update
@forum = Forum.find(params[:id])
@forum.update_attributes(params[:forum])
flash[:notice] = "You have updated #{@forum.name  }"
redirect_to forum_path
end
 
def destroy
@forum = Forum.find(params[:id])
@forum.destroy
flash[:notice] = "You have deleted #{@forum.id}"
redirect_to forums_path
end
 
end

You’ll see here that I’ve twice made a call to redirect_to using the argument of forums_path. 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: new_forum_path and edit_forum_path(@forum) will take you to the corresponding actions. Remember that you don’t need to specify an argument for the edit_forum_path 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
before_filter :get_forum
 
def index
@topics = @forum.topics
end
 
#other actions
private
def get_forum
@forum = Forum.find(params[:forum_id]) if params[:forum_id]
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 :has_many 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/:forum_id/topics/", :controller => "topics", :action => "index"

and then call it like this:

{ :controller => "topics", :action => "index", :forum_id => @forum.id }

Instead you’ve already defined :has_many => :topics, so instantly you’ll gain access to forum_topics_path. Again the wonderful Rails will realise that you want all topics for the @forum object and then direct you to “/forums/1/topics/” through forum_topic_path. To edit a single topic, you could do edit_forum_topic_path as of Rails 2.0, or forum_edit_topic_path 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 edit_topic_path because we’ve defined map.resources :topics.

Throwing one more controller into the mix now, called posts_controller. This would be very similar to the topics controller but instead of get_forum 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:

In 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: quote_post_path(@post), for example. The request methods can be in string or symbol format, it doesn’t matter.

def quote
@old_post = Post.find(params[:id])
@post = Post.new
@post.text = "[quote='#{@old_post.user}']#{<wbr></wbr>@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:

<strong>routes.rb</strong>
map.resources :posts, :member => { :quote => :get }, :collection => { :destroy_all => :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 get_topic):

<strong>posts_controller.rb</strong>
def destroy_all
@posts = @topic.posts  - @topic.posts.first
@posts.each { |post| post.destroy }
flash[:notice] = "All posts have been deleted.
end

You would link_to 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.