Ransack + FlagShihTzu + Active Admin don't play well together - ruby-on-rails

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?

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

So, I finally figured out the answer after luckily stumbling on to this post ActiveAdmin Filters with Ransack. The gist of it is properly defining the Active Admin filter using the DSL and more importantly, the appropriate ransacker in the model for the FlagShihTzu flag you want to filter on.
Here's a working example:
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
And voila, a working sidebar filter for flag-shih-tzu flags. The key was adding the in predicate at the end of the flag name in the filter declarion, instead of excluding it, which defaults to the eq Ransack predicate. Defining the ransacker itself took trial and error using pry and the debugger, but was based largely on the aforementioned post.
Ultimately, I ended up pulling out the inline methods in the two files into modules that I include in the necessary models and AA resource definitions that need them.
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
Then more cleanly, in the model:
class User
include FlagShihTzu
include FlagShihTzuRansack
# define the flag
has_flags 1 => :identity_verified
end
and in the resource definition:
ActiveAdmin.register User do
flag_filter :identity_verified
end
There are probably more elegant implementations of the methods, but having a working solution, I'm moving on. Hope this helps whoever up-voted this question. Ransack documentation leaves a bit to be desired. Thanks to Russ for his post on Jaguar Design Studio, and to the commenters on https://github.com/activerecord-hackery/ransack/issues/36, who helped me better understand how Ransack works. In the end I had to dig into the gems to get my final solution, but I would not have known where to start without their contributions.

Related

In RoR, how do I initialize fields in my model based on a field in my model that has no underlying database column?

I’m using Rails 4.2.7. I have an attribute in my model that doesn’t have a database field underneath it
attr_accessor :division
This gets initialized when I create a new object.
my_object = MyObject.new(:name => name,
:age => get_age(data_hash),
:overall_rank => overall_rank,
:city => city,
:state => state,
:country => country,
:age_group_rank => age_group_rank,
:gender_rank => gender_rank,
:division => division)
What I would like is when this field gets set (if it is not nil), for two other fields that do have mappings in the database to get set. The other fields would be substrings of the “division” field. Where do I put that logic?
I'd probably drop the attr_accessor :division and do it by hand with:
def division=(d)
# Break up `d` as needed and assign the parts to the
# desired real attributes.
end
def division
# Combine the broken out attributes as needed and
# return the combined string.
end
With those two methods in place, the following will all call division=:
MyObject.new(:division => '...')
MyObject.create(:division => '...')
o = MyObject.find(...); o.update(:division => '...')
o = MyObject.find(...); o.division = '...'
so the division and the broken out attributes will always agree with each other.
If you try to use one of the lifecycle hooks (such as after_initialize) then things can get out of sync. Suppose division has the form 'a.b' and the broken out attributes are a and b and suppose that you're using one of the ActiveRecord hooks to break up division. Then saying:
o.division = 'x.y'
should give you o.a == 'x' but it won't because the hook won't have executed yet. Similarly, if you start with o.division == 'a.b' then
o.a = 'x'
won't give you o.division == 'x.b' so the attributes will have fallen out of sync again.
I see couple of options here
You can add it in your controller as follows
def create
if params[:example][:division]
# Set those params here
end
end
Or you can use before_save In your model
before_save :do_something
def do_something
if division
# Here!
end
end

ActiveAdmin Filter on postgres Array field

I added the following filter in ActiveAdmin.
filter :roles, as: :select, collection Model::ROLES, multiple: true
but when i choose the filter value to search the roles. it gives me following error
PG::InvalidTextRepresentation: ERROR: malformed array literal: "teacher"LINE 1: ...ted" = $1 AND roles" IN('teacher
DETAIL: Array value must start with "{" or dimension information. ^
Any idea ? How we can search/Filter ARRAY field using AA filters? I'm using Rails 4.2.4,
ruby 2.2.2p95
I came up to a solution slightly different (and inspired by) this one over here: https://stackoverflow.com/a/45728004/1170086
Mine involves some changes (and prevent breaking contains operator in other cases). So, you're going to basically create two initializer files:
This one is for Arel, in order to support #> operator (array's contain operator in PG) for a given table column.
# config/initializers/arel.rb
module Arel
class Nodes::ContainsArray < Arel::Nodes::Binary
def operator
:"#>"
end
end
class Visitors::PostgreSQL
private
def visit_Arel_Nodes_ContainsArray(o, collector)
infix_value o, collector, ' #> '
end
end
module Predications
def contains(other)
Nodes::ContainsArray.new self, Nodes.build_quoted(other, self)
end
end
end
The other file aims to create a new Ransack predicate but I also decided to support the :array type (that's not natively supported in Ransack in terms of predicates).
# config/initializers/ransack.rb
module Ransack
module Nodes
class Value < Node
alias_method :original_cast, :cast
def cast(type)
return Array(value) if type == :array
original_cast(type)
end
end
end
end
Ransack.configure do |config|
config.add_predicate 'contains_array',
arel_predicate: 'contains',
formatter: proc { |v| "{#{v.join(',')}}" },
validator: proc { |v| v.present? },
type: :array
end
And in other to use it. All you need to do is:
User.ransack(roles_contains_array: %i[admin manager])
Or as a filter in ActiveAdmin (which is my case):
ActiveAdmin.register User do
# ...
filter :roles_contains_array, as: :select, collection: User.roles_for_select
# ...
end
I hope it works for you as it worked for me. ;)
You can set up a custom ransacker method to first collect the ids you want returned using a regular postgres search, and then return the results based on those ids:
class User < ApplicationRecord
ransacker :roles,
formatter: proc { |str|
data = where("? = ANY (roles)", str).map(&:id)
data.present? ? data : nil
} do |parent|
parent.table[:id]
end
end
If your filter is a select drop-down, then this should work fine. If you have a free-form text box, then make sure to use the "in" predicate:
filter :roles_in, as: :string
leandroico solutions works well.
But if you add the predicate with this formatter
formatter: proc { |v| "{#{v.join(', ')}}" }, (note the space after the comma)
Then you could use the multiple: true keyword in the filter input and filter by more than one value:
filter :roles_contains_array, as: :select, multiple: true, collection: User.roles_for_select
I used the answer from #leandroico to come up with the below wiki-type approach to doing this.
How to Create Custom SQL Searches for ActiveAdmin (using Arel and Ransack)
In ActiveAdmin, filters are declared in app/admin/model.rb like:
ActiveAdmin.register Model do
filter 'column_name', label: 'column_name', as: :string
end
That will make a searchbox available on the front-end with options to choose between
contains
equals
starts with
ends with
You can even do something like...
filter 'column_name_contains', label: 'column_name', as: :string
...to only have a contains type search available on the front-end.
You can also (after defining some custom methods elsewhere) specify other, non-built-in search methods, like:
filter 'column_name_custom_contains', label: 'column_name', as: :string
The rest of this doc will be about how to define this custom search method, custom_contains
Within config/initializers/arel.rb, define the following:
module Arel
# this example of custom_contains will cast the SQL column as ::text and then do a wildcard-wrapped ILIKE
class Nodes::CustomContains < Arel::Nodes::Binary
def operator
'::text ILIKE'.to_sym
end
end
class Visitors::PostgreSQL
private
def visit_Arel_Nodes_CustomContains(o, collector)
infix_value o, collector, '::text ILIKE '
end
end
module Predications
def custom_contains(column_value)
column_value = self.relation.engine.column_types[self.name.to_s].type_cast_for_database(column_value)
column_value = "%#{self.relation.engine.send(:sanitize_sql_like, column_value)}%" # wrap escaped value with % wildcard
column_value = Nodes.build_quoted(column_value, self)
Nodes::CustomContains.new(self, column_value)
end
end
end
module ActiveRecord::QueryMethods
def custom_contains(predicates)
return none if predicates.length == 0
predicates.map{ |column_name, column_value|
column_value = table.engine.column_types[column_name.to_s].type_cast_for_database(column_value)
column_value = "%#{table.engine.send(:sanitize_sql_like, column_value)}%" # wrap escaped value with % wildcard
column_value = Arel::Nodes.build_quoted(column_value)
where Arel::Nodes::CustomContains.new(table[column_name], column_value)
}.inject(:merge)
end
end
module ActiveRecord::Querying
delegate :custom_contains, :to => :all
end
Within config/initializers/ransack.rb, define the following:
Ransack.configure do |config|
config.add_predicate(
'custom_contains',
arel_predicate: 'custom_contains',
formatter: proc { |v| v.to_s },
validator: proc { |v| v.present? },
type: :string
)
end
The above has accomplished a couple of things:
1) You can use the custom_contains method that was delegate'd to all ActiveRecord models:
puts Model.custom_contains(column_name: 'search for me').to_sql
2) You can use Ransack to search against the Arel predicates that were defined:
puts Model.ransack(column_name_custom_contains: 'search for me').result.to_sql
However, in order to do the below in ActiveAdmin...
filter 'column_name_custom_contains', label: 'column_name', as: :string
...we must add a scope to Model so that there is a method, column_name_custom_contains, on Model
scope_name = "#{column_name}_custom_contains".to_sym
unless Model.methods.include?(scope_name)
Model.scope(
scope_name,
->(value) {
Model.custom_contains({column_name.to_sym => value})
}
)
end
Voila!

Programmatically get all database column types

I am building a Rails gem for which I might need to know the currently available column types. So say for Postgres, I am looking for something like: ActiveRecord::Base.available_column_types. I looked through the source with no success so far.
I can't find an ActiveRecord method to get what you want. But I can show you two ways you can achieve this:
With any path you need to create an initializer and Monkey Patch ActiveRecord. For example: /config/initializers/active_record_extensions.rb. Then, the options:
OPTION 1: get data types based on your models
class ActiveRecord::Base
def self.available_column_types
types = []
ActiveRecord::Base.subclasses.collect{ |type| type.name }.each do |model_name|
types += eval("#{model_name}.columns.map(&:type)")
end
types.uniq
end
end
Then you can do rails console on your terminal and write:
irb(main):001:0> User.available_column_types
=> [:integer, :string, :text, :datetime, :boolean, :date, :hstore]
irb(main):002:0> ActiveRecord::Base.available_column_types
=> [:integer, :string, :text, :datetime, :boolean, :date, :hstore]
irb(main):003:0>
OPTION 2: get all posible data types based on you db adapter
class ActiveRecord::Base
if defined?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter) and
ActiveRecord::Base.connection.instance_of?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)
types = ActiveRecord::Base.connection.execute("select * from pg_type;")
return types.inject([]) { |result, record| result << record["typname"] }
# Too much info on pg_type table, you can get whatever you need.
end
if defined?(ActiveRecord::ConnectionAdapters::MysqlAdapter) and
ActiveRecord::Base.connection.instance_of?(ActiveRecord::ConnectionAdapters::MysqlAdapter)
# I don't know, it's just an example. Yo can add all adapters you want
return
end
# maybe raise an Exception with NO ADAPTER! message
end
end
Once again, on your console, you can do ActiveRecord::Base.available_column_types to see the result.
Note: you need to adapt this in order to make it work with your gem.

Rails meta_search greater and less conditions in one combobox

I have a model Phone with checked_by field; if this field is equal to 1, then we know this phone is unchecked, else(>1) - checked. On admin side I can review a list of Phones and I need to create a filter using meta_search to review:
All Phones
Checked
Unchecked
I can see checked_by_greater_than, or checked_by_less_than methods in meta_search, but how to combine those methods in a single select box?
Thanks in any advise
With a scope and a made-up field.
The scope:
class Phone < ActiveRecord::Base
scope :checked, lambda { |value|
!value.zero? ? checked_by_greater_than(1) : where(:checked_by => 1)
}
end
Then add a select-box with three values, returning [nil, 0, 1] as values, and in your controller use that parameter to apply the new scope.
class PhonesController < ApplicationController
def index
# ...
#phones ||= Phone.scoped
checked_select_value = params.delete("checked_select") # here use the name of your form field
if checked_select_value.present?
#phones = #phones.checked(checked_select_value.to_i)
end
# now apply the rest of your meta-search things to the #phones
#
end
end

Ransack: How to use existing scope?

Converting a Rails 2 application to Rails 3, I have to replace the gem searchlogic. Now, using Rails 3.2.8 with the gem Ransack I want to build a search form which uses an existing scope. Example:
class Post < ActiveRecord::Base
scope :year, lambda { |year|
where("posts.date BETWEEN '#{year}-01-01' AND '#{year}-12-31'")
}
end
So far as I know, this can be achieved by defining a custom ransacker. Sadly, I don't find any documentation about this. I tried this in the Postclass:
ransacker :year,
:formatter => proc {|v|
year(v)
}
But this does not work:
Post.ransack(:year_eq => 2012).result.to_sql
=> TypeError: Cannot visit ActiveRecord::Relation
I tried some variations of the ransacker declaration, but none of them work. I Need some help...
UPDATE: The scope above is just on example. I'm looking for a way to use every single existing scope within Ransack. In MetaSearch, the predecessor of Ransack, there is a feature called search_methods for using scopes. Ransack has no support for this out of the box yet.
ransack supports it out of the box after merging https://github.com/activerecord-hackery/ransack/pull/390 . you should declare ransakable_scopes method to add scopes visible for ransack.
From manual
Continuing on from the preceding section, searching by scopes requires defining a whitelist of ransackable_scopes on the model class. The whitelist should be an array of symbols. By default, all class methods (e.g. scopes) are ignored. Scopes will be applied for matching true values, or for given values if the scope accepts a value:
class Employee < ActiveRecord::Base
scope :activated, ->(boolean = true) { where(active: boolean) }
scope :salary_gt, ->(amount) { where('salary > ?', amount) }
# Scopes are just syntactical sugar for class methods, which may also be used:
def self.hired_since(date)
where('start_date >= ?', date)
end
private
def self.ransackable_scopes(auth_object = nil)
if auth_object.try(:admin?)
# allow admin users access to all three methods
%i(activated hired_since salary_gt)
else
# allow other users to search on `activated` and `hired_since` only
%i(activated hired_since)
end
end
end
Employee.ransack({ activated: true, hired_since: '2013-01-01' })
Employee.ransack({ salary_gt: 100_000 }, { auth_object: current_user })
Ransack let's you create custom predicates for this, unfortunately the documentation leaves room for improvement however checkout: https://github.com/ernie/ransack/wiki/Custom-Predicates
Also I believe the problem you're trying to tackle is up on their issue tracker. There's a good discussion going on there: https://github.com/ernie/ransack/issues/34
I wrote a gem called siphon which helps you translate parameters into activerelation scopes. Combining it with ransack can achieves this.
You can read full explanation here. Meanwhile here's the gist of it
The View
= form_for #product_search, url: "/admin/products", method: 'GET' do |f|
= f.label "has_orders"
= f.select :has_orders, [true, false], include_blank: true
-#
-# And the ransack part is right here...
-#
= f.fields_for #product_search.q, as: :q do |ransack|
= ransack.select :category_id_eq, Category.grouped_options
```
ok so now params[:product_search] holds the scopes and params[:product_search][:q] has the ransack goodness. We need to find a way, now, to distribute that data to the form object. So first let ProductSearch swallow it up in the controller:
The Controller
# products_controller.rb
def index
#product_search = ProductSearch.new(params[:product_search])
#products ||= #product_formobject.result.page(params[:page])
end
The Form Object
# product_search.rb
class ProductSearch
include Virtus.model
include ActiveModel::Model
# These are Product.scopes for the siphon part
attribute :has_orders, Boolean
attribute :sort_by, String
# The q attribute is holding the ransack object
attr_accessor :q
def initialize(params = {})
#params = params || {}
super
#q = Product.search( #params.fetch("q") { Hash.new } )
end
# siphon takes self since its the formobject
def siphoned
Siphon::Base.new(Product.scoped).scope( self )
end
# and here we merge everything
def result
Product.scoped.merge(q.result).merge(siphoned)
end
end

Resources