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
Related
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
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.
For example in my Car model i have such fields:
color, price, year
and in form partial i generate form with all this fields. But how to code such logic:
user could enter color and year and i must find with this conditions, user could enter just year or all fields in same time...
And how to write where condition? I could write something like:
if params[:color].present?
car = Car.where(color: params[:color])
end
if params[:color].present? && params[:year].present?
car = Car.where(color: params[:color], year: params[:year])
end
and so over....
But this is very ugly solution, i'm new to rails, and want to know: how is better to solve my problem?
Check out the has_scope gem: https://github.com/plataformatec/has_scope
It really simplifies a lot of this:
class Graduation < ActiveRecord::Base
scope :featured, -> { where(:featured => true) }
scope :by_degree, -> degree { where(:degree => degree) }
scope :by_period, -> started_at, ended_at { where("started_at = ? AND ended_at = ?", started_at, ended_at) }
end
class GraduationsController < ApplicationController
has_scope :featured, :type => :boolean
has_scope :by_degree
has_scope :by_period, :using => [:started_at, :ended_at], :type => :hash
def index
#graduations = apply_scopes(Graduation).all
end
end
Thats it from the controller side
I would turn those into scopes on your Car model:
scope :by_color, lambda { |color| where(:color => color)}
scope :by_year, lambda { |year| where(:year => year)}
and in your controller you would just conditionally chain them like this:
def index
#cars = Car.all
#cars = #cars.by_color(params[:color]) if params[:color].present?
#cars = #cars.by_year(params[:year]) if params[:year].present?
end
user_params = [:color, :year, :price]
cars = self
user_params.each do |p|
cars = cars.where(p: params[p]) if params[p].present?
end
The typical (naive, but simple) way I would do this is with a generic search method in my model, eg.
class Car < ActiveRecord::Base
# Just pass params directly in
def self.search(params)
# By default we return all cars
cars = all
if params[:color].present?
cars = cars.where(color: params[:color])
end
if params[:price1].present? && params[:price2].present?
cars = cars.where('price between ? and ?', params[:price1], params[:price2])
end
# insert more fields here
cars
end
end
You can easily keep chaining wheres onto the query like this, and Rails will just AND them all together in the SQL. Then you can just call it with Car.search(params).
I think you could use params.permit
my_where_params = params.permit(:color, :price, :year).select {|k,v| v.present?}
car = Car.where(my_where_params)
EDIT: I think this only works in rails 4, not sure what version you're using.
EDIT #2 excerpt from site I linked to:
Using permit won't mind if the permitted attribute is missing
params = ActionController::Parameters.new(username: "john", password: "secret")
params.permit(:username, :password, :foobar)
# => { "username"=>"john", "password"=>"secret"}
as you can see, foobar isn't inside the new hash.
EDIT #3 added select block to where_params as it was pointed out in the comments that empty form fields would trigger an empty element to be created in the params hash.
I have a model
class Transaction < ActiveRecord::Base
end
I have a transaction_type column which is an integer.
How can I create an enumeration that I could map values to names like:
one_time = 1
monthly = 2
annually = 3
So in the db column, the values would be 1, 2 or 3.
Also, whenever I create a new instance, or save a model and the field wasn't set like:
#transaction = Transaction.new(params)
It should default to 1 (on_time).
I'm not sure how I can do this?
basically the same answer as Amit, slight variation
class TransactionType
TYPES = {
:one_time => 1,
:monthly => 2,
:annually => 3
}
# use to bind to select helpers in UI as needed
def self.options
TYPES.map { |item| [item[0], item[1].to_s.titleize] }
end
def self.default
TYPES[:one_time]
end
end
one way to control the default value
class Transaction < ActiveRecord::Base
before_create :set_default_for_type
def set_default_for_type
type = TransactionType.default unless type.present?
end
end
but - best way is to just apply the defaults on your database column and let ActiveRecord get it from there automatically
NOTE: it might also make sense to just have a TransactionType ActiveRecord object instead of above, depends on your situation, i.e.
# on Transaction with type_id:integer
belongs_to :type, class_name: "TransactionType"
You can map the values by creating a constant either in the same Transaction model or by creating a new module and place it inside that as explained by #KepaniHaole
In Transaction model, you can do it like :
class Transaction < ActiveRecord::Base
TRANSACTION_TYPES = { 'one_time' => 1, 'monthly' => 2, 'monthly' => 3 }
end
You can access these values by accessing the constant as
Transaction::TRANSACTION_TYPES['one_time'] # => 1
Transaction::TRANSACTION_TYPES['monthly'] # => 2
Transaction::TRANSACTION_TYPES['monthly'] # => 3
To add a default value to transaction_type column just create a new migration with :
def up
change_column :transactions, :transaction_type, :default => Transaction::TRANSACTION_TYPES['one_time']
end
With this, every time you create a Transaction object without passing transaction_type, the default value 1 with be stored in it.
Maybe you could try something like this? Ruby doesn't really support c-style enums..
module TransactionType
ONCE = 1
MONTHLY = 2
ANUALLY = 3
end
then you could access their values like so:
#transaction = Transaction.new(TransactionType::ONCE)
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