Ransack + FlagShihTzu + Active Admin 不能一起玩

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

我正在使用出色的 gem flag_shih_tzu 在单个整数列上创建按位布尔标志,而无需为每个标志创建单独的 DB 列。我喜欢这个 gem 很多年了,它在以您通常期望的所有方式与 ActiveRecord 属性交互方面非常出色。

但是,开箱即用的 Ransack 和 Active Admin 效果不佳。 Active Admin 要求我为每个标志添加允许的参数:

  permit_params do
   :identity_verified
  end

:identity_verified "flag" 属性甚至出现在过滤器或索引列中,这很好;我不介意。但我遇到的真正问题是当我尝试使用 :identity_verified 标志作为过滤器(当然是布尔值)时,Active Admin 显示正常的 select 选项 Any/Yes/No,但是当我第一次提交过滤查询时,我得到了一个异常:undefined method identity_verified_eq for Ransack::Search

好的,所以我做了一些研究,发现我需要为 :identity_verified 添加一个 ransacker。这样做了,我再也没有得到异常,但是劫掠者似乎根本没有做任何事情。事实上,我故意在块中放置一个语法错误以导致异常,但 Active Admin 只是 returns 所有用户,无论他们是否:identity_verified。 ransacker 块中的代码似乎甚至没有被执行。谁能帮我弄清楚如何为 :identity_verified?

创建正确的 Ransack 定义

代码如下:

用户模型:

  #  ===============
  #  = 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

活跃管理员:

ActiveAdmin.register User do
  permit_params do
    :identity_verified
  end

  # ...

  filter :identity_verified, as: :boolean

  # ...

end

Identity Verified 过滤器在 Active Admin 中显示为布尔值 select,正如我预期的那样,但是当我提交过滤器时(正如我上面提到的),我得到了所有用户,并且掠夺者块似乎甚至没有被执行。我读过 Ransack examples。我把四个gem的代码都挖了一遍(包括Formtastic),还是整理不出来

这是 Active Admin 在查询提交时的 POST URL:

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

这是确认 :identity_verified 参数正在传递的 Rails 日志:

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

同样,劫掠者块的内部似乎没有被执行。我需要了解为什么,如果确实如此,如何编写正确的 Arel 语句以便我可以过滤此标志。

帮忙?

所以,幸运地偶然发现了这个 post ActiveAdmin Filters with Ransack,我终于找到了答案。它的要点是使用 DSL 正确定义 Active Admin 过滤器,更重要的是,在模型中为您要过滤的 FlagShihTzu 标志定义适当的掠夺者。

这是一个工作示例:

models/user.rb:

class User
  include FlagShihTzu

  # define the flag
  has_flags 1 => :identity_verified

  # convenience method to define the necessary ransacker for a flag

  def self.flag_ransacker(flag)
    ransacker flag.to_sym,
    formatter: proc { |true_false|
      if true_false == "true"
        results = object_class.send("#{flag}")
      else
        results = object_class.send("not_#{flag}")
      end
      results = results.map(&:id)
      results = results.present? ? results : nil
    }, splat_params: true do |parent|
    parent.table[:id]
  end

end

admin/user.rb:

ActiveAdmin.register User do

  # A method used like a standard ActiveAdmin::Resource `filter` DSL call, but for FlagShizTzu flags
  # A corresponding `flag_ransacker` call must be made on the model, which must include
  # the FlagShizTzuRansack module defined in app/concerns/models/flag_shih_tzu_ransack.rb
  def flag_filter(flag)
    @resource.flag_ransacker flag.to_sym # call the ransacker builder on the model
    flag = flag.to_s
    filter_name = "#{flag}_in" # we use the 'in' predicate to allow multiple results
    filter filter_name.to_sym,
      :as => :select,
      :label => flag.gsub(/[\s_]+/, ' ').titleize,
      :collection => %w[true false]
  end

  flag_filter :identity_verified

end

瞧,一个用于 flag-shih-tzu 标志的边栏过滤器。关键是在过滤器声明中的标志名称末尾添加 in 谓词,而不是排除它,默认为 eq Ransack 谓词。定义掠夺者本身需要使用 pry 和调试器进行反复试验,但主要基于上述 post。

最终,我最终将这两个文件中的内联方法提取到我包含在必要模型和需要它们的 AA 资源定义中的模块中。

app/concerns/models/flag_shih_tzu_ransack.rb:

# Used to define Ransackers for ActiveAdmin FlagShizTzu filters
# See app/admin/support/flag_shih_tzu.rb
module FlagShihTzuRansack
  extend ActiveSupport::Concern

  module ClassMethods
    # +flags are one or more FlagShihTzu flags that need to have ransackers defined for
    # ActiveAdmin filtering
    def flag_ransacker(*flags)
      object_class = self
      flags.each do |flag|
        flag = flag.to_s
        ransacker flag.to_sym,
          formatter: proc { |true_false|
            if true_false == "true"
              results = object_class.send("#{flag}")
            else
              results = object_class.send("not_#{flag}")
            end
            results = results.map(&:id)
            results = results.present? ? results : nil
          }, splat_params: true do |parent|
          parent.table[:id]
        end
      end
    end
  end
end

app/admin/support/flag_shih_tzu.rb:

# Convenience extension to filter on FlagShizTzu flags in the AA side_bar
module Kandidly
  module ActiveAdmin
    module DSL

      # used like a standard ActiveAdmin::Resource `filter` DSL call, but for FlagShizTzu flags
      # A corresponding `flag_ransacker` call must be made on the model, which must include
      # the FlagShizTzuRansack module defined in app/concerns/models/flag_shih_tzu_ransack.rb
      def flag_filter(flag)
        @resource.flag_ransacker flag.to_sym # call the ransacker builder on the model
        flag = flag.to_s
        filter_name = "#{flag}_in" # we use the 'in' predicate to allow multiple results
        filter filter_name.to_sym,
          :as => :select,
          :label => flag.gsub(/[\s_]+/, ' ').titleize,
          :collection => %w[true false]
      end
    end
  end
end

ActiveAdmin::ResourceDSL.send :include, Kandidly::ActiveAdmin::DSL

然后更干净,在模型中:

class User
  include FlagShihTzu
  include FlagShihTzuRansack

  # define the flag
  has_flags 1 => :identity_verified

结束

并且在资源定义中:

ActiveAdmin.register User do

  flag_filter :identity_verified

end

这些方法可能有更优雅的实现,但有了一个可行的解决方案,我继续前进。希望这对投票赞成这个问题的人有所帮助。 Ransack 文档还有一些不足之处。感谢 Russ 在 Jaguar Design Studio 上的 post,以及 https://github.com/activerecord-hackery/ransack/issues/36 上的评论者,他们帮助我更好地理解了 Ransack 的工作原理。最后我不得不深入研究这些精华以获得我的最终解决方案,但如果没有他们的贡献我将不知道从哪里开始。

重新提出这个问题,因为它是 G* 上搜索 "flag shih tzu activeadmin" 的第一个结果。另外,OP 的解决方案似乎并不理想,因为它为满足这部分标志条件的所有记录加载和实例化 AR 对象:

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

所以这是我目前对其他人的解决方案:

# 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