Rails has_many through with conditions

Does your join table require an extra column for additional context?

Fortunately Rails has got you covered.

class User < ActiveRecord::Base
  has_many :user_groups, :dependent => :destroy

  attr_accessible :name
end

class UserGroup < ActiveRecord::Base
  belongs_to :user
  belongs_to :group

  validates :role, :presence => true,
                   :inclusion => { :in => %w(moderator normal),
                                   :message => "%{value} is not a valid role" }

  attr_accessible :user, :group, :role
end

class Group < ActiveRecord::Base
  # Without conditions
  has_many :user_groups, :dependent => :destroy
  has_many :users, :through => :user_groups

  # Define conditions on the relationship to the join model.
  has_many :moderator_groups, :class_name => 'UserGroup', :conditions => { :role => 'moderator' }

  # Define source on the through relationship.
  has_many :moderators, :through => :moderator_groups, :source => :user

  attr_accessible :name
end

Now to add moderators to a group, we can just do:

group.moderators << @user

or

group.moderators.create(user_attributes)

How to locate missing test files in your Rails app

Wanna make sure you have each of your controllers, models, templates, etc.. all have corresponding tests? I wrote a little bash script to make sure each class has its own spec file. Simple yet effective :). For more in-depth analysis, try a complete code-coverage tool like https://github.com/colszowka/simplecov.

spec/untested.sh:

#!/bin/bash
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
#echo $DIR

echo ""
echo "Untested Controllers"
echo "===================="
CONTROLLERS="$(find -E "${DIR}/../app/controllers" -iregex ".*\.rb")"
for f in $CONTROLLERS;
do
        CONTROLLER=${f##*/}
        SPEC=${CONTROLLER/.rb/_spec.rb}
        FOUND="$(find "${DIR}" -path "*${SPEC}")"
        if [[ $FOUND == "" ]]; then
                echo "-${CONTROLLER}"
        fi
done

echo ""
echo "Untested Models"
echo "==============="
MODELS="$(find -E "${DIR}/../app/models" -iregex ".*\.rb")"
for f in $MODELS;
do
        MODEL=${f##*/}
        SPEC=${MODEL/.rb/_spec.rb}
        FOUND="$(find "${DIR}" -path "*${SPEC}")"
        if [[ $FOUND == "" ]]; then
                echo "-${MODEL}"
        fi
done

echo ""
echo "Untested Templates"
echo "=================="
TEMPLATES="$(find -E "${DIR}/.." -iregex ".*(_default|_builder|_player).rabl")"
for f in $TEMPLATES;
do
        TEMPLATE=${f##*/views/}
        SPEC=${TEMPLATE/.rabl/_spec.rb}
        #echo $SPEC
        FOUND="$(find "${DIR}" -path "*${SPEC}")"
        #echo $FOUND
        if [[ $FOUND == "" ]]; then
                echo "-${TEMPLATE}"
        fi
done

echo ""

Sinatra ActiveRecord Mock Server

So you’ve built your awesome shiny new RESTful Rails app. You’ve been developing the client and find it pretty tough to unit test it against your service. The problem is that data on your service persists causing inconsistent test results. The solution is to usually mock requests on the client which can be sometimes difficult. When that’s the case, I’ve found it easier to mock the service instead using Sinatra — a light-weight web server. This easy-to-setup server can leverage your existing ActiveRecord models using a temporary in-memory database enabling you to mock requests to responses consistently in any manner required.

/MyRailsApp/spec/Gemfile:

source 'http://rubygems.org'
gem 'sinatra'
gem 'sinatra-activerecord'
gem 'sqlite3'
gem 'require_all'

/MyRailsApp/spec/mock-server.rb:

require 'sinatra'
require 'sinatra/activerecord'
require 'require_all'

# Require ActiveRecord models
require_all '../app/models/*.rb'

# Use UTC timezone
Time.zone = "UTC"
ActiveRecord::Base.default_timezone = :utc

# Disable Concurrency
set :lock, true

before do
  ActiveRecord::Base.logger.level = Logger::ERROR

  # Setup in-memory Database
  ActiveRecord::Base.establish_connection({:adapter => 'sqlite3', :database => ':memory:'})
  ActiveRecord::Migration.verbose = false
  ActiveRecord::Migrator.up('../db/migrate')
  ActiveRecord::Base.logger.level = Logger::DEBUG

  # Any additional setup
end

after do
  #nothing yet
end

post '/' do
  "Hello World!"
end

# Example POST User
post '/users' do
  post = JSON.parse(request.body.read)
  puts post.inspect

  begin
    raise if User.find_by_id(post["user"]["id"])
    @user = User.create!(post["user"])
    content_type 'application/json'
    status 201
    response = @user.to_json
    puts response
    response
  rescue
    status 400
  end
end

Install Gems:

$ cd /MyRailsApp/spec
$ bundle install

Start Server:

$ ruby -rubygems mock-server.rb

Check out the gist:
https://gist.github.com/1439361