Monday, 18 August 2008

Rails: Work Around to Run Tests While Having Full-Text Index

While working in a Rails project, I was having some field in the DB in Text data type and I need to create a full-text index on this column. So, I created a migration that have the following line:

execute "ALTER TABLE transcriptions ADD FULLTEXT text_index (text)"

After running this migration, the full-text index is created and the application runs with no problems, but!

When I tried to run the unit tests, I got the following error:

Mysql::Error: #42000BLOB/TEXT column 'text' used in key specification without a key length: CREATE INDEX `text_index` ON 'transcriptions' ('text')


That is because when you run db:migrate, a schema file is created from the DB called schema.rb. This file contains the complete DB schema including the indices. The problem is that creating index in the schema.rb file is written using add_index method which is used for creating regular indices only, not the full-text index.

In this case when you are trying to run the unit tests, Rails try to prepare the test DB using this schema.rb file and fail due to it can't create index on text type column.

If we take a look on what test rake task do, we can see the following trace:


** Invoke test:units (first_time)
** Invoke db:test:prepare (first_time)
** Invoke environment (first_time)
** Execute environment
** Invoke db:abort_if_pending_migrations (first_time)
** Invoke environment
** Execute db:abort_if_pending_migrations
** Execute db:test:prepare
** Invoke db:test:clone (first_time)
** Invoke db:schema:dump (first_time)
** Invoke environment
** Execute db:schema:dump
** Invoke db:test:purge (first_time)
** Invoke environment
** Execute db:test:purge
** Execute db:test:clone
** Invoke db:schema:load (first_time)
** Invoke environment
** Execute db:schema:load


And the problem fall in the line "Execute db:schema:load" which creates the test DB using the schema.rb file. To work around this issue, we can replace this rake sub-task (thanks for Matthew Bass’s blog post) by test:clone_structure against the development environment. This will clone the development DB schema into the test DB schema without using the corrupted schema.rb file.

To replace this rake task by the clone one, open the Rakefile file in your project and write the following lines:

require 'tasks/rails'

Rake::TaskManager.class_eval do

def remove_task(task_name)

@tasks.delete(task_name.to_s)
end

end

def remove_task(task_name)
Rake.application.remove_task(task_name)

end

remove_task 'db:schema:load'
namespace :db do

namespace :schema do
task :load do
RAILS_ENV = 'development'
Rake::Task['db:test:clone_structure'].invoke
RAILS_ENV = 'test'

end
end

end


Then when you run the test rake task you will get different trace contains the db:test:clone_structure instead of db:schema:load and everything will go as expected.

Wednesday, 16 July 2008

Ruby: Run Code Blocks at Specific Time Intervals

While working in Rails project, some of my business logic parts need to run at specific time intervals. First, I created a rake tasks for my code blocks and use "cron" jobs to run those rake tasks at the required time intervals. But, as You know , every rake task loads the whole Rails application into the memory to run and this is costly specially if my rake tasks run at high frequency (every ~1 min).

So, I decided to load the tasks into the memory and make them sleep and run at specific time intervals. One of the great Rails gems that helps me doing this job is EventMachine. It is event processing library for Ruby applications. Using it you can define specific code blocks to run at specific time and also define a periodic timer for code blocks that will run periodically.

So, what I did is creating one rake task contains the event machine running and defined some periodic timers to run code blocks at the required time intervals. In this case my rake task will be loaded into the memory and will be sleep (using the event machine) and run my code blocks at the defined time intervals.

  • To install this gem:

    gem install eventmachine

    and then choose the required gem version from the given list.

  • If you need some code block to run periodically every 10 secs, create a periodic timer:

    EventMachine.run {
    @periodic_job = EventMachine::PeriodicTimer.new(10){
    # code block to run every 10 secs
    }
    }
  • You can cancel the periodic job at any time by calling:

    @periodic_job.cancel
  • Note Do not forget to stop the event machine at the end of your periodic job, e.g. if you need your periodic job to run just 10 times:

    x = 0
    EventMachine.run {
    EventMachine::PeriodicTimer.new(0.1){
    x += 1

    # your code go here

    EventMachine.stop if x == 10
    }
    }

Checkout the full documentation of EventMachine.

Sunday, 22 June 2008

Centralized Static Authorization Using Just a Hash

Deciding how your site authorization will be implemented is not a simple part. You can implement centralized authorization, distribute it over the controllers or let each model exposes its authorized resources. Of course that depends on how your authorization process will be (static, dynamic, roles appear at run time, etc).

Using centralized authorization:

  1. Define one entry point for authorization. So, you can easly disable/enable it.
  2. You will maintain only one scope of code without touching the other business logic code.
  3. If code failure happened, you can easily isolate it from the application process without dropping down the whole logic.
  4. Other business logic is independent and don't aware of the authorization. So, you can easily upgrade your authorization process if you need without touching other application parts.

In RoR, if your authorization will be static over a predefined list of roles, then you can easily implement your authorization check based on user/controller/action triple using the magic word "hash". This will done by creating a nested hash of the following levels:

  • Level one: Users Roles (Admin, Client, etc)
  • Level two: Controllers (Users, Products, Items, etc)
  • Level Three: Actions (Index, Create, Update, Destroy, etc)

Now, you need to catch the request, extract the request entities (requesting user role, controller and action) and then hash these keys to see whither this request is authorized or not.

Lets see small example. If you have two user roles in your application; Admin and User. And you have two controllers; Products and Users. Your hash will be as following:


@roles = {:Admin => {
:Users => :all,
:Products => :all
}
:User => {
:Users => {:update => :all}
:Products => {:index => :all, :show => :all}
}
}

The above hash gives the "Admin" user role the whole access to the both controllers and the whole controller actions. But, for "User" role, he can request "update" action in "Users" controller and "index" or "show" actions in the "Products" controller only.

Note I am using the value ":all" as the end of my roles permissions tree. So, it means "full access" either on the whole controller's actions or specific action.

The next step is putting this roles and the checking code into small module, include it in your ApplicationController then call the "authorize" method using before_filter.

module Authorization

@roles = {:Admin => {
:Users => :all,
:Products => :all
}
:User => {
:Users => {:update => :all}
:Products => {:index => :all, :show => :all}
}
}

def authorize
# check controllers list for the requested user
@controllers_list = @roles[@user.class.to_s.to_sym]
if (@controllers_list.nil?)
# no controllers permitted for this user role
render :text => "", :status => :unauthorized
return false
else
# check actions list inside the controller
@actions_list = @controllers_list[controller_name.to_sym]
if (@actions_list.nil?)
# the requested controller is not permitted
render :text => "", :status => :unauthorized
return false
elsif (@actions_list == :all)
return true
else
# check the called action
@called_action = @actions_list[action_name.to_sym]
if (@called_action.nil?)
render :text => "", :status => :unauthorized
return false
elsif (@called_action == :all)
return true
else
render :text => "", :status => :unauthorized
return false
end
end
end
end

end

class ApplicationController <>

include Authorization

before_filter :authorize

..............

end

You can enlarge or shrink the hash as you need (add/remove roles, controllers, etc) but the "authorize" method code will not change.

Freshbooks Ruby Gem and Its Patch

For who does not know freshbooks, it is an online service for sending invoices and tracking time. You can access this service either through its web-interface or through its exposed APIs. It is not free, but you can create a test account and try it. I used it while working in project at eSpace company.

To access your freshbooks account through the APIs, you need first to enable its APIs and use the given URL and authentication token in your API connection. You will notice that freshbooks APIs have different versions and they distinguish between them using the API access URL, e.g. for API version 2.1 (latest one at post time), your URL will be "https://espacetest.freshbooks.com/api/2.1/xml-in"

You can access freshbooks APIs from inside Ruby on Rails project either directly through http connections or using a Ruby wrapper freshbooks.rb. To install it:

gem install freshbooks

The latest version of this gem (at post time) is freshbooks-2.2. This version contains a small issue regarding API version number. When using this library, you need to pass both your freshbooks account simple URL (espacetest.freshbooks.com) and authentication token.

FreshBooks.setup(@freshbooks_url, @freshbooks_token)

Then, library code concatenates the rest of the URL. Look at this code lines taken from freshbooks.rb:

Line 35: VERSION = '2.2' # Only works with 2.1

Line 37: SERVICE_URL = "/api/#{VERSION}/xml-in"


You will notice that the version number is "2.2" and this variable "VERSION" is used in the API url. But, no API version 2.2 yet!!
So, if you used this gem as it you will get error from freshbooks API saying "Invalid API version.". All you need to do is replacing this value with "2.1"

Line 35: VERSION = '2.1' # Only works with 2.1:

Line 37: SERVICE_URL = "/api/#{VERSION}/xml-in"

Now, enjoy accessing great online invoices service through simplified Ruby wrapper ( after patching :) ).

Wednesday, 18 June 2008

PayPal Sandbox

While working in a project in eSpace contains e-commerce handling, I wanted to test my code against PayPal payment gateway. I found that PayPal have a test server called PayPal Sandbox. Using this test server you can simulate all the transactions you want to do over the actual PayPal account.

PayPal Sandbox contains the following features:

  1. Simulate actual transactions (authorize, purchase, credit, etc).
  2. You can create test accounts for sellers/buyers as many as you want.
  3. Every created test account will have its own credit card (fake one). You can use it to test you transactions and its balance will reflect your applied transactions.
  4. You can use any of your seller/buyer test accounts to enter the paypal test site and trace its transactions, create reports, etc.
  5. You can access this test sandbox from your code through PayPal APIs.

But, it contains some drawbacks:

  1. The given test credit cards does not contain verification code (CVV code), but you can use "000" instead of it in your tests.
  2. When you enable the API in your Paypal account, you will get API user name, API password and either a signature string or certificate file. Using Sandbox you will get only the signature string.