Rails: Dynamically selecting an attribute for comparison - ruby-on-rails

I have a database of different food menus that I am trying to search through. In general everything works fine, but I think that there must be a cleverer way in writing the code compared to what I am doing now.
Every menu has a set of boolean attributes describing the kind of kitchen (e.g. cuisine_thai, cuisine_italian, etc.). In my view I have a dropdown allowing the user to select the type of food he wants and then I am passing the param on and save it in my search-object.
#search.cuisine_type = params[:cuisine_type]
I then continue to check for the different kitchen types and see if there is a match.
#Filter for Thai cuisine
if(#search.cuisine_type == "cuisine_thai")
#menus = #menus.select{ |menu| menu.cuisine_thai == true}
end
#Filter for French cuisine
if(#search.cuisine_type == "cuisine_italian")
#menus = #menus.select{ |menu| menu.cuisine_italian == true}
end
#Filter for Peruvian cuisine
if(#search.cuisine_type == "cuisine_peruvian")
#menus = #menus.select{ |menu| menu.cuisine_peruvian == true}
end
Eventhough the way I do it works, there must be a better way to do this. I feel that the value stored in #search.cuisine_type could just determine the attribute I check on #menus.
Any help on this is greatly appreciated. Thanks!

Yes, your intuition is correct!
I'll assume #menus is an array are ActiveRecord Menu objects, and that the cuisine_* attributes correspond to database columns. In this case you can use ActiveRecord's attributes method.
Every ActiveRecord object has an attributes property. The docs (Rails 4.2.1) say:
attributes()
Returns a hash of all the attributes with their names as keys and the values of the attributes as values.
To verify this is the case, if you peek at the attributes of a given menu, you should see a hash containing:
{
"cuisine_italian" => true,
"cuisine_thai" => false,
# etc.
}
Also, as a minor point of code readability, these two statements are effectively the same:
#menus = #menus.select { ... }
#menus.select! { ... }
Therefore, your search can be rewritten as:
if #search.cuisine_type.present?
#menus.select! { |m| m.attributes[#search.cuisine_type] }
end

Wouldn't it be better if you had column "cuisine" in database and have it set to thai, italian and so on?
Then you'd only check if certain food matches array of kitchens selected by the user.

Related

How to pass default filter into Filterrific get

I finally got my filterrific get working and its a great gem, if not a little complex for a noob like me.
My original index page was filtering the active records based on those nearby to the user like this:
def index
location_ids = Location.near([session[:latitude], session[:longitude]], 50, order: '').pluck(:id)
#vendor_locations = VendorLocation.includes(:location).where(location_id: location_ids)
#appointments = Appointment.includes(:vendor).
where(vendor_id: #vendor_locations.select(:vendor_id))
end
So this pulls in all of the Appointments with Vendors in the area, but how do I pass this over to the Filterrific search:
#filterrific = initialize_filterrific(
params[:filterrific],
select_options:{ sorted_by: Appointment.options_for_sorted_by, with_service_id: Service.options_for_select },
) or return
#appointments = #filterrific.find.page(params[:page])
respond_to do |format|
format.html
format.js
end
It seems like the Filterrerrific is loading ALL of the appointments by default, but I want to limit to the ones nearby. What am I missing?
What you appear to be missing is a param default_filter_params to filterrific macro in the model. (Your question didn't mention that you made any adjustments to the VendorLocation model, since that is the object that you want to filter, that's where the macro should be called. Maybe you just omitted it from your question...)
From the model docs:
filterrific(
default_filter_params: { sorted_by: 'created_at_desc' },
available_filters: [
:sorted_by,
:search_query,
:with_country_id,
:with_created_at_gte
]
)
You probably found this already, it was on the first page of the documentation, but there's more important stuff in the example application that you need (I ran into this too, when I was just recently using Filterrific for the first time.)
The information on the start page is not enough to really get you started at all.
You have to read a bit further to see the other ways you may need to change your models, model accesses, and views in order to support Filterrific.
The part that makes the default filter setting effective is this default_filter_params hash (NOT select_options, which provides the options for "select" aka dropdown boxes. That's not what you want at all, unless you're doing a dropdown filter.) This hash holds a list of the scopes that need to be applied by default (the hash keys) and the scope parameter is used as the hash value.
That default_filter_params hash may not be the only thing you are missing... You also must define those ActiveRecord scopes for each filter that you want to use in the model, and name these in available_filters as above to make them available to filterrific:
scope :with_created_at_gte, lambda { |ref_date|
where('created_at >= ?', ref_date)
end
It's important that these scopes all take an argument (the value comes from the value of the filter field on the view page, you must add these to your view even if you want to keep them hidden from the user). It's also important that they always return ActiveRecord associations.
This is more like what you want:
scope :location_near, lambda { |location_string|
l = Location.near(location_string).pluck(:id)
where(location_id: l)
end
The problem with this approach is that in your case, there is no location_string or any single location variable, you have multiple coordinates for your location parameters. But you are not the first person to have this problem at all!
This issue describes almost exactly the problem you set out to solve. The author of Filterrific recommended embedding the location fields into hidden form fields in a nested fields_for, so that the form can still pass a single argument into the scope (as in with_distance_fields):
<%= f.fields_for :with_distance do |with_distance_fields| %>
<%= with_distance_fields.hidden_field :lat, value: current_user.lat %>
<%= with_distance_fields.hidden_field :lng, value: current_user.lng %>
<%= with_distance_fields.select :distance_in_meters,
#filterrific.select_options[:with_distance] %>
<% end %>
... make that change in your view, and add a matching scope that looks something like (copied from the linked GitHub issue):
scope :with_distance, -> (with_distance_attrs) {
['lng' => '-123', 'lat' => '49', 'distance_in_meters' => '2000']
where(%{
ST_DWithin(
ST_GeographyFromText(
'SRID=4326;POINT(' || courses.lng || ' ' || courses.lat || ')'
),
ST_GeographyFromText('SRID=4326;POINT(%f %f)'),
%d
)
} % [with_distance_attrs['lng'], with_distance_attrs['lat'], with_distance_attrs['distance_in_meters']])
}
So, your :with_distance scope should go onto the VendorLocation model and it should probably look like this:
scope :with_distance, -> (with_distance_attrs) {
lat = with_distance_attrs['lat']
lng = with_distance_attrs['lng']
dist = with_distance_attrs['distance']
location_ids = Location.near([lat, lng], dist, order: '').pluck(:id)
where(location_id: location_ids)
end
Last but not least, you probably noticed that I removed your call to includes(:location) — I know you put it there on purpose, and I didn't find it very clear in the documentation, but you can still get eager loading and have ActiveRecord optimize into a single query before passing off the filter work to Filterrific by defining your controller's index method in this way:
def index
#appointments = Appointment.includes(:vendor).
filterrific_find(#filterrific).page(params[:page])
end
Hope this helps!

Fetch ActiveRecord query result as an array of hashes with chosen attributes

The model User has first, last and login as attributes. It also has a method called name that joins first and last.
What I want is to iterate through the Users records and create an array of hashes with the attributes I want. Like so:
results = []
User.all.map do |user|
record = {}
record["login"] = user.login
record["name"] = user.name
results << record
end
Is there a cleaner way in Ruby to do this?
Trying to map over User.all is going to cause performance issues (later, if not now). To avoid instantiating all User objects, you can use pluck to get the data directly out of the DB and then map it.
results = User.all.pluck(:login, :first, :last).map do |login, first, last|
{ 'login' => login, 'name' => first << last }
end
Instantiating all the users is going to be problematic. Even the as_json relation method is going to do that. It may even be a problem using this method, depending on how many users there are.
Also, this assumes that User#name really just does first + last. If it's different, you can change the logic in the block.
You can use ActiveRecord::QueryMethods#select and ActiveRecord::Relation#as_json:
User.select(:login, '(first || last) as name').as_json(except: :id)
I would write:
results = User.all.map { |u| { login: u.login, name: u.name } }
The poorly named and poorly documented method ActiveRecord::Result#to_hash does what you want, I think.
User.select(:login, :name).to_hash
Poorly named because it does in fact return an array of Hash, which seems pretty poor form for a method named to_hash.

Ruby - less expensive way for chained ActiveRecord queries

What I have is currently working, but seems to be very expensive, any ideas on making it less expensive would be great!
A User has many Plans, which has many PlanDates. Each PlanDates has a certain recipe denoted by a recipe_id attribute. Each Plan has a meal_type attribute which is either Meat, Vegetarian, or Choice, the latter means mixed. Each Recipe has a type_of_meal attribute that is either Meat or Vegetarian. Each Recipe also has a friendly name attribute.
For a given PlanDate, I need to build an options_for_select in the following format:
[ [recipe_id, "recipe_name"], [recipe_id, "recipe_name"] ... ]
The options:
must remove all the recipe_ids that have previously been given to the User (regardless of Plan)
must remove all the recipe_ids with a type mismatch (i.e., if a Plan has Meat designated, the options must not have any Vegetarian recipe_ids), certainly this is not true if the Plan has Choice designated
Here's the code I currently have:
# builds an array of all the recipe_ids that have been given to this User on some PlanDate on some Plan
recipes_used_before_for_this_user = PlanDate.select { |pd| pd.plan.user.id == user_id }.map { |pd| pd.recipe_id }
# narrows down the world of recipes to those that do NOT have an id of a recipe_used_before_for_this_user
recipes_not_used_before = Recipe.select { |r| (recipes_used_before_for_this_user.include? r.id) == false }
# going forward, let's assume current_pd = the PlanDate object in question
if current_pd.plan.meal_type == "Choice"
# easiest: if the meal_type is "Choice" then we just take the recipes_not_used_before and map them into the appropriate format
recipe_choices_array = recipes_not_used_before.map { |r| [ r.id, r.name ] }
else
# if the plan has a "Meat" or "Vegetarian" specification, we need to first narrow the recipes_not_used_before down by the right type and then map into the appropriate format
recipe_choices_array = recipes_not_used_before.select { |r| r.type_of_meal == potential_pd.first.plan.meal_type }.map { |r| [ r.id, r.name ] }
end
Again, working, but I have a lot of PlanDates and a lot of Recipes, so if there is any way to streamline even further, would love your ideas. Thanks!
The reason you're experiencing expensive queries is because you're not actually using ActiveRecord's query interface, or even SQL to narrow your query, but instead are loading the entire dataset into Ruby memory objects and then looping over the result in Ruby.
I suspect that if you inspect your logfiles you'll see something like this:
>> PlanDate.select{ |pd| pd.plan.user.id == user_id }.map { |pd| pd.recipe_id }
PlanDate Load (1.3ms) SELECT "plan_dates".* FROM "plan_dates"
=> [#<PlanDate....
What you want to do is to use ActiveRecord's query interface to build the query, something like this:
PlanDate.includes(plan: [:user]).where("plan.user_id == ?", :user_id).pluck('recipe_id')
What that does is first: Specify relationships to be included in the result set, then specify the where conditions of your SQL query, and finally pull out the recipe ids using pluck.
See http://guides.rubyonrails.org/active_record_querying.html for more info.

rails "where" statement: How do i ignore blank params

I am pretty new to Rails and I have a feeling I'm approaching this from the wrong angle but here it goes... I have a list page that displays vehicles and i am trying to add filter functionality where the user can filter the results by vehicle_size, manufacturer and/or payment_options.
Using three select form fields the user can set the values of :vehicle_size, :manufacturer and/or :payment_options parameters and submit these values to the controller where i'm using a
#vehicles = Vehicle.order("vehicles.id ASC").where(:visible => true, :vehicle_size => params[:vehicle_size] )
kind of query. this works fine for individual params (the above returns results for the correct vehicle size) but I want to be able to pass in all 3 params without getting no results if one of the parameters is left blank..
Is there a way of doing this without going through the process of writing if statements that define different where statements depending on what params are set? This could become very tedious if I add more filter options.. perhaps some sort of inline if has_key solution to the effect of:
#vehicles = Vehicle.order("vehicles.id ASC").where(:visible => true, if(params.has_key?(:vehicle_size):vehicle_size => params[:vehicle_size], end if(params.has_key?(:manufacturer):manufacturer => params[:manufacturer] end )
You can do:
#vehicles = Vehicle.order('vehicles.id ASC')
if params[:vehicle_size].present?
#vehicles = #vehicles.where(vehicle_size: params[:vehicle_size])
end
Or, you can create scope in your model:
scope :vehicle_size, ->(vehicle_size) { where(vehicle_size: vehicle_size) if vehicle_size.present? }
Or, according to this answer, you can create class method:
def self.vehicle_size(vehicle_size)
if vehicle_size.present?
where(vehicle_size: vehicle_size)
else
scoped # `all` if you use Rails 4
end
end
You call both scope and class method in your controller with, for example:
#vehicles = Vehicle.order('vehicles.id ASC').vehicle_size(params[:vehicle_size])
You can do same thing with remaining parameters respectively.
The has_scope gem applies scope methods to your search queries, and by default it ignores when parameters are empty, it might be worth checking

How to column sort using will_paginate where the data is a record array, not a model

I have a service I query and I get data I filter through and create a an array of records.
Unless I missed something, ActiveResource::Base does not qualify since the access to the service is not via rest and I can't use the raw data as delivered.
I am displaying the data in a table and use will_paginate to page the data. But I am not currently married to will_paginate.
I do need to sort the columns as well as paginate.
I have found two version of ujs_sort_helper.
https://github.com/pengwynn/ujs_sort_helper
https://github.com/sikachu/ujs_sort_helper
I am trying to understand:
- http://javathehutt.blogspot.com/2009/06/mo-simple-sortable-tables-in-rails.html
What have other done in rails 3? Or is one of the ujs_sort_helper packages just he correct way to go.
In term of data refresh, this is a dashbaord. Multiple data source will address the various DIVs.
Also, I am a Rails noob. But not a programming noob.
You could use meta_search's sort_link if you wish.
I like it because it also does filtering incredibly easy with meta_where.
You can also make the behavior through ajax by adding the data-remote attribute to 'a.sort_link' (i have done that through javascript).
I would welcome the maintainer of ujs_sort_helper to comment. Just a bug here and there in the rails 3 version of the code. Now ujs_sort_helper works, for me.
What I have not done is create ANOTHER branch on this package. I emailed the file to the author.
sort order now compares symbols, instead of symbol to string.
def sort_order(column, initial_order='asc')
#safe since to_sm on a sym is a nil operation. At least for now.
if session[#sort_name][:key].to_sym == column.to_sym
session[#sort_name][:order].downcase == 'asc' ? 'desc' : 'asc'
else
initial_order
end
end
The icon us set via the current order value. The sort clause should be the opposite. So show down arrow for the list being displayed in ascending order, but the 'url' is set to redisplay the table in descending order.
I have no clue what the :q symbol is supposed to be used for.
def sort_header_tag(column, options = {})
options[:initial_order].nil? ? initial_order = "asc" : initial_order = options[:initial_order]
key = session[#sort_name][:key].to_sym
order = sort_order(column, initial_order)
caption = options.delete(:caption) || column.to_s.titleize
url = { :sort_key => column, :sort_order => order, :filter => params[:filter]}
url.merge!({:q => params[:q]}) unless params[:q].nil?
content_tag('th', link_to(caption, url, :class=>session[#sort_name][:order] ), :class => "sort_link #{order if key == column}")
end

Resources