Techblog

Tech Blog

Our latest geek adventures!

Archive for the ‘Ruby on Rails’ Category

21 September HTTP status exception handling plugin

Some time ago, I wrote about putting HTTP status code to use for your Rails application. For my reinvigorated project, I wanted to apply the same technique. Instead of re-implementing it once again, I created a Rails plugin called http_status_exceptions to easily add this functionality and I have put it on Github. For more information on how to install and use the plugin, see the project’s wiki.

No Comments - Tags: , , , , ,

20 September Batch file renaming

I just started working on an old Rails project after having neglected it for 15 months. Most of the view files still had the good old .rhtml extension. I was too lazy to rename these files by hand, both on my file system and in the git repository. I used the following Bash commands to do the job:

First, I renamed all the partials to the .erb extension. Note: I am not using .html.erb, as some of these partials are used in js-formatted responses as well:

for i in `find app/views/**/_*.rhtml`; do 
  git mv $i `echo $i | sed s/\\.rhtml$/.erb/`; 
done

The remaining files could now be renamed to .html.erb with a similar command:

for i in `find app/views/**/*.rhtml`; do 
  git mv $i `echo $i | sed s/\\.rhtml$/.html.erb/`; 
done

Note that this technique works with Subversion as well: just substitute git with svn in the command above. A regular rename is possible as well by leaving out git altogether!

Now my file names are Rails-compliant again, I can start refactoring all the code that is not up to current Rails standards anymore. Ah, the virtues of developing with a rapidly evolving framework…

No Comments - Tags: , , , , ,

16 September Converting a Rails application from Gettext to I18n

Posted by jaap in Ruby on Rails, i18n

Last week we had to convert our existing Rails application, which uses Gettext to the new I18n API in combination with the SimpleBackend. I personally never liked Gettext, there was simply not enough control over translations as PO/MO files are not native ruby or at least can be easily accessed by Ruby (like YAML files).

We therefore decided to switch to the brand new, not even released, I18n API. But now we had a serious problem, our code base isn’t small and all that code had to be converted in some way. We could do it by hand, but hey, that’s a lot of work and especially very error-prone. Some convertor had to be written. Here it is as a rails plugin: GettextToI18n. 

What does the I18n convertor do?

It scrapes your whole application and searches for gettext calls, like this:

_("to be translated")

It will convert this gettext call to the newly I18n format: 

I18n.t :message_id

Then it builds up a big hash containing all the the translations. We decided it was handy to use the scopes that are introduced in the new I18n api. So it stores the translations in the following format: 

For models:

["model"]["model_name"]={:message_1 => "to be translated"}

For controller:

["controller"]["controller_name"]={:message_1 => "to be translated"}

After this hash of translations has been built up, the convertor writes it as a YAML file to:  config/locales/template.yml.That’s all!

What’s supported?

It supports basic gettext calls. We have run it over our code base and it converts all gettext calls we use without any problem. 

A normal gettext call

_("to be translated")

converts to:

I18n.t :message_0, :scope => [:txt, :controller, :controller_name])

 

A gettext call with variables

_("My name is %{name}" % {:name => "Jaap"})

converts to:

I18n.t :message_0, :name => "Jaap", :scope => [:txt, :controller, :controller_name])

 

A gettext call with variables that contain gettext calls

_("Click %{link} to go to the homepage" % {:link => link_to(_("Here"), root_path)})

converts to:

I18n.t :message_0, :link => link_to(I18n.t(:message_1, :scope => [:txt, :controller, :controller_name]), root_path), :scope => [:txt, :controller, :controller_name])

 

Installation

./script/plugin install git://github.com/japetheape/gettext_to_i18n.git

Usage

To convert your application:
rake gettext_to_i18n:transform

Please make sure you backup your complete application as it can screw things up.

Contribution

Please contribute to this plugin and make it better, as I won’t use it anymore, cause we are not going to convert another time (I think ;-)). Things that has to be done are:

unnamed variables:

_("I play the %s" % "saxophone")

Go to the development location at github and fork this plugin!

5 Comments - Tags: , ,

29 August Rails log analyzer matures

Since I announced rails-log-analyzer some weeks ago, quite a lot has happened! Apparently there is some interest in such a tool: on this blog we get a lot of traffic looking for more info, the github project already has 22 watchers and it even has been forked!

In the mean time, Bart and I worked hard to add new functionality and refactored the internal design. As a result, I have released request-log-analyzer 0.1.0 today!

Changes: 

  • The project is renamed to request-log-analyzer, because we plan to support log files from other frameworks as well; Merb is planned to be supported in the near future.
  • The tool is distributed as a gem, making it much easier to install and update.
  • More reports, colorized output, parsing progress bars, command line arguments, etc…
  • Added a tool to create a SQLite database with all the parsed info from the log file, so you can do your own analysis.

Installation:

gem sources -a http://gems.github.com
sudo gem install wvanbergen-request-log-analyzer

Usage:

request-log-analyzer  [LOG FILES*]
request-log-analyzer -c 20 -z log/production.log

Please let me know what you think! If you have any problems using the tool, do not hesitate to contact me!

5 Comments - Tags: , , ,

15 August Rails log analyzer

My friend Bart from movesonrails.com just blogged about Rails log analyzer, a command line tool to get performance statistics for your Rails application by parsing its log file.

What started as an exercise for me to write a command line ruby program, has been extended and improved by Bart to be actually useful! We decided to release it under an MIT license. You can found the source on github. The project’s wiki contains usage information and an example of the output it will produce.

5 Comments - Tags: , , ,

29 July Active OLAP released

Remember my post about easy OLAP queries in Rails? I rewrote it almost completely and published is as a Rails plugin for anyone to use on github! It is now called: Active OLAP.

Although it is a complete rewrite, the API I demoed in my previous post should still work with some small changes. The most important: you have to enable it for every class you want to use it on with the enable_active_olap method. You can provide a block to this method with dimension definitions, but is not mandatory:

class User < ActiveRecord::Base
 
  enable_active_olap do |olap|
 
    # create a simple dimension on the account_type field
    olap.dimension :account_type
 
    # create a dimension with custom categories
    # the order of the categories will be kept in the results 
    # if you use an array to define the categories.
    olap.dimension :nationality, :categories => [
      [:usa, { :country => 'US' }],
      [:china, { :country => 'CN' }]
      # other is automatically added
    ]
 
    # Easily create a trend dimension
    olap.dimension :created_daily, :trend => {
      :timestamp_field => :created_at,
      :period_length => 1.day, 
      :period_count => 20
    }
  end
end

Now, we can use these dimensions for our OLAP queries. Multiple dimensions are supported too!

# simple query
@result = User.olap_query(:nationality)
# @result[:usa] == 123, @result[:china] == 456, @result[:other] = 789
 
# do drilldown using will_paginate to paginate the results
# olap_drilldown is implemented as a named_scope
@users = User.olap_drilldown(:nationality => :china).paginate(:page => 1)
 
# multiple dimensions!
@result = User.olap_query(:nationality, :created_daily)
@users = User.olap_drilldown(:nationality => :china, 
                        :created_daily => :period_19)

I am working on a generic controller that can easily be added to your Rails project. Just define dimensions for your models and the controller will let you execute OLAP queries and display the results as a table or a graph.

Keep an eye on this weblog or the github project if you want to stay up-to-date! Or, contact me if you have questions, suggestions or want to help out.

1 Comment - Tags: , , ,

26 July Easy search with ActiveRecord

A couple of minutes ago I released scoped_search, a Rails/ActiveRecord plugin that makes it easy to search your models. It is very easy to use:

  1. Install the plugin in your vendor/plugins directory from http://github.com/wvanbergen/scoped_search
    Add the gem to your rails environment.rb:

    config.gem 'wvanbergen-scoped_search', :lib => 'scoped_search', 
        :source => 'http://gems.github.com'

    Call rake gems:install afterwards to ensure the gem is installed.

  2. Define in what fields your model should be searched by calling
    searchable_on :some, :field, :names
  3. Find your records by calling search_for("query keywords")

That’s all! A short example:

class Project < ActiveRecord::Base
  searchable_on :name, :description
end
 
Project.search_for("search keywords").each do |project| 
  puts project.name
end
 
# SELECT * FROM projects WHERE 
#      (name LIKE '%search%' OR description LIKE '%search%') 
#  AND (name LIKE '%keywords%' OR description LIKE '%keywords%')

This functionality is completely build upon named_scope. The search_for method is actually a named scope that was created by the call to searchable_on. Because these scopes can be chained, this offers some great possibilities.

For example, in Floorplanner, we only want you to search on the projects you have access to. We have implemented this access logic in another named scope. The calls can simply be chained:

class Project < ActiveRecord::Base
  searchable_on :name, :description
 
  named_scope :accessible_by, lambda { |user| ... }
  named_scope :published, :conditions => 'published_at IS NOT NULL'
end
 
@projects = Project.accessible_by(current_user).published.search_for('query')
@projects.each { |project| ... }

This plugin is released under the MIT license, so please use it for any purpose you see fit. There are some TODOs: you currently can not search on fields in other tables, and splitting the search string into keywords is very basic. Please contact me if you have implemented any of these features and you are willing to share them! Do not hesitate to contact me in case or problems either.

Update: I added support for quotes and the minus sign to the query language:
Project.search_for('willem -"van bergen"').count

Update #2: Wes Hays implemented the OR keyword:
Project.search_for('wes OR hays').count.
A big thanks to Wes for helping out on this project!!

5 Comments - Tags: , , , ,

14 July Easy OLAP queries in ActiveRecord

Because I love statistics so much, I decided to add some neat statistics functionality to the Floorplanner administration interface, so we can get better insight in what is going on. Instead of writing complete OLAP SQL queries myself and adding a custom interface for each one of them so our management can use them (yes Jeroen, that means you!), I built an ActiveRecord extension to ease the work. Right now, I only have to define some categories, and it automagically generates the right SQL query to generate charts and tables with the number of records that fall in each category. Moreover, by clicking on these numbers, I can drill down to the individual records.

I can define the categories like this:

olap_definition = { :categories => {
  :project_is_private   => { :public => false, :publishd_at => nil },
  :project_is_public    => { :public => true,  :publishd_at => nil },
  :project_is_published => 'projects.published_at IS NOT NULL'
}}

Not too hard, was it? Now, I can easily feed this to Project.olap_query:

@query_result = Project.olap_query(olap_definition) 
# @query_result == {
#   :project_is_private   => 123,
#   :project_is_public    => 456,
#   :project_is_published => 3,
#   :other                => 2
# }

Note that the category other is added automatically, but can be omitted if you wish. (I found that the other-category is nice to spot data integrity problems in your dataset you didn’t think of beforehand). The result can be used to create a table with the results, plot a pie chart with the Google Charts API. Because this setup is completely generic, this functionality only has to be written once. DRY!

The SQL for other-category is “calculated” by OR-ing all the categories and checking whether the result is false, or NULL. The check for NULL is necessary if you have NULL-values in your table: this is a weird characteristic of SQL that defines that TRUE AND NULL equals NULL (see Wikipedia).

The actual SQL query for this example would be:

SELECT 
  SUM(projects.public = 0 AND projects.published_at IS NULL) AS project_is_private,
  SUM(projects.public = 1 AND projects.published_at IS NULL) AS project_is_public,
  SUM(projects.published_at IS NOT NULL) AS project_is_published,
  SUM( NOT (
    (projects.public = 0 AND projects.published_at IS NULL) OR
    (projects.public = 1 AND projects.published_at IS NULL) OR
    (projects.published_at IS NOT NULL)
  ) OR (
    (projects.public = 0 AND projects.published_at IS NULL) OR
    (projects.public = 1 AND projects.published_at IS NULL) OR
    (projects.published_at IS NOT NULL) IS NULL)) AS other
FROM projects

Some notes about this query:

  • It is complety built using the fragments from the categories. The fragment for the other-cagegory is a little verbose, but what do I care? It works and is generated automatically! :-)
  • Note that a record can be in multiple categories, depending on the category definitions. The other category only contains records that conform to none of the provided categories.
  • SUM is used in stead of COUNT. This way, I can query all the categories at once and it solves the problems with NULL-values, while keeping my WHERE and GROUP BY clause nice and clean :-)
  • The query is built completely using ActiveRecords find method by using anonymous scopes. Therefore, Rails 2.1 is required, but this makes some neat tricks possible as well.

I also have a Project.olap_drilldown method that I can use to find the individual projects in a category:

@projects = Project.olap_drilldown(olap_definition, :project_is_public)
# SELECT projects.* FROM projects 
# WHERE (projects.public = 1 AND projects.published_at IS NULL)
 
@projects.each do |project|
  puts project.name
end

Because this functionality is built on anonymous scopes, it offers some interesting additional functionality. You can use your own scopes to limit the input dataset

class Project < ActiveRecord::Base
  named_scope :recent, lambda { { :conditions => 
              ['created_at > ?', Time.now - 7.days]} }
  ...
end
# This will add a WHERE-clause to the OLAP query
results = Project.recent.olap_query(olap_definition)
 
# Or, use :conditions for the same effect
results = Project.olap_query(olap_definition.merge(
            :conditions => ['created_at > ?', Time.now - 7.days]))

As I noted before, the GROUP BY-clause is not used. I already built an extension to use the GROUP BY clause to group the results in periods of a given timestamp field of the model (e.g. created_by). When I pass the result of such a query to the Google Chart API, I can generate trend graphs to see how my dataset is evolving.

If I have time and there is any interest, I may release this extension as a gem or Rails plugin.

UPDATE: I rewrote it and released this project on github.

1 Comment - Tags: , , , ,

22 May Developing RESTful APIs in Rails

As you may have read on this blog, we are working on a RESTful API for Floorplanner. This post contains some random observations I have made and questions I had (and still have) during the development.

to_xml incompatible with to_json
ActiveRecord#to_json does not seem to be fully compatible with ActiveRecord#to_xml. With to_xml, it is possible to overwrite the to_xml method of your models. The overwritten method will be called, even if an instance of such a model is :included within the XML of another model. For to_json however, only the overwritten method of the instance you call to_json on will be executed; for every included model, the default implementation is used.

A workaround in most cases is to pass every option to the initial call of to_json:

@object.to_json(:except => [:id],
    :include =&gt; {:related_objects => {:except => [:id, :object_id]}})

Weird behavior in to_xml called on an array of objects
One model in our project seems to have some caching issues in production mode. If to_xml is called on a collection of instances of this model, the results seems to get stored in cache. On every call, the result is appended to this cached value. The result is a lot of repetition of the same XML, which is invalid XML. The weird part is that it works OK if it is only a single instance of this model or if config_cache_classes == false (a development environment). to_json does not seem to have this problem either. All other models are unaffected as well. A more complete write-up of the problem can be found here.

I am still not able to figure out what causes this behavior and I am currently working around this issue by using some String#split-magic on the result of the to_xml-call. I know this is extremely ugly, so if anybody has experienced a similar problem, please let me know! It’s driving me nuts!

Testing an XML API
What is the best way to test a REST API, besides the Unit-tests that are already in place? Currently, I have an integration test suite, with a lot of testing code that looks like this:

def test_create_project
  post projects_path(:format => :xml),
      {:project => {:description => 'Original description'}}
  assert_response :created
  @new_project_location = headers['location'].first # array?
 
  get @new_project_location
  assert_response :success
  project_doc = REXML::Document.new(response.body)
  assert_equal 'Original description',
      project_doc.elements['//description'].text
 
end

It works, but is isn’t very elegant in my eyes. Moreover, all that XML parsing is making the test suite slower and slower. Does anybody have suggestions to build a cleaner and faster test suite for a RESTful XML API? By the way, is there an easy way to POST an XML document rather than “normal” POST-parameters in these calls?

1 Comment -

7 May RAPIDoc Rails Rest Api documentation generation, well just RAPIDoc…

Today I setup the first version of a RAPIDoc, a Rest API Rails Documentation Generator and we decided to open source this thing, from now on let’s call it RAPIDoc. It is a API code generator for Rails, describing your Rest resources. We we’re looking for a way to fully integrate documentation of our API into the code base, like Rdoc does. Rdoc didn’t suit our needs, cause it has got nothing to do with Rest and resource stuff, so we decided to hack something together and gave it a name: RAPIDoc.

What does it do?
It generates a API controller containing documenation maked up in a special language. It parses controllers you specify, and generates a ApiController with appropriate views for it. This makes it very easy to document a Rest API. For methods you use, and according to ERB templates you specify it generates the views for you. It doesn’t parse your routes.rb, it was not needed for us, but may be a nice extension for it.

Well how is that RAPI doc looking? Put this in front of a Restful method.

1
2
3
4
5
6
7
8
9
10
11
=begin rapidoc
url:: /projects
method:: GET
access:: FREE
return:: [JSON|XML] - some project
param:: page:int - the page
param:: per_page:int - max items per page
 
Get a list of projects. This method uses pagination. If you want to retreive project 1-10 for example:
/projects?page=1&per_page=10.
=end

For each resource you specify, a view is created and it is put into the index.

Opensource
It is available from now on on:

http://code.google.com/p/rapidoc/

If you find it useful and you want to change some things, become a submitter. Ow yeah, just a note: really experimental code

No Comments - Tags: , , ,