Rails 5 Checkbox filter - ruby-on-rails

I can't seem to find a simple solution to this anywhere. Simply put, I just want to be able to filter items with checkboxes and have the option to select multiple boxes and the filter work. The parameters get inputted, but the results don't appear (see below).
If I remove the [] from view, I can get single items to search but not multiples. The [] encodes %5B%5D into the URL which I think is part of reason search won't work. Please anyone who can help.
Controller:
#cars = if params[:colour_category]
Car.where('colour_category LIKE ? OR design LIKE ?', "%#{params[:colour_category]}%", "%#{params[:design_category]}%")
else
Car.all
end
View:
<%= form_tag cars_path, method: :get do %>
<%= check_box_tag('colour_category[]', "Yellow") %>
<%= check_box_tag('colour_category[]', "Blue") %>
<%= check_box_tag('design_category[]', "Luxury") %>
<%= submit_tag "Submit" %>
<% end %>
Example of parameters in server after doing a submit: Parameters: {"utf8"=>"✓", "colour_category"=>["Yellow", "Blue"], "commit"=>"Submit"}

A cleaner approach, which chains different scope/filter methods based on the params values. I'm posting an example from one of my apps, you can adapt the same concept:
module Client::Filterable
extend ActiveSupport::Concern
include PgSearch::Model
included do
scope :find_by_company, -> (company_id) { where(company_id: company_id) }
pg_search_scope :search, against: %w( name ), using: {
tsearch: { prefix: true }
}
end
class_methods do
def filter(params = {})
relation = self
relation = relation.search(params[:query]) if params.fetch(:query, false).present?
relation = relation.find_by_company(params[:company_id]) if params.fetch(:company_id, false).present?
relation.all
end
end
end
then in your controller:
#clients = Client.all.filter(query: 'John Smith', company_id: 4356)
You can add as many filter options into the #filter method, they'll only be applied if the relevant param key is present.
def filter(params = {})
relation = self
relation = relation.by_color_category(params[:color_category]) if param.fetch(:color_category, false).present?
relation = relation.by_design_category(params[:design_category]) if param.fetch(:design_category, false).present?
relation.all
end

Related

Multi find search in ruby with date

I have created a multi find search, where I need to filter records by date / category / title. Searching by a category and/or title works, however, when date is typed it doesn't change anything (the results is the same like there was no date typed). I have no idea what else I could do to fix it, I am just a beginner in Ruby. Any idea?
Model:
def self.multi_find(cat_id, search, date_search)
search_condition = "%" + search + "%"
#date_condition = date_search
# test if cat_id is not blank
if not cat_id.blank?
# assign the value of cat_id to a ‘scope’ named :cat
scope :cat, -> { where('category_id = ?', cat_id) }
# using the ‘scope’ cat find where a search string is like a title or an author’s name
self.cat.where("title LIKE ? or event_date = ?", search_condition, date_search.to_date)
else
# find where a search string is like a title or an author’s name
self.where("title LIKE ? or event_date = ?", search_condition, date_search.to_date)
end
end
Controller:
def multi_find
# call an Event class method, using two parameters; a category unique identifier and a search string (author or title)
events = Event.multi_find(params[:cat_id], params[:search_string], params[:event_date_search])
# use Kaminari pagination ...
#events = Kaminari.paginate_array(events.order :title).page(params[:page]).per(6)
# if no products have been found
if #events.empty?
# display a notice
flash.now[:alert] = "No events found - so displaying all events"
# then display all products
#events = Event.order(:title).page(params[:page]).per(6)
end
# use the index view
render :action => "index"
end
The console outputs the SQL Query
Event Load (0.0ms) SELECT "events".* FROM "events" WHERE (category_id = '1') AND (title LIKE '%%' or event_date = '2018-02-14') ORDER BY "events"."title" ASC
View file:
<%= form_tag my_path, :method=>'post', :multipart => true do %>
<%= select_tag ('cat_id'),
options_from_collection_for_select(#categories, :id, :cat_name, 0 ),
:prompt => "Select a Category" %>
<div class="datepicker">
<% #event_date_format %>
<%= text_field_tag :event_date_search %>
</div>
<!-- Key word:-->
<%= text_field_tag :search_string %>
<%= submit_tag 'Search' %>
<% end %>
It's because you have an or in your sql statement. However you should also clean up your code a bit.
def self.multi_find(cat_id, search, date_search)
result = self.all
result = result.where(category_id: cat_id) if cat.id.present?
result = result.where('title LIKE ?', "%#{search}%") if search.present?
result = result.where(event_date: date_search) if date_search.present?
result
end

Params hash from a SimpleForm with multiple records has a different structure for one record than for two

I struggled to digest this into a title.
I'm using SimpleForm to construct a bulk-edit page with one or more fieldsets - one for each record in a collection of ActiveRecord models that have been built but not yet saved.
My form looks like this:
= simple_form_for :courses, method: :patch do |f|
- #courses.each do |course|
= field_set_tag do
= f.simple_fields_for 'courses[]', course do |c|
= c.input :title
.row
.medium-6.columns
= c.input :start_date, as: :string, input_html: { class: 'input-datepicker' }
.medium-6.columns
= c.input :end_date, as: :string, input_html: { class: 'input-datepicker' }
= f.submit 'Save', class: 'primary button'
The params hash for one record looks like this:
"courses"=>{"courses"=>[{"title"=>"Course Y", "start_date"=>"2017-09-26", "end_date"=>"2017-07-31"}]}
with an array, while for two records it looks like this:
"courses"=>{"courses"=>{"1"=>{"title"=>"Course X", "start_date"=>"2018-01-16", "end_date"=>"2018-07-30"}, "2"=>{"title"=>"Course Y", "start_date"=>"2017-09-26", "end_date"=>"2018-07-30"}}}
with a stringy-integer-keyed hash.
This becomes a problem when I try and use strong parameters. After much hacking, I ended up with this piece of code, which works for multiple records but fails when only one is submitted:
ActionController::Parameters
.new(courses: params[:courses][:courses].values)
.permit(courses: [:title, :start_date, :end_date])
.require(:courses)
It fails with param is missing or the value is empty: courses highlighting the .require(:courses) line above.
The problem is "solved" by harmonising the single-record case with the multiple-record case:
if params[:courses][:courses].is_a?(Array)
params[:courses][:courses] = { '1': params[:courses][:courses][0] }
end
but it feels like there should be a simpler way of doing it.
Is there a better way to write the form for this use-case? Am I missing a trick with strong parameters?
I'm using rails 5.0.5 and simple_form 3.5.0.
"but it feels like there should be a simpler way of doing it."
Yes, use ajax to send individual create/update requests. This can be done transparently to the user and provides simpler code and a far better user experience.
Rails has fields_for and accepts_nested_attributes that can be used to create/update multiple child records and the parent record in a single request. But it really requires a association that groups the records together and even at this can get really hacky and convoluted when it comes to validations.
You want to set it up so that you have a seperate form for each record:
- courses.each do |c|
= render partial: 'courses/_form', course: c
There is really nothing to the form:
# courses/_form.haml.erb
= simple_form_for course, remote: true, html: { 'data-type' => 'json', class: 'course_form'} do |f|
= c.input :title
.row
.medium-6.columns
= c.input :start_date, as: :string, input_html: { class: 'input-datepicker' }
.medium-6.columns
= c.input :end_date, as: :string, input_html: { class: 'input-datepicker' }
= f.submit 'Save', class: 'primary button'
Instead of using a js.erb template we use 'data-type' => 'json' and write our own handler as its easier to target the correct form:
$(document).on('ajax:success', '.course_form', function(event, xhr, status){
var $form = $(this);
alert('Course created');
if (this.method.post) {
// changes form to update instead.
this.method = 'patch';
this.action = xhr.getResponseHeader('Location');
}
});
$(document).on('ajax:error', '.course_form', function(event, xhr, status){
var $form = $(this);
// #todo display errors
});
Creating the controller is very straight forward:
class CoursesController
def create
#course = Course.new(course_params)
respond_to do |format|
if #course.save(course_params)
format.json { head :created, location: #course }
else
format.json do
render json: {
errors: #course.errors.full_messages
}
end
end
end
end
def update
#course = Course.find(params[:id])
respond_to do |format|
if #course.update(course_params)
format.json { head :ok }
else
render json: {
errors: #course.errors.full_messages
}
end
end
end
end
Keep your form, change strong params to this:
params.require(:courses).permit(
courses: [
:id,
:title,
:start_date,
:end_date
]
)
With this code params should be without index key, #courses is just an array:
# CoursesController
def new
#courses = []
# creating 3 items for example
3.times do
#courses << Course.new
end
end
def create
errors = false
#courses= []
# keep courses in the array for showing errors
courses_params[:courses].each do |params|
course = Course.new(params)
#courses << course
unless course.valid?
errors = true
end
end
if errors
render :new
else
# if no errors save and redirect
#courses.each(&:save)
redirect_to courses_path, notice: 'courses created'
end
end
It turns out that the f.simple_fields_for 'courses[]' ... method only gives that fieldset an ID if the form is populated by an existing record, and the params structure of a string ID mapping to a course hash is only used in this case. For "fresh" records, there is no ID and the course hashes are placed in a plain array.
This bit of code was running in the context of "rolling over" courses from one year to another - copying a previous course and changing the dates. This meant that each fieldset had the ID of the original course.
When the form was submitted, a new record was created and validated with the new attributes, and it was this fresh record with no ID that repopulated the form. The "it only happens when one course is submitted" thing was a red herring - a product of the test scenario.
So worth noting: f.simple_fields_for 'courses[]' ... creates an array for new records and a hash mapping IDs to attributes for existing records.

Searching for parameter value in a postgres column

I'm attempting to implement an advanced search on my Rails 5 site. The user passes in a parameter "provider_type", and I would like to return all records that contain that value. The value is chosen from a dropdown list using simple-form. My new.html.erb looks like this:
<%= simple_form_for Search.new, :html => {:class => 'form-horizontal' } do |f| %>
<%= f.input :provider_type, collection: ['Mental Health', 'Medical'] %>
<%= f.button :submit %>
<% end %>
My Search model looks like this:
class Search < ApplicationRecord
def search_providers
providers = Provider.all
providers = providers.where("provider_type LIKE ?", ['Mental Health', 'Medical']) if provider_type.present?
providers
end
end
And my Searches controller:
def SearchesController < ApplicationController
def new
#types = Provider.uniq.pluck(:provider_type)
end
private
def search_params
params.require(:search).permit(:provider_type)
end
end
end
When I try to search for 'Mental Health' in the search form, I get this error: PG::UndefinedFunction: ERROR: operator does not exist: character varying[] ~~ unknown
EDIT
When I reword it as
providers.where(provider_type: provider_type) if provider_type.present?
This produces the error "PG::InvalidTextRepresentation: ERROR: malformed array literal: "%Mental Health%" DETAIL: Array value mut start with "{" or dimension information.
Probably you need not LIKE operator but IN. IN (or ANY) checks if fields match to one of element of array:
providers.where(provider_type: provider_type) if provider_type.present?
Rails 3 / Ruby: ActiveRecord Find method IN condition Array to Parameters single quote issue

Search/ filter method for all attributes in my index table

I'm trying to write a row for my index table that filters my objects regarding a specific value of a specific column. What I have until now is this:
pimps_controller.rb:
def index
#pimps = Pimp.search(params[:search])
end
pimp.rb:
def self.search( search)
if search
where('title LIKE ?', "%#{search}%")
else
scoped
end
end
A part of view:
<%= text_field_tag :search, params[:search] %>
That filters after the objects title only so I tried to alter it to make it functional for different search fields that can filter after different attributes. I want to pass a second parameter value if someone fires the search function to make sure it triggers for the right attributes. That's what I've tried:
pimps_controller.rb
#pimps = Pimp.search(params[:search_column],params[:search])
pimp.rb:
def self.search(search_column, search)
if search
col = "%#{search_column}"
s = "%#{search}%"
where(col 'LIKE ?', s)
else
scoped
end
end
The view:
<%= text_field_tag :search, params[:search], params[:search_column => title] %>
But it's not working. I get an error message for passing the both parameters in one search field I guess. How would you do it?
Here's a simple tutorial on how to do it:
https://we.riseup.net/rails/simple-search-tutorial
In the model, you will have to add the fields with or condition to the query.
def self.search(search)
search_condition = "%" + search + "%"
find(:all, :conditions => ['title LIKE ? OR description LIKE ?', search_condition, search_condition])
end
If you want to define the field to search in the params you can use string interpolation with simple quotes:
%q(text contains "#{search.query}")
You need 2 text fields, one for the column, one for the value:
# view
<%= text_field_tag :search_value, params[:search_value] %>
<%= text_field_tag :search_column, params[:search_column] %>
# controller
#pimps = Pimp.search(params[:search_column], params[:search_value])
# Pimp model
def self.search(search_column, search_value)
if search_value.present? && search_column.present?
column = self.column_names.include?(search_column.to_s) ? search_column : 'title'
value = "%#{search_value}%"
where("#{self.table_name}.#{column} LIKE ?", value)
else
scoped
end
end
The problem about this method is that if you don't type the exact name of the column, it will search the value in the column title. I think you should use a select_tag, listing all searchable columns of the model:
# view
<%= select_tag :search_column, options_for_select(Pimp.column_names.map { |col| [col, col] }, params[:search_column]) %>
This view code will display a select tag with the available columns of the Pimp model. You can easily limit the searchable columns by defining a class method on Pimp:
# Pimp model
def searchable_columns
self.column_names - ['id', 'created_at', 'updated_at']
end
# view
<%= select_tag :search_column, options_for_select(Pimp.searchable_columns.map { |col| [col, col] }, params[:search_column]) %>

Form for taggable post model

I'm working in a form for post than can have tags. The relationship is a classic has_and_belongs_to_many between Post and Tag.
Problem is I can't initialize a post with an array of tag ids, like this:
Post.new(tags: [1, 2, 3, 4]) # won't wotk. ActiveRecord expects Tag instances
So my current form is like this:
= form_for #post do |f|
= f.text_field :title, placeholder: 'title...'
= f.text_area :body
= fields_for :'post[tags]' do |ft| # hacky. using #post.tags raised 'undefined `model name` for `#post.tags`'
- Post.tags.each do |tag| # I defined Post::tags since I can't Acces Tag class here
= ft.check_box tag.id
= tag.name
= f.submit 'Save'
This form forces me to hack a little in either the controller, but seems like a bad practice. I also thought I could override ActiveRecord::Base initializators to allow ids so my first example works, but surely I'm missing something here.
Try setting tags_singular_ids to your ids. You can check out http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#method-i-has_and_belongs_to_many for more of the methods that has_and_belongs_to_many provides.
Easy and bit hacky solution:
# defined in my posts controller
def post_params
p = params.require(:post).merge(user: current_user)
p[:tags] = p[:tags].map do |id, value|
value == '1' ? Tag.find(id) : nil
end.compact!
p
end

Resources