The author, smiling winningly Scott Raymond home

What's new in Rails 1.1

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.

  1. Railties
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!
<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

  1. ActiveSupport
<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>
<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!

  1. ActiveRecord
<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>
<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>
<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>
<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>
  1. ActionPack
The RJS templates are passed an page object that represents the JavaScriptGenerator, which has many tricks up its sleeve:
<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>
<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>&lt;% form_for :person => @person, :url => { :action => "update" } do |f| %&gt; First name: &lt;%= f.text_field :first_name %&gt; Last name : &lt;%= f.text_field :last_name %&gt; Biography : &lt;%= f.text_area :biography %&gt; Admin? : &lt;%= f.check_box :admin %&gt; &lt;% end %&gt; &lt;% form_for :person => person, :url => { :action => "update" } do |person_form| %&gt; First name: &lt;%= person_form.text_field :first_name %&gt; Last name : &lt;%= person_form.text_field :last_name %&gt; &lt;% fields_for :permission => person.permission do |permission_fields| %> Admin? : &lt;%= permission_fields.check_box :admin %&gt; &lt;% end %&gt; &lt;% end %&gt;</pre>
<pre>&lt;% form_for :person, @person, :url => { :action => "update" }, :builder => LabellingFormBuilder do |f| %&gt; &lt;%= f.text_field :first_name %&gt; &lt;%= f.text_field :last_name %&gt; &lt;% end %&gt;</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>
<pre>render :action => "atom.rxml", :content_type => "application/atom+xml"</pre>
<pre>auto_link(post.body) { |text| truncate(text, 10) }</pre>
  1. Prototype
<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>
  1. Scriptaculous