Showing posts with label rails. Show all posts
Showing posts with label rails. Show all posts

Thursday, July 17, 2008

callback chains and metaclasses

For whatever reason (don't ask) our Rails app needed to be able to define callbacks on objects. As it stands now, you can only define callbacks on classes. Well, metaclasses are classes that pertain to a single specific object, so I figured I could add the callbacks there and everything will work.

Please excuse my extremely contrived example.

p = Post.find(...)
if p.contains_porn?
  class << p
    before_save :filter_content
  end
end
p.save # should trigger filter_content

Well it doesn't work.

After digging through the ActiveSupport source code, I saw that it only looks for callback chains in the object's class, not metaclass. That's not too hard to fix...

module ActiveSupport
  module Callbacks
    def run_callbacks(kind, options = {}, &block)
      callback_chain_method = "#{kind}_callback_chain"
      # Meta class inherits Class so we don't have to merge it in 1.9
      if RUBY_VERSION >= '1.9'
        metaclass.send(callback_chain_method).run(self, options, &block)
      else
        callbacks = self.class.send(callback_chain_method) | metaclass.send(callback_chain_method)
        callbacks.run(self, options, &block)
      end
    end
  end
end

Credit to Josh Peek for the Ruby 1.9 fix

Ok, so that got it working... with one major caveat: you cannot serialize objects that use callbacks. Ruby cannot serialize objects that have metaclasses and the run_callbacks method creates a metaclass whether you use it or not.

That's fine with me, I don't really like serializing objects anyways, but apparently it's a major problem for other people...

http://github.com/rails/rails/commit/e0846c8417093853f4f7f62732983e990c28d669
http://rails.lighthouseapp.com/projects/8994/tickets/575-callbacks-don-t-work-from-extended-modules

Josh Peek suggested a solution where we store the callback chains on the objects themselves, so object specific callbacks would be done like this...

p = Post.find(...)
p.before_save :filter_content if p.contains_porn?
p.save # should trigger filter_content

That seems like a very simple, clean and rational solution. I'll look into coding it up later.

Sunday, July 6, 2008

named scopes pwns will_paginate

will_paginate has its place. It's good for paginating when your queries are simple. My queries usually aren't simple, are yours? I noticed recently that one of my pages was taking a long time to display, around 1.9 seconds. I tracked it down to the will_paginate.

The problem is that will_paginate takes a single hash of ActiveRecord::Base#find arguments. That means if your query is really complex, then it's going to use that same complex query for both the result set and the count.

This is more or less what I was doing with will_paginate that it was choking on.

class Post < ActiveRecord::Base
  STATUS_OK = 1
  cattr_accessor :per_page
  per_page = 15
  has_many :watchers, ...
end

class PostsController < ApplicationController
  
  def index
    page_no = (params[:page] && params[:page].to_i) || 1
    @posts = Post.paginate :conditions => ["posts.status_id = ?", Post::STATUS_OK],
                           :order => 'group_id DESC, checksum', # don't ask, it's complicated
                           :include => :watchers,
                           :page => page_no
  end
  
end

The counting sql it makes out of that query does an outer join on watchers and selects distinct posts.id. Ouch. So the obvious solution is to simply do the count and result set queries ourselves. No biggie, but let's use named scopes to pretty things up.

class Post < ActiveRecord::Base
  STATUS_OK = 1
  cattr_accessor :per_page
  per_page = 15
  has_many :watchers, ...
  
  named_scope :ok, :conditions => {"posts.status_id" => Post::STATUS_OK}
  named_scope :recent, :order => "group_id DESC, checksum"
  named_scope :paginate, lambda { |page_no| {:offset => {(page_no-1)*per_page}, :limit => per_page} }
end

class PostsController < ApplicationController
  
  def index
    page_no = (params[:page] && params[:page].to_i) || 1
    @posts = Post.ok.recent.paginate(page_no).all(:include => :watchers)
    @post_count = Post.ok.count
  end
  
end

Pretty slick... and since we're doing the pagination "explicitly", we know that the counting is as simple as it needs to be. Let's take a look at the benchmarks, before and after.

# The SQL calls generated by will_paginate
Post Load (0.341523)
SELECT * FROM "posts" WHERE (post.status_id = 1) ORDER BY group_id DESC, checksum LIMIT 15 OFFSET 0
Watcher Load (0.005799)
SELECT "watchers".* FROM "watchers" WHERE ("watchers".page_id IN (...)) ORDER BY id
SQL (1.335042)
SELECT count(DISTINCT "posts".id) AS count_all FROM "posts" LEFT OUTER JOIN "watchers" ON watchers.post_id = posts.id WHERE (posts.status_id = 1)

# The SQL calls generated when we "manually paginate".
Post Load (0.350336)
SELECT * FROM "posts" WHERE ("posts"."status_id" = 1) ORDER BY group_id DESC, checksum LIMIT 15 OFFSET 0
Watcher Load (0.004582)
SELECT "watchers".* FROM "watchers" WHERE ("watchers".post_id IN (...)) ORDER BY id
SQL (0.023346)
SELECT count(*) AS count_all FROM "posts" WHERE ("posts"."status_id" = 1)

We save roughly 1.3 seconds by intelligently doing the counting. I really don't think using will_paginate buys you any cleaner code over using named scopes. Now all you have to do is implement a method similar to the will_paginate view helper and you can remove will_paginate from your project.

Monday, March 24, 2008

param_accessible

By popular (cue laughter) demand, I have added whitelisting to the popular (cue hysterical laughter) param_protected plugin. It is done via the param_accessible method.

I have also added support to properly handle array params.

For more details (i.e. documentation), please see the README file.

Wednesday, March 19, 2008

An HTML checkbox that submits when unchecked.

I call this... The Fake Checkbox!   :)

See, the problem is a normal HTML checkbox only submits to the server when it's checked. If it's unchecked, nothing gets sent to the server. Sometimes I want a value (like, umm, false?) sent to the server whether it's checked or unchecked.

So how do we go about making a checkbox that does this? The idea is simple -- make a normal checkbox with Javascript attached to it that updates a hidden input field when checked or unchecked.

Making a Rails helper to do this is simple -- put this in application_helper.rb.

    1 def fake_check_box_tag(name, checked_values, is_checked, html_options = {})
    2   onchange        = html_options[:onchange] || ""
    3   checked_value   = checked_values[:checked] || 'true'
    4   unchecked_value = checked_values[:unchecked] || 'false'
    5   unique_id       = rand.to_s[-5, 5]
    6   poser_id        = "fake_check_box_poser_#{unique_id}"
    7   value_id        = "fake_check_box_value_#{unique_id}"
    8   onchange        = onchange + "; doFakeCheckBoxClick(this, #{unique_id}, '#{checked_value}', '#{unchecked_value}')"
    9   
   10   html_options.delete(:object_id)
   11   html_options[:onchange] = onchange
   12   html_options[:id] = poser_id
   13   
   14   html = ''
   15   html += hidden_field_tag  name, is_checked ? checked_value : unchecked_value, :id => value_id
   16   html += check_box_tag     nil, 'true', is_checked, html_options
   17   html
   18 end

Then you need to define this Javascript function.

    1 function doFakeCheckBoxClick(me, unique_id, checked_value, unchecked_value)  {
    2   unique_id = 'fake_check_box_value_' + unique_id
    3   if (me.checked)
    4     $(unique_id).value = checked_value;
    5   else
    6     $(unique_id).value = unchecked_value;
    7 }

Now you can call it just like almost like the Rail's built in check_box_tag method.

    1 fake_check_box_tag "person[is_female]",
    2                    { :checked   => 'yes',
    3                      :unchecked => 'no' },
    4                    true,
    5                    { :onchange => "alert('checkbox clicked!')" }

In this example, if the checkbox is checked, the following will be true in your action:

params[:person][:is_female] == 'yes'

If it isn't checked, then this will be true:

params[:person[:is_female] == 'no'

The 3rd parameter (true) says that the checkbox will initially be checked.

Monday, January 21, 2008

Rail Plugin: param_protected

It is a Ruby on Rails plugin that provides param_protected and param_accessible methods on controllers analogous to the attr_protected and attr_accessible methods for models.

It is a very simple -- all it does it filter out specified parameters from a request.

Why?


Good question... you can ready about why attr_protected sucks here, or you can just read my following little rant...

What's the goal of attr_protected? To protect us from user input, not from ourselves. When I used attr_protected, I had to refactor tons of code in models, controllers and tests (that already worked well) to not use the mass attribute setters.

Was this massive code refactoring really worth the protection from the very few places were I do something like:

User.new(params[:user])

or

User.update_attributes(params[:user])

Truth of the matter, I was hardly ever passing params (or a subset thereof) to a mass attribute setter. So no, it wasn't worth the massive refactoring job.

Installation


git clone git://github.com/cjbottaro/param_protected.git vendor/plugins/param_protected

Usage


class UsersController < ApplicationController
  param_protected :user_id
end

class AccountController < ApplicationController
  param_accessible :account_id
end

param_protected is used to blacklist and param_accessible is used to whitelist.

You can give it an array of param names to filter:

param_protected [:user_id, :some_other_param]

param_protected and param_accessible are both just before filters, so the usual :only and :except arguments can be used:

param_protected :user_id, :only => :some_action
param_protected :user_id, :only => [:some_action, :another_action]
param_protected :user_id, :except => :some_action
param_protected :user_id, :except => [:some_action, :another_action]

You can protect nested params also (removes params[:user][:user_id]):

param_protected 'user/user_id'

param_protected is aware of array params and handles them properly.

Caveats (IMPORTANT!!!!)


Because param_protected is really a before filter (uses prepend_before_filter), you must take special care to ensure that it runs before any of your other before filters!! If it is not, some of your before filters might have access to some params they shouldn't.

Tests


rake test should work (from the plugin's root dir), though it's far from comprehensive.

Documentation


Please see the README for usage instructions and examples.

Monday, October 22, 2007

migration_izzle

Managing migration version numbers in Rails among multiple developers using the same database is a pain in the ass. You know the drill, you svn up, create a migration, try to run db:migrate but low and behold, the db version is already 10 versions above your migration's.

This plugin offers a simple solution: create and manage a "history" table that records which migrations have and have not been run and modify rake db:migrate to behave accordingly.

For documentation, installation and usage instructions, please see the migration_izzle github page (scroll down).

Sunday, October 21, 2007

Better AppConfig Plugin for Rails

There are plenty of "AppConfig" plugins for Rails already out there, so why did I make one? Three reasons:

  • Break the config files out into different files, one for each environment plus a shared common one.
  • Allows for nested sections and preserves object member notation for them.
  • Allows for lists (arrays).

Installation


git clone git://github.com/cjbottaro/app_config.git vendor/plugins/app_config

Usage


See the README on github (scroll down on the page).

Tests


All the unit tests should pass.

cd RAILS_ROOT/vendor/plugins/app_config
rake test