cancan considered harmful

written by Raphael

TL;DR overwrite as_json on all your CanCan Ability classes.

Since I've started using squash for bug tracking I started noticing exceptions which prevented squash client from transmitting exceptions to the squash backend.

The exceptions squash was rescueing made squash raise a new exception!

Digging through the stack traces it became obvious that Ryan Bates cancan is the root of all evil.

To reproduce fire up irb and paste the following lines:

# dependencies to reproduce
require 'cancan'         # v1.6.10
require 'json'           # v1.7.7
require 'active_support' # v3.2.12
require 'active_record'
require 'sqlite3'        # v1.3.7

# we need active record
ActiveRecord::Base.establish_connection({
  :adapter => "sqlite3",
  :database  => ":memory:"
})

# and two sample classes
class User < ActiveRecord::Base
end

ActiveRecord::Migration.create_table :users do |t|
end

class Project < ActiveRecord::Base
  belongs_to :user
end

ActiveRecord::Migration.create_table :projects do |t|
  t.belongs_to :user
end

# as well as an Ability
class Ability
  include CanCan::Ability

  def initialize user
    user

    can [:new, :create, :edit, :update, :destroy], Project do |project|
      project.user == user
    end
  end
end

ability = Ability.new(User.new)
ActiveSupport::JSON.encode(ability) # => boom

You'll see an ActiveSupport::JSON::Encoding::CircularReferenceError: object references itself exception.

As far as I can tell this is due to the data structure CanCan is using to store the permissions.

Defining as_json as follows solves this problem:

def as_json *args
  return {
    class_name: 'Ability',
    user_id: null # your metadata here
  }
end

I'd vote to make this a default in CanCan, but for the time being this solves my problem while still leaving enough debugging informations for me to reproduce error states.

That being said, I really like cancan - it's a great gem. But those circular reference errors are something to watch out for.


syncing database content with mina

written by Raphael

Imagine you are working on an application which is already in production, or being used by a lot of people.

While you're working on a new version, problems occur in production, which are hard to reproduce for you because all you've got on your local machine is a static dataset.

Often you'll find yourself creating database dumps from production and importing them on your local machine. A repetitive and somewhat time consuming task.

Now you might be using capistrano and there are already working solutions out there for this problem. STOP READING.

But in case you're using mina (which you should ;)) here are two example tasks which can sync down the database content for a typical Ruby on Rails application using PostgreSQL or MySQL.

Note that these examples can be easily adjusted to work with non-rails setups. Really anything which stores the information about the database anywhere accessible is just fine.

Usage

The example snippets below can be used like this once you've added them to your deploy.rb:

bundle exec mina sync:db

PostgreSQL

RYAML = <<-BASH
function ryaml {
  ruby -ryaml -e 'puts ARGV[1..-1].inject(YAML.load(File.read(ARGV[0]))) {|acc, key| acc[key] }' "$@"
};
BASH
namespace :sync do
  task :db do
    isolate do
      queue RYAML
      queue "USERNAME=$(ryaml ./shared/config/database.yml #{rails_env} username)"
      queue "PASSWORD=$(ryaml ./shared/config/database.yml #{rails_env} password)"
      queue "DATABASE=$(ryaml ./shared/config/database.yml #{rails_env} database)"
      queue "PGPASSWORD=$PASSWORD pg_dump -U $USERNAME $DATABASE -c -f dump.sql"
      queue "gzip -f dump.sql"

      mina_cleanup!
    end

    %x[scp #{user}@#{domain}:#{deploy_to}/dump.sql.gz .]
    %x[gunzip -f dump.sql.gz]
    %x[#{RYAML} psql -d $(ryaml config/database.yml development database) -f dump.sql]
    %x[rm dump.sql]
  end
end

Here's a gist for the PostgreSQL example.

Note I'm using pg_dump to create a dump of the used PostgreSQL database. Also note the -c flag which makes pg_dump emit drop table statements to ease the import on your local machine.

MYSQL

RYAML = <<-BASH
function ryaml {
  ruby -ryaml -e 'puts ARGV[1..-1].inject(YAML.load(File.read(ARGV[0]))) {|acc, key| acc[key] }' "$@"
};
BASH
namespace :sync do
  task :db do
    isolate do
      queue RYAML
      queue "USERNAME=$(ryaml ./shared/config/database.yml #{rails_env} username)"
      queue "PASSWORD=$(ryaml ./shared/config/database.yml #{rails_env} password)"
      queue "DATABASE=$(ryaml ./shared/config/database.yml #{rails_env} database)"
      queue "mysqldump $DATABASE --user=$USERNAME --password=$PASSWORD > dump.sql"
      queue "gzip -f dump.sql"

      mina_cleanup!
    end

    %x[scp #{user}@#{domain}:#{deploy_to}/dump.sql.gz .]
    %x[gunzip -f dump.sql.gz]
    %x[#{RYAML} mysql --verbose --user=$(ryaml config/database.yml development username) --password=$(ryaml config/database.yml development password) $(ryaml config/database.yml development database) < dump.sql]
    %x[rm dump.sql]
  end
end

Here's a gist for the MySQL example.

Important notes

  1. The above example assumes that you are able to ssh on your production/ staging system without the use of a password. I've also assumed that you are storing your database credentials in your shared directory, symlinking it to the current version upon deploy.

  2. the ryaml snippet was taken from Toms Coderwall. Thanks, Tom!

    That's it!

    If you've got questions or feedback I'd be glad to hear them.


Asset Pipeline and I18n using i18next

written by Raphael

About one year ago I wrote about i18n and the rails asset pipeline with jQuery.

Last week I took the time to update my jquery-localization-engine to support i18next instead of jquery-localize.
I also added support for an external service, Copycopter. Copycopter enables you to update your localizations without being forced to redeploy your application. The new package is called rails-asset-localization.

So far it's working really great. To summarize - the upsides

  • all my locales are compiled on deployment. Users accessing my application for the first time wont have any additional network requests for localizations. They might be served outdated locales, but only for a short amount of time.
  • returning users will be served new locales every five hours, leaving them with a more up to date version of the locales. This is handled by i18next without any work on my side.
  • I can update all locales using Copycopter without having to worry to redeploy the application.

The downsides:

  • i18next supports default values, but those default values are not sent back to Copycopter at the moment. This means I'm forced to memorize which keys to add with which default values. Note that i18next has support for posting new translations to a backend but I've not added support for this just yet.
  • there's no way to force the client to redownload locales at the moment. This could be added easily by removing the localStorage elements i18next creates, though.

Here's a short integration example which I'm running in production:

I've set up my to use rails-asset-localization and my application.coffee to load i18next + a simple UI initialization code:

# Gemfile
gem 'rails-asset-localization', git: 'git@github.com:nicolai86/rails-asset-localization.git', branch: :master
# application.coffee
#= require jquery
#= require jquery_ujs
#= require i18next
#= require i18n/translations

window.UI = {
  currentLocale: null

  load: (locale) ->
    @currentLocale = locale

    for bundledLocale of bundledLocales
      storedLocale = window.localStorage.getItem("res_#{bundledLocale}")
      unless storedLocale?
        object = {}
        object[bundledLocale] = bundledLocales[bundledLocale]
        i18n.sync._storeLocal object

    options = {
      interpolationPrefix: '%{'
      interpolationSuffix: '}'
      lng: locale
      resGetPath: "/locales/%{lng}.json"
      useLocalStorage: true
      localStorageExpirationTime: 60 * 60 * 5 * 1000
    }
    i18n.init options
}

inside my application.haml I'm calling out to UI.load

!!!
%html
  %head
  %body
    :coffeescript
      UI.load("#{I18n.locale}");

my client side templates are using @leshills handlebars_assets

# welcome.hamlbars
%h1
  {{t "app.welcome"}}

I've added a Handlebars helper method which enables me to use i18n.t inside my views:

Handlebars.registerHelper 't', ->
  args = [].slice.call(arguments)
  result = i18n.t.apply(i18n,args)
  return new Handlebars.SafeString(result)

A future version of railsassetlocalization will have support for the posting of new translations - but for now this works just great.

That's it! Happy hacking - and happy easter!