Refactoring: Implementing filtering June 16th, 2008
For a project at Openminds we needed an implementation for filtering some data from a list with the aid of checkboxes. Our first solution looked like this:
def index session[:order_filter] = params[:filter] if params[:filter] session[:order_filter] ||= { :active => '1', :inactive => '1' } if session[:order_filter][:active] == '0' && session[:order_filter][:inactive] == '0' @orders = [] elsif session[:order_filter][:active] == '0' @orders = @user.orders.find(:all, :conditions => {:state => 'active'}) elsif session[:order_filter][:inactive] == '0' @orders = @user.orders.find(:all, :conditions => {:state => 'inactive'}) else @orders = @user.orders.find(:all) end end
So this code takes the filter-parameters from params, or initializes them from defaults, and then does the correct finds.
In the next iteration, I stumbled onto this code and thought to myself “This can be done better”. After making sure the index action was sufficiently covered with tests, I had a go at it, this is what I came up with:
def list if filter.active? && filter.inactive? @orders = @user.orders.all elsif filter.active? @orders = @user.orders.active elsif filter.inactive? @orders = @user.orders.inactive else @orders = [] end end protected def filter return @filter if @filter session[:order_filter] = params[:filter] if params[:filter] session[:order_filter] ||= { :active => '1', :inactive => '1' } @filter = SearchFilter.new("Order", session[:order_filter]) end helper_method :filter # search_filter.rb class SearchFilter attr_reader :name def initialize name, hash @name = name hash.each do |key, value| instance_eval <<METHOD def #{key}? #{value} == 1 end METHOD end end end
This code is more readable in my opinion. I extracted the session-storing and parameter-juggling to the filter-method, which then returns a SearchFilter object. Thanks to SearchFilter I can now query filter for set values in a nice way. I can also call filter in my views, which is nicer then calling session[:order_filter][:active]. I also used named_scope instead of doing the finds manually, which improves readability a lot too!
How would you refactor this?
l 2 comments »acts_as_state_machine with auto named_scope powers May 21st, 2008
Rails 2.1 (Soon to be released?) brings us integrated named_scope‘ing. I’ve already mentioned named_scope here and there, and you can find a complete writeup on Ryan Daigle’s Blog
While working on projects on Rails Edge, we used to have the same pattern over and over.
class Mail < ActiveRecord::Base include AASM aasm_state :created aasm_state :sending aasm_state :sent aasm_state :failed aasm_state :delayed named_scope :created, :conditions => {:status => 'created'} named_scope :sending, :conditions => {:status => 'sending'} named_scope :sent, :conditions => {:status => 'sent'} named_scope :failed, :conditions => {:status => 'failed'} named_scope :delayed, :conditions => {:status => 'delayed'} end
For each state we had, we created a named_scope, because it is so great and easy to be able to say Mail.failed.each .... However, this isn’t really DRY, and we can see a clear pattern here. When you think about it, you really should have a scope for every state you define. So I forked from AASM on github and started hacking away.
The result:
class Mail < ActiveRecord::Base include AASM aasm_state :created aasm_state :sending aasm_state :sent aasm_state :failed aasm_state :delayed end ... >> Mail.failed => [] >> Mail.sent => [#<Mail id: 1, email: "jan@domain.com", content: "hallo", :aasm_state: "sent", created_at: "2008-05-21 15:39:38", updated_at: "2008-05-21 15:39:38">, #<Mail id: 2, email: "jan@domain.com", status: "sent", created_at: "2008-05-21 15:39:39", updated_at: "2008-05-21 15:39:39">]
You can find my
This has all been implemented in the AASM Core now.named_scope enabled AASM on github. At the moment only the master branch has been patched with my functionality, I’ll do the no_aasm_prefix branch somewhere this evening / tomorrow. Let me know if you’re using it, and what could be better
For those interested in the code, this is the bit that does the magic.
module AASM::NamedScopeMethods def self.add_named_scope base # Don't add unless it's a class which understands `named_scope` return unless base.respond_to? :named_scope base.extend AASM::NamedScopeMethods::ClassMethods base.class_eval do class << self alias_method :aasm_state_without_named_scope, :aasm_state alias_method :aasm_state, :aasm_state_with_named_scope end end end module ClassMethods def aasm_state_with_named_scope name, options = {} aasm_state_without_named_scope name, options self.named_scope name, :conditions => {self.aasm_column => name.to_s} unless self.scopes.include?(name) end end end
Gotcha
While testing the plugin we found out 1 gotcha to this: do not define a aasm_state :new, because this will override the constructor of the class, and you’ll lose all creating-functionality.
Update
This is now default AASM behaviour as of this commit
l 3 comments »#2 May 13th, 2008
My 5 Rails Tips blog post has won second place in the RailsCast contest (Actually it was third, but for some reason the numbering is different there).
This is a great boost for me, to know I’m doing a good job posting this stuff. I also won a few things:
- a printed Peepcode Minibook
- a Code Spaces Enterprise Plan for 1 Year
- $50 to the Pragmatic Bookshelf (by the Pragmatic Programmers
- $50 Amazon gift card (by Shine Technologies)
- a 16bugs Big Plan for 1 Year
All great prices, and I’ll certainly be doing some reviews of them here! I’m allready excited to start using the 16Bugs plan, because good bugtracking was something I was missing in some of my projects..
A big thanks goes out to Ryan Bates for organizing and judging this contest (and picking me as a winner)!
l 1 comments »As seen in the Commit History: script/dbconsole May 8th, 2008
While going to the recent Rails Git logs, I stumbled across a cool new feature: dbconsole.
We all know and love the Rails console, which gives us an easy interface to our Rails environment, but sometimes we have to be in the database itself.. When this happened, most of the time we had to look up how our database was called, what kind of database it was, basically look at our database.yml file, but no more!
jan:railsproject jan$ script/dbconsole SQLite version 3.5.6 Enter ".help" for instructions sqlite> select * from products; ...
and with easy access to each environment:
jan:railsproject jan$ script/dbconsole production Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 13 Server version: 5.0.41 MySQL Community Server (GPL) Type 'help;' or '\h' for help. Type '\c' to clear the buffer. mysql>
How cool is that!
l 3 comments »Don't be too DRY - Filter Abuse April 29th, 2008
Controller Filters are a great way to add behaviour to your controller which isn’t directly connected to your actions. The Hitchhiker’s Guide to the GalaxyRails Documentation has this to say on the subject of Filters
Filters enable controllers to run shared pre- and post-processing code for its actions. These filters can be used to do authentication, caching, or auditing before the intended action is performed. Or to do localization or output compression after the action has been performed. Filters have access to the request, response, and all the instance variables set by other filters in the chain or by the action (in the case of after filters). source
A good example to this is authentication. A controller action’s role isn’t authentication, but you should be sure that whoever reaches that action is authenticated. This can be solved with a simple before_filter
class PostsController < ApplicationController before_filter :check_auth, :except => [:show, :index] def index @posts = Post.all end def new @post = Post.new end ... private def check_auth User.authenticate(session[:user]) || redirect_to login_path end end
Lately, however, I’ve seen some very bad use of this functionality. We all like the Dont Repeat Yourself principle, but this shouldn’t interfere with code readability. Actions should be descriptive. When reading an actions code, you should know what it does.
This is bad:
class PostsController < ApplicationController before_filter :find_post, :only => [:show, :edit, :update] before_filter :all_posts, :only => [:index] before_filter :find_comments, :only => [:show] def index end def show end def edit end def update @post.update_attributes(params[:post) redirect_to @post end ... private def find_post @post = Post.find(params[:id]) end def all_posts @posts = Post.find(:all) end def find_comments @comments = Comment.find_by_post_id(params[:id]) end end
The reason people use filters this way is to be ultra DRY. You don’t need to do Post.find(params[:id]) 3 times in your controller, and it works this way.
I personally hate this pattern, and everyone who uses it. This code may be DRY, but it’s not readable at all.
When I see an action with nothing in it, and I look at the view template and see @post being used, I have to look back at the controller, look at which before_filters apply, find the methods called, interpret what is in there.
Other than that, it also removes the ability to do effective Action Caching, because action caching will interpret before_filters, thus running the SQL queries the find-methods trigger.
If you have too much code you’d have to replicate, there are other solutions. For example if you had
class PostController def show @post = Post.find(params[:id], :conditions => {:site_id => current_site.id, :deleted_at => nil}, :include => :comments) end def edit @post = Post.find(params[:id], :conditions => {:site_id => current_site.id, :deleted_at => nil}, :include => :comments) end
You should refactor this with a model-action instead of going for a before_filter. This could become
class Post named_scope :active, :conditions => {:deleted_at => nil}, :include => :comments end class PostController def show @post = current_site.posts.active.find(params[:id]) end def edit @post = current_site.posts.active.find(params[:id]) end end
Believe me, readability is way more important then being DRY
l 0 comments »5 Rails Tips April 25th, 2008
Ryan Bates at railscasts.com has written out a contest where he asks to publish your own Ruby on Rails tips, 5 of them, and let him know about it. I’m keen on the idea of sharing some Rails wisdom with the world, so here are my humble 5 Rails Tips.
using alias_method_chain to improve your models
In Ruby on Rails projects I like to use the Fat Models, Skinny Controllers principle. Sometimes I have models with associations that are quite evident, and where I want it to be very transparent how to set them. For example, I have a Post, which belongs to a category, but I want to be able to create a post like this:
Post.new :title => "Lorem Ipsum", :content => "Lorem Ipsum dolor...", :category => 'Lorem'
And this should create the Lorem category, or use the one which exists. To reach that goal, I use alias_method_chain
class Post < ActiveRecord::Base belongs_to :category def category_with_name_recognition=(name) if name.is_a? Category self.category_without_name_recognition = name else self.category_without_name_recognition = Category.find_or_initialize_by_name(name) end end alias_method_chain :category=, :name_recognition end
The alias_method_chain function magically aliases category= to category_without_name_recognition, and then replaces category= to link to category_with_name_recognition. That way I can do a call to the old function. For more info on aliasmethodchain, take a look at your local active_support/core_ext/module/aliasing.rb file.
This comes in very handy when category is an autocomplete_field!
everything you always wanted to know about rake tasks, but were too afraid to ask
Everybody knows the default rake tasks (rake db:migrate, rake test), but do you know the next ones:
rake db:create:all–> This will create databases for all defined environments in your database.yml file.rake routes–> This will print out all routes your application knowsrake db:fixtures:load–> This will load your test fixtures in your current environments database, I use this all the time
To see all rake tasks, you can just run rake --tasks for a list of all tasks with a short description. For a full description of all tasks, run rake --describe
use read_attribute and write_attribute
If you’re overriding an attribute setter or getter in your models, it’s not possible setting or getting these attributes like you normally would, because that would cause an infinite loop. Luckily, there are methods you can use to get and set the attribute. Lets take a User object for example:
class User < ActiveRecord::Base ... def password=(pass) write_attribute(:password, Security.encode(pass)) end def password Security.decode(read_attribute(:password)) end end
using your own date and time formats
I personally don’t want to be bothered with time formatting, I can read about any format the internet throws at me. Sadly, some of my clients have different, strong opinions on the way time should be formatted on their website. You really don’t want to be bothered with using strftime everywhere. Luckily, Rails has a solution to this problem.
In your environment.rb, you can define your own time formats:
ActiveSupport::CoreExtensions::Time::Conversions::DATE_FORMATS.merge!( :client_readable => "%A %B %d at %H:%M", # => Friday April 25 at 11:10 :client_reports => "%d/%m/%Y at %H:%M" # => 25/04/2008 at 11:10 )
And then you can use this formatting in your views really easy:
Last updated on <%= @post.updated_at.to_s(:client_readable) %> # => "Last updated on Friday April 25 at 11:20"
you need plugins!!!
Rails is a great framework, but it doesn’t implement every need of every project. Most of the time however, you’re not the first person to need feature A, or something B, most of the time the feature you want is already in a plugin. These are 5 plugins I can’t live without:
- attachment_fu - For everything that needs to be uploaded to your application
- will_paginate - Pagination done right
- restful_authentication - Use this if you need authentication in your system
- phonemic_passwords - Generates passwords really easy
- exception_notification - Keeps you aware of errors thrown in production
cool Rails 2.1 features
I couldn’t constrain myself to 5 rails tips. Today, and today only, you get a free 6th tip!
If you’re running on Rails Edge (which I do for most projects), or you’re reading this when Rails 2.1 is already out, there are a bunch of cool new features that aren’t documented yet.
render :partial => form
# Before Rails 2.1 <% form_for @client do |form| %> <%= render :partial => 'form', :object => form %> <%= form.submit "Save" %> <% end %> # Now, with sparkly new Rails <% form_for @client do |form| %> <%= render :partial => form %> # Automaticly renders _form.erb <%= form.submit "Save %>
find(:last)
# Before Rails 2.1 @last_post = Post.find(:first, :order => 'created_at DESC') # with Rails 2.1 @last_post = Post.find(:last, :order => 'created_at')
Easy finding
# Before Rails 2.1 Post.find :all Post.find :first # with Rails 2.1, thanks to scoping Post.all Post.first Post.last
These are all small changes that I find particularly handy in my day-to-day work. For the “big” changes, you should read Ryan’s Scraps, he keeps track of all big changes in Rails.
And with that, I’m all out of tips, at least for now. Comments are always appreciated!
l 2 comments »Testing your Rails Plugins April 24th, 2008
A great feature in Ruby is the integrated testing. Ruby on Rails also implements and extends the Ruby testing framework, and if you’re not testing your Ruby code you should start doing it now!
Normally, when I write tests for a class, I mock out everything not related directly to the class (I do this with mocha). For example, when writing tests for the Mollom gem, I mocked out the whole XMLRPC API, because I can’t rely on them to give me all test cases.
# from http://github.com/DefV/ruby-mollom/tree/master/test/mollom_test.rb def test_server_list xml_rpc = mock xml_rpc.expects(:call). with('mollom.getServerList', is_a(Hash)). returns(['http://172.16.0.1', 'http://172.16.0.2', 'https://172.16.0.2']) XMLRPC::Client.stubs(:new).with('xmlrpc.mollom.com', '/1.0').returns(xml_rpc) assert_equal [ {:ip => '172.16.0.1', :proto => 'http'}, {:ip => '172.16.0.2', :proto => 'http'}, {:ip => '172.16.0.2', :proto => 'https'} ], @mollom.server_list end
This way, I’m only testing the behaviour of that specific class, so if something fails somewhere, I can pinpoint the problem, while being sure it’s not a problem in the Mollom API.
While writing a test for some view plugin the other day, I was doing the same thing. I stubbed out everything from ActionView/ActionController which I used (content_tag and url_for and such). Suddenly I realised that wasn’t a good idea.
Rails plugins tests should use the Rails Framework, because the plugins themselves rely on it deeply. To be sure the plugin keeps working while the framework changes, I just have to run my tests, which is exactly what I want.
So I do it this way now:
# from http://github.com/DefV/title_helper/tree/master/test/title_helper_test.rb require 'test/unit' require File.dirname(__FILE__) + '/../../../../config/boot.rb' require File.dirname(__FILE__) + '/../../../../config/environment.rb' class TitleHelperTest < Test::Unit::TestCase def setup @helper = ActionView::Base.new end # Replace this with your real tests. def test_title_method_with_no_title_set assert_equal "foobar", @helper.title(:site_name => 'foobar') end ... end
How do you test your Rails Plugins?
l 0 comments »Symbols vs Strings April 22nd, 2008
When to use Symbols and when to use Strings in Ruby is really a personal preference. Most of the time I program without taking the difference in performance in consideration.
I just use a few simple rules
in a hash it’s always :key => ‘value’
redirect_to :controller => 'pages', :action => 'index'
special keywords are symbols
Page.find(:all) Page.find(:first) redirect_to(:back)
attributes and status’es are symbols
update_attribute(:name, 'Jan') comment.state = :approved
Except for those rules it’s really what feels right while coding it.
l 1 comments »