Conditions on a has_many :through and ActiveRecord 4.1 enum

Say you have a set of models (model names sanitized for generic posting) where the join table in the middle of a has_many :through has an enum that you want to add a filtered association for.

class Student < ActiveRecord::Base
    has_many :student_grades
    has_many :grades, through: :student_grades
  end
 
  class StudentGrade < ActiveRecord::Base
    enum approval_status: [:not_reviewed, :rejected, :approved]
    belongs_to :student
    belongs_to :grade
  end
 
  class Grade
  end

I ended up creating two additional has_many associations on Student to accomplish this. I have to specify a condition on :approved_student_grades, and tell it what class name (StudentGrade) to point to because reflection through the association name won't work. I really dislike the verbosity of StudentGrade.approval_statuses[:approved] as a way to get the integer value of the enum (in this case, 2). For the :approved_grades association, I also have to repeat a similar process, except I have to specify both the class_name: and the source: due to reflection not working for :approved_grades.

class Student < ActiveRecord::Base
  has_many :student_grades
  has_many :grades, through: :student_grades
  has_many :approved_student_grades, -> {where approval_status: StudentGrade.approval_statuses[:approved] }, class_name: "StudentGrade"
  has_many :approved_grades, through: :approved_student_grades, class_name: "Grade", source: :grade
end

I'd be surprised if there wasn't a cleaner way to do this, but enum is new in Rails 4.1, so that may be part of my problem.

has_many :through, self referential models

Hat tip to [has_many :through self-referential example] that sent me down the correct path on this. The challenge I had went a little deeper than the aforementioned post, however. I'm going to reuse the friend example, but with a twist.

class Person < ActiveRecord::Base
  # id :integer
end
class Friendship < ActiveRecord::Base
  # source_friend_id :integer
  # target_friend_id :integer
end

In the above contrived example, Person is a friend who initiated the friendship (source_friend), and a friend that was sought for friendship (target_friend). For the friendship, reflection won't work on either connection, so we need to specify the class_name:.

class Friendship < ActiveRecord::Base
  belongs_to :source_friend, class_name: "Person"
  belongs_to :target_friend, class_name: "Person"
end

Including the friendship would normally be easy. That's has_many :friendships. However, there's no explicit mapping back to Person for the Friendship. ActiveRecord will attempt to use friendships.person_id to no avail, so foreign_key must be specified to tell ActiveRecord how to map back the has_many relationship.

  class Person < ActiveRecord::Base
    has_many :friendships, foreign_key: :source_friend_id
  end

Now, the friends you've "made" can be mapped by putting the target_friend as the source for the has_many :through.

  class Person < ActiveRecord::Base
    has_many :friendships, foreign_key: :source_friend_id
    has_many :made_friends, through: :friendships, source: :target_friend
  end

Principle of Least Surprise (Astonishment), foreign keys, and Rails

In yesterday's post, I sorted through the foreigner gem to figure out how to change the reference column (primary key) that a foreign_key maps to.

The problem here is that, unless your Rails project(s) has grown up referencing "natural" primary keys instead of the autoincremented id implicit in an ActiveRecord::Migration, avoiding creating surprise by not referring to an artificial primary key actually adds surprise: The instinct of someone reading your code will be to assume that the foreign key maps to the auto-id of the foreign model.

foreigner foreign_key on a different column in source and target table.

In the foreigner gem, you can specify the foreign_key column on the source table by using the column option.

class Cats < ActiveRecord::Migration
  def change
    create_table :cats do |t|
      t.integer :color_id
      t.integer :second_color_id

      t.timestamps
    end
    add_foreign_key :cats, :colors, column: :second_color_id
  end
end

But what about the target table? This can be accomplished using the primary_key option.

class Dogs < ActiveRecord::Migration
  def change
    create_table :dogs do |t|
      t.integer :color_id
      t.integer :breed_name

      t.timestamps
    end
    add_foreign_key :dogs, :breed, column: :breed_name, primary_key: :name
  end
end

Of course, the model has to change as well:

class Dog < ActiveRecord::Base
  belongs_to :breed, primary_key: :name
end

includes and has_many :through associations

I've been trying to mentally absorb how includes and associations work when trying to query a has_many :through association.

My understanding at this point:

The symbol in the includes() is the same as the association name on whatever model you have. For example:

class Account < ActiveRecord::Base
  has_many :account_destinations
  has_many :destinations, through: :account_destinations
end

class AccountDestination < ActiveRecord::Base
  has_one :destination
  # has an active_destination attribute
end

class Destination < ActiveRecord::Base
  # has several attributes that we might want to use to filter account_destinations on
end

The association used in includes for account_destinations.includes is :destination, because AccountDestination has_one :destination. Meanwhile, in the where clause, the hash key for specifying what values to match in Destination refers to the table name, or destinations.

class Account
.
.
  def some_method
    account_destinations.includes(:destination)
      .where
      .not(destinations: { name: %w(some list of names) } )
  end
end

Passing Additional Arguments to FactoryGirl Factories

I wanted to create a factory that would link various states to my model, but from a specified list instead of automatically generated. I discovered that these are transient attributes:

FactoryGirl.define do
  factory :business do
    name "MyString"
#
#
#
    trait :with_states do
      ignore do
        state_list []
      end
      # add association equal to state code
      after(:create) do |business, evaluator|
        evaluator.state_list.each do |state_code|
          business.states << State.find_by(state_code: state_code)
        end
      end
    end
  end
end

This factory can now be called with:

  FactoryGirl.create(:business, :with_states. state_list: %w(KY IN))

Of course, now that I'm passing in the attributes, I can probably just use:

FactoryGirl.define do
  factory :business do
    name "MyString"
#
#
#
    ignore do
      with_states []
    end
    # add association equal to state code
    after(:create) do |business, evaluator|
      evaluator.with_states.each do |state_code|
        business.states << State.find_by(state_code: state_code)
      end
    end
  end
end

and then the call is even simpler:

  FactoryGirl.create(:business, with_states: %w(KY IN))

Rails 4, Empty Hashes, Strong Parameters, exception_notification and filtering out my new ActionController::ParameterMissing errors

I'm finally coming around to the "strong parameters" pattern encouraged (required?) in Rails 4.

One thing that I did notice was that missing the base parameter raised an ActionController::ParameterMissing error. I discovered this via controllers tests generated by rspec-rails when I created a controller scaffold, which generates tests for "Invalid Params":

In the controller spec:

describe "with invalid params" do
  it "re-renders the &#039;new&#039; template" do
    # Trigger the behavior that occurs when invalid params are submitted
    User.any_instance.stub(:save).and_return(false)
    post :create, {:user => {  }}, valid_session
    response.should render_template("new")
  end 
end

In the controller:

  def user_params
    params.require(:licensee).permit(:parameter1, :parameter2)
  end

Despite the fact that params[:licensee] exists, the empty hash triggers the ActionController::ParameterMissing error. That's fine. I can expect { post :create, {:user => { }}, valid_session }.to raise_error(ActionController::ParameterMissing), or eliminate the test altogether.

However, with exception_notifier, I don't really want to be notified about these errors, so I configured my environment files to filter this exception (and also CanCan::AccessDenied).

  config.middleware.use ExceptionNotification::Rack,
    :ignore_exceptions => %w(CanCan::AccessDenied ActionController::ParameterMissing) + ExceptionNotifier.ignored_exceptions,
    :email => {
      :email_prefix => "[#{Rails.env}] project name exception: ",
      :sender_address => %{"Exception Notifier" <exceptions@example.com>},
      :exception_recipients => [ `git config user.email`.chomp ]
   }

Bundler reports errors across several different projects when running `bundle install`

When running bundle install, I was getting the following for several projects:

Bundler gave the error "Could not find {gem_name_version} in any of the sources" while processing "{project_path}/Gemfile". Perhaps you forgot to run "bundle install"?
.
.
.

I found someone else was having the same problem too.

In my case, I had uninstalled and reinstalled a version of ruby using rbenv and began seeing this error reported for every project using the same ruby version as the one I had uninstalled and reinstalled. I haven't exactly gotten to the bottom of why this happens, but going around and running bundle install in each of the project directories got rid of the error.

method_missing: undefined method `increment_open_transactions` database_cleaner

When using database_cleaner (0.9.1) with rails (4.1.0)

{my home path}/.rbenv/versions/2.1.1/lib/ruby/gems/2.1.0/gems/activerecord-4.1.0/lib/active_record/dynamic_matchers.rb:26:in `method_missing': undefined method `increment_open_transactions' for ActiveRecord::Base:Class (NoMethodError)
    from {my home path}/.rbenv/versions/2.1.1/lib/ruby/gems/2.1.0/gems/database_cleaner-0.9.1/lib/database_cleaner/active_record/transaction.rb:13:in `start'
    from {my home path}/.rbenv/versions/2.1.1/lib/ruby/gems/2.1.0/gems/database_cleaner-0.9.1/lib/database_cleaner/base.rb:73:in `start'
    from {my home path}/.rbenv/versions/2.1.1/lib/ruby/gems/2.1.0/gems/database_cleaner-0.9.1/lib/database_cleaner/configuration.rb:75:in `block in start'
    from {my home path}/.rbenv/versions/2.1.1/lib/ruby/gems/2.1.0/gems/database_cleaner-0.9.1/lib/database_cleaner/configuration.rb:75:in `each'
    from {my home path}/.rbenv/versions/2.1.1/lib/ruby/gems/2.1.0/gems/database_cleaner-0.9.1/lib/database_cleaner/configuration.rb:75:in `start'

Somehow we were using an old version of database_cleaner,

gem "database_cleaner",   "~> 0.9.1"

because the fix was authored a year ago.

gem "database_cleaner", "~> 1.2.0"

I guess that will teach me to dig too deeply before simply trying the latest version of a gem.

Getting Fast File Finder Functionality Out of Vim

Problem:

All of these other editors have handy functionality to quickly find a file

Solution:

ctrlp.vim is a plugin that allows you to do that.

The plugin's page has full instructions, but the most important detail after work done in yesterday's post to declutter my vim windows:

After pressing Ctrl-P, use <c-t> or <c-v>, <c-x> to open the selected entry in a new tab or in a new split.

Otherwise, you'll end up replacing your file in the currently open window with the file you're opening.