Minitest with Kaminari

While converting my tests from FactoryGirl.create to FactoryGirl.build_stubbed, I ran into trouble with Kaminari. Here’s how I fixed it.

Kaminari

Kaminari functions by adding .page() (and some other methods) at the end of your ActiveRecord call chain. A typical example of paginating via Kaminari is as follows:

class UsersController < ApplicationController
  def active_users
    @users = User.active_users.page(params[:page])
  end
end

This is a problem when you’re trying to stub out the return of the AR call to return an array. Here’s a corresponding test that would fail:

require 'test_helper'

describe UsersController, "#active_users" do
  test "success" do
    users = FactoryGirl.build_list :user, 3
    User.expects(:active).returns(users)
    get :search, term: "admin_user"
    assert_response :success
  end
end

Because users is just a regular array here, and Kaminari tries to call .page() on the array, it would fail with a NoMethodError: undefined method `page’ for # because Array#page isn’t defined.

Trying to stub out .page() by users.stubs(page: users) would give you ActionView::Template::Error: undefined method `model_name’ for #. And so on, as long as you’re using Kaminari-specific methods.

The solution

The solution is pretty simple (but took me 2+ hours to research). Call Kaminari.paginate_array on the FactoryGirl.build_list.

users = Kaminari.paginate_array FactoryGirl.build_list :user, 3

From the documentation:

Kaminari::paginate_array method converts your Array object into a paginatable Array that accepts page method.

For now, I’m putting the Kaminari.paginate_array inside the test, but long term, it makes sense to put it in the controller code. This way, even if we change User.active to return an array, no other code would break. Then again, that’s why we have tests, right?

Using Dropbox Chooser within ActiveAdmin

We use Dropbox quite heavily in Loudcloud. I’m working on an internal CRM system and since all of our files are on Dropbox anyway, I figured it would be super helpful to just have to link to our Dropbox files.

Here are the relevant files:

# app/inputs/dropbox_input.rb
class DropboxInput < Formtastic::Inputs::FileInput
  def to_html
    input_wrapping do
      label_html <<
      builder.text_field(method, input_html_options.merge({ type: "dropbox-chooser", style: "visibility: hidden;" }))
    end
  end
end

# app/admin/invoice.rb
ActiveAdmin.register Invoice do
  form do |f|
    within @head do
      script src: "https://www.dropbox.com/static/api/1/dropins.js", id: "dropboxjs", type: "text/javascript", "data-app-key" => "APP_KEY_HERE"
    end

    # ....

    f.inputs "Official Receipt" do
      f.input :or_file_url, as: :dropbox, label: "OR File URL"
    end

    # ....
  end
end

Resources:
https://www.dropbox.com/developers/dropins/chooser/js
http://stackoverflow.com/questions/12809237/insert-specifics-js-in-the-header-of-some-pages-active-admin
https://github.com/justinfrench/formtastic/blob/master/lib/formtastic/inputs/base/html.rb
http://stackoverflow.com/questions/12465020/getting-formtastic-in-activeadmin-to-use-custom-form-input-builder

Nested forms with polymorphic association in Active Admin/Formtastic

I spent nearly the whole day making this work.

Given models:
Invoice, has_many :items
Item belongs_to :itemizable, polymorphic: true
Domain & Service has_many :items, as: :itemizable

The problem was multiple things:

  1. The automagic of Formtastic can’t detect the collection if it’s a polymorphic association
  2. Formtastic doesn’t really play well with non-existent attributes

Initially, I’ve thought of just doing:

ActiveAdmin.register Invoice do
  form do |f|    
    # ...
    f.has_many :items do |item|
      item.input :itemizable, collection: (Domain.all + Service.all)
      item.input :quantity
      item.input :price_per_piece
    end

    f.actions
  end
end

But this fails because 1) domains and service can share the same id and 2) I have no way to tell what the item was.
A few hours in and I was going nowhere. It’s surprisingly hard to look for anything related to polymorphic associations on Formtastic. This post gave me an idea however.

So, I’ve thought, why not just hold the id temporarily on an accessor attribute and just do the assignment from a callback before validation kicks in based on which attribute it went into? Raise an error if both were filled up.

It worked! I can now save new polymorphic records. (look at Item#assign_itemizable)

There’s a small problem however. The form to edit an existing record doesn’t pre-populate the corresponding select dropdowns. The solutions was rather simple, override the reader method to return the id of the itemizable if the itemizable is a member of the class.

Maintenance-wise, everything here would add overhead for every new itemizable model I would associate to item, but overall, I think it was a pretty elegant hack. *pats self at back*

Here’s the complete code:

# app/models/invoice.rb
class Invoice < ActiveRecord::Base 
  has_many :items

  accepts_nested_attributes_for :items
end

# app/models/item.rb
class Item < ActiveRecord::Base
  before_validation :assign_itemizable

  belongs_to :invoice
  belongs_to :itemizable, polymorphic: true
  
  validates :itemizable, presence: true

  attr_accessor :itemizable_domain, :itemizable_service

  def itemizable_domain
    self.itemizable.id if self.itemizable.is_a? Domain
  end

  def itemizable_service
    self.itemizable.id if self.itemizable.is_a? Service
  end

  protected
  def assign_itemizable
    if [email protected]_domain.blank? && [email protected]_service.blank?
      errors.add(:itemizable, "can't have both a domain and a service") 
    end

    unless @itemizable_domain.blank?
      self.itemizable = Domain.find(@itemizable_domain)
    end

    unless @itemizable_service.blank?
      self.itemizable = Service.find(@itemizable_service)
    end
  end
end

# app/admin/invoice.rb
ActiveAdmin.register Invoice do
  form do |f|
    f.inputs "Invoice" do 
      f.input :customer
      f.input :invoice_number
      f.input :issuing_person
      f.input :issued_on
      f.input :remarks
    end
    
    f.has_many :items do |item|
      item.input :itemizable_domain, collection: Domain.all
      item.input :itemizable_service, collection: Service.all
      item.input :quantity
      item.input :price_per_piece
    end

    f.actions
  end
end

Edit: Fixed a bug in Item because I overwrote the accessors, which were being called in the validation. This caused a sort of chicken-egg situation, and ultimately causes the validation to fail all the time. Changed .empty? to .blank? so it doesn't barf out on nils.