Notes posted to Ruby on Rails

RSS feed
April 25, 2010
0 thanks

Avoid DoubleRenderError

One can not invoke render twice during an action. Thus if You have a complicated rendering logic but at the end would like to render some default content, or just would like to find out whether render has been called during the current action, use performed?. This also works with “empty” renderings such as head.

April 24, 2010
0 thanks

Rendering After Exception In respond_to() Block

Remember, format blocks set the response’s content type. This can present problems when handling errors.

class MediaController
  rescue_from  ActionController::MissingFile do |e|
    # User's browser probably wont display this 
    # Content-Type is application/x-shockwave-flash
    render :file => File.join(Rails.public_path, '404.html'), :status => 404 
  end

  # show details or stream video
  def show
    @media = Media.find params[:id]
    respond_to do |format|
      format.html
      format.flv { send_file @media.path, :disposition => 'inline' }
    end
  end
end

For these situations you must set :content_type when calling render:

render :file => File.join(Rails.public_path, '404.html'), :status => 404, :content_type => 'text/html' 
April 22, 2010
2 thanks

Dynamic exists? methods

There are no dynamic exists? methods analogous to dynamic finders, which means that while you can do this:

Person.find_by_name('David')

you can’t do this:

Person.exists_by_name('David') # DOES NOT WORK

nor this:

Person.exists_by_name?('David') # DOES NOT WORK

However, you can simulate this with dynamic scope:

Person.scoped_by_name('David').exists?

You’ll have to admit that this is so much better than the plain old method:

Person.exists?(:name => "David")
April 22, 2010
1 thank

nil Argument Raises An I18n::ArgumentError

You might want to do this:

module ActionView
 module Helpers
   module TranslationHelper
     def localize(*args)
       #Avoid I18n::ArgumentError for nil values
       I18n.localize(*args) unless args.first.nil?
     end
     # l() still points at old definition
     alias l localize
   end
 end

end

April 20, 2010
2 thanks

Does not work with polymorphic relations

If you have polymorphic relations, e.g.:

class Bookmark < ActiveRecord::Base
  belongs_to :thing, :polymorphic => true
  belongs_to :owner, :polymorphic => true
end

and you want to ensure that a thing can bookmarked by an owner at most once, you can’t do this:

validates_uniqueness_of :thing, :scope => :owner

Instead, you must use the real column names, e.g.:

validates_uniqueness_of :thing_id, :scope => [:thing_type, :owner_id, :owner_type]
April 9, 2010
2 thanks

The :method goes in the :html option

When using a restful form helper and you want to use a method other than POST, remember to put the :method in the :html option.

e.g. To send a DELETE request instead of the usual POST (with a nested resource thrown in for good measure) use:

<% form_for [@post, @comment], :html => { :method => :delete } do |f| -%>
April 3, 2010
0 thanks

:autosave => false vs. :autosave => nil

The documentation above mentions that :autosave => true always saves the association and that it’s “off” by default. What it doesn’t mention what they mean by “off”.

  • :autosave => nil (the default “off” behavior) will still autosave the association if it has changed or is a new record.

  • :autosave => false seems to prevent autosaving of the association, even if it has changed.

I’ve found :autosave => false to be useful behavior when trying to prevent cyclical dependency loops; there are likely other useful use cases out there.

April 3, 2010 - (>= v2.2.1)
3 thanks

Careful with this method.

Despite the name and description, it will actually update any changed fields on the model rather than just the desired attribute.

def update_attribute(name, value)
  send(name.to_s + '=', value)
  save(false)
end

See? Use update_all and pass in the model ID as a condition, instead.

April 1, 2010
0 thanks

Fetching records when column is set to nil or false

If you want to fetch all records when one column (boolean) is set to nil or false, try this:

Project.all(:conditions => "archived IS NULL OR archived = 'f'")
March 31, 2010
0 thanks

Always gracefully degrade if JS isn't available

If you always want to degrade when JS isn’t available you can add something like to environment.rb

module ActionView
  module Helpers
    module PrototypeHelper
      def link_to_remote(name, options = {}, html_options = nil)
         html_options ||= {}
         html_options[:href] ||= options[:url]
         link_to_function(name, remote_function(options), html_options || options.delete(:html))
      end
    end
  end
end
March 29, 2010
0 thanks

Wrong example

In the authentication filter example above, the time condition should be reversed: we only want to find the user if time is still in the future (because it’s the valid-until time).

So the example should look like this:

id, time = @verifier.verify(cookies[:remember_me])
if time > Time.now
  self.current_user = User.find(id)
end
March 25, 2010
0 thanks

How to test callback methods

When testing callback methods, try to test the callback chain separate from its implementation.

Say this is your model:

class Project

  belongs_to :owner
  has_many :milestones

  after_save :create_milestones
  after_save :notify_owner

  private

  def notify_owner
    owner.project_created!
  end

  def create_milestones
    milestones.create(:name => 'Milestone 1')
  end

end

You should write your spec like this:

describe Project do

  describe 'create_milestones' do
    it 'should create an initial milestone' do
      project = Project.new
      project.milestones.should_receive(:create)
      project.send(:create_milestones)
    end
  end

  describe 'notify_owner' do
    it 'should notify its owner' do
      project = Project.new(:owner => mock_model(User))
      project.owner.should_receive(:project_created!)      
      project.send(:notify_owner)
    end
  end

  describe 'after_save' do
    it 'should run the proper callbacks' do
      project = Project.new
      project.should_receive(:create_milestones)
      project.should_receive(:notify_owner)
      project.run_callbacks(:after_save)
    end
  end

end

Here is some more advice on how to test callback methods in Rails:

http://gem-session.com/2010/03/how-to-test-callback-methods-in-rails

March 25, 2010
0 thanks

Using models in your migration

Here is some advice how to call your models in a migration without shooting yourself in the foot:

http://gem-session.com/2010/03/how-to-use-models-in-your-migrations-without-killing-kittens

Basically you can inline models into your migrations to decouple them from changes in your model:

class AddCurrentToVendor < ActiveRecord::Migration

  class Vendor < ActiveRecord::Base
  end

  class Article < ActiveRecord::Base
    has_many :vendors, :class_name => 'AddCurrentToVendor::Vendor', :order => 'created_at'
  end

  def self.up
    add_column :vendors, :current, :boolean
    Article.all.each do |article|
      article.vendors.first.andand.update_attribute(:current, true)
    end
  end

  def self.down
    remove_column :vendors, :current
  end
end
March 25, 2010
0 thanks

How to test custom error pages

Here is some advice for testing custom error pages using Webrat and Cucumber:

http://gem-session.com/2010/03/testing-your-custom-error-pages-with-webrat-and-cucumber

March 24, 2010
1 thank

Setting primary key from hash

If you try to specify the value for your primary key (usually “id”) through the attributes hash, it will be stripped out:

Post.new(:id => 5, :title => 'Foo') #=> #<Post @id=nil @title="Foo">

You can solve this by setting it directly, perhaps by using a block:

Post.new(:title => "Foo") { |p| p.id = 5 } #=> #<Post @id=5 @title="Foo">

This behavior is something you’d probably only have a problem with when you have custom primary keys. Perhaps you have a User model with a primary key of “name”…

class User < ActiveRecord::Base
  set_primary_key :name
end

User.new(params[:user]) # This will never work

You can solve this on a case-to-case basis by calling attributes= directly with the “ignore protected” option:

User.new { |user| user.send(:attributes=, params[:user], false) } # BAD BAD BAD!

You should not do the above example, though. If you do, all protected attributes are ignored, which is very, very bad when you only care about the primary key.

I’d recommend one of the following instead:

# Option 1 – Always allow primary key. Avoid with models created by users
class User
  private
    def attributes_protected_by_default
      super - [self.class.primary_key.to_s]
    end
end

# Option 2 – Add a new method for this case
class User
  def self.new_with_name(attributes = nil)
    new(attributes) { |u| u.name = attributes[:name] }
  end
end

As always when something is hard to do in Rails: Think about your design? Is it recommended? Is it sound? Do you really need to have a custom primary key?

March 23, 2010
1 thank

named_scopes and Acts as State Machine

As of AASM 2.1 named_scope(s) are defined by default for each state of a model.

March 19, 2010
1 thank

singular_class_name method missing

If this method is undefined in ActionView::Base, add to your ApplicationController:

class ApplicationController < ActionController::Base
  helper_method :singular_class_name
end
March 17, 2010
0 thanks

Validations

out of the box touch will run with validations

March 16, 2010 - (>= v2.2.1)
0 thanks

config/environments/production.rb

old

ActionController::AbstractRequest.relative_url_root= "/exampleapp"

new

config.action_controller.relative_url_root= "/exampleapp"
March 13, 2010
3 thanks

Can be used with has_many associations

You can also use this to validate that a has_many association has a specified number of records on the other end:

has_many :members

validates_length_of :members, :minimum => 1
March 11, 2010
1 thank

Skips validations and callbacks

The method skips validations and callbacks. That is why it should be used with caution.

Code example

person.toggle :active
March 11, 2010
3 thanks

Available statuses

All the available statuses (extracted from SYMBOL_TO_STATUS_CODE hash) in a slightly more readable form:

:continue                        => 100
:switching_protocols             => 101
:processing                      => 102
:ok                              => 200
:created                         => 201
:accepted                        => 202
:non_authoritative_information   => 203
:no_content                      => 204
:reset_content                   => 205
:partial_content                 => 206
:multi_status                    => 207
:im_used                         => 226
:multiple_choices                => 300
:moved_permanently               => 301
:found                           => 302
:see_other                       => 303
:not_modified                    => 304
:use_proxy                       => 305
:temporary_redirect              => 307
:bad_request                     => 400
:unauthorized                    => 401
:payment_required                => 402
:forbidden                       => 403
:not_found                       => 404
:method_not_allowed              => 405
:not_acceptable                  => 406
:proxy_authentication_required   => 407
:request_timeout                 => 408
:conflict                        => 409
:gone                            => 410
:length_required                 => 411
:precondition_failed             => 412
:request_entity_too_large        => 413
:request_uri_too_long            => 414
:unsupported_media_type          => 415
:requested_range_not_satisfiable => 416
:expectation_failed              => 417
:unprocessable_entity            => 422
:locked                          => 423
:failed_dependency               => 424
:upgrade_required                => 426
:internal_server_error           => 500
:not_implemented                 => 501
:bad_gateway                     => 502
:service_unavailable             => 503
:gateway_timeout                 => 504
:http_version_not_supported      => 505
:insufficient_storage            => 507
:not_extended                    => 510
March 11, 2010
2 thanks

Good way to see what went wrong

Use the message parameter like that:

assert_response :success, @response.body

If this fails (the response isn’t a success), it will display the response body along with the failure message, thus allowing you to quickly find out what went wrong. If the response is e.g. 500, there will probably be some exception stacktrace displayed in the body. And so on.

March 10, 2010
0 thanks

Specify your own template

You can specify you own template this way:

def notice
  ...
  @template = "some_other_name.html.erb"
end
March 4, 2010
1 thank

AASM named scopes

If you are using the aasm plugin/gem, this will generate all named scopes for your various states.

Code example

Class Article <  ActiveRecord::Base

  include AASM

  aasm_initial_state :created

  aasm_state :published
  aasm_state :unpublished
  aasm_state :deleted
  aasm_state :created

  aasm_event :publish do
    transitions :to => :published, :from => [:created]
  end

  aasm_event :unpublish do
    transitions :to => :unpublished, :from => [:created, :published]
  end

  aasm_event :delete do
    transitions :to => :deleted, :from => [:published, :unpublished]
  end

  aasm_states.each { |s| named_scope s, :conditions => { :state => s.to_s } }  

end
March 4, 2010
3 thanks

Re: Caveat when using dynamic layouts

Since there’s no way to edit posts on here, I need to correct myself and say that what I posted before doesn’t work, since you can’t specify layout multiple times:

class OrdersController < BaseController
  layout :determine_layout, :only => :new
  layout "public", :except => :new
  # ...
end

So don’t do that. The only way to ensure that the other actions get the default theme is to drop :only/:except and do the conditions yourself:

class OrdersController < BaseController
  layout :determine_layout

private
  def determine_layout
    %w(new).include?(action_name) ? "some_layout" : "public"
  end
end

All this to say, beware of :only/:except – they aren’t as useful as you think they are.

February 27, 2010
4 thanks

Caveat when using dynamic layouts

Worth noting that if you have a controller which inherits from another controller which has a layout, and in this child controller you’re determining the layout at runtime using a method for specific actions, the other actions you are excluding will not inherit the layout from the parent controller.

For example, if you’ve got this

class BaseController < ApplicationController
  layout "public"
end
class OrdersController < BaseController
  layout :determine_layout, :only => :new
  # index, show, new, create, edit, update, destroy ...
end

then OrdersController#index, #show, and #edit won’t get the “public” layout – in fact they won’t get a layout at all. So you’ll need to do this instead:

class OrdersController < BaseController
  layout :determine_layout, :only => :new
  layout "public", :except => :new
  # ...
end
February 26, 2010
3 thanks

default_scope on create

If you specify :conditions in your default_scope in form of a Hash, they will also be applied as default values for newly created objects. Example:

class Article
  default_scope :conditions => {:published => true}
end

Article.new.published? # => true

However:

class Article
  default_scope :conditions => 'published = 1'
end

Article.new.published? # => false
February 25, 2010
2 thanks

configuration no longer in environment.rb

configure session store in config/initializers/session_store.rb

February 25, 2010
1 thank

Saving other objects inside before_save

Don’t call .save or .update_attribute on other objects inside before_save callback.

Saving other objects inside of before_save callback results in flushing changed hash and the original object is not updated.

UPDATE observed sometimes, still investigating