Rails - Order index by complex sort - ruby-on-rails

How do I order my index results by featured_end_date >= Time.now() in :asc order and then have the rest of the results sort by by publish_at: :desc.
Currently I have BlogPost.order(featured_end_date: :asc, publish_at: :desc). I am missing that >= Time.now() comparison.
I assume scopes might need to be used, but I am not sure how to achieve this.

BlogPost model
scope :featuredfuture, -> { where("featured_end_date >= ?", Time.now()).order(featured_end_date: :asc) }
scope :other, -> { where("featured_end_date < ? or featured_end_date is null", Time.now()).order(publish_at: :desc) }
Controller
#blogposts = BlogPost.featuredfuture + BlogPost.other

You need to specify your collection before ordering. You have to use where.
BlogPost.where('featured_end_date >= ?', Time.now).order(featured_end_date: :asc, publish_at: :desc)

Related

Re-write ActiveRecord query in Arel

I just got started with ARel. I'm finding it difficult converting this bit of complex AR query into Arel:
Offer.where(
"offers.ended_at IS NULL OR offers.started_at < ? AND offers.ended_at >= ?",
Time.zone.now, Time.zone.now
)
I think having this in Arel will aid readability
I think using chained scopes would make it more readable too:
# in app/models/offer.rb
scope :without_end, -> { where(ended: nil) }
scope :still_valid, -> { where('started_at < :now AND offers.ended_at >= :now', now: Time.current) }
And to be used like this:
Offer.still_valid.or(Offer.without_end)
This should work:
offers = Offer.arel_table
offers_with_nil_ended_at = offers[:ended_at].eq(nil)
offers_within_range = offers[:started_at].lt(Time.zone.now).and(
offers[:ended_at].gteq(Time.zone.now)
)
Offer.where(offers_with_nil_ended_at.or(offers_within_range))

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

Order a model with conditions in Rails (3)

Lets say there is a model called Event. I want to display important and current events first, so i have the attributes important (boolean) and enddate (date).
Now I want to get all events where important == true and where enddate >= today first, all others should be ordered by created_at.
I want to avoid doing events = important_events + not_important_events as this would return an array insted of an activerecord. Does anyone knows an elegant way to order a model in rails?
Try (for ActiveRecord 5+):
#events = Event.where(:important => true, 'enddate >= ?', Date.today).or(Event.where.not(:important => true, 'enddate >= ?', Date.today).order(:created_at => :desc))
Try this, It will return newest record first and fulfil your conditions.
important == true
enddate >= today
#events1 = Event.where(:important => true).where('enddate >= ?', Date.today).order(:created_at => :desc)
#events2 = Event.where(:important => false).where('enddate <= ?', Date.today).order(:created_at => :desc)
#events = #events1.or(#events2)
"OR" works only in ActiveRecod(5+).

Wrapping 'next' and 'previous' functions

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.

Resources