Programifications

A mashup of technical quirks

RSpec Formatter Documentation With Instant Stack Traces Upon Failure

| Comments

The documentation rspec formatter is nice to read, but the bad thing about it is that when a failure occurs, it doesn’t show a stack trace until the end of the test run. If you have a long running test suite, you might want to look into the first few failures you encounter while the test suite finishes running.

With this simple formatter, it accomplishes exactly that.

Now when the test suite runs, the stack trace gets printed right away upon failures.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
$ bundle exec rspec spec/models/player_spec.rb

Player
  #next
    in the main stream
      should return the next song
      should decrement play count if the song was skipped
      playing the first song
        should fetch and return the first song (FAILED - 1)
  1) Player#next in the main stream playing the first song should fetch and return the first song
     Failure/Error: fail
     RuntimeError:
     # ./spec/models/player_spec.rb:24:in `block (5 levels) in <top (required)>'
     # /Users/derrick/.rvm/gems/ruby-1.9.2-p0@player/gems/rspec-core-2.6.4/lib/rspec/core/example.rb:48:in `instance_eval'
     # /Users/derrick/.rvm/gems/ruby-1.9.2-p0@player/gems/rspec-core-2.6.4/lib/rspec/core/example.rb:48:in `block in run'
     # /Users/derrick/.rvm/gems/ruby-1.9.2-p0@player/gems/rspec-core-2.6.4/lib/rspec/core/example.rb:107:in `with_around_hooks'
     # /Users/derrick/.rvm/gems/ruby-1.9.2-p0@player/gems/rspec-core-2.6.4/lib/rspec/core/example.rb:45:in `run'
     # /Users/derrick/.rvm/gems/ruby-1.9.2-p0@player/gems/rspec-core-2.6.4/lib/rspec/core/example_group.rb:294:in `block in run_examples'
     # /Users/derrick/.rvm/gems/ruby-1.9.2-p0@player/gems/rspec-core-2.6.4/lib/rspec/core/example_group.rb:290:in `map'
     # /Users/derrick/.rvm/gems/ruby-1.9.2-p0@player/gems/rspec-core-2.6.4/lib/rspec/core/example_group.rb:290:in `run_examples'
     # /Users/derrick/.rvm/gems/ruby-1.9.2-p0@player/gems/rspec-core-2.6.4/lib/rspec/core/example_group.rb:262:in `run'
     # /Users/derrick/.rvm/gems/ruby-1.9.2-p0@player/gems/rspec-core-2.6.4/lib/rspec/core/example_group.rb:263:in `block in run'
     # /Users/derrick/.rvm/gems/ruby-1.9.2-p0@player/gems/rspec-core-2.6.4/lib/rspec/core/example_group.rb:263:in `map'
     # /Users/derrick/.rvm/gems/ruby-1.9.2-p0@player/gems/rspec-core-2.6.4/lib/rspec/core/example_group.rb:263:in `run'
     # /Users/derrick/.rvm/gems/ruby-1.9.2-p0@player/gems/rspec-core-2.6.4/lib/rspec/core/example_group.rb:263:in `block in run'
     # /Users/derrick/.rvm/gems/ruby-1.9.2-p0@player/gems/rspec-core-2.6.4/lib/rspec/core/example_group.rb:263:in `map'
     # /Users/derrick/.rvm/gems/ruby-1.9.2-p0@player/gems/rspec-core-2.6.4/lib/rspec/core/example_group.rb:263:in `run'
     # /Users/derrick/.rvm/gems/ruby-1.9.2-p0@player/gems/rspec-core-2.6.4/lib/rspec/core/example_group.rb:263:in `block in run'
     # /Users/derrick/.rvm/gems/ruby-1.9.2-p0@player/gems/rspec-core-2.6.4/lib/rspec/core/example_group.rb:263:in `map'
     # /Users/derrick/.rvm/gems/ruby-1.9.2-p0@player/gems/rspec-core-2.6.4/lib/rspec/core/example_group.rb:263:in `run'
     # /Users/derrick/.rvm/gems/ruby-1.9.2-p0@player/gems/rspec-core-2.6.4/lib/rspec/core/command_line.rb:24:in `block (2 levels) in run'
     # /Users/derrick/.rvm/gems/ruby-1.9.2-p0@player/gems/rspec-core-2.6.4/lib/rspec/core/command_line.rb:24:in `map'
     # /Users/derrick/.rvm/gems/ruby-1.9.2-p0@player/gems/rspec-core-2.6.4/lib/rspec/core/command_line.rb:24:in `block in run'
     # /Users/derrick/.rvm/gems/ruby-1.9.2-p0@player/gems/rspec-core-2.6.4/lib/rspec/core/reporter.rb:12:in `report'
     # /Users/derrick/.rvm/gems/ruby-1.9.2-p0@player/gems/rspec-core-2.6.4/lib/rspec/core/command_line.rb:21:in `run'
     # /Users/derrick/.rvm/gems/ruby-1.9.2-p0@player/gems/rspec-core-2.6.4/lib/rspec/core/runner.rb:80:in `run_in_process'
     # /Users/derrick/.rvm/gems/ruby-1.9.2-p0@player/gems/rspec-core-2.6.4/lib/rspec/core/runner.rb:69:in `run'
     # /Users/derrick/.rvm/gems/ruby-1.9.2-p0@player/gems/rspec-core-2.6.4/lib/rspec/core/runner.rb:11:in `block in autorun'
        should increment play count and track listen for the current user
      attempting to play a post with an error
        should mark a song as erroneous
      going back a song then going forward
        should play the next song that it once played before
  #previous
    in the main stream
      should return the previous song
      should increment play count and track listen for the current user

Introducing the Autosuggest-rb Gem

| Comments

I recently released a gem that wraps the jQuery autoSuggest Plugin. It provides helpers that make it easy to use in Rails 3. It supports ActiveRecord, Mongoid, and MongoMapper. The code is hosted on github. Feel free to fork and improve it!

INSTALLING

Include the gem on your Gemfile

1
gem 'autosuggest-rb'

Install it

1
bundle install

Run the generator

1
rails generate autosuggest

And include jquery.autoSuggest.js and autoSuggest.css on your layouts or add them to your assets file

1
2
javascript_include_tag "jquery.autoSuggest.js"
stylesheet_link_tag "autoSuggest.css"

USAGE

Model Example

Assuming you have a Tag model:

1
2
3
4
5
6
class Tag < ActiveRecord::Base
end

create_table :tags do |t|
  t.string :name
end

Controller

Your controller will need an action to respond to the autosuggest textfield. To add it to your controller call the autosuggest method and pass it the name of the model and column name as in the following example:

1
2
3
class RecipesController < ApplicationController
  autosuggest :tag, :name
end

This will create a autosuggest_tag_name action. You then need to add a route for that action

1
2
3
resources :recipes do
  get :autosuggest_tag_name, :on => :collection
end

From the view you can create an autosuggest field with the autosuggest_field_tag helper

1
2
3
4
5
6
7
8
9
# autosuggest_field_tag(name, value, source_path, options={})
# @param [String] name - name you want to use for the text field
# @param [String] value - value to show up in the field by default
# @param [String] source_path - url to the autosuggest path
# @param [Hash] options - options that you can normally pass into text_field_tag

form_tag('/some_path') do
  autosuggest_field_tag "tags", "", autosuggest_tag_name_recipes_path
end

You can also use the autosuggest_field helper

1
2
3
form_for @recipe do |f|
  f.autosuggest_field :tags, autosuggest_tag_name_recipes_path
end

By default, autosuggest only queries the db for existing entries, but if you want to be able to create new ones, just pass these options:

1
f.autosuggest_field :tags, autosuggest_tag_name_recipes_path, :autosuggest_options => { "newValuesInputName" => recipes[new_tags]" }

Then you can do whatever you want from the controller using params[:recipes][:new_tags]

These are the default options:

1
2
3
4
5
"queryParam" => "query",
"selectedItemProp" => "name",
"searchObjProps" => "name",
"neverSubmit" => "true",
"asHtmlName" => "#{object_name}[set_#{method}]" # recipes[set_tags] in our example

But you can pass options in by using the autosuggest_options param

1
f.autosuggest_field :tags, autosuggest_tag_name_recipes_path, :autosuggest_options => {"neverSubmit" => "true"}

Here are the other options you can pass in:

asHtmlName: string (false by default) – Enables you to specify your own custom name that will be attributed to the text field

newValuesInputName: string (false by default) – Enables you to define a name for a hidden field that will catch new names that don’t match any in the db

asHtmlID: string (false by default) – Enables you to specify your own custom ID that will be appended to the top level AutoSuggest UL element’s ID name. Otherwise it will default to using a random ID. Example: id=“CUSTOM_ID”. This is also applies to the hidden input filed that holds all of the selected values. Example: id=“as-values-CUSTOM_ID”

startText: string (“Enter Name Here” by default) – Text to display when the AutoSuggest input field is empty.

emptyText: string (“No Results” by default) – Text to display when their are no search results.

preFill: object or string (empty object by default) – Enables you to pre-fill the AutoSuggest box with selections when the page is first loaded. You can pass in a comma separated list of values (a string), or an object. When using a string, each value is used as both the display text on the selected item and for it’s value. When using an object, the options selectedItemProp will define the object property to use for the display text and selectedValuesProp will define the object property to use for the value for the selected item. Note: you must setup your preFill object in that format. A preFill object can look just like the example objects laid out above.

limitText: string (“No More Selections Are Allowed” by default) – Text to display when the number of selections has reached it’s limit.

selectedItemProp: string (“value” by default) – Name of object property to use as the display text for each chosen item.

selectedValuesProp: string (“value” by default) – Name of object property to use as the value for each chosen item. This value will be stored into the hidden input field.

searchObjProps: string (“value” by default) – Comma separated list of object property names. The values in these objects properties will be used as the text to perform the search on.

queryParam: string (“q” by default) – The name of the param that will hold the search string value in the AJAX request.

retrieveLimit: number (false by default) – If set to a number, it will add a ‘&limit=’ param to the AJAX request. It also limits the number of search results allowed to be displayed in the results dropdown box.

extraParams: string (“” by default) – This will be added onto the end of the AJAX request URL. Make sure you add an ‘&’ before each param.

matchCase: true or false (false by default) – Make the search case sensitive when set to true.

minChars: number (1 by default) – Minimum number of characters that must be entered into the AutoSuggest input field before the search begins.

keyDelay: number (400 by default) – Number of milliseconds to delay after a keydown on the AutoSuggest input field and before search is started.

resultsHighlight: true or false (true by default) – Option to choose whether or not to highlight the matched text in each result item.

neverSubmit: true or false (false by default) – If set to true this option will never allow the ‘return’ key to submit the form that AutoSuggest is a part of.

selectionLimit: number (false by default) – Limits the number of selections that are allowed to be made to the number specified.

showResultList: true or false (true by default) – If set to false, the Results Dropdown List will never be shown at any time.

start: callback function – Custom function that is run only once on each AutoSuggest field when the code is first applied.

selectionClick: callback function – Custom function that is run when a previously chosen item is clicked. The item that is clicked is passed into this callback function as ‘elem’.

1
Example: selectionClick: function(elem){ elem.fadeTo(slow, 0.33); }

selectionAdded: callback function – Custom function that is run when a selection is made by choosing one from the Results dropdown, or by using the tab/comma keys to add one. The selection item is passed into this callback function as ‘elem’.

1
Example: selectionAdded: function(elem){ elem.fadeTo(slow, 0.33); }

selectionRemoved: callback function – Custom function that is run when a selection removed from the AutoSuggest by using the delete key or by clicking the “x” inside the selection. The selection item is passed into this callback function as ‘elem’.

1
Example: selectionRemoved: function(elem){ elem.fadeTo(fast, 0, function(){ elem.remove(); }); }

formatList: callback function – Custom function that is run after all the data has been retrieved and before the results are put into the suggestion results list. This is here so you can modify what & how things show up in the suggestion results list.

beforeRetrieve: callback function – Custom function that is run right before the AJAX request is made, or before the local objected is searched. This is used to modify the search string before it is processed. So if a user entered “jim” into the AutoSuggest box, you can call this function to prepend their query with “guy_”. Making the final query = “guy_jim”. The search query is passed into this function. Example: beforeRetrieve: function(string){ return string; }

retrieveComplete: callback function – Custom function that is run after the ajax request has completed. The data object MUST be returned if this is used. Example: retrieveComplete: function(data){ return data; }

resultClick: callback function – Custom function that is run when a search result item is clicked. The data from the item that is clicked is passed into this callback function as ‘data’.

1
Example: resultClick: function(data){ console.log(data); }

resultsComplete: callback function – Custom function that is run when the suggestion results dropdown list is made visible. Will run after every search query.

Rails 3 Cheat Sheet

| Comments

Envy Labs just released some Rails 3 cheat sheets to aid the API usage of several components. The pdf can be found here.

ROUTING API

Basic Routing

1
2
3
4
5
YourApp::Application.routes.draw do
  resources :posts
  match '/all' => 'posts#index'
  root :to => "home#index"
end

Optional Parameters

1
2
3
4
5
6
7
8
match '/posts/(/:yy(/:mm))' => "posts#index"

class PostsController < ApplicationController
  def index
    # params[:yy]
    # params[:mm]
  end
end

Nested Routes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
namespace :api do
  namespace :internal do
    resources :accounts do
      resources :orders
      member do
        get  :summary
        post :suspend
      end
      post :confirm, :on => :member
      collection do
        get :pending
      end
      get :blocked, :on => :collection
    end
  end
end

Redirection

1
2
3
match '/sign_out'     => redirect("/signout")
match '/users/:name'  => redirect {|params| "/#{params[:name}"}
match '/google'       => redirect('http://google.com')

Named Routes

1
2
match '/sign_in' => 'session#new', :as => 'sign_in' # Gives you sign_in_path helper
match '/reset_password/:token' => 'users#reset_password', :as => 'reset_password' # Gives reset_password_path('key')

Rack Routing

1
2
3
get '/hello'          => proc {|env| [200, {}, "Hello Rack"]}
get '/rack_endpoint'  => PostsController.action(:index)
get '/rack_app'       => CustomRackApp

Constraints

1
2
3
4
5
6
7
8
9
10
match '/:year' => "posts#index",
  :constraints => {:year => /\d{4}/, :ip => /192\.168\.1\.\d{1,3}/}

constraints(:host => /localhost/) do
  resources :posts
end

constraints IpRestrictor do # Responds to self.matches?(request)
  get 'admin/accounts' => "queenbee#accounts"
end

Legacy Route

1
match '/:controller(/:action(/:id(.:format)))' # Commented out by default

Scope

1
2
3
4
5
6
7
8
9
10
scope ':token', :token => /\w{5}/ do # Requires token parameter to get to resources and must by 5 alphanumeric characters
  resources :rooms do
    resources :meetings
  end
end

scope '(:locale)', :locale => /en|pl/ do # Local is optional for these routes
  resources :posts
  root :to => 'posts#index'
end

BUNDLER

Bundler Commands

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
$ bundle
'Make sure all dependencies in your Gemfile are available to your application. If they are not available, go install them.


$ bundle --without 
'Installs everything except gems included in .


$ bundle --deployment
'Isolates all gems into vendor/bundle, requires up-to-date Gemfile.lock, use gems in vendor/cache if they exist.  


$ bundle check
'Checks if the dependencies listed in Gemfile are satisfied by currently installed gems.


$ bundle show [gem_name]
'Shows all libraries which are included by the Gemfile and their dependencies. If [gem_name] is given, shows where it is located in the filesystem.  


$ bundle open   
'Opens the gem source in the default editor.


$ bundle update [gem_name]
'Recreates Gemfile.lock and runs 'bundle' to install new dependencies.  


$ bundle package
'Copies all project gems to vendor/cache/ -- use this if you don't want to rely on external servers for deployment.

Gemfile Syntax

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
source "http://rubygems.org"

gem "hpricot", "0.6"
gem "sqlite3-ruby", :require => "sqlite3"
gem "local_gem", :path => "~/Sites/local_gem"
gem "rails", :git => "git://github.com/rails/rails.git"

# Additional parameters
# :branch => "branch_name"
# :tag => "tag_number"
# :ref => "ref_number"

git "git://github.com/rails/rails.git" do
  # only these two gems will be fetched from the git repository
  gem "railties"
  gem "active_model"
end

group :test do
  gem "webrat"
end

Creating a gemset for your app

1
2
3
4
5
$ rvm use _ruby_version_
$ rvm gemset create _name_
$ rvm gemset use _name_
$ gem install bundler
$ bundle

Workflow

Developing a new application

  1. $ cd new_app/
  2. $ bundle init # creates a new Gemfile
  3. Add project dependencies
  4. Check Gemfile and Gemfile.lock into VCS

After adding or removing dependencies from Gemfile

  1. $ bundle
  2. Commit Gemfile and Gemfile.lock After modifying existing dependency versions

  3. $ bundle update

  4. Commit Gemfile and Gemfile.lock Deploying your application
1
$ bundle pacakge # locally  

Copies all app gems to vendor/cache

1
$ bundle # server  

Installs gems from vendor/cache - no external server communication

For more info, visit http://gembundler.com

ACTIVERELATION

Lazy Loading

1
2
3
4
5
6
7
8
9
10
@posts = Post.where(:published => true)
if params[:order]
  @posts = @posts.order(params[:order])
end
@posts.each {|p| ... }


posts = Post.order(params[:order])
@published = posts.where(:published => true)
@unpublished = posts.where(:published => false)

CRUD Methods

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
new(attributes)
create(attributes)
create!(attributes)
find(id_or_array)
destroy(id_or_array)
destroy_all
delete(id_or_array)
delete_all
update(ids, updates)
update_all(updates)
exists?


sports_posts = Post.where(:category => 'sports')
new_sports_post = sports_posts.new
new_sports_post.category # => 'sports'

sports_posts.update_all(:published => false)
sports_posts.exists? # => true

Chain Methods

1
2
3
4
5
6
7
8
9
10
11
12
where
having
select
group
order
limit
offset
joins
includes
lock
readonly
from

Chaining

1
2
3
@posts = Post.where(:published => true).order(params[:order])

@joe_posts = Post.where(:auther => "Joe").includes(:comments).limit(10).all # .all forces query execution and returns an array, not a relation

(Named) Scopes

1
2
3
4
class Post < ActiveRecord::Base
  default_scope order('title')
  scope :published, where(:published => true)
  scope :unpublished, where(:published => false)

Deprecated

1
2
3
4
5
6
find(id_or_array, options) # All options are now sent using the chain methods
find(:first, options)
find(:all, options)
first(options)
all(options)
update_all(updates, conditions, options)

XSS PROTECTION & UJS

XSS Protection

1
2
3
4
5
<%= @post.body # safe by default %>

<%= raw(@post.body) # unsafe %>

<%= link_to raw("<span class='cart'>#{h @user_input}</span>"), cart_path # only h @user_input escaped, not the spans %>

Unobtrusive JS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<%= link_to 'Show', @post, :remote => true %>
# => <a href="/posts/1" data-remote="true">Show</a>

<%= form_for(@post, :remote => true) do |f| %>
# => <form action="/posts" class="new_post" data-remote="true" id="new_post" method="posts">

<%= link_to 'Destroy', @post, :method => :delete %>
# => <a href="/posts/1" data-method="delete" rel="nofollow">Destroy</a>

<%= link_to 'Destroy', @post, :confirm => 'Are you sure?', :method => :delete %>
# => <a href="/posts/1" data-confirm="Are you sure?" data-method="delete" rel="nofollow">Destroy</a>

<%= f.submit :disable_with => "Please wait..." %>
# => <input data-disable-with="Please wait..." id="post_submit" name="commit" type="submit" value="Create Post" />

HTML5 Custom Data Attributes

1
2
3
4
5
data-remote
data-confirm
data-method
data-disable-with
# Parsed by JavaScript drivers -- defaults to rails.js

Deprecated

1
2
3
4
5
6
7
8
9
10
11
link_to_remote
remote_form_for
observe_field
observe_form
form_remote_tag
button_to_remote
submit_to_remote
link_to_function
periodically_call_remote

# If you need to use any of the deprecated helpers, visit http://github.com/rails/prototype_legacy_helper

Using JQuery

Go to https://github.com/rails/jquery-ujs for instructions

ACTIONMAILER & ACTIONCONTROLLER

Rails Mail Generator

1
2
3
4
5
rails generate mail UserMail welcome forgot_password
# Creates:
# app/mailers/user_mailer.rb
# app/views/user_mailer/welcome.text.erb
# app/views/user_mailer/forgot_password.text.erb

Basic Mailer Syntax

1
2
3
4
5
6
7
8
9
10
class UserMailer < ActionMailer::Base
  def welcome(user, subdomain)
    # instance variables are available within your view
    @user = user
    @subdomain = subdomain
    mail(:from => 'admin@app.com',
         :to => @user.email,
         :subject => 'Welcome')
  end
end

Delivering Messages

1
2
3
4
5
6
UserMailer.welcome(user, subdomain).deliver

# or

message = UserMailer.welcome(user, subdomain)
message.deliver

Defaults and Attachments

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class UserMailer < ActionMailer::Base
  default :from => 'admin@test.com',
          :reply_to => 'noreply@test.com',
          "X-Time-Code" => Time.now.to_i.to_s

  def welcome(user, subdomain)
    @user = user
    @subdomain = subdomain
    attachments['test.pdf'] = File.read(Rails.root.join('public/test.pdf'))
    attachments['photo.jpg'] = { :content => generate_image() }
    mail(:to => @user.email, :subject => 'Welcome to TestApp') do |format|
      # Defaults to: welcome.html.erb, welcome.text.erb
      format.html { render 'other_html_welcome' }
      format.text { render 'other_text_welcome' }
    end
  end
end

respond_to and respond_with

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class UsersController < ApplicationController
  respond_to :html, :json, :only => :index
  respond_to :xml, :except => :index

  def index
    respond_with(@users = User.all, :status => :ok)
  end

  def create
    @user = User.create(params[:user])
    respond_with(@user) do |format|
      format.html { redirect_to(users_path) }
    end
  end
end

ACTIVEMODEL

Modules

1
2
3
4
5
6
7
8
AttributeMethods
Callbacks
Dirty
Errors
Naming
Observing
Serialization
Validations

Dirty

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Person
  include ActiveModel::Dirty
  define_attribute_methods [:name]

  def name
    @name
  end

  def name=(val)
    name_will_change!
    @name = val
  end

  def save
    @previously_changed = changes
    @changed_attributes.clear
  end
end


person = Person.find(id)
person.changed? # => false

person.name = 'Bob'
person.changed? # => true

person.name_changed? # => true

person.name_was # => 'Uncle Bob'

person.name_change # => ['Uncle Bob', 'Bob']

person.name = 'Bill'
person.name_change # => ['Uncle Bob', 'Bill']

Validations

1
2
3
4
5
6
7
8
9
class Person
  include ActiveModel::Validations
  attr_accessor
  validates_presence_of :email
end

p = Person.new # => #
p.valid? # => false
p.errors # => {:email => ["can't be blank"]}

SHORTCUTS

1
2
3
4
5
6
7
8
9
10
11
validates :terms, :acceptance => true
validates :password, :confirmation => true
validates :username, :exclusion => { :in => %w(admin) }
validates :email, :format => {
  :with => /\A[^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i,
  :on => :create }
validates :age, :inclusion => { :in => 0..9 }
validates :first_name, :length => { :maximum => 30 }
validates :age, :numericality => true
validates :username, :presence => true
validates :username, :uniqueness => true

Serialization

1
2
3
4
5
6
7
8
9
10
11
12
class Person
  include ActiveModel::Serializers::JSON
  attr_accessor :name
  def attributes
    {:name => name}
  end
end


p = Person.new # => #
p.name = "Gregg" # => "Gregg"
p.to_json => "{\"name\":\"Gregg\"}"

Callbacks

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Person
  extend ActiveModel::Callbacks
  define_model_callbacks :save
  before_save :action_before_save

  def save
    _run_save_callbacks do
      # Your save action methods here
    end
  end
  private

  def action_before_save
    # Your code here
  end
end

Encapsulating Application Config Variables in a YAML File

| Comments

Having different configurations for different environments can get messy especially as your project grows. Environment files start to get long and switching between all the different files becomes a hassle. One technique I like is to encapsulate all of the configuration variables in a YAML file and then loading it up into a global hash during initialization.

I recently dealt with a case using paperclip and had different settings depending on the environment. On production servers we wanted to use s3 for storage and locally we wanted to use the filesystem. At first we set up constants (to hold paperclip settings) in each of our environment files. So we had something like this in each of our environment(test.rb, development.rb, production.rb, etc) files:

1
2
3
4
5
6
7
PAPERCLIP_STORAGE_OPTIONS = {
  :storage => :s3,
  :s3_credentials => "#{Rails.root}/config/s3.yml",
  :styles => { :original => "800x600>", :medium => "300x300#", :thumb => "100x100#"
  :path => "/user_images/:id/:style/:filename",
  :convert_options => { :all => "-strip" }
}

In translating it to YAML (config/config.yml in my case), it looked like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
paperclip_storage_options: &paperclip
  styles:
    original: "800x600>"
    medium: "300x300#"
    thumb: "100x100#"
  path: ":rails_root/public/user_images/:id/:style/:filename"
  convert_options:
    all: "-strip"

defaults: &defaults
  paperclip_storage_options:
    <<: *paperclip

deployed_defaults: &deployed_defaults
  <<: *defaults
  paperclip_storage_options:
    <<: *paperclip
    storage: "s3"
    s3_credentials: "<%= RAILS_ROOT %>/config/s3.yml"

development:
  <<: *defaults

test:
  <<: *defaults

qa:
  <<: *deployed_defaults

production:
  <<: *deployed_defaults

And so now each environment can have its own configurations and it’s nicely encapsulated in one file. You may notice that we have embedded ruby in the YAML file. We just have to feed it into ERB when we load the YAML. Let’s take a look at config/initializers/config.rb:

1
APP_CONFIG = YAML.load(ERB.new(File.read("#{Rails.root}/config/config.yml")).result)[Rails.env].recursive_symbolize_keys

You can actually ignore the recursive_symbolize_keys, as I only had to do that since paperclip only takes symbolized arguments. While this snippet of code isn’t the prettiest, it works well and enables global config encapsulation. First it’s reading the YAML file, interpreting the ruby code, converting it into a hash, and then finally storing it into a constant.

Hash#recursive_symbolize_keys

| Comments

Surprisingly there isn’t a Hash#recursive_symbolize_keys method so here’s code to accomplish that:

1
2
3
4
5
6
7
class Hash
  def recursive_symbolize_keys
    symbolize_keys!
    values.select{|v| v.is_a? Hash}.each{|h| h.recursive_symbolize_keys}
    self
  end
end

Sometimes gems/plugins only take arguments in the form of symbols so this can method can be handy if you have some sort of nested config hash.

Credits to: http://grosser.it/2009/04/14/recursive-symbolize_keys/

Using Bundler With Capistrano

| Comments

To make capistrano build your gems with bundler you can use the following code snippet:

1
2
3
4
5
6
7
8
9
namespace :bundle do
  task :install, :roles => :app do
    shared_dir = File.join(shared_path, 'bundle')
    run "mkdir -p #{shared_dir}"
    run "cd #{release_path} && bundle install --without development test --path #{shared_dir}"
  end
end

after 'deploy:update_code', 'bundle:install'

Of course, this is assuming that you already have bundler installed on your remote server.

Sunspot (Solr) on Rails 3

| Comments

Sunspot is a ruby driver for Solr and is pretty easy to integrate into rails using sunspot_rails.

Installation

First add it to your Gemfile:

1
gem "sunspot_rails", "1.2.rc4"

Update your bundle:

1
bundle install

Run this task to create your sunspot.yml file:

1
rails g sunspot_rails:install

To start the sunspot server run:

1
rake sunspot:solr:start RAILS_ENV=development

You probably don’t need the rails env variable set but I usually do it to be explicit. The first time you run this command, it will create a solr/ directory in your Rails root. This contains Solr’s configuration, as well as the actual data files for the Solr index. You’ll probably want to add solr/data to your .gitignore.

Setting up your Models

There are two ways to setup your models for indexing. First way (and more appropriate for rails) is to use the ‘searchable’ macro. Suppose we have a model called Provider with first_name, middle_name, and last_name columns in the database. We can setup those fields to indexing with the following:

1
2
3
4
5
class Provider < ActiveRecord::Base
  searchable do
    text :first_name :middle_name, :last_name
  end
end

The second way is the following:

1
2
3
Sunspot.setup Provider do
  text :first_name, :middle_name, :last_name
end

This code can be stored at the end of the model (outside of the class), or in an initializer, though it isn’t too pretty. For more information on setting up classes with search and indexing, check out this page

Don’t Forget to Reindex

To reindex, run the following rake task:

1
rake sunspot:reindex RAILS_ENV=development

You can also reindex in the console by running:

1
Provider.reindex

Testing Provider search with RSpec

Normally I use before(:each) blocks to set up my data, but setting up search data is a use case for using a before(:all) block. With search, we’re not manipulating the data, so having test data setup before all search specs is not only acceptable, but efficient. Below are specs for testing that a provider can be found by all parts of his/her name:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
describe "search" do
  before(:all) do
    @provider = Factory(:provider, :first_name => "Dexter", :middle_name => "Berry", :last_name => "Pilsner")
    Provider.reindex
  end

  it "should find a provider by first name" do
    Provider.search{ fulltext 'dexter' }.results.should == [@provider]
  end

  it "should find a provider by middle name" do
    Provider.search{ fulltext 'berry' }.results.should == [@provider]
  end

  it "should find a provider by first name" do
    Provider.search{ fulltext 'pilsner' }.results.should == [@provider]
  end
end

One gotcha. Don’t forget to start your sunspot server in the test environment:

1
rake sunspot:solr:start RAILS_ENV=test

Using .rvmrc

| Comments

If you’re not already using RVM, I’d highly recommend it as it makes it easier to manage different versions of ruby. You can have different projects running on different versions of ruby and switch rubies with ease. One way to make this easier is to make use of the .rvmrc file. It will automatically switch to the ruby version that your project is running on when you cd into it.

Suppose we have a rails project called donut running on ruby 1.9.2.

1
cd donut

Once in the rails root of the project, create a file called .rvmrc and add this:

1
rvm use ruby-1.9.2-p0@donut --create

To list known rubies you can use this:

1
rvm list known

Now when you cd into the directory, it will automatically use ruby 1.9.2.

1
2
3
4
cd ..
cd donut

info: Using ruby 1.9.2 p0 with gemset donut

Notice that it is using the gemset donut. Gemsets are basically compartmentalized ruby setups. Gems installed in this gemset will be separated from other projects (assuming they use different gemsets). You can read more about it here

Loading and Dumping a MySQL Database

| Comments

You can dump the database into a file using:

1
mysqldump -h hostname -u user --password=password databasename > filename

You can load the dump into the database using:

1
mysql -h hostname -u user --password=password databasename < filename