Scott Raymond
home
28 Feb 2006
It’s been just over two months since the [Rails 1.0 milestone](http://scottraymond.net/articles/2005/12/13/whatev), and the long push of testing and refining that lead up to it. Surely, the contributors have been taking a much-deserved rest in the time since then. Surely?
In fact, the core team (and over 120 other contributors) haven’t slowed down one bit, and the next major release of Rails is here. If you’re running Edge Rails, you already have access to all the latest features, but perhaps a few have missed your radar. So I’d like to round-up what’s new since 1.0 (or at least, everything that’s interesting to me — I’ve skipped a ton of bug fixes, performance improvements, environment-specific enhancements, and smaller changes.) Let’s start with the easier parts.
:ruby instead of :sql. This wins the award for best changelog comment in 1.1:This means that we’ll assume you want to live in the world of db/schema.rb where the grass is green and the girls are pretty… Brought to you by the federation of opinionated framework builders!
-r/--repeat option to script/process/spawner.-c/--config option on script/server allows you to specify a path to your lighttpd.conf.ENV["RAILS\_ENV"] = "production" in config/environment.rb doesn’t wreak havoc. (I’ve been bitten by that nasty.)javascript\_include\_tag :defaults. (I’d love it if the generator also created a blank application.css, and an app/views/layouts/application.rhtml with the standard XHTML boilerplate.)reload! reloads all models, and app is an accessor for an instance of Integration::Session. Handy!<pre>>> puts helper.options_for_select([%w(a 1), %w(b 2), %w(c 3)])
option value="1">a
option value="2">b
option value="3">c
> nil
load_fixtures is now db:fixtures:load (which you can also use to load a subset of the application’s fixtures, e.g. rake db:fixtures:load FIXTURES=customers,plans). All the old task names will still work. Run rake --tasks to see the new task names.test:uncommitted tests changes since last checkin to Subversion.<pre>ActionController::Routing::Routes.draw do |map|
# Account routes
map.with_options(:controller => 'account') do |account|
account.home '', :action => 'dashboard'
account.signup 'signup', :action => 'new'
account.logout 'logout', :action => 'logout'
end
end</pre>
<pre>[1,2,3].to_json => "[1, 2, 3]"
"Hello".to_json => "\"Hello\""
Person.find(:first).to_json =>
"{\"attributes\": {\"id\": \"1\", \"name\": \"Scott Raymond\"}}"
</pre>
<pre>transcripts.group_by(&:day)</pre>
<pre>%w(1 2 3 4 5 6 7).in_groups_of(3) {|g| p g}
["1", "2", "3"]
["4", "5", "6"]
["7", nil, nil]</pre>
5.minutes + 30.seconds instead of 5.minutes + 30.<pre>logger.around_info("Start rendering component (#{options.inspect}): ",
"End of component rendering") { yield }</pre>
<pre>Time.now.beginning_of_quarter => Sun Jan 01 00:00:00 CST 2006</pre>
<pre>class Account < ActiveRecord::Base
has_one :subscription
delegate :free?, :paying?, :to => :subscription
delegate :overdue?, :to => "subscription.last_payment"
end
account.free? # => account.subscription.free?
account.overdue? # => account.subscription.last_payment.overdue?</pre>
Now for the fun stuff!
<pre>class Author < ActiveRecord::Base
has_many :authorships
has_many :books, :through => :authorships
end
class Book < ActiveRecord::Base
has_many :authorships
has_many :authors, :through => :authorships
end
class Authorship < ActiveRecord::Base
belongs_to :author
belongs_to :book
end
Author.find(:first).books.find(:all, :include => :reviews)</pre>
<pre>class Firm < ActiveRecord::Base
has_many :clients
has_many :invoices, :through => :clients
end
class Client < ActiveRecord::Base
belongs_to :firm
has_many :invoices
end
class Invoice < ActiveRecord::Base
belongs_to :client
end</pre>
<pre>class Address < ActiveRecord::Base
belongs_to :addressable, :polymorphic => true
end
class User < ActiveRecord::Base
has_one :address, :as => :addressable
end
class Company < ActiveRecord::Base
has_one :address, :as => :addressable
end</pre>
<pre>Developer.with_scope(:find => { :conditions => "salary > 10000", :limit => 10 }) do
Developer.find(:all) # => SELECT * FROM developers WHERE (salary > 10000) LIMIT 10
# inner rule is used. (all previous parameters are ignored)
Developer.with_exclusive_scope(:find => { :conditions => "name = 'Jamis'" }) do
Developer.find(:all) # => SELECT * FROM developers WHERE (name = 'Jamis')
end
# parameters are merged
Developer.with_scope(:find => { :conditions => "name = 'Jamis'" }) do
Developer.find(:all) # => SELECT * FROM developers WHERE (( salary > 10000 ) AND ( name = 'Jamis' )) LIMIT 10
end
end</pre>
<pre>Person.count
Person.average :age
Person.minimum :age
Person.maximum :age
Person.sum :salary, :group => :last_name</pre>
Author.find(:all, :include=> { :posts=> :comments }), which will fetch all authors, their posts, and the comments belonging to those posts in a single query. For example:<pre>Author.find(:all, :include=>{:posts=>:comments})
Author.find(:all, :include=>[{:posts=>:comments}, :categorizations])
Author.find(:all, :include=>{:posts=>[:comments, :categorizations]})
Company.find(:all, :include=>{:groups=>{:members=>{:favorites}}})</pre>
find calls on has\_and\_belongs\_to\_many and has\_many assosociations. For example:<pre>class Post
has_many :recent_comments, :class_name => "Comment", :limit => 10, :include => :author
end
post.recent_comments.find(:all) # Uses LIMIT 10 and includes authors
post.recent_comments.find(:all, :limit => nil) # Uses no limit but include authors
post.recent_comments.find(:all, :limit => nil, :include => nil) # Uses no limit and doesn't include authors</pre>
to_xml. For example:<pre>topic.to_xml
topic.to_xml(:skip_instruct => true, :skip_attributes => [ :id, bonus_time, :written_on, replies_count ])
firm.to_xml :include => [ :account, :clients ]</pre>
validate\_uniqueness\_of to be scoped by multiple columns. See [this](http://dev.rubyonrails.org/ticket/1559).:exclusively\_dependent option has been deprecated in favor of :dependent => :delete_all..find() method, and the has\_and\_belongs\_to\_many and has\_many associations, now all take :group, :limit, :offset, and :select options.:conditions. See [this](http://dev.rubyonrails.org/ticket/3569).validates\_length\_of now works on UTF-8 strings — it counts characters instead of bytes.page object that represents the JavaScriptGenerator, which has many tricks up its sleeve:
alert 'Howdy'redirect_tocallassignreplaceinsert\_html :bottom, 'list', '<li>Last item</li>'visual\_effect :highlight, 'list'show 'status-indicator'hide 'status-indicator', 'cancel-link'['blank\_slate']['blank\_slate'].show # => $('blank_slate').show();select('p')select('p.welcome b').first # => $$('p.welcome b').first();select('p.welcome b').first.hide # => $$('p.welcome b').first().hide();<<draggable 'product-1'drop_receiving 'wastebasket', :url => { :action => 'delete' }sortable 'todolist', :url => { action => 'change_order' }delay(20) { page.visual_effect :fade, 'notice' }alert() (set config.action_view.debug_rjs = true)page.select('#items li').collect('items'){ |element| element.hide } generates var items = $$('#items li').collect(function(value, index) { return value.hide(); });<pre>class UserController < ApplicationController
def refresh
render :update do |page|
page.replace_html 'user_list', :partial => 'user', :collection => @users
page.visual_effect :highlight, 'user_list'
end
end
end</pre>
<pre>module ApplicationHelper
def update_time
page.replace_html 'time', Time.now.to_s(:db)
page.visual_effect :highlight, 'time'
end
end
class UserController < ApplicationController
def poll
render :update { |page| page.update_time }
end
end</pre>
respond_to lets an action output different formats according to the HTTP Accept header. In other words, you’ve got instance REST web services. Blinksale 2.0 already uses this. For example:<pre>class WeblogController < ActionController::Base
def index
@posts = Post.find :all
respond_to do |wants|
wants.html # using defaults, which will render weblog/index.rhtml
wants.xml { render :xml => @posts.to_xml } # generates XML and sends it with the right MIME type
wants.js # renders index.rjs
end
end
end</pre>
<pre># Assign a new param parser to a new content type
ActionController::Base.param_parsers['application/atom+xml'] = Proc.new do |data|
node = REXML::Document.new(post)
{ node.root.name => node.root }
end
# Assign the default XmlSimple to a new content type
ActionController::Base.param_parsers['application/backpack+xml'] = :xml_simple</pre>
<pre><% form_for :person => @person, :url => { :action => "update" } do |f| %>
First name: <%= f.text_field :first_name %>
Last name : <%= f.text_field :last_name %>
Biography : <%= f.text_area :biography %>
Admin? : <%= f.check_box :admin %>
<% end %>
<% form_for :person => person, :url => { :action => "update" } do |person_form| %>
First name: <%= person_form.text_field :first_name %>
Last name : <%= person_form.text_field :last_name %>
<% fields_for :permission => person.permission do |permission_fields| %>
Admin? : <%= permission_fields.check_box :admin %>
<% end %>
<% end %></pre>
form_for and friends can take a :builder option, where you can pass a custom subclass of FormBuilder. For example:<pre><% form_for :person, @person, :url => { :action => "update" }, :builder => LabellingFormBuilder do |f| %>
<%= f.text_field :first_name %>
<%= f.text_field :last_name %>
<% end %></pre>
<pre>require "#{File.dirname(__FILE__)}/test_helper"
require "integration_test"
class ExampleTest < ActionController::IntegrationTest
fixtures :people
def test_login
# get the login page
get "/login"
assert_equal 200, status
# post the login and follow through to the home page
post "/login", :username => people(:jamis).username,
:password => people(:jamis).password
follow_redirect!
assert_equal 200, status
assert_equal "/home", path
end
end</pre>
Integration Tests can also have multiple session instances open per test, and even extend those instances with assertions and methods to create a very powerful testing DSL that is specific for your application. You can even reference any named routes you happen to have defined. For example (think [Campfire](http://campfirenow.com/) here):
<pre>def test_login_and_speak
jamis, david = login(:jamis), login(:david)
room = rooms(:office)
jamis.enter(room)
jamis.speak(room, "anybody home?")
david.enter(room)
david.speak(room, "hello!")
end
private
module CustomAssertions
def enter(room)
# reference a named route, for maximum internal consistency!
get(room_url(:id => room.id))
assert(...)
...
end
def speak(room, message)
xml_http_request "/say/#{room.id}", :message => message
assert(...)
...
end
end
def login(who)
open_session do |sess|
sess.extend(CustomAssertions)
who = people(who)
sess.post "/login", :username => who.username,
:password => who.password
assert(...)
end
end</pre>
render(:xml => xml) works just like render(:text => text), but sets the content-type to application/xml and the charset to UTF-8.:content_type option to render, so you can change the content type on the fly. For example:<pre>render :action => "atom.rxml", :content_type => "application/atom+xml"</pre>
<pre>auto_link(post.body) { |text| truncate(text, 10) }</pre>
content\_for and capture now work in .rxml (and any non-rhtml template). See [this](http://dev.rubyonrails.org/ticket/3287).visual\_effect supports scoped queues. See [this](http://dev.rubyonrails.org/ticket/3530).observe\_field now has an :on option to specify a different callback hook to have the observer trigger on.link\_to\_function, but uses a button instead of a link.link\_to\_function will now honor existing :onclick definitions when adding the function call.submit\_tag now has a :disable\_with option to change the text of disabled submit buttons.visual\_effect can now toggle visual effects. See [this](http://dev.rubyonrails.org/ticket/3323).auto\_complete\_field now has a :select option for to only use part of the auto-complete suggestion as the value for insertion.$$ function) matches elements by CSS selector tokens. For example:<pre>// Find all <img> elements inside <p> elements with class
// "summary", all inside the <div> with id "page". Hide
// each matched <img> tag.
$$('div#page p.summary img').each(Element.hide)
// Attributes can be used in selectors as well:
$$('form#foo input[type=text]').each(function(input) {
input.setStyle({color: 'red'});
});</pre>
$ and $$, so you can now write $('foo').show() instead of Element.show('foo').truncate, gsub, sub, scan, and strip.Element.childOf(element, ancestor) returns true when element is a child of ancestor.