I'm a bit confused with using an advanced ransack search. I'm trying do make a custom search where not all table names can be selected as search terms and not all predicates are used. I used railscast as a tutorial for this but I can't find anything how to limit the number of predicates. Is there also a possibility to use the names of preicates and table fields in different language (just labels)?
My search form
= search_form_for #q, :url => search_offers_path, :html => { :method => :post } do |f|
= f.condition_fields do |c|
.field
= f.attribute_fields do |a|
= a.attribute_select
= f.predicate_select
= f.value_fields do |v|
= v.text_field :value
= link_to "#{t :destroy}", '#', class: "remove_fields"
= link_to_add_fields "#{t :add}", f, :condition
.field
= t :sort
= f.sort_fields do |s|
= s.sort_select
= f.submit "#{t :search}"
My controller
def index
select_offers = Offer.where { (user_id != id) & (ended == false) & ((created_at + life_time ) > DateTime.now) }
#q = select_offers.search(params[:q])
#offers = #q.result(:distinct => true).page(params[:page])
#q.build_condition
#q.build_sort if #q.sorts.empty?
end
I have found a solution.
To change predicate labels I used i18n.
en.yml
ransack:
asc: "ascending"
desc: "descending"
predicates:
cont: "contains"
not_cont: "not contains"
start: "starts with"
end: "ends with"
gt: "greater than"
lt: "less than"
...
There is also a possibility to change the names of attribute fields
attributes:
model_name:
model_field1: "field name1"
model_field2: "field name2"
...
To limit the search predicates instead of
= f.predicate_select
I used
= f.attribute_fields do |a|
= a.attribute_select
= f.select :p, { 'custom predicate name1' => :predicate1, 'custom predicate name2' => :predicate2 }
To limit table search fields I added to model
UNRANSACKABLE_ATTRIBUTES = ["id", "published", "created_at"]
def self.ransackable_attributes auth_object = nil
(column_names - UNRANSACKABLE_ATTRIBUTES) + _ransackers.keys
end
To limit the predicates in the predicate_select form helper you can pass an array as :only-option:
<%= f.predicate_select only: [:cont, :not_cont, :eq, :not_eq, :blank, :null, :not_null] %>
Unfortunately the element order in the passed array doesn't matter at all so sorting the predicates doesn't work this way.
If you don't like the compounds (_any & _all) or don't even know what they are good for (as I) you can also pass the :compounds-option:
<%= f.predicate_select compounds: false %>
Hope that helped. Ernie did a lot of interesting and amazing stuff so far but their documentations are sort of sticky tape: very, very thin and you'll get stuck if you try to get your hands on it.
Related
At the moment I have a form in which the user can input price per person and/or duration and/or team_size. What I would like to accomplish is to retrieve all records from a table that match the user input and for this I set scope in the model:
scope :filter_by_team_size, -> (team_size) { where("team_size = ?", team_size) }
scope :filter_by_duration, -> (duration) { where("duration = ?", duration) }
scope :filter_by_price, -> (price) { where("price = ?", price) }
And then in the controller use that again to retrieve the records by doing so:
#experiences = policy_scope(Experience).order(team_size: :desc).geocoded.filter_by_team_size(params[:team_size]) if params[:team_size].present?
#experiences = policy_scope(Experience).order(duration: :desc).geocoded.filter_by_duration(params[:duration]) if params[:duration].present?
#experiences = policy_scope(Experience).order(price: :desc).geocoded.filter_by_price(params[:price]) if params[:price].present?
However, this only gives me only the records for which the first input value matches but ignores all other values. Additionally, when you are viewing the search results and use the filter again it should apply the filter only for the records it found already.
Any suggestion on how to solve this would be much appreciated!
One way to handle this is to use a virtual model that handles binding parameters to and from the form:
class SearchQuery
include ActiveModel::Model
include ActiveModel::Attributes
attribute :team_size, :integer
attribute :duration
attribute :price
end
You can then setup the form:
<%= form_with(model: (#search_query || SearchQuery.new), url: '/experiences', method: :get) %>
<div>
<%= f.label :team_size %>
<%= f.number_field :team_size %>
</div>
# ..
<% end %>
And then you can just bind the params to the model with ActionController::Parameters#permit just like you would with a normal ActiveRecord model:
class ExperiencesController
before_action :set_search_query, only: :index, if: ->{ params[:search_query].present? }
# ...
def index
#experiences = if #search_query
#search_query.build_scope(policy_scope(Experience))
else
policy_scope(Experience)
end.geocoded
end
private
def set_search_query
#search_query = SearchQuery.new(search_query_params)
end
def search_query_params
params.fetch(:search_query).permit(:team_size, :duration, :price)
end
end
This loopback will make the form stateful just like your normal CRUD forms. We have not actually implemented #build_scope yes so lets do so:
class SearchQuery
include ActiveModel::Model
include ActiveModel::Attributes
attribute :team_size, :integer
attribute :duration
attribute :price
def build_scope(base_scope)
compacted_attributes = attributes.reject { value.nil? || value.empty? }
compacted_attributes.each_with_object(base_scope) do |(k,v), base|
if base.respond_to? "filter_by_#{k}"
# lets you customize the logic with a scope
base.send("filter_by_#{k}", v) # the scope is responsible for ordering
else
# convention over configuration!
base.where(Hash[k,v]).order(Hash[k,:desc])
end
end
end
end
Since this uses convention over configuration you can get rid of those pointless scopes in your model.
I'd like to include a rails object in my keywords as well as straight text but the code is clearly not the right way to do it...how can I do this?
set_meta_tags :keywords => %w[keyword1 keyword2 #{params[:hospital]}]
You might want to have a look at two plug-ins for including rails object in meta tags:
Meta Magic: https://github.com/lassebunk/metamagic
Head Liner: https://github.com/mokolabs/headliner
Edit: For Meta tag gem
What I usually do is write a meta helper that I simply stick in my ApplicationHelper, that looks like this:
def meta(field = nil, list = [])
field = field.to_s
#meta ||= {
'robots' => ['all'],
'copyright' => ['My Copyright'],
'content-language' => ['en'],
'title' => [],
'keywords' => []
}
if field.present?
#meta[field] ||= []
case list.class
when Array then
#meta[field] += list
when String then
#meta[field] += [list]
else
#meta[field] += [list]
end
case field
when 'description' then
content = truncate(strip_tags(h(#meta[field].join(', '))), :length => 255)
else
content = #meta[field].join(', ')
end
return raw(%(<meta #{att}="#{h(field)}" content="#{h(content)}"/>))
else
tags = ''
#meta.each do |field, list|
tags += meta(field)+"\n"
end
return tags.rstrip
end
end
You can simply set meta tags in your views, by adding a call to meta() in it. So in an articles/show.html.erb you might add this to the top of your view:
<% meta(:title, #article.title) %>
And in your layouts, you add it without any parameters, so it'll spit out the meta tags.
<%= meta %>
Or have it output an individual tag:
<%= meta(:title) %>
I bet you there's more elegant solutions, though.
But if you were looking for something already implemented in Rails you're out of luck.
Thanks.
Try this in your view as it worked for me (using meta-tags gem):
<% keywords [[#modelname.keyword1], [#modelname.keyword2]] %>
and you cad additional keywords in text format by adding them within the ruby in the following format ['keyword3']
In my view, I've got a fiddly loop which creates 500 SQL queries (to get the info for 500 books). How can I avoid lots of SQL queries by loading a variable up in the controller?
My current (pseudo) code:
controller index action:
#books = Book.scoped.where(:client_id => #client.id).text_search(params[:query])
#feature_root = Book.multiple_summary_details_by_category( #books )
#...returns a hash of books
#features = #feature_root.to_a.paginate(:page => params[:page], :per_page => 4)
index.html.haml
= render :partial => "feature", :locals => { :features => #features }
_features.html.haml
- features.each_with_index do |(cat_name, array_of_books), i|
%h2
= cat_name
- array_of_books[0..10].each do |feature|
= link_to image_tag(feature[:cover], :class => "product_image_tiny"), book_path(feature[:book])
# more code
- array_of_books.sort_by{ |k, v| k["Author"] }.each do |feature|
- feature.each do |heading,value|
%span.summary_title
= heading + ':'
%span.summary_value
= value
What have you tried so far? It should be quite easy with standard ActiveRecord queries as documented in http://guides.rubyonrails.org/active_record_querying.html.
Also, instead of
array_of_books.sort_by{ |k, v| k["Author"] }
try something like
Book.order("author DESC")
(not sure about your exact model here) to let the db do the sorting rather than putting them in an array and let ruby handle it.
This seems like it should be a common problem but I'm having trouble finding an answer. Basically I want to have a form with 10 or so checkboxes which I'm creating with check_box_tag. When the form is submitted I want to generate a query that return all records that match ANY of the checked selections. So, the number of checked selections will vary.
So, for example, if I have
class Book < ActiveRecord::Base
belongs_to :author
end
I want to generate something like
Book.where("author_id = ? or author_id = ?", params[authors[0]], params[authors[1]]) if there are two boxes checked, etc.
Thanks for any insight.
Will this work for you?
Book.where(author_id: [array_of_author_ids])
You need to collect author_ids from params first
I recently had to do something similar, this is how I achieved this. It's pretty clever (at least I think so. :))
I created a query model that serializes the query column (text field) in JSON. I use a form to get the query data from the user with selection fields.
class BookQuery < ActiveRecord::Base
has_many :books
# loop through each foreign key of the Book table and create a hash with empty selection
def self.empty_query
q = {}
Book.column_names.each do |column_name|
next unless column_name.ends_with?("_id")
q.merge column_name => []
end
end
end
I'm using Author as an example below:
<%= form_for #book_query do |f| %>
<% for author in Author.all %>
<%= check_box_tag "book_query[query][author_ids][]", author.id, false%>
<%= author.name %>
<% end %>
<%= f.submit "Save Query" %>
<% end %>
When this form is submitted you ended up with parameters like this:
When the form is submitted it generates this parameter:
Parameters: {"utf8"=>"✓", "authenticity_token"=>"XXXXXXXXXXX", "book_query"=>{"query"=>{"author_ids"=>["2", "3"]}}, "commit"=>"Save Query"}
Now in the BookQuery controller's create action you can just do what create function always does:
def create
#book_query = BookQuery.build(params[:book_query])
if #book_query.save
flash[:success] = "Book query successfully saved."
redirect_to ...
else
flash[:error] = "Failed to save book query."
render :new
end
end
But by default rails serializes the data in hash type:
1.9.3p194 :015 > pp BookQuery.find(9).query
BookQuery Load (0.7ms) SELECT "book_queries".* FROM "book_queries" WHERE "book_queries"."id" = $1 LIMIT 1 [["id", 9]]
"--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess\nauthor_ids:\n- '2'\n- '3'\n"
=> "--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess\nauthor_ids:\n- '2'\n- '3'\n"
In BookQuery model, add the following:
serialize :query, JSON
But rail would change the IDs to string:
1.9.3p194 :018 > query = JSON.parse(BookQuery.find(10).query)
BookQuery Load (0.5ms) SELECT "book_queries".* FROM "book_queries" WHERE "book_queries"."id" = $1 LIMIT 1 [["id", 10]]
=> {"author_ids"=>["2", "3"]}
1.9.3p194 :019 > query["author_ids"]
=> ["2", "3"]
What I did then is override the attribute accessors in BookQuery model:
The below has to be done because the hash returns strings, not ids in integer.
def query=(query)
query.each_pair do |k, v|
if query[k].first.present?
query[k].map!(&:to_i)
else
query.except!(k)
end
end
write_attribute(:query, query)
end
# just want to avoid getting nil query's
def query
read_attribute(:query) || {}
end
To find book with this query, you can simply add this function to your Book model:
def self.find_by_book_query(book_query, options = {})
options[:conditions] = book_query.query
find(:all, options)
end
Now you get a customizable query string based on the model definition Book and everything works like the Rails way. :)
I'm sure there is a better way to do this, but in my inexperience, I'm having trouble sorting that better way out. I have a link that needs to send and object to controller for processing. It isn't working properly:
views/home/index.html.erb
<% search_term = "pizza" %>
<% #tag = Tag.find(:all, :conditions => ["name = ?", search_term ]).first %>
<li> <%= link_to(search_term, {:controller => "restaurants", :action => "index", :search_item => #tag}) %> </li>
controllers/restaurants.rb
def index
search_tag = params[:search_item]
#restaurants = Restaurant.search_by_tag(search_tag)
models/restaurant.rb
Class Restauarant < ActiveRecord::Base
def self.search_by_tag(search_tag)
search_condition = search_tag.name
#tags = Tag.find(:all, :conditions => [" name = ?", search_condition ])
#tag = #tags.first
#Restaurants = #tag.restaurants
end
end
This causes a NoMethodError in ResataurantsController#index
parameters:
{"searchitem" => "15"}
For some reason, the tag object isn't being passed properly from the home/index.html.erb and is only passing along the Tag-object :id to the restaurants controller. Isn't possible to pass a full object this way. what am I doing wrong?
You can't submit an object through get params like that. Typically, you'll just pass the object's id (which you are already doing), and then do a lookup in the controller:
#tag = Tag.find(params[:search_item])
It would make more sense to rename the "search_item" param to "tag_id".