Search code examples
ruby-on-railsactiveadminransack

Ransack + FlagShihTzu + Active Admin don't play well together


I'm using the brilliant gem flag_shih_tzu to create bitwise boolean flags on a single integer column without requiring a separate DB column for each flag. I have loved this gem for many years now, and it's quite excellent at interplaying with ActiveRecord attributes in all the ways you would normally expect.

However, it does not play well with Ransack and Active Admin out of the box. Active Admin requires me to add permitted params for each flag:

  permit_params do
   :identity_verified
  end

for the :identity_verified "flag" attribute to even show up in filters or index columns, which is fine; I don't mind that. But the real problem I'm having is when I try to use the :identity_verified flag as a filter (it's boolean, of course), Active Admin shows the normal select option for it with Any/Yes/No, but when I first submitted the filter query, I got an exception: undefined method identity_verified_eq for Ransack::Search.

Ok, so I did some research and figured out that I need to add a ransacker for :identity_verified. Did that, and I don't get the exception anymore, but the ransacker doesn't appear to do anything at all. In fact, I intentionally put a syntax error in the block to cause an exception, but Active Admin just returns all the Users, regardless of whether they're :identity_verified or not. That code inside the ransacker block doesn't seem to even get executed. Can anyone help me figure out how to create a proper Ransack definition for :identity_verified?

Here's the code:

User Model :

  #  ===============
  #  = FlagShihTzu =
  #  ===============

  has_flags 1  => :guest,             
            2  => :prospect,         
            3  => :phone_verified,   
            4  => :identity_verified,


  #  ==============
  #  = Ransackers =
  #  ==============

  # this avoids the identity_verified_eq missing method exception, but 
  # doesn't appear to do anything else
  ransacker :identity_verified, args: [:ransacker_args] do |args|
    asdf # <-- should cause an exception
    Arel.sql(identity_verified_condition)
  end

Active Admin:

ActiveAdmin.register User do
  permit_params do
    :identity_verified
  end

  # ...

  filter :identity_verified, as: :boolean

  # ...

end

The Identity Verified filter shows up as a boolean select in Active Admin, like I expect, but when I submit the filter, (as I metioned above), I get all the Users back, and the ransacker block doesn't even seem to get executed. I've read the Ransack examples. I've dug into the code of all four gems (including Formtastic), and I still can't sort it out.

Here's the POST URL from Active Admin on query submit:

http://example.com/admin/users?q%5Bidentity_verified%5D=true&commit=Filter&order=id_desc

Here's the Rails log to confirm the :identity_verified param is getting passed:

Processing by Admin::UsersController#index as HTML
  Parameters: {"utf8"=>"✓", "q"=>{"identity_verified"=>"true"}, "commit"=>"Filter", "order"=>"id_desc"}

Again, the inside of the ransacker block doesn't seem to get executed. I need to understand why, and if it ever does, how to write the proper Arel statement so I can filter on this flag.

Help?


Solution

  • Resurrecting this question as it is the first result on G* here searching for "flag shih tzu activeadmin". Plus it seems OP's solution is not ideal in that it loads & instantiates AR objects for all records fulfilling the flag condition in this part:

    results = object_class.send("#{flag}")
    results = results.map(&:id)
    

    So here's my current solution for others:

    # config/initializers/ransack.rb
    Ransack.configure do |config|
      config.add_predicate 'flag_equals',
        arel_predicate: 'eq',
        formatter: proc { |v| (v.downcase == 'true') ? 1 : 0 },
        validator: proc { |v| v.present? },
        type: :string
    end
    
    # app/concerns/has_flags.rb
    module HasFlags
      extend ActiveSupport::Concern
    
      included { include FlagShihTzu }
    
      class_methods do
        def flag_ransacker(flag_name, flag_col: 'flags')
          ransacker(flag_name) do |parent|
            Arel::Nodes::InfixOperation.new('DIV',
              Arel::Nodes::InfixOperation.new('&', parent.table[flag_col], flag_mapping[flag_col][flag_name]),
              flag_mapping[flag_col][flag_name])
          end
        end
      end
    end
    
    # app/models/foo.rb
    class Foo < ActiveRecord::Base
      include HasFlags
    
      has_flags 1 => :bar, 2 => :baz
      flag_ransacker :bar
    end
    
    # app/admin/foos.rb
    ActiveAdmin.register Foo do 
      filter :bar_flag_equals, as: :boolean, label: 'Bar'
    end