rails OR query based on multiple checkbox selections - ruby-on-rails-3

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. :)

Related

Search for multiple possible param values

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.

Why isn't my search method working in Ruby on Rails?

In my Ruby on Rails application, I have a cinema system and am trying to return the screen a showing is in when a user searches for the showing.
To display the search drop down I am using this code in my _application.html.erb:
<%= render( :partial => '/screen_lookup', :locals => {:showings => #showings = Showing.all, :my_path => '/screens/display_screens_by_showing' })%>
Which renders the search from the _screen_lookup.html.erb:
<%= form_tag my_path, :method=>'post', :multipart => true do %>
<%= select_tag ('showings_id'),
options_from_collection_for_select(#showings, :id, :showing_times, 0 ),
:prompt => "Showings" %>
<%= submit_tag 'Search' %>
<% end %>
And uses the display_screens_by_showing in the screens_controller:
def display_screens_by_showing
#screens = Screen.showing_search(params[:showing_id])
if #screens.empty?
# assign a warning message to the flash hash to be displayed in
# the div "feedback-top"
flash.now[:alert] = "There are no films of that genre."
# return all products, in alphabetical order
#screens = Screen.all
end
render :action => "index"
end
And this searches using the method in the screen.rb model:
def self.showing_search(showing_id)
screen = Showing.where("id = ?", showing_id).screen_id
self.where("id = ?", screen)
end
Now, the problem I am having is that because a showing belongs_to a screen, and a screen has_many showings, I need to be able to search for the showing, and store that showing's screen_id in a variable to search for the screen that showing is in with, which I have tried doing in the model:
screen = Showing.where("id = ?", showing_id).screen_id
self.where("id = ?", screen)
But the error I am getting is:
NoMethodError in ScreensController#display_screens_by_showing
undefined method `screen_id' for #<ActiveRecord::Relation []>
These are the model relationships:
showing.rb:
class Showing < ActiveRecord::Base
belongs_to :screen
end
screen.rb:
class Screen < ActiveRecord::Base
has_many :showings
end
What code will get my search working?
The problem is that where doesn't return a record, it returns a relation that can be enumerated or chained, instead you want to use find or find_by to return a single record, which is kind equivalent to where + first
screen = Showing.find(showing_id).screen_id
which is sort of like doing
screen = Showing.where(id: showing_id).first.screen_id
If you want to pass a hash you can use find_by which will be like this
screen = Showing.find_by(id: showing_id).screen_id
PS:
I'm not sure what you're doing exactly, but i think those two lines can be merged into a single query ( not sure what it should be returning, but I'm assuming a screen )
def self.showing_search(showing_id)
Showing.find(showing_id).screen
end

Access a query in Ruby on Rails

I have in my controller this:
#itemsok = Search.where("first_item_id = ?", params["3"])
This is sopposed to be a query in the search table of the database asking for all the searches that have a first_item_id = 3 ...
Question 1 .- The syntax is I found it in http://guides.rubyonrails.org/active_record_querying.html but im not sure if im using it right?
Ok the question 2 is, I have this on the controller, is it ok to have querys in the controller?
In the view im printing the variable <%= #itemsok %> and all I get is a
ActiveRecord::Relation:0x007fd3d3e894d8
Any suggestions?
Thanks in advance.
ActiveRecord 3 lets you chain relations together so you can do something like this:
#itemsok = Search.where("first_item_id = ?", params["3"]).where("foo = ?", "bar")
The where() function returns an ActiveRecord::Relation. Generally this isn't a problem, since if you use the object it'll automatically run the query and return the results on the object so you'll get the database objects. AR doesn't run the query until it's actually needed.
Where will return a list of items (Array), so if you're just debugging, change your view to this:
<%= debug #itemsok.to_a %>
You seem to be constructing the query wrong way.
If you want to search for records with first_item_id = 3, you should do:
Search.where("first_item_id = ?", 3)
This will return an array of matching records, something you can't easily print with <%= #itemsok %>. You should iterate over the elements and print each one:
<% #itemsok.each do |item| %>
<%= item.name %>
<% end %>
I'd also suggest defining to_s method for the objects you want to print.
class Search
def to_s
name
end
end
Then you can simply print the object and to_s method will be automatically called for you:
<% #itemsok.each do |item| %>
<%= item %>
<% end %>
The right way to do is to define a namedscope in the model and then use it in the controller.
Something similar to this :
class Search < ActiveRecord::Base
named_scope:item_ok,lambda {|*args|{:conditions=>["item_id >= ?", args.first]}}
end
and then call the namedscope from the controller like this :
#itemsok = Search.item_ok(params[:value])

Rails filtering with acts_as_taggable gem

I am working the acts-as-taggable-on gem and have a question about how to filter down search results based on tags users select. Here's an abridged look at my controller:
class PhotosController < ApplicationController
def index
#photos = Photo.where(["created_at > ? AND is_approved = ?", 1.months.ago, true])
#tags = ["Animals", "Architecture", "Cars", "Flowers", "Food/Drink", "General", "Landscape", "Mountains", "Nature"]
end
def search_by_tag(tag)
#photos = Photo.where('tagged_with LIKE ?', tag)
end
end
Photos/index
<% #tags.each do |tag| %>
<%= link_to tag, {:search_by_tag => tag}, :class => "tag" %>
<% end %>
This lists out all of the tags from the hash #tags defined in index, but clicking them doesn't actually filter anything down. Here's a look at what clicking one of those links produces in the log:
Started GET "/photos?search_by_tag=Animals" for 127.0.0.1 at Sun Oct 09 17:11:09 -0400 2011
Processing by PhotosController#index as HTML
Parameters: {"search_by_tag"=>"Animals"}
SQL (0.5ms) SELECT name FROM sqlite_master WHERE type = 'table' AND NOT name = 'sqlite_sequence'
The result I want is for the page to display only Photos that are tagged_with whichever tag was clicked on. I can't quite figure out how to accomplish this.
(Side-question: I can't seem to find a way to list out all of the tags from the tags table that acts-as-taggable-on generated. Doing something like Photo.tagged_with or Photo.tags doesn't work. I am able to see the "tags" table the gem created, and the entries inside of it; I'm just not really clear how to handle that using this gem)
Any thoughts greatly appreciated.
UPDATE
I've updated my code and am a bit closer.
class PhotosController < ApplicationController
def search_by_tag
#photos = Photo.tagged_with(params[:tag_name])
end
photos/index
<% Photo.tag_counts_on(:tags).map(&:name).each do |tag| %>
<%= link_to tag, {:action => 'search_by_tag', :tag_name => tag}, :class => "tag" %>
<% end %>
I believe this is closer, but still working through this...
You have a number of errors in your code:
Your link_to call is actually calling the index action.
Your search_by_tag method is expecting an argument, where it should be using the params hash to access the parameters passed to it in the web request.
tagged_with is a class method added by acts_as_taggable_on, not a field in your table - therefore you can't use it in the where method like you have done.
Finally, to get all the tag names: Photo.tag_counts_on(:tags_or_whatever_you_tagged_on).map(&:name)
Take a look at the acts_as_taggable_on documentation and you'll see examples of how to use tag_counts_on and tagged_with.
As for the Rails things: http://guides.rubyonrails.org/ http://railsforzombies.org/ and/or http://railscasts.com/

Rails 3 noob - difficulty populating an array

UPDATED WITH SOLUTION FROM BELOW PRODUCES NEW ERROR.
Users invite others to review their work. To track this, the invitations table has reviewer_id and reviewee_id fields. The model contains:
belongs_to :user
has_many :users
To display all invitations for a user we first get all the invitations:
#invitations = Invitation.where("reviewee_id = ?", current_user.id ).select(:reviewer_id).order("updated_at")
Then we get all the users who were invited: (this part is wrong)
#reviewers = []
#invitations.each do |i|
#reviewers << User.where("id = ?", i.reviewer_id )
end
This current solution produces the following:
undefined method `first_name' for []:ActiveRecord::Relation
I did a test with the following code to see what is in #reviewers:
<% #reviewers.each do |r| %>
<%= r.id %><br>
<% end %>
Instead of returning the ids it returned:
2173491700
2173491200
2173490540
So the array is not getting populated appropriately.
I am grateful for your help and most grateful for specific code.
You want to gather up all the IDs then pass them to where.
reviewer_ids = #invitations.collect { |i| i.reviewer_id }
#reviewers = User.where(:id => reviewer_ids)
This grabs all the reviewers in a single database call.
Do the following to get the reviewers:
#reviewers = []
#invitations.each do |i|
#reviewers << User.where("id = ?", i.reviewer_id )
end
The << adds elements to the array. And, before the iteration we create an array.