Wrapping 'next' and 'previous' functions - ruby-on-rails

In my Rails 4 app, I defined functions in my model than get the (nth) next or previous row in de database, wrapping around the entire database, so that Item.last.next will refer to Item.first:
def next(n=0)
following = Item.where("id > ?", self.id).order("id asc") + Item.where("id < ?", self.id).order("id asc")
following[n % following.length]
end
def prev(n=0)
n = n % Item.count-1
previous = Item.where("id < ?", self.id).order("id desc") + Item.where("id > ?", self.id).order("id desc")
previous[n % previous.length]
end
This results in three database queries per method call, and I've learned to keep database queries to a minimum, so I wonder if there is a way do get this result with only one query.

What you are looking for seems a bit high level. So let's prepare the basic API at first.
def next(n=1)
self.class.where('id > ?', id).limit(n).order('id ASC')
end
def previous(n=1)
self.class.where('id > ?', id).limit(n).order('id DESC')
end
Then higher level methods
def next_recycle(n=1)
klass = self.class
return klass.first if (n = 1 && self == klass.last)
next(n)
end
def previous_recycle(n=1)
klass = self.class
return klass.last if (n == 1 && self == klass.first)
previous(n)
end
You can pick methods according to needs.

Related

Better solution for "one, other or both" cases

I was checking some code, and something similar to the following showed up:
def between_dates(date_1, date_2)
if date_1 && date_2
conditions "created_at >= date_1 AND created_at <= date_2"
elseif date_1
conditions "created_at >= date_1"
elseif date_2
conditions "created_at <= date_2"
end
end
It looked the kind of code that could be improved, but I couldn't find a more elegant solution for such a trivial and common conditional statement.
I'm looking for a better answer for this problem when we must return a value for one, other or both.
Rails lets you build a query dynamically. Here's an example using scopes and a class method. Since scopes always return an ActiveRecord::Relation object (even if the block returns nil), they are chainable:
class Event < ApplicationRecord
scope :created_before, -> (date) { where('created_at <= ?', date) if date }
scope :created_after, -> (date) { where('created_at >= ?', date) if date }
def self.created_between(date_1, date_2)
created_after(date_1).created_before(date_2)
end
end
Example usage:
Event.created_between(nil, Date.today)
# SELECT `events`.* FROM `events` WHERE (created_at <= '2018-05-15')
Event.created_between(Date.yesterday, nil)
# SELECT `events`.* FROM `events` WHERE (created_at >= '2018-05-14')
Event.created_between(Date.yesterday, Date.today)
# SELECT `events`.* FROM `events` WHERE (created_at >= '2018-05-14') AND (created_at <= '2018-05-15')
I'd use something like this:
def between_dates(date_1, date_2)
parts = []
if date_1
parts << "created_at >= date_1"
end
if date_2
parts << "created_at <= date_2"
end
full = parts.join(' AND ')
conditions(full)
end
This can be further prettified in many ways, but you get the idea.
def between_dates(date_1, date_2)
date_conditions = []
date_conditions << 'created_at >= date_1' if date_1
date_conditions << 'created_at <= date_2' if date_2
conditions date_conditions.join(' AND ') unless date_conditions.empty?
end
I am not sure if this is more elegant, but I always do reduce everything to avoid typos:
[[date_1, '>='], [date_2, '<=']].
select(&:first).
map { |date, sign| "created_at #{sign} #{date}" }.
join(' AND ')

less than or greater than query in RoR

Is there a way to generate a less than or greater than query in rails like the between range query. I have multiple query params and hence I do not want to use string literal for comparison.
if params["end_time"]
if params["start_time"]
params["end_time"] = params["end_time"].to_datetime
query[:created_at] = ((params["start_time"])..params["end_time"])
else
query[:created_at] = #Need help with this
end
end
def with_duration
if(params['start_time'] && params['end_time'])
{created_at: params['start_time']..params['end_time']}
elsif(params['start_time'])
return ["created_at > ?", "#{params['start_time']}"]
elsif(params['end_time'])
return ["created_at < ?", "#{params['end_time']}"]
else
return {}
end
end
ModelName.where(with_duration)
As I understood you need to do nothing if params empty. So you can try this implementation.
def query_method(scope)
return scope unless params["end_time"] && params["start_time"]
scope.where.not(created_at: (params["start_time"]..params["end_time"]))
end

Greater/Lower than works but equals doesn't on >= and <= on .where()

I have a Rails 4 app and I'm trying to make a simple search for my invoices with 3 optional arguments: Name of the client, Start Date, End Date.
The search works fine mostly, if I put a start date and an end date it works for < and >, but eventhough i used >= and <=, if the invoice date is the same to either start or end, it just won't show on the result list.
The tables used look like this:
Client Table
ID
Name
The rest of the fields aren't necessary
Invoice Table
ID
Client_ID
Total_Price
Created_At *only here for relevance*
My Invoice Controller Search method looks like this:
def search
if request.post?
#count = 0
#invoices = Invoice.all
if params[:start_date].present?
#invoices = Invoice.invoices_by_date(#invoices, params[:start_date], 'start')
if #invoices.present?
#count = 1
else
#count = 2
end
end
if params[:end_date].present?
#invoices = Invoice.invoices_by_date(#invoices, params[:end_date], 'end')
if #invoices.present?
#count = 1
else
#count = 2
end
end
if params[:name].present?
#invoices = Invoice.invoices_by_client(#invoices, params[:name])
if #invoices.present?
#count = 1
else
#count = 2
end
end
if #count == 2
flash.now[:danger] = "No results found."
#invoices = nil
end
#name = params[:name]
#start_date = params[:start_date]
#end_date = params[:end_date]
end
end
And the Invoice Model methods i use look like this:
def self.invoices_by_client(invoices, name)
invoices= invoices.includes(:client)
.select('invoices.created_at', 'invoices.total_price', 'clients.name')
.where("clients.name LIKE ?", "%#{name}%")
.references(:client)
return invoices
end
def self.invoices_by_date(invoices, date, modifier)
if modifier == 'start'
invoices = invoices.includes(:client)
.select('invoices.created_at', 'invoices.total_price', 'clients.name')
.where("invoices.created_at >= ?", date)
.references(:client)
elsif modifier == 'end'
invoices = invoices.includes(:client)
.select('invoices.created_at', 'invoices.total_price', 'clients.name')
.where("invoices.created_at <= ? ", date)
.references(:client)
end
return invoices
end
It probably isn't the best solution overall and I don't know if i did anything wrong so it would be great if you guys could help me with this.
I followed Alejandro's advice and messed around with the time aswell as the date, something like this:
if modifier == 'start'
invoices = invoices.includes(:client)
.select('invoices.created_at', 'invoices.total_price', 'clients.name')
.where("invoices.created_at >= ?", "#{date} 00:00:00") // Added the start time
.references(:client)
elsif modifier == 'end'
invoices = invoices.includes(:client)
.select('invoices.created_at', 'invoices.total_price', 'clients.name')
.where("invoices.created_at <= ? ", "#{date} 23:59:59") // Added end time aswell
.references(:client)
end
I forced the time for the start date as 00:00:00 and the time for the end date as 23:59:59 and it worked as desired. Thank you for the help man and i hope this helps other people!

Undefined method on class method

I am have an app where users post. I have a filter that will order and retrieve different posts based on a value passed through the params[] hash. I keep getting a no method error on a class method that clearly exists. Here is the query I am trying to run (posts_controller.rb):
def room
select_filter = params[:post_filter]
course_id = params[:id].to_i
#posts = Post.where_filter(select_filter, course_id).order_filter(select_filter).page(params[:page])
end
I am getting this error whenever 'select_filter' has a value of 3 :
undefined method `order_filter' for #<Array:0x007fa012d6eef8>
Here is my model (post.rb, remember whenever 'select_filter' is 3 I get an error):
def self.where_filter(select_filter, course_id_params)
case select_filter.to_i
when 1
where('course_id = ?', course_id_params)
when 2
where('course_id = ?', course_id_params)
when 3
where('course_id = ? AND created_at > ?', course_id_params.to_i, 48.hours.ago.utc.to_s(:db))
.reject! {|i| i.net_reputation <= 0 }
else
where('course_id = ?', course_id_params)
end
end
def self.order_filter(select_filter)
case select_filter.to_i
when 1
order('created_at DESC')
when 2
sort_by {|i| i.net_reputation}
when 3
sort_by {|i| i.net_reputation}
else
order('created_at DESC')
end
end
The strange thing is if I run this in the console, everything is fine. Like this:
Post.where('course_id = ? AND created_at > ?', 10, 48.hours.ago.utc.to_s(:db)).reject {|i| i.net_reputation <= 0 }.sort_by {|i| i.net_reputation}
Any and all input is appreciated.
It's because your "where_filter" method is changing it from an active record relation object to an array when it gets to:
.reject! {|i| i.net_reputation <= 0 }
Once this happens, you can no longer chain other query methods onto it. Why not just make the "i.net_reputation <= 0" part of the where query?
For some reason, when I collapsed the queries & sorts into one Class method, it worked. However, since Kiminari (which I am using for pagination) cannot pagniate arrays by default, I have to use the Kaminari.pageinate_array() method.
My posts_controller.rb:
def room
#course = Course.find(params[:id])
select_filter = params[:post_filter]
course_id = params[:id].to_i
query_posts = Post.select_input_filter(select_filter, course_id)
#posts = Kaminari.paginate_array(query_posts).page(params[:page])
end
My post.rb:
def self.select_input_filter(select_filter, course_id_params)
case select_filter.to_i
when 1
where('course_id = ?', course_id_params)
.order('created_at DESC')
when 2
where('course_id = ?', course_id_params)
.sort_by {|i| i.net_reputation}
.reverse
when 3
where('course_id = ? AND created_at > ?', course_id_params.to_i, 48.hours.ago.utc.to_s(:db))
.reject! {|i| i.net_reputation <= 0 }
.sort_by {|i| i.net_reputation}
.reverse
else
where('course_id = ?', course_id_params)
.order('created_at DESC')
end
end

Rails - Fetch results on the basis of number of params in query string

I am working on an events application where i want to filter events depending on the 3 parameters location or starts_at or ends_at in the query string. There can be any one, two or all the parameters in the query string. In i use if-else statement i need to make 6 cases which will make my code clumsy. Rather i am thinking to implement something this way:
class EventsController < ApplicationController
def index
unless params.empty?
unless params[:location].nil?
#events = Event.where("location = ?", params[:location])
end
unless params[:starts_at].nil?
unless #events.empty?
#events = #events.where("start_date = ?", params[:start_date])
else
#events = Event.where("Date(starts_at) = Date(?)", params[:starts_at])
end
end
unless params[:ends_at].nil?
unless #events.empty?
#events = #events.where("end_date = ?", params[:end_date])
else
#events = Event.where("Date(ends_at) = Date(?)", params[:ends_at])
end
end
end
end
end
But this code doesnt work since where query doen not work on an array. Can someone suggest me some solution for this..
You should be able to pass your params hash directly to where, and it will form the correct SQL based on the keys and values of that hash:
Event.where(params)
An example in the console:
1.9.3p194 :001 > puts Example.where(:location => 'here', :started_at => '2012-08-13').to_sql
SELECT "examples".* FROM "examples" WHERE "examples"."location" = 'here' AND "examples"."started_at" = '2012-08-13'
Try Following
def index
unless params.empty?
where_array, arr = [], []
if params[:location]
where_array << "location = ?"
arr << params[:location]
end
if params[:starts_at]
where_array << "start_date = ?"
arr << params[:starts_at]
end
if params[:ends_at]
where_array << "end_date = ?"
arr << params[:ends_at]
end
#events = arr.blank? ? [] : Event.where([where_array.join(" AND "), *arr])
end
end

Resources