So, I'm pretty much a newbie at creating ruby methods. I've got the following method from a gem (meta_search), but I need to change the default behavior. Here's the method:
def sort_link(builder, attribute, *args)
raise ArgumentError, "Need a MetaSearch::Builder search object as first param!" unless builder.is_a?(MetaSearch::Builder)
attr_name = attribute.to_s
name = (args.size > 0 && !args.first.is_a?(Hash)) ? args.shift.to_s : builder.base.human_attribute_name(attr_name)
prev_attr, prev_order = builder.search_attributes['meta_sort'].to_s.split('.')
current_order = prev_attr == attr_name ? prev_order : nil
new_order = current_order == 'asc' ? 'desc' : 'asc'
options = args.first.is_a?(Hash) ? args.shift : {}
html_options = args.first.is_a?(Hash) ? args.shift : {}
css = ['sort_link', current_order].compact.join(' ')
html_options[:class] = [css, html_options[:class]].compact.join(' ')
options.merge!(
builder.search_key => builder.search_attributes.merge(
'meta_sort' => [attr_name, new_order].join('.')
)
)
link_to [ERB::Util.h(name), order_indicator_for(current_order)].compact.join(' ').html_safe,
url_for(options),
html_options
end
This method returns a sort link for a search. The output looks like this:
<a class="sort_link asc" href="/photos?search[meta_sort]=average_rating.desc">Best Photography ▲</a>
Here's the problem: this method assumes that you want to sort in ascending order on the first click, and descending order on the second. I want the opposite behavior. I see that I could change this...
new_order = current_order == 'asc' ? 'desc' : 'asc'
to this...
new_order = current_order == 'desc' ? 'asc' : 'desc'
...but that just reverses the situation. What I really need is to be able to specify an option and reverse the behavior if that option is passed.
So here's my problem: I don't really understand how the *args are passed. From what I can tell these lines are taking the option and html_option hashes for rails link_to method...
options = args.first.is_a?(Hash) ? args.shift : {}
html_options = args.first.is_a?(Hash) ? args.shift : {}
What I'd like to do is add a custom option to the options hash, and if that option is defined reverse the sort equation. I tried to do this...
if defined? options[:sort_preference] && options[:sort_preference]==:desc
new_order = current_order == 'desc' ? 'asc' : 'desc'
options.delete(:sort_preference)
else
new_order = current_order == 'asc' ? 'desc' : 'asc'
options.delete(:sort_preference)
end
... but that didn't work, it just kept the standard behavior and passed :sort_preference into the URL like so:
<a class="sort_link asc" href="/photos?search[meta_sort]=average_rating.desc&sort_preference=desc">Best Photography ▲</a>
In the link above, sort_preference shouldn't appear in the URL, and search[meta_sort] should be average_rating.asc -- since the link shows the opposite of what rendered.
So, obviously, my attempt didn't work because I don't really understand what's going on in this method. What I'm hoping for in answer to this question is:
A little help understanding how *args works
An explanation of why my attempt to add an option to this method failed
An example of how to fix it
Thank you very much for taking a look!
*args just means that you can pass unlimited number of arguments. So this method needs 2 arguments first (builder & attribute) and then you could pass any number of args after that. Here is a description of what is going on in the method relating to the args:
If the third argument passed to this method is not a hash, then it sets that arg as the name and removes it (args.shift), otherwise use the builder name.
Then it looks at the next arg (which would be the 4th). If its a hash, then it sets options to that arg and deletes that arg, otherwise it sets it to empty hash.
Then it looks at the next arg (5th arg). If its a hash, then it sets that as html_options and deletes that arg, otherwise html is set to {}.
Using args* can make calling the method a little cleaner (although the implementation in the method might not be). If you did not have a "name" to pass, instead of doing this:
sort_link(some_builder,some_attribute,nil,options)
you can do this:
sort_link(some_builder,some_attribute,options)
It does not force you to add a arg for name, if you don't have one.
So for your code to work, this method would need to take options as the 4th arg, and the options hash would need to contain :sort_preferences. What does the code look like that is calling sort_link? You need to insert this option into that hash before it gets to this method.
this method assumes that you want to sort in ascending order on the first click, and descending order on the second. I want the opposite behavior.
The meta_search gem supports this. It is not necessary to modify the gem's code. In your controller, after you have queried for your data, set the meta_sort attribute on the #search object to the field and sort order you would like. When the view loads, your data will be sorted as expected.
For example, in a Posts controller, you would do the following to set the default sort to the title field, descending:
def index
#search = Post.search(params[:search])
#search.meta_sort ||= 'title.desc'
#posts= #search.all
end
Related
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!
Is there a way to make this situation more compact in rails views?
Eg I have haml
= object.count unless object.count ==0
I sort of don't like that has I'm repeating the function there, I would much rather have something like
= object.count unless ==0
Eg if I had more complex statements
= object.relations.where(attribute: "something").count unless zero?
I could split that into two lines say
- cnt = object.relations.where(attribute: "something").count
= cnt unless cnt==0
But for each situation I would have multiple lines, and storing a variable to use once sucks.
EDIT: just to elaborate I want to check if the number is 0, and if so not display anything. It looks nicer in the view that way.
UPDATE:
One of the answers made come up with a solution along these lines
class Object
def unless
self unless yield(self)
end
end
So I can call whatever object I have with a block eg. .unless{|c| c<1}
This lets me tack the conditionals on, and keeps it pretty clear what is going on :), bonus is as it's block driven I can use this on any object :P.
Thanks everyone :)
UPDATE EVEN MORE
Having |c| in the block sucked. So I looked up the api and changed it too
class Object
def unless(&block)
self unless instance_eval(&block)
end
end
So now I can use .count.unless{zero?} to accomplish this :P. Or if I have a complicated condition I can add that in with |c| etc.
If object is an array you can use object.empty? (or object.any? for the reverse case)
Just create a view helper:
def display_count_or_nothing(array)
array.count unless array.count == 0
end
In the view you can use it like this:
<%= display_count_or_nothing(array) %>
i think the following is nice and clear, although i hate the variable "object",
it would be much nicer if the name of the variable described the contents of the array (as plural)
= object.count unless object.empty?
If this is only about count, you can monkey patch Enumerable:
module Enumerable
def count_or_empty_string
self.any? ? self.count : ''
end
end
If object is an enumerable, you can do this:
= object.count_or_empty_string
This will return an "" if object.count == 0 else it will return an integer. So there is no need for unless or if in your HAML anymore.
I am new to rails. Here is the following code with Foo as model object:
a = Foo
a = Foo.where(age: 18)
if params[:sort] == "desc"
a = a.order("name desc")
end
Here two queries are performed, I want to combine them to one or you can say i want to perform Foo.where(age=18).order("name asc")
Remember there can be the case when order is no needed i.e. params[:sort] is not equal to desc.
Please don't give solution like
if params[:sort] == "desc"
a = a.where(age=18).order("name desc")
else
a = a.where(age=18)
end
as it makes code redundant and also for more parameters it might not work.
No, you're mistaken. Actually, no queries are performed here.
a = Foo
a = Foo.where(age=18)
if params[:sort] == "desc"
a = a.order("name desc")
end
The actual query is sent where you start retrieving data. That is, do something like
a.each do |b|
# do something with b
end
Until then you can safely chain criteria building methods (where, order, select and others).
Actually your code will only execute one query. this is because in rails the calls to the database are only done once you access the result. So when you will write a.first (or something similar) it will make the DB call.
If its that what you mean... A simple solution would be:
a.where(age: 18).order("name #{params[:sort] || 'asc'}")
So if params[:sort] is nil it will default to asc.
I get a bool value as a parameter and assign it to a variable
#package = params[:package]
Now, I have a method which should return a string based on the value of package. The method looks like this:
def get_description(package)
if package == 1
"foo"
elsif package == 2
"bar"
end
end
In another method, I try to get the #description by:
#description = get_description(#package)
The problem is, the string is not displayed. The #description variable stays empty. Maybe I'm too stupid to see the bug?
If you want you can use your code changing the value of package to integer:
def get_description(package)
package = package.to_i
if package == 1
"foo"
elsif package == 2
"bar"
end
end
Or using a case statement:
def get_description(package)
case package.to_i
when 1
"foo"
when 2
"bar"
end
end
All parameters arrive in your controller as strings, regardless of what type you think they should be. This is even true if the parameter was originally written to the browser by a View using an Integer. This is just because of the way browsers send the data back to the server.
You'll need to convert the parameter to an integer, or compare it with the strings "1" and "2".
Given a query like:
current_user.conversations.where("params[:projectid] = ?", projectid).limit(10).find(:all)
params[:projectid] is being sent from jQuery ajax. Sometimes that is an integer and the above works fine. But if the use selects "All Projects, that's a value of '' which rails turns into 0. which yields an invalid query
How with rails do you say search params[:projectid] = ? if defined?
Thanks
I think you may have mistyped the query a bit. "params[:projectid] = ?" shouldn't be a valid query condition under any circumstances.
In any case, you could do some sort of conditional statement:
if params[:project_id].blank?
#conversations = current_user.conversations.limit(10)
else
#conversations = current_user.conversations.where("project_id = ?", params[:project_id]).limit(10)
end
Although, I'd probably prefer something like this:
#conversations = current_user.conversations.limit(10)
#converstaions.where("project_id = ?", params[:project_id]) unless params[:project_id].blank?
Sidenotes:
You don't have to use .find(:all). Rails will automatically execute the query when the resultset is required (such as when you do #conversations.each).
Wherever possible, try to adhere to Rails' snakecasing naming scheme (eg. project_id as opposed to projectid). You'll save yourself and collaborators a lot of headaches in the long run.
Thanks but if the where query has lets say 3 params, project_id, project_status, ... for example, then the unless idea won't work. I'm shocked that Rails doesn't have a better way to handle conditional query params
EDIT: If you have multiple params that could be a part of the query, consider the fact that where takes a hash as its argument. With that, you can easily build a parameter hash dynamically, and pass it to where. Something like this, maybe:
conditions = [:project_id, :project_status, :something_else].inject({}) do |hsh, field|
hsh[field] = params[field] unless params[field].blank?
hsh
end
#conversations = current_user.conversations.where(conditions).limit(10)
In the above case, you'd loop over all fields in the array, and add each one of them to the resulting hash unless it's blank. Then, you pass the hash to the where function, and everything's fine and dandy.
I didn't understand why you put:
where("params[:projectid] = ?", projectid)
if you receive params[:project] from the ajax request, the query string shouldn't be:
where("projectid = ?", params[:projectid])
intead?
And if you are receiving an empty string ('') as the parameter you can always test for:
unless params[:projectid].blank?
I don't think i undestood your question, but i hope this helps.