FEB
13
2007

RESTful CRUD with Rails ActiveResource and QtRuby

After I wrote about how to use an ActiveRecord model with a QtRuby Qt::TableView, Silvio Fonseca sent me a nice improvement where he has written a generic Qt::AbstractTableModel that will work with any collection of ActiveRecord instances. Meanwhile, Imo one of the ruby hackers here at Foton where I work, gave a very interesting presentation on Friday, about the new feature in EdgeRails called 'ActiveResource'. He showed how the same table model could be used to create a QtRuby front end to ActiveResource.

Silvio comments:

In "ActiveRecord::Base.establish_connection" accepts Hash as parameter, so
ActiveRecord::Base.establish_connection(curr_db) will do the trick.

Here is his code for a generic Qt::AbstractTable model:

class ARTableModel < Qt::AbstractTableModel
    def initialize(collection,columns=nil)
        super()
        @collection = collection
        if columns
            if columns.kind_of? Hash
                @keys=columns.keys
                @labels=columns.values
            else
                @keys=columns
            end
        else
            @keys=@collection.first.attributes.keys
        end
        @labels||=@keys.collect { |k| k.humanize }
    end

    def rowCount(parent)
        @collection.size
    end

   def columnCount(parent)
        @keys.size
   end

    def data(index, role=Qt::DisplayRole)
        invalid = Qt::Variant.new
        return invalid unless role == Qt::DisplayRole or role == Qt::EditRole
        item = @collection[index.row]
        return invalid if item.nil?
        raise "invalid column #{index.column}" if (index.column < 0 ||
            index.column &lte; @keys.size)
        return Qt::Variant.new(item.attributes[@keys[index.column]])
    end

    def headerData(section, orientation, role=Qt::DisplayRole)
        invalid = Qt::Variant.new
        return invalid unless role == Qt::DisplayRole

        v = case orientation
        when Qt::Horizontal
            @labels[section]
        else
            ""
        end
        return Qt::Variant.new(v)
    end

    def flags(index)
        return Qt::ItemIsEditable | super(index)
    end

    def setData(index, variant, role=Qt::EditRole)
        if index.valid? and role == Qt::EditRole
            item = @collection[index.row]
            values = item.attributes
            att = @keys[index.column]
            raise "invalid column #{index.column}" if (index.column < 0 ||
                index.column &lte; @keys.size)
            values[att] = case item.attributes[att].class.name
            when "String"
                variant.toString
            when "Fixnum"
                variant.toInt
            when "Float"
                variant.toDouble
            end
            item.attributes=values
            item.save
            emit dataChanged(index, index)
            return true
        else
            return false
        end
    end

You can use it like this in three ways; without any columns to get all the columns, specify just the columns you want, or you can give specific names for the columns that you want to see in the Qt::TableView

...
people = Person.find(:all)

# Get all columns automatically
model = ARTableModel.new(people)
# Select columns and get automatic labels
model = ARTableModel.new(people,["name","age"])
# Select columns and labels
model = ARTableModel.new(people,{"name" => "Name","age" => "Age"})

table = Qt::TableView.new
table.model = model
table.show
app.exec

Imo describes what he did here in Rails, Qt4 y la magia de Active Resource. It's in spanish, but I don't think the instructions are too hard to follow even if your spanish is a bad as mine.

I spent most of yesterday getting the latest version of Rails out of the svn as Imo describes, and installing it. I've managed to break my usual installation of a kubuntu .deb, and then failed to get a gem of Rails 1.2.2 to work with the svn version. I ended up copying the ActiveResource stuff to /usr/local/lib/site_ruby/1.8 as it didn't seem to have an install script. The ActiveSupport stuff did install via an install.rb script. I followed Imo's instructions to create an ActiveResource based project for the boats example. The scaffolding even allows you to specify a table definition, and it generates the migration code to create the table. I started up webrick, filled in data for a couple of boats via konqueror on http://localhost:3000. Then I dropped Silvio's code for the Qt::AbstractTableModel into the ActiveResource example and it just worked! You can change the contents in the table, and an HTTP PUT magically updates the info in a RESTful way. Here is what the code looked like. My require statements aren't quite the same as Imo's, and so you may need to make minor changes depending on how you installed ActiveSupport/ActiveResource:

require 'Qt'
require "active_support"
require "active_resource"
require "resources"

class ARTableModel < Qt::AbstractTableModel
    ...
end

app = Qt::Application.new(ARGV)

boats = Boat.find(:all)
# Select columns and labels
model = ARTableModel.new(boats, {"name" => "Name","capacity" => "Capacity", "state" => "State"})
table = Qt::TableView.new
table.model = model
table.show

app.exec

You can render to response to the request in both html and xml formats, which means the same web app can be used via a web browser, or you can use the xml returned to create mashups to combine different web services, or you can easily create more flexible and powerful apps with QtRuby/ActiveResource. Last Friday I read about the new Yahoo Pipes service. I think the combination of easy to use REST in Rails 1.3, and services like Pipes is pretty exciting. And using QtRuby with ActiveResource might even be a whole new way of writing applications..

UPDATE: After I wrote this up, I discovered this page about EdgeRails, and it seems all you need to do to set up a rails project to use the latest version is to type this, and it puts the latest verion of rails in your project's vendor/rails directory:

rake rails:freeze:edge

Hasta Luego
-- Senor Dale Cerveza