Why is thinking sphinx restricting filtering to one attribute at a time in ruby on rails? - ruby-on-rails

Am I doing something wrong?
If I don't have the with options added location works and I get results shown as soon as I enter the browser page of my website.
When i hit the page all users are shown by 20 per page. If I add with_all gender => params[:gender], location still works and I type in a location and filter results by gender and results are successfully returned.
If I add ethnicity to the with_all hash then ethnicity works and results are turned but gender and location no longer work.
It's like it only allows 1 attribute for filtering.
I have rebuilt the index several times so I don't get what's going on.
I've got text search for location and 2 filters set 1. gender, 2. ethnicity
Here is my Profile model for the profiles table that stores all the attributes above:
define_index do
indexes location
has ethnicity, :type => :integer
has gender, :type => :integer
end
Here is my controller:
class BrowsersController < ApplicationController
def index
#default_image = "/assets/default_avatar.jpg"
#gender = params[:gender].to_i
#users = Profile.search params[:location],
:page => params[:page],
:per_page => 20,
:with_all =>
{
:gender => params[:gender],
:ethnicity => params[:ethnicity]
}
end
end
my view form:
<%= form_tag browsers_path, :method => 'get' do %>
<p>
Location: <%= text_field_tag :location, params[:location] %><br />
Gender: <%= select_tag :gender,
options_for_select([["Select", nil],
["Male", 1],
["Female", 2]], params[:gender]) %>
<br />
Ethnicity: <%= select_tag :ethnicity,
options_for_select([["Select", nil],['Black', 1 ],['White / Caucasian', 2 ],['European', 3 ],['Asian', 4 ],['Indian', 5 ],['Middle Eastern', 6 ],['Native American', 7 ],['Hispanic', 8 ],['Mixed Race', 9 ],['Other Ethnicity', 10 ]], params[:ethnicity]) %>
<br />
<%= submit_tag "Search", :name => nil %>
</p>
<% end %>

There's a lot to digest in your question, but here's a few things to note - perhaps they will help:
:with_all is for matching multiple values in a single multi-value attribute - for example, matching an article that has all three tag ids would use this: :with_all => {:tag_ids => [1, 2, 3]}.
:with, however, is perfectly fine for having filters on more than one attribute - which is what you seem to be after (although :with_all with single filter values behaves in just the same way).
Sphinx treats nils/NULLs as 0's - so, if you're filtering by a gender but not an ethnicity, then what your controller code is doing is searching for profiles with the given gender and an ethnicity of 0. Perhaps try something like this instead:
filters = {}
filters[:gender] = params[:gender].to_i if params[:gender].present?
filters[:ethnicity] = params[:ethnicity].to_i if params[:ethnicity].present?
#users = Profile.search params[:location],
:page => params[:page],
:per_page => 20,
:with => filters
Finally - the gender and ethnicity columns are integers, yes? If so, you don't need to specify :type => :integer in your index definition - that'll be done automatically.

Related

Combine multiple scope or where queries with OR

How do I get the arel components in such a ways that I can do something like:
queries = []
queries << MyModel.some_scope.get_the_arel_component
queries << MyModel.some_scope_with_param("Dave").get_the_arel_component
queries << MyModel.where(:something => 'blah').get_the_arel_component
queries << MyModel.some_scope_with_join_and_merge.get_arel_component
# etc ... (may be any number of queries)
# join each query with OR
combined_query = nil
queries.each do |query|
combined_query ||= query
combined_query = combined_query.or(q)
end
# run the query so it just works
MyModel.where(combined_query)
I've encountered some issues with accepted answers of similar questions.
Lets say I have a class like so:
class Patient
has_one :account
scope :older_than, ->(date) { where(arel_table[:dob].lt(date)) }
scope :with_gender, ->(gender) { where(:gender => gender) }
scope :with_name_like, ->(name) { where("lower(name) LIKE ?", name.downcase) }
scope :in_arrears, -> { joins(:account).merge( Account.in_arrears ) }
end
The goal is to combine any scope or where clause with an OR.
One way would be Patient.with_name_like("Susan") | Patient.with_name_like("Dave"). This seems to run each individual query separately instead of combine into a single query. I've ruled this solution out.
Another method that only works in some instances is:
# this fails because `where_values` for the `with_name_like` scope returns a string
sues = Patient.with_name_like("Susan").where_values.reduce(:and)
daves = Patient.with_name_like("Dave").where_values.reduce(:and)
Patient.where(sues.or(daves))
# this works as `where_values` returns an `Arel::Nodes::Equality` object
ages = Patient.older_than(7.years.ago).where_values.reduce(:and)
males = Patients.with_gender('M').where_values.reduce(:and)
Patient.where(ages.or(males))
# this fails as `in_arrears` scope requires a joins
of_age = Patient.older_than(18.years.ago).where_values.reduce(:and)
arrears = Patients.in_arrears.where_values.reduce(:and)
Patient.where(of_age.or(arrears)) # doesn't work as no join on accounts
Patient.join(:account).where(of_age.or(arrears)) # does work as we have our join
To sum up, the issues with ORing queries arise when where is passed a string or the query requires a join.
I'm pretty sure where converts anything passed to it into an arel object, it's just a matter of getting access to the correct pieces and recombining them in the correct way. I just haven't managed to work it out yet.
Preferably the answer will only make use of ActiveRecord and AREL and not a third party library.
Since you're open to using a third party library, how about Ransack?
It has a very robust implementation allowing for all kinds of and and or condition combinations and works well with associated models as well.
For a use case like yours where there are a few predefined queries/scopes that I want the user to be able to select from and run the or combination of them, I use ransack's out of the box implementation and then on the view level, I use javascript to insert hidden fields with values that will result in the structured params hash ransack is expecting in the controller.
All of your scopes are simple to define in a view using ransack helpers. Your code should look like:
Controller
def index
#q = Patient.search(params[:q])
#patients = #q.result(distinct: true)
end
View
<%= search_form_for #q do |f| %>
<%= f.label :older_than %>
<%= f.date_field :dob_lt %>
<%= f.label :with_gender %>
<%= f.text_field :gender_eq %>
<%= f.label :with_name_like %>
<%= f.text_field :name_cont %>
<%= f.label :in_arrears_exceeding %>
<%= f.text_field :accounts_total_due_gte %>
<%= f.submit %>
<% end %>
Also, if you want more control over the anding and oring take a look at the complex search form builder example using ransack.
I had worked on a similar problem in one of my previous projects. The requirement was to find a set of volunteers to scribe matching a set of criteria like email, location, stream of study etc. The solution that worked for me is to define fine-grained scopes and writing up my own query builder like this:
class MatchMaker
# Scopes
# Volunteer => [ * - 'q' is mandatory, # - 'q' is optional, ** - 's', 'e' are mandatory ]
# active - activation_state is 'active'
# scribes - type is 'scribe'
# readers - type is 'reader'
# located - located near (Geocoder)
# *by_name - name like 'q'
# *by_email - email like 'q'
# educated - has education and title is not null
# any_stream - has education stream and is not null
# *streams - has education stream in 'q'
# #stream - has education stream like 'q'
# #education - has education and title like 'q'
# *level - education level (title) is 'q'
# *level_lt - education level (title) is < 'q'
# *level_lteq - education level (title) is <= 'q'
# *marks_lt - has education and marks obtained < 'q'
# *marks_lteq - has education and marks obtained <= 'q'
# *marks_gt - has education and marks obtained > 'q'
# *marks_gteq - has education and marks obtained >= 'q'
# *knows - knows language 'q'
# *reads - knows and reads language 'q'
# *writes - knows and writes language 'q'
# *not_engaged_on - doesn't have any volunteering engagements on 'q'
# **not_engaged_between - doesn't have any volunteering engagements betwee 'q' & 'q'
# #skyped - has skype id and is not null
def search(scope, criteria)
scope = scope.constantize.scoped
criteria, singular = singular(criteria)
singular.each do |k|
scope = scope.send(k.to_sym)
end
if criteria.has_key?(:not_engaged_between)
multi = criteria.select { |k, v| k.eql?(:not_engaged_between) }
criteria.delete(:not_engaged_between)
attrs = multi.values.flatten
scope = scope.send(:not_engaged_between, attrs[0], attrs[1])
end
build(criteria).each do |k, v|
scope = scope.send(k.to_sym, v)
end
scope.includes(:account).limit(Configuration.service_requests['limit']).all
end
def build(params)
rejects = ['utf8', 'authenticity_token', 'action']
required = ['by_name', 'by_email', 'by_mobile', 'streams', 'marks_lt', 'marks_lteq', 'marks_gt',
'marks_gteq', 'knows', 'reads', 'writes', 'not_engaged_on', 'located', 'excluding',
'level', 'level_lt', 'level_lteq']
optional = ['stream', 'education']
params.delete_if { |k, v| rejects.include?(k) }
params.delete_if { |k, v| required.include?(k) && v.blank? }
params.each { |k, v| params.delete(k) if optional.include?(k.to_s) && v.blank? }
params
end
def singular(params)
pattrs = params.dup
singular = ['active', 'scribes', 'readers', 'educated', 'any_stream', 'skyped']
original = []
pattrs.each { |k, v| original << k && pattrs.delete(k) if singular.include?(k.to_s) }
[pattrs, original]
end
end
The form would be something like this:
...
<%= f.input :paper ... %>
<%= f.input :writes ... %>
<%= f.input :exam_date ... %>
<%= f.time_select :start_time, { :combined => true, ... } %>
<%= f.time_select :end_time, { :combined => true, ... } %>
<fieldset>
<legend>Education criteria</legend>
<%= f.input :streams, :as => :check_boxes,
:collection => ...,
:input_html => { :title => 'The stream(s) from which the scribe can be taken' } %>
<%= f.input :education, :as => :select,
:collection => ...,
:input_html => { :class => 'input-large', :title => configatron.scribe_request.labels[:education]}, :label => configatron.scribe_request.labels[:education] %>
<%= f.input :marks_lteq, :label => configatron.scribe_request.labels[:marks_lteq],
:wrapper => :append do %>
<%= f.input_field :marks_lteq, :title => "Marks", :class => 'input-mini' %>
<%= content_tag :span, "%", :class => "add-on" ... %>
<% end %>
</fieldset>
...
And finally
# Start building search criteria
criteria = service_request.attributes
...
# do cleanup of criteria
MatchMaker.new.search('<Klass>', criteria)
This has worked for me very well in the past. Hope this would lead you in the right direction in solving the problems you are facing. All the best.

Ruby on Rails - How to use a select box to change an attribute of the selected items?

I'll start off with a bit of context to my question. I have a group of offices which each have reviewers associated with them. A reviewer can only be associated with one office. I want to create two select boxes. One lists all of the reviewers that are associated with the office I am viewing, the other lists all of the reviewers that are available (which is basically all of the reviewers that aren't already assigned to this office).
The goal of the current reviewer's listbox is to set their office to nil when they are selected. The goal of the available reviewers listbox is to set their office to this office's id when they are selected. I'm not sure how to change only the reviewers' office_id when using a select box.
Code-wise, what I have so far is this:
office_controller.rb
def edit
#office = Group.find params[:id] if params[:id]
#current_reviewers = Reviewer.find_all_by_group_id(#office.id)
#available_reviewers = Reviewer.where('group_id <> ?',[#office.id])
end
def update
?
end
office/edit.html.erb
<% form_for(#office, :url => {:controller => :office, :action => :update, :id => #office.id}, :html => {}) do |f| %>
...
<%= select_tag 'removedReviewers', options_from_collection_for_select(#current_reviewers, "id", "display_name"), :multiple => true %>
<%= select_tag 'chosenReviewers', options_from_collection_for_select(#available_reviewers, "id", "display_name"), :multiple => true %>
...
<% end %>
Any suggestions would be greatly appreciated. Thanks!
I solved this by adding the ability to get a select_tag to get the selected objects as a collection via the [] modifier of the select_tag name. So, in this example it was:
<%= select_tag 'removedReviewers[]', options_from_collection_for_select(#current_reviewers, "id", "display_name"), :multiple => true %>
And for the controller code to handle it, I did this:
#removedReviewers = params[:removedReviewers]
if !#removedReviewers.nil?
#removedReviewers.each do |reviewer|
#reviewer = Reviewer.find(reviewer)
#reviewer.group_id = nil
#reviewer.save
end
end
And the equivalent for the chosen/available reviewers.

How do I search for multiple records in a search form?

I am trying to allow the user to be able to choose multiple records in a field on the search form.
Something like this:
<%= f.input_field :neighborhood_id, collection: Neighborhood.order(:name), :url => autocomplete_neighborhood_name_searches_path, :as => :autocomplete, 'data-delimiter' => ',', :multiple => true, :class => "span8" %>
It sends it to my search model like this: #search = Search.create!(params[:search])
This is what the Search.rb model does with it:
key = "%#{keywords}%"
listings = Listing.order(:headline)
listings = listings.includes(:neighborhood).where("listings.headline like ? or neighborhoods.name like ?", key, key) if keywords.present?
listings = listings.where(neighborhood_id: neighborhood_id) if neighborhood_id.present?
listings
The issue is that this is just accepting 1 neighborhood_id, so I am getting this error when I choose multiple objects:
undefined method `to_i' for ["Alley Park, Madison"]:Array
Where Alley Park and Madison are the names of 2 neighborhoods, not the IDs.
So how do I get this working?
Thanks.
Edit 1
The issue seems to not be in the lookup of the params[:search] per se, but rather in the conversion of the form input to an array of entries. I tried changing the search method to be something like:
listings = listings.includes(:neighborhood).where("neighborhoods.name like ?", neighborhood_id) if neighborhood_id.present?
Don't get hung up on the fact that I am looking up neighborhood.name and passing in neighborhood_id. I just did that because I know that the params for the field neighborhood_id were actually the names of the neighborhood. If this had worked, I would have refactored some stuff, but it didn't. So don't get hung up on that.
But that still returns the error undefined method 'to_i'....
Also, I still get that error even if I just pass in 1 option.
listings = listings.where("neighborhood_id in (?) ", neighborhood_id)
You can get the id instead of neighborhood names from the input field like this:
<%= f.input_field :neighborhood_id, collection: Neighborhood.order(:name), :url => autocomplete_neighborhood_name_searches_path, :as => :autocomplete, 'data-delimiter' => ',', :multiple => true, :class => "span8", :input_html => { :id => "neighborhood_id" } %>

multiparameter error with datetime_select

I have the following code in my form.
<%= f.datetime_select(:date_time, :prompt => {:day => 'Day', :month => 'Month', :year => 'Year'}, :start_year => Date.today.year, :end_year => Date.today.year + 2, :minute_step => 15, :include_blank => false) %> if either one is blank.
When one of the fields is left blank, I get:
1 error(s) on assignment of multiparameter attributes
The params that are being passed are:
{"utf8"=>"✓",
"authenticity_token"=>"kQpfsj5RxnDtxkvBdwPEFnX1fY6euKnMQeDRAkvJvIE=",
"event"=>{"description"=>"",
"venue"=>"",
"street"=>"",
"city"=>"",
"country_id"=>"",
"date_time(1i)"=>"",
"date_time(2i)"=>"",
"date_time(3i)"=>"",
"date_time(4i)"=>"00",
"date_time(5i)"=>"00",
"ticket_url"=>""},
"x"=>"94",
"y"=>"12"}
Anyone know why this is occurring?
There seems to be a "dirty" fix for this at this link, but perhaps there is a better solution in Rails 3?
Christian. This is a bug in Rails that checks the database to infer the type needed for the multiparameter attributes. My guess is that your "date_time" attribute is not associated with a time column in your database.
I recently tackled this problem where I wanted a non-database attribute to accepted multiparameter attributes, this was the best solution I could come up with:
I found myself wanting to set an attr_accessor to handle passing a date to my model in a form_for tag with the f.datetime_select helper. So this is what I had:
Model:
attr_accessor :my_time
View:
<%= f.datetime_select :my_time %>
Unfortunately when I submit my form I get this:
1 error(s) on assignment of multiparameter attributes
Well it turns out that this is actually a Rails bug a ticket for which has been submitted. In the meantime how do we make this work? The only solution I could find that was remotely attractive was to make use of composed_of as a replacement for attr_accessor. so...
Model:
composed_of :my_time,
:class_name => 'Time',
:mapping => %w(Time to_s),
:constructor => Proc.new{ |item| item },
:converter => Proc.new{ |item| item }
I know almost nothing about the composed_of method so you should probably do your own reading on it, but what I do know is that it creates both a reader and writer for the given instance variable, and more importantly, the setter accepts multiparameter attributes. How I chose the options:
class_name: the name of our expected class. In this case, Time
mapping: the first argument is the class and the second argument seems to work with any method that an instance of the class responds to. I chose to_s
constructor: Not really sure how this is supposed to work. Seems to be called when #my_time is nil.
converter: Not really sure how this is supposed to work. Seems to be called when from my_time=, but doesn't seem to be applied with mass assignment.
One problem I ran into with this solution was that times were getting set in UTC instead of the environment's time zone. So unfortunately we cannot use my_time directly, but instead need to convert it to the proper time zone:
Time.zone.parse(my_time.to_s(:number))
What Does ActiveRecord::MultiparameterAssignmentErrors Mean?
def initialize(attributes={})
date_hack(attributes, "deliver_date")
super(attributes)
end
def date_hack(attributes, property)
keys, values = [], []
attributes.each_key {|k| keys << k if k =~ /#{property}/ }.sort
keys.each { |k| values << attributes[k]; attributes.delete(k); }
attributes[property] = values.join("-")
end
I had the same problem using a date dropdown that wasn't backed by a database attribute. I wrote a little Rack middleware to cope with the problem:
class DateParamsParser
def initialize(app)
#app = app
end
def call(env)
if %w{POST PUT}.include? env['REQUEST_METHOD']
params = Rack::Utils.parse_query(env["rack.input"].read, "&")
# selects only relevant params like 'date1(1i)'
filtered_params = params.select{ |key, value| key =~ /\(\di\)/ }
# delete date params
filtered_params.each { |key, value| params.delete(key) }
# returns something like {'date1' => [2012, 5, 14], 'date2' => [2002, 3, 28]}
date_array_params = filtered_params.sort.reduce({}) do |array_params, keyvalue|
date_key = keyvalue.first.match(/(.+)\(/)[1] + ']'
array_params[date_key] ||= []
array_params[date_key] << keyvalue.last
array_params
end
# Creates params with date strings like {'date1' => '2012-5-14', 'date2' => '2002-3-28'}
date_params = Hash[date_array_params.map{ |key, date_array| [key, date_array.join('-')] }]
params.merge! date_params
env["rack.input"] = StringIO.new(Rack::Utils.build_query(params))
env["rack.input"].rewind
end
#app.call(env)
end
end
And in application.rb I put
config.middleware.insert_before ActionDispatch::ParamsParser, "DateParamsParser"
Note that I only build a date string here. So if you also require time you'll need to build the date_params differently.
I faced the same problem with the model below
class Reservation < ActiveRecord::Base
attr_accessor :sid, :check_in, :credit_card_number, :expiration_date
attr_accessible :expiration_date
end
The corresponding form with the field for the expiration date:
<div class="field">
<%= f.label :expiration_date %>
<%= f.date_select(:expiration_date, start_year: Time.now.year + 3, :end_year => Time.now.year - 3, discard_day: true) %>
</div>
as mentioned by #gabeodess the problem is checking the database to infer the type accordingly the solution I did for it was adding the following code to the model to put the type of the needed attribute in this case :expiration_date so the model is modified to be the following
class Reservation < ActiveRecord::Base
attr_accessor :sid, :check_in, :credit_card_number, :expiration_date
attr_accessible :expiration_date
columns_hash["expiration_date"] = ActiveRecord::ConnectionAdapters::Column.new("expiration_date", nil, "date")
end
Hope this is useful
Remove :include_blank => false from your code.
<%= f.datetime_select(:date_time, :prompt => {:day => 'Day', :month => 'Month', :year => 'Year'}, :start_year => Date.today.year, :end_year => Date.today.year + 2, :minute_step => 15 %>
Thanks....
I was facing the same problem.
I just added attr_accessible for that attribute and it works fine.
Hope it helps.

Rails date_select helper and validation

I have a date field in a model backed form in my Rails App:
<%= f.date_select :birthday,
{:start_year => Time.now.year,
:end_year => 1900,
:use_short_month => true,
:order => [:month, :day, :year],
:prompt => {:month => 'Month', :day => 'Day', :year => 'Year'}},
{:class => 'year',
:id => 'user_birthday'}
%>
It is being validated in the model code using:
validates_presence_of :birthday, :message => 'is a required field'
Unfortunately, if the user enters a partial value such as just the year, the form still submits without an error. Instead a funky date value gets written to the db. How do I make all three fields be mandatory?
I'd like to write a custom validation for this, but I don't know how to properly access the indvidual pieces of the birthday element. How can I do this?
Thanks!
Moe
I think you would have to create the validation in the controller itself.
The date parts are being passed to birthday(1i), birthday(2i) and birthday(3i). The problem here is that they are assigned immediately when passing the attributes and thus before any validations occur.
You could also overwrite the attributes= method to create your own validation there, but I would not suggest you to do that.
Keep in mind that if you do validations, it might be good to validate against any incorrect date as well. (for instance 31st of February, which when passed will yield 2nd of March and not an error).
I think the main issue here is that ActiveRecord is actually replacing the empty values with 1 before creating the Date, which also means that if the visitor pass only the year, the date will be created on the 1st of January that year. I guess that is an expected behaviour to allow use of only one of year/month/day select and still create a useful date.
Related to this post, this is the best solution I've found. However I should add :day, :month, :year as attr_accessible, thing I don't understand why.. (because of validation? please let me know..)
User.rb
MONTHS = ["January", 1], ["February", 2], ...
DAYS = ["01", 1], ["02", 2], ["03", 3], ...
START_YEAR = Time.now.year - 100
END_YEAR = Time.now.year
YEAR_RANGE = START_YEAR..END_YEAR
attr_accessible :day, :month, :year
attr_accessor :day, :month, :year
before_save :prepare_birthday
validate :validate_birthday
private
def prepare_birthday
begin
unless year.blank? # in order to avoid Year like 0000
self.birthday = Date.new(self.year.to_i, self.month.to_i, self.day.to_i)
end
rescue ArgumentError
false
end
end
def validate_birthday
errors.add(:birthday, "Birthday is invalid") unless prepare_birthday
end
user registration form
<%= f.select :month, options_for_select(User::MONTHS), :include_blank => "Month" %>
<%= f.select :day, options_for_select(User::DAYS), :include_blank => "Day" %>
<%= f.select :year, options_for_select(User::YEAR_RANGE), :include_blank =>"Year" %>
You could override the validate_on_create method, like the following:
class MyModel < ActiveRecord::Base
def validate_on_create
Date.parse(birthday)
rescue
errors.add_to_base("Wrong date format")
end
end
After following Benoitr's suggestions I came up with something similar using virtual attributes. On the View side there are 3 separate select's (year,mon,day) inside of a 'fields_for'. The data is submitted to the controller's mass assignment (no modifications in controller, see asciicasts #16) and then passed to a getter/setter (i.e. virtual attribute) in the model. I'm using Rails 3.0.3, and simpleForm for the view code.
In the View:
<%= f.label "Design Date", :class=>"float_left" %>
<%= f.input :design_month, :label => false, :collection => 1..12 %>
<%= f.input :design_day, :label => false, :collection => 1..31 %>
<%= f.input :design_year, :label => false, :collection => 1900..2020 %>
In the Model:
validate :design_date_validator
def design_year
design_date.year
end
def design_month
design_date.month
end
def design_day
design_date.day
end
def design_year=(year)
if year.to_s.blank?
#design_date_errors = true
else
self.design_date = Date.new(year.to_i,design_date.month,design_date.day)
end
end
def design_month=(month)
if month.to_s.blank?
#design_date_errors = true
else
self.design_date = Date.new(design_date.year,month.to_i,design_date.day)
end
end
def design_day=(day)
if day.to_s.blank?
#design_date_errors = true
else
self.design_date = Date.new(design_date.year,design_date.month,day.to_i)
end
end
#validator
def design_date_validator
if #design_date_errors
errors.add(:base, "Design Date Is invalid")
end
end
'design_date_attr' is the virtual attribute which sets the value of design_date in the database. The getter passes back an hash similar to what gets submitted in the form. The setter checks for blanks and creates a new date object and sets it and also sets the error variable. The custom validator :design_date_validator checks for the error instance variable and sets the errors variable. I used ':base' because the variable name was not human readable and using base removes that value from the error string.
A few things to refactor might be the error checking instance variable, but it seems to work at least. If anyone knows a better way to update the Date objects I'd love to hear it.

Resources