Good notes posted to Ruby on Rails
RSS feeddefault_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
Easy workaround for missing :through option
Note that belongs_to does not support :through option like has_many (although IMHO it would make sense in some cases), but you can easily simulate it with delegate.
For example:
class Person < ActiveRecord::Base belongs_to :team ... end class Task < ActiveRecord::Base belongs_to :person delegate :team, :to => :person end
There is of course more ways to do it, but this seems to be the easiest to me.
Extract the aggregated scoping options
If you want to get the aggregated scoping options of a chain of named scopes use ActiveRecord::Base.current_scoped_methods
It works in the fashion of:
Shirt.red.medium.alphabetical.current_scoped_methods # ==> { :create => {}, :find => { :conditions => {:color => 'red', :size => 'medium'}, :order => 'shirts.name ASC' } }
reload equivalent for models
The reset_column_information method provides a similar function for the model itself. Most useful during migrations.
Paying attention to query parameters
Standard action caching ignores query parameters, which means you’d get the same results for a URL with and without query parameters if it was action cached. You can make it pay attention to them by using a custom cache path like so:
caches_action :my_action, :cache_path => Proc.new { |c| c.params }
Or, maybe you want some of the query parameters, but not all to factor into different versions of that action’s cache:
:cache_path => Proc.new { |c| c.params.delete_if { |k,v| k.starts_with?('utm_') } }
Beware of things like pagination if you use expires_in to expire the cache, as pages could get out of sync.
W3CDTF Format
Here is the formatted string for the W3CDTF datetime format (http://www.w3.org/TR/NOTE-datetime). It has a semicolon in the timezone part, therefore you cannot use ‘%z’:
Time::DATE_FORMATS[:w3cdtf] = lambda { |time| time.strftime("%Y-%m-%dT%H:%M:%S#{time.formatted_offset}") }
Using html text instead of default response
If you have a string containing html and want to assert_select against it, as the doc states you have to pass in an element (HTML::Node) as the first argument. You can do something like this:
doc = HTML::Document.new('<p><span>example</span></p>') assert_select doc.root, 'span'
Reverse version of camelize
Reverse version of camelize is underscore
Use this in controllers
Sometimes you’re gonna need this in controllers. Just put this in the controller:
include ActionView::Helpers::NumberHelper
Doesn't return nil if the object you try from isn't nil.
Note that this doesn’t prevent a NoMethodError if you attempt to call a method that doesn’t exist on a valid object.
a = Article.new a.try(:author) #=> #<Author ...> nil.try(:doesnt_exist) #=> nil a.try(:doesnt_exist) #=> NoMethodError: undefined method `doesnt_exist' for #<Article:0x106c7d5d8>
This is on Ruby 1.8.7 patchlevel 174
Attribute names are Strings, not Symbols
Another possible gotcha – the returned hash keys are of type String, not Symbol:
user.attributes["login"] # => "joe" user.attributes[:login] # => nil
Version Ranges
To specify a version range, use array syntax like this:
config.gem 'paperclip', :version => ['>= 2.3.1.1', '< 3.0']
The example will, of course, match any version 2.3.1.1 or newer up until (not including) 3.0 or later.
The docs are in the base class
Look in ActionController::Base for the docs.
Use hash form of updates argument
The examples are unfortunate, because passing a string as the updates argument is an invitation to SQL injection attacks. Don’t do this!
Billing.update_all("author='#{author}'")
Use the hash form of updates instead:
Billing.update_all(:author => author)
Then the SQL adapter will quote everything safely. Even if [you think] you’re sure there’s no quoting issue, it’s better to cultivate the habit of using the hash form just in case you missed something.
Same with conditions–use the hash or array form rather than a string if there are variables involved.
BTW, to do this and give options, of course you’ll need to put the braces back in:
Billing.update_all({:author => author}, ['title like ?', "#{prefix}%"])
Named scope better than conditions
In modern versions of Rails, in most cases a named_scope is a better alternative to using :conditions on your has_many relations. Compare:
class User has_many :published_posts, :conditions => {:published => true} end user.published_posts
with:
class Post named_scope :published, :conditions => {:published => true} end class User has_many :posts end user.posts.published
It’s better because the Post’s logic (“am I published?”) should not be coupled within User class. This makes it easier to refactor: e.g. if you wanted to refactor the boolean :published field into a :status field with more available values, you would not have to modify User class. Having to modify User when you refactor some implementation detail of Post class is clearly a code smell.
This also applies to :order, :group, :having and similar options.
How FormBuilders work
What, you were expecting documentation? :)
An excellent survey of how FormBuilders work is here:
http://code.alexreisner.com/articles/form-builders-in-rails.html
Update statement won't include all attributes with ActiveRecord::Dirty
With the addition of ActiveRecord::Dirty, the update statement will only feature changed columns, as opposed to the comment of railsmonk below.
Have check_box checked by default
In addition to comment below, you can make a column with default value so in your forms it will be enabled by default and behave correctly with validation errors unlike :checked => true
in your migration
add_column :accounts, :ssl_enabled, :boolean, :default => 1
Streaming XML with Builder
To generate larger XMLs, it’s a good idea to a) stream the XML and b) use Active Record batch finders.
Here’s one way of doing it:
def my_action @items = Enumerable::Enumerator.new( Item.some_named_scope, :find_each, :batch_size => 500) respond_to do |format| format.xml do render :text => lambda { |response, output| extend ApplicationHelper xml = Builder::XmlMarkup.new( :target => StreamingOutputWrapper.new(output), :indent => 2) eval(default_template.source, binding, default_template.path) } end end end
The Builder template does not need to be modified.
Use camelize with singular words
To make the business example work, use camelize instead of classify:
"business".camelize # => "Business"
form_tag with named route and html class
<% form_tag position_user_card_path(@user, card), :method => :put, :class => ‘position-form’ do %>
Pluralize Without Count
Helper method that returns the word without the count.
application_helper.rb
def pluralize_without_count(count, noun, text = nil) if count != 0 count == 1 ? "#{noun}#{text}" : "#{noun.pluralize}#{text}" end end
Example usage:
_form.html.erb
<%= pluralize_without_count(item.categories.count, 'Category', ':') %>
Will discard any order option
order_by(:created_at).find_each == FAIL!!!
class ActiveRecord::Base # normal find_each does not use given order but uses id asc def self.find_each_with_order(options={}) raise "offset is not yet supported" if options[:offset] page = 1 limit = options[:limit] || 1000 loop do offset = (page-1) * limit batch = find(:all, options.merge(:limit=>limit, :offset=>offset)) page += 1 batch.each{|x| yield x } break if batch.size < limit end end end
Auto-submitting select tag
If you want your form to be submitted when user selects something, use:
:onchange => "this.form.submit();"
For example:
select_tag "people", "<option>David</option>", :onchange => "this.form.submit();"
With multiple parameters
Example
remote_function( :url => some_remote_function_path, :with => "'key1='+$('elem_id').value +'&key2='+$('elem_id').value+ '&this_elem_value='+value" )
with_exclusive_scope example by Ramon broken in latest Rails
The example Ramon gave works within the model itself, i.e.
class Article def closed with_exclusive_scope { find(:all) } end end
However, from what I can see, this approach does not work within a controller. You may be wanting to use
Article.with_exclusive_scope { find(:all) } #=> "SELECT * FROM 'articles'
But it will error out about find(:all) not existing on ArticlesController. To get around this, you must now do
Article.with_exclusive_scope { Article.find(:all) } #=> "SELECT * FROM 'articles'
In otherwards, find(:all) isn’t being executed in the scope of the model, but in the controller in which its called.
Took me a minute or two to find out, so I thought I’d let others know.
Join multiple tables
It’s easy to join multiple tables too. In this case we have:
class Article belongs_to :feed end class Feed has_many :articles belongs_to :source end class Source has_many :feeds # t.bool :visible end
You can search articles and specify a condition on the sources table.
Article.find(:all, :conditions => { :feeds => { :sources => { :visible => true }}}, :joins => [:feed => :source],