Slug that slash

Thu, 17.03.2011 rails 3, url, slug

I was doubting, whether to title this post Slash that slug, but let’s leave it like it is…

Anyways, the known way to embellish your URL in Rails is the definition of to_param in your model. Most of the solutions offered out there, including the Rails Guides, propose joining the id with the name of the model with a score, like this: /posts/478-a-post-title/. But I don’t really like that. I rather separate concerns. The id should always be the same, while the name should be modifiable. If editing a model’s name changes your URL, you can always redirect the old one to the new one (or the other way round) with permalinks, a task where that constant id will help enormously. So I prefer this URL: /posts/478/a-post-title/. But how to accomplish it easily?

There are just two things to do. The first is to define it in your model, as said:

1
2
3
4
5
6
7
def to_param
  "#{id}/#{slug}"
end

def slug
  title #or whatever, here you can make use of a plugin like 'friendly_id'
end

The second step is to inform your routing system. Rails 3 turns out to offer a very simple way to do it. If you checked the above link, you might already have a clue:

1
resources :posts, :constraints => { :id => /[0-9]+\/.+/ }

or, if you aren’t defining a resource:

1
match 'posts/:id' => 'posts#show', :constraints => { :id => /[0-9]+\/.+/ }

Here we are defining a constraint on the id through a regular expression. Beware that this id is the one that the routing system understands as directly coming from the model as our to_param defines it, and not the real id of the post object as in the database. As this regex is defined, we are supposing that the model’s id is an integer number, and the slug is a normal string.

You can even simplify the route’s definition, if you please:

1
2
3
resources :posts, :id => /[0-9]+\/.+/
# or
match 'posts/:id' => 'posts#show', :id => /[0-9]+\/.+/

An added benefit is that you won’t have to clutter your routing calls, only a clean link_to is needed:

1
link_to @post.title, @post

This is a simple method to get modern and friendly URLs for your models in Rails 3. Less code, more beauty. Nice?

1 comment

The case of 'case/when' vs 'if/include'

Mon, 17.01.2011 rails 3, splat, include

I am not a native English speaker (and probably not a native developer either), so I had to search for splat, as I found this word the other day. It turned out to be the name of that funny operator which looks like an asterisk and is written pressing the asterisk key of your keyboard. But don’t get confound, it is not an asterisk, it is a splat, like so: *

As a result, I ended up reviewing my knowledge of that somehow ignored Ruby operator. Usually, most of us know from doing

1
2
3
4
def some_method(important_parameter, *args)
  do_something_with(important_parameter)
  super(args)
end

Kind of a Mary Poppins bag, where anything can pop in and out. On this respect, I found this very detailed post about the possibilities of the splat, which motivates mine now. Go through it and perhaps improve your understanding of this array operator, and afterward go back to the section 4.Case/When. The code in example is actually been taken from production code in current Rails 3.0.3:

1
2
3
4
5
6
7
8
9
10
def find(*args)
   ...
   case args.first
   when :first, :last, :all
      send(args.first)
   else
      find_with_ids(*args)
   end
   ...
end

That code called my attention: a case statement with a single when. I immediately thought about an obvious alternative:

1
2
3
4
5
6
7
8
9
def find(*args)
   ...
  if [:first, :last, :all].include? args.first
    send(args.first)
  else
    find_with_ids(*args)
  end
   ...
end

Both sets of code will give the same result. The Rails version allows having more alternatives if we wanted (like following different paths for :first, :last and :all ), but for now let us follow YAGNI and ask ourselves: if both do the same thing, which one of them is going to be faster? Let’s refactor and make use of that one!

So, I made my homework, and wrote the following file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class BM
  def self.a
    [1,2,3]
  end

  # rails benchmarker 'BM.casewhen'
  def self.casewhen
    9999.times do
      case 4
      when *a
        b=1
      else
        b=2
      end
    end
  end
  
  # rails benchmarker 'BM.ifinclude'
  def self.ifinclude
    9999.times do
      if a.include? 4
        b=1
      else
        b=2
      end
    end
  end
end

The evaluation will result as false since 4 is not in the array and Ruby will follow the else branch. This to ensure the worst case, so that the array is always gone through. I will measure the difference in running time for a standard Ruby 1.9.2 installation on a Debian Squeeze.

To make it fast and simple, I put this file into some rails project at app/model/bm.rb and run both methods manually 7 times each (which I suppose enough to avoid random delays due to other processes in my system), with the following results:

  • rails benchmarker ‘BM.casewhen’
    => real time: 14489 13849 14862 16344 16667 15016 15047
    => Mean: 15182, Standard Deviation:996.73

  • rails benchmarker ‘BM.ifinclude’
    => real time: 14266 13886 12886 14044 13973 14484 12896
    => Mean: 13776.42857, Standard Deviation:636.4741

As you can see, the second method is taking about a 10% less time to execute. I have to say that it doesn’t surprise me, a case statement should intuitively take more time to evaluate than an if statement, but you can run the test with other configurations (JRuby, Windows, you name it). If you do so, please comment here on your results.

And we all love our servers spending less time doing finds, isn’t it? I know that you’re thinking: “these three lines don’t make the world”, but I bet that ‘find’ is in the top ten of executed sentences in our applications. So why not get this little boost for free?

I am on my way of making it into the next Rails version.

Update 1:
With the help of rafmagana I realized that my code was incomplete. He had a much more clear code which I squeezed for newer, discouraging results (real time offered):

  • first with when: 0.923457
  • first with include?: 1.201857
  • last with when: 0.926521
  • last with include?: 1.301077
  • all with when: 0.945277
  • all with include?: 1.354739
  • find_with_ids with when: 3.035178
  • find_with_ids with include?: 1.816336

As we can see, :first, :last and :all prefer when (25-30% faster) whereas find_with_ids prefers include? (40% faster). How logical is that?
I mean, find_with_ids has (should have) to go through each item in the list and see whether it is there or not before the final diversion into the else. So it has more work to do and does it faster?
I hope I have a lesson learned: check every possibility, even if it seems unlogical, you never know…

Update 2:
The next time I want to do something on performance, remind to see again this incredible video from Aaron Paterson at the RubyConf 2010. I already downloaded a copy for my own future reference.

0 comments

Star Rating for Rails 3 and jQuery

Sun, 28.11.2010 rating, rails 3, jquery, ajax

You might be starting a project (or upgrading an older one) on our shiny newly branded Rails 3, and in that process, you might as well be looking for a solution to the problem of offering a trendy Star Rating system in your app. Googling for “Rails rating” tells you to go with the ajaxful-rating plugin, to which I won’t offer you the link, since that won’t work (for now) with Rails 3.

There are Other Solutions TM out there, but some won’t work if you have more than one Star Rating Form in a single page (although much of this post was inspired by it). But there are good news: you needn’t reinvent the wheel. You are probably using jQuery anyways, so why not just use one of its plugins? jQuery Star Rating Plugin is the way to go. And why? Because it is great, and I am telling you here how to make use of it! (but be careful, I use it only in a prototype of an app that we are building, no production stories yet).

So, you installed the jquery-rails plugin (at least version 0.2.5 if downloading from github). You also downloaded the files from the jQuery Star Rating Plugin and put them into your public/javascripts folder, along with its (only!) two images under public/images and its stylesheet under public/stylesheets/jquery.rating.css . What’s next? Obviously, you need a set of models, controller and views. I went with the good ol’ acts_as_rateable plugin, also available as a gem, but you might not need so much power (it polymorphically allows to rate different models). Let’s say you want to rate comments on a post.

1
2
3
4
class Comment
  belongs_to :post
  acts_as_rateable
end

And under app/views/posts/show.html.erb you have something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<% for comment in @post.comments %>
  <h4><%= comment.title %></h4>
  <p><%= comment.body %></p>
  <p>by <%= comment.author %> on <%= comment.date %></p>
  <p>
    Rating: <%= comment.average_rating %>
    <%= form_for(Ratings.new) do |f| %>
      <% 1.upto(5) do |value| %>
        <%= star_button f, comment, value, (value==5) %>
      <% end %>
      <%= hidden_field_tag("comment_id", comment.id) %>
    <% end %>
  </p>
<% end %>

That’s a simple example of how comments would look like (you may extract everything inside the for into a partial, but that’s another story). Notice a couple of things:

  • There is no way to change your rating once you cast it. For what I know, acts_as_rateable doesn’t let you do it either, but I did not investigate much on that yet.
  • We are offering 5 stars, you might want other solutions for your upto(5) (at line 8).
  • There are two helper methods on that code:
    1. average_rating (at line 6) comes with acts_as rateable and gives us, well, eh…, yes, the average rating of the given comment.
    2. star_button (at line 9) I wrote:

      1
      2
      3
      
        def star_button(f, comment, value, checked)
          radio_button_tag("rating[#{comment.id}]", value, checked, :class => 'star')
        end
      

      This key method is self explaining, and builds the markup that our jQuery Star Rating Plugin understands. I would only point out that you could make the checked radiobutton/star appear a value different than my chosen 5, for instance referring to the last rating score given by the current_user or to the mean of users (average_rating) or something else. I chose it quite neutral.
      Put it inside app/helpers/posts_helper.rb , and that will provide the following code:

      1
      2
      3
      4
      5
      
      <input class="star" id="rating_15_1" name="rating[15]" type="radio" value="1" />
      <input class="star" id="rating_15_2" name="rating[15]" type="radio" value="2" />
      <input class="star" id="rating_15_3" name="rating[15]" type="radio" value="3" />
      <input class="star" id="rating_15_4" name="rating[15]" type="radio" value="4" />
      <input checked="checked" class="star" id="rating_15_5" name="rating[15]" type="radio" value="5" />
      

      where 15 is the comment’s id. Respect the class star for the plugin to work.


You can already let it run and see how it looks like. Not that bad for that little work. And the best news is: we are almost finished. Get this at app/controllers/ratings_controller.rb :

1
2
3
4
5
6
7
8
9
10
11
class RatingsController < ApplicationController
  def create
    @comment = Comment.find_by_id(params[:comment_id])
    if @comment.rate_it(params[:rating][@comment.id.to_s], current_user.id)
      respond_to do |format|
        format.html { redirect_to post_path(@comment.post) :notice => "Your rating has been saved" }
        format.js
      end
    end
  end
end

The method rate_it (at line 4) also comes with acts_as_rateable , and wants a value (coming from the POST params) and a user rating the comment.
Notice that we are trying to allow nice degradation when JS is not activated. That’s why we provide format.html (at line 6). You can try this as a pure HTML form (add a submit button on your form!, like this: <%= f.submit :rate %>).
But for AJAX to work, we need a last thing. The one that motivates this post, since the jQuery Star Rating Plugin doesn’t provide a way to save your rating.
Put the following under public/javascripts/jquery.rating.save.js :

1
2
3
4
5
6
7
8
9
10
$(document).ready(function() {
  // Hides the submit button
  $('.new_rating').children('input[type=submit]').addClass('hide');

  // Submits the form (saves data) after user makes a change.
  $('.star').click(function(){
        form = $(this).parent().parent();
        form[0].submit();
    });
});

Provided that you have a CSS class in your stylesheets called hide saying display:none , your submit button will disappear. And when you click on that rating star, it will automatically fetch its parent form and submit it. You can even have as many simultaneous forms in your page as you like.

Let me know if you get some issues. Happy rating!

14 comments