Rails on the Run

Rails experiments by Matt Aimonetti

Browsing Posts tagged failure

DrNic the most famous Australian Rails developer surprisingly doesn’t spend most of his time working on Rails read interview
That’s maybe why he recently became so active in the Rails community ( see Magic Multi-Connections, Magic Models, map_by_methods, Gem Generator etc..)

Today he released something very helpful for the Rails Community, not another Gem or another cool plugin to extend Rails but a web application to help you planning your conferences. We already had other tools such as conference meetup but it’s the first time that we get a product helping you to plan a conference by scheduling the sessions you want to attend.

screenshot

screenshot of the entire schedule

I had a quick chat with DrNic about his latest creation and here is what he said:

“its been fun building just how I thought session selection might look + feel
web2.0 = permissive voyeurism I think!”

I share the same vision than Nic on session selection and I really enjoyed booking my Rails sessions and seeing what other people selected. (I guess that’s my voyeur side, don’t you like web2.0?)

If you wanna see what I planned on attending, checkout my schedule: my schedule

Thanks Nic for the good work and too bad you are not presenting anything at the RailsConf (move your bum to come up with a better submission next year!)

With the recent Buzz around Adobe Apollo I figured out that since I recently switched to Mac and that I didn’t try the latest Flex upgrade, I should try Flex 2.01 for Mac.

I really like what Adobe did with Flex, Unit testing, better accessibility, etc… but one thing I regret, it’s getting closer and closer to Java and AS3 syntax is a pain to use when you got used to Ruby.

Anyway, what I really wanted to do was to have Flash quickly access my Restful Rails app. The adobe guys came up with a RoR Ria SDK but well…. it only covers Flex and requires FlashPlayer 9.
I also found some great tutorials on how to use the efficient AMF messaging protocol with Rails using the WebOrb for Rails plugins

All that was really nice and I had fun, but it was an overkill for what I wanted to do. Let me show you how in less than 5 minutes how you can access you Rails Model from Flash and add some new item directly from Flash.

Create your new Rails app and use the script/generate scaffold_resource command to generate your Event Model.

script/generate scaffold_resource Event

It should create all that for you:

exists  app/models/
  exists  app/controllers/
  exists  app/helpers/
  create  app/views/events
  exists  test/functional/
  exists  test/unit/
  create  app/views/events/index.rhtml
  create  app/views/events/show.rhtml
  create  app/views/events/new.rhtml
  create  app/views/events/edit.rhtml
  create  app/views/layouts/events.rhtml
  identical  public/stylesheets/scaffold.css
  create  app/models/event.rb
  create  app/controllers/events_controller.rb
  create  test/functional/events_controller_test.rb
  create  app/helpers/events_helper.rb
  create  test/unit/event_test.rb
  create  test/fixtures/events.yml
  exists  db/migrate
  create  db/migrate/001_create_events.rb
  route  map.resources :events

let’s edit the migration file:
db/migrate/001createevents.rb

class CreateEvents < ActiveRecord::Migration
  def self.up
    create_table :events do |t|
      t.column :title, :string
      t.column :description, :string
      t.column :location, :string
        t.column :starts_at, :datetime
        t.column :ends_at, :datetime
    end
  end

  def self.down
    drop_table :events
  end
end

And let’s add some fixtures:
test/fixtures/events.yml

meeting:
  id: 1
  title: Meeting
  description: Boring meeting with the whole staff
  location: conference room
  starts_at: 2007-11-02 09:00:00
  ends_at: 2007-11-02 10:30:00
Joe_bday:
  id: 2
  title: Joe Bday Party
  description: Come and celebrate Joe's Birthday
  location: Lapin Agile Pub
  starts_at: 2007-09-07 20:00:00
  ends_at: 2007-09-07 23:30:00

Ok, now simply migrate your database,load the fixtures and start the webrick:

rake db:migrate
rake db:fixtures:load
script/server

Great, we are done with Rails.

Let’s launch Flash

Create a new Flash document and create a new .As fie in TextMate (or your favorite editor). We’ll write a quick ActionScript class to access Rails.

class Restfulflash{
    public var gateway:String;

    function Resftfulflash(gateway:String){
        this.set_gateway(gateway);
    }
    public function set_gateway(gateway:String){
        this.gateway = gateway;
        trace("gateway set to:"+gateway);
    }

    public function get(model, callback){
        var railsReply:XML = new XML();
        railsReply.ignoreWhite = true;
        railsReply.onLoad = function(success:Boolean){
            if (success) {
                    trace ('Rails responded: '+railsReply);
                    callback.text = railsReply;
            } else {
                    trace ('Error while waiting for Rails to reply');
               callback.text = 'error';
            }
        }
        var railsRequest:XML = new XML();
        railsRequest.contentType="application/xml";
        railsRequest.sendAndLoad(this.gateway+model, railsReply);
        delete railsRequest;
    }

    public function create(model, newItem){
        railsRequest.onLoad = function(success){
                trace("Item creation success: " + success);
                trace(this);
        };
        var railsRequest:XML = new XML();
        railsRequest.parseXML(newItem);
        railsRequest.contentType="application/xml";
        railsRequest.sendAndLoad(this.gateway+model+'/create/index.html', railsRequest,'POST');
        delete railsRequest;
    }
}

Save this file in the same directory as your .fla file

In your fla file add:

// Create a XML object to hold the events from our Rails app
rails_events = new XML();

// Prepare the connection to Rails (it would be nicer to do that in 1 step, but to make things clearer i decided to do it in 2)
var rails:Restfulflash = new Restfulflash();
rails.set_gateway("http://localhost:3000/");

// Get the events from rails and load the result in the rails_event XML object.
rails.get('events', rails_events);
trace(rails_events);

// Let's create a new event
newEvent = new XML('<event><description>Spend some time with Grandma before its too late</description><ends-at type="datetime">2007-11-02T18:30:00-07:00</ends-at><id type="integer">1</id><location>Paris, France</location><starts-at type="datetime">2007-11-02T16:00:00-07:00</starts-at><title>Visit Grandma</title></event>&#8217;)
rails.create(&#8216;events&#8217;, newEvent);

// Verify  that the event was added
rails.get(&#8216;events&#8217;, rails_events);
trace(rails_events);

There you go, you have all the events provided to you by Rails nicely prepared in an easy to parse XML object. You can bind the results to a Datagrid or display the info the way you want it. Ohh and by the way, we just added a new Event to the database… easy, isn’t it? The code is a bit dirty but it’s still a good example why you need to use REST and how easy it is to get Flash to talk with Rails. (I strongly encourage that you also look at the very good WebOrb plugin for Rails)

In part I I quickly explained what I had to do, my limitations and a potential solution to connect to a legacy database.

In this post I’ll try to go through setting up a plugin for migration and start using RSpec for developing the migration plugin.

What we want is to migrate sites using the legacy application to our new Rails application. That means that new users won’t be be migrated. It therefore makes sense not to add the migrating logic to the main application but to create a plugin. (if you are not familiar with Rails plugins check this blog post from Geoffrey Grosenbach about plugins)

Let’s create our plugin

./script/generate plugin legacy_migration

Rails should have generated something like that:

legacy_migration
|-- init.rb
|-- install.rb
|-- uninstall.rb
|-- Rakefile
|-- README
|-- lib/
|   |-- legacy_migration.rb
|-- tasks/
|   |-- legacy_migration_tasks.rake
|-- test
|   |-- legacy_migration_test.rb

Since we are going to use RSpec, we can remove the test folder and create a spec folder. In our spec folder, let’s add some subfolders to organize our files. Let’s create a fixtures folder to hold, a helpers folder, a migrate folder (we’ll use that to migrate our legacy database) and finally, a models folder.

Our plugin folder should look more or less like that:

legacy_migration
|-- init.rb
|-- install.rb
|-- uninstall.rb
|-- Rakefile
|-- README
|-- lib/
|   |-- legacy_migration.rb
|-- tasks/
|   |-- legacy_migration_tasks.rake
|-- spec
|   |-- fixtures
|   |-- helpers
|   |-- migrate
|   |-- models

Great, let’s get started and let’s create our first spec. We should probably start by migrating users so I’ll create a new spec in the spec/models folder called legacy_user_spec.rb and add the following code:

require File.dirname(FILE) + '/../helpers/legacy_user_helper'
require File.dirname(FILE) + '/../helpers/spec_helper'
describe "a connection to the legacy application" do
  setup do
    @connection_status = LEGACY.connect
  end
end
it "should be connected to the legacy database" do
  @connection_status.current_database.should == ActiveRecord::Base.configurations'legacy'

end

Note that I’m using RSpec trunk/edge and I use “describe” instead of “context” and “it” instead of “specify”. For more information on how to run RSpec edge with TextMate read this previous post.

If we look at the code above, we start by requiring 2 helpers, a general helper called spec_helper and a helper just for this spec called legacy_user_helper (we will obviously need to create them otherwise our spec will failed).

Then we start our first spec by describing a connection to the legacy application and we specify that it should be connected to the legacy database.

Here is our setup code:

setup do
  @connection_status = LEGACY.connect
end

What I want is to retrieve a connection status after I connect to our legacy application. To manage the connection to the legacy application we will create a LEGACY module. We will need to connect to many legacy applications/sites and our module should help us doing that.

Then we can read that our spec checks that we are connected to the legacy database.

it "should be connected to the legacy database" do
  @connection_status.current_database.should == ActiveRecord::Base.configurations'legacy'
end

That means we want to compare the connection status to the ‘legacy’ environment defined in the database.yml file.

We now need to get this spec to pass.

Let’s get started by adding a legacy environment to our database.yml and creating our LEGACY module in our legacy_migration.rb file.

Add the following to your database.yml

legacy:
  adapter: mysql
  database: legacy
  username: root
  password:
  host: localhost

Now, let’s create the LEGACY module in our legacy_migration.rb file


module LEGACY

  # Connect to a Legacy database.
  # Usage:
  # Manual connection: LEGACY.connect(:database => 'legacy-database', :adapter => 'mysql', :username => 'root', :password => '', :host => 'localhost')
  # Auto connection to the database.yml defined legacy DB: LEGACY.connect
  # Connection to any database available in database.yml LEGACY.connect('legacy_test')
  # Connect a class to a specific database: LEGACY.connect(LegacyInstaller, ActiveRecord::Base.configurations['legacy_installer'])
  def self.connect(spec = nil, opt_env = nil)
    case spec
      # Automatically connect to the legacy environment database defined in the database.yml
    when nil
      raise 'Legacy Database not defined' unless defined? ActiveRecord::Base.configurations['legacy']
      LegacyActiveRecord.establish_connection(ActiveRecord::Base.configurations['legacy'])
      # Return the connections status
      LegacyActiveRecord.connection
      # A connection's name from the database.yml can be passed
    when Symbol, String
      if configuration = ActiveRecord::Base.configurations[spec.to_s]
        LegacyActiveRecord.establish_connection(configuration)
      else
        raise "#{spec} database is not configured"
      end
      # Connect a Model to a specific Database
    when Class
      raise 'Environment connection not provided or nil' unless defined? opt_env || opt_env['database'] == nil
      if spec.connection.current_database == opt_env['database']
        spec.connection
      else
        spec.establish_connection(opt_env)
      end
      # An array can be passed to establish the connection
    else
      spec = spec.symbolize_keys
      unless spec.key?(:adapter) then raise "database configuration does not specify adapter" end
        adapter_method = "#{spec[:adapter]}_connection"
        LegacyActiveRecord.establish_connection(spec)
      end
    end
  end

There we go, we have a really cool connect function, we can easily connect to the default legacy environment defined in the database.yml file, we can specify the connection settings, connect to another environment database and even connect one specific class/model to a specific environment. (that will useful since we have many databases). If we wanted to follow the TDD rules, I shouldn’t have written so much code… as a matter of fact, when I worked on this project I did not, but since I don’t have much time, I won’t go through the re-factoring steps.

One thing you might have noticed is that we establish a connection between LegacyActiveRecord and the legacy database. (instead of connecting ActiveRecord to the legacy database).

LegacyActiveRecord.establish_connection(ActiveRecord::Base.configurations['legacy'])

The problem is that we didn’t create the LegacyActiveRecord model yet. Let’s do that right away. Let’s add a new folder called models in our lib folder. In the models folder, let’s create a legacy_active_record.rb file and add the following code:

class LegacyActiveRecord < ActiveRecord::Base
  self.abstract_class = true
end

Cool, now let’s have fun with our new module, fire the console (./script/console) and try LEGACY.connect
Here is what we get back:


    >> LEGACY.connect
    => #nil, :database=>”legacy”, :allow_concurrency=>false, :host=>”localhost”, :username=>”root”, :adapter=>”mysql”}, @connection_options=[“localhost”, “root”, ””, “legacy”, nil, nil], [...]

let’s try to get the database we connected to:

>>LEGACY.connect.current_database
    => “legacy”

Awesome, we can now get our LegacyActiveRecord model to connect to our legacy database and return the connections status. Let’s run our specs……….. they pass.

Sweet, we setup our migration plugin, got our first spec written, added the code needed to get the spec to pass, I think we are done with PART 2 :)