Rails Model Next/Prev Navigation Scope with datetime field - ruby-on-rails

I have a model lets say Post which has a column called published_at (datetime). My model has two instance methods called next and prev.
I'm using mysql as a database adapter.
def next
self.class.unscope(:order).online
.where('published_at > ?', published_at)
.order('published_at ASC').first
end
def prev
self.class.unscope(:order).online
.where('published_at < ?', published_at)
.order('published_at DESC').first
end
With the code above the navigation works if every post has another date. If some posts are on the same date just one record of those gets shown.
Goal: Get the next or previous record ordered by published_at datetime column.
Any ideas?

Neither "next" , nor "previous" make sense in SQL. You have to explicitly define sort order by uniq key.
If you have non-uniq column as sort base, you have to add uniq one, id for example.
Then you have to change WHERE clause to accept rows wich have same value for your non-uniq column:
def next
condition = <<~SQL
(published_at = :published_at AND id > :id) OR
(published_at > :published_at)
SQL
self.class.unscope(:order).
online.
where(condition, published_at: published_at, id: id).
order(published_at: :asc, id: :asc).
take
end
prev is constructed similarly.

Related

how can I paginate records not by N value quantity, but by created_at.day?

I have a table Post.
Columns: created_at, title, body
Default pagination behaviour is to show n items per page and than add link_to prev/next.
How can I paginate not by n items, but by created_on a date?
I've figured out the gem https://github.com/ankane/groupdate, but it only helps to group, not to do the pagination.
In this case, it sound like you don't want pagination, you want query filtering based on a bucketed value. You will first need a distinct list of post days. You may need to use some database-specific query features to get that from timestamps. For example, with Postgres, you can use the date() function to extract just the date portion from your created_at timestamp, and we can get a distinct list of them:
dates = Post.pluck("distinct date(created_at)").sort
Note that this will induce a full table scan, so it'll be slow for a large number of posts.
How you use that list of dates is up to you. You might choose to render a list of them as links for the user to click, for example. You might then have your index method accept a date param, then use that to find posts on that date:
def index
scope = Post.all
if params[:date].present?
date = DateTime.parse(params[:date])
scope = scope.
where("created_at >= ? AND created_at <= ?",
date.beginning_of_day, date.end_of_day).
order("created_at ASC")
end
# For the given date, paginate the posts on that date in pages of 25
#posts = scope.page(1).per(25)
end

Order Model by the date difference of its has_many model

I don't know this is the exact and simple way I am implementing. I have a two model. Modal 'A' belongs to Modal 'B' and 'B' has many to Modal 'A'. I have a date field 'expiry date' in Model B.
Now I want to list out records the Modal 'A'. The list should be in ascending order with the difference of its associated Modal 'B' last record expiry date and today date. I calculated the date difference in view, but still confused to order it.
diff = (f.bill_histories.last.ndate.to_date - DateTime.now.to_date).to_i
I tried and google it, but still didn't find suggestion. Please suggest me.
assuming:
class Foo < ActiveRecord::Base
belongs_to :bar
end
class Bar < ActiveRecord::Base
has_many :foos
end
You should be able to sort on the database level using a join statement on the Bars table:
Foo.joins(:bar).order(bars: { expired_at: :desc })
Explanation:
ascending order with the difference of its associated Modal 'B' last record expiry date and today date
This means that the newest date should be first, so a simple ORDER BY expired_at DESC should do what you need.
The join is necessary so you can use the Bars expired_at column.
The hash syntax in the order method is needed to access the join table's attribute.
Checking the difference Time.zone.now - model_b_object.expiry_date is an unnecessary step. Just go directly to the ordering part. This will show the model B records that expired nearest to Today's date.
ModelB.all.order("expiry_date ASC")

How to sort the ActiveAdmin index based on a filter and a column

I have a Rails application with a Movie model. The Movie model has 'name' and 'release_date' as regular attributes, as well as a scope used to search for movie names with elasticsearch.
class Movie < ActiveRecord::Base
scope :movie_name_search, -> (term) {
movie_ids = elasticSearch(term, :name).map(&id)
Movie.where(id: movie_ids).reorder('').order_by_ids(movie_ids) unless movie_ids.nil?
}
end
I then set up my active admin to show this data
ActiveAdmin.register Promotion do
filter :movie_name_search, as: :string, label: "Movie Name"
index do
actions
column :name
column :release date, sortable: :release_date
end
end
Putting in a movie name into the search bar works perfectly, and sorting against release_date works perfectly, but I can't do both at the same time. Once I'm using the filter for movie names, the sort by date doesn't work. It only works when I remove the reorder and new order.
scope :movie_name_search, -> (term) {
movie_ids = elasticSearch(term, :name).map(&id)
Movie.where(id: movie_ids) unless movie_ids.nil?
}
It would appear that the ordering I enforce in the scope takes precedence over the sort of the column but I have no idea why. Any ideas?
You're resetting the scope chain when you call Movie.where in movie_search_name. You want to send where to self instead (i.e. just delete the Movie. part), so that prior conditions are preserved.
scope :movie_name_search, -> (term) {
movie_ids = elasticSearch(term, :name).map(&id)
where(id: movie_ids) unless movie_ids.nil?
}
Edit: I understand the issue now
Like you say, Elastic Search is returning an array of ids in sorted order, but where does not respect that order. It just pulls records from the database as it finds them, so long as their ids are in the array. It's no good for us to sort the records afterwards as an array, because then ActiveRecord can't apply additional clauses to the query. It has to be done as part of the query.
SQL does provide a way to enforce an arbitrary order: ORDER BY CASE, but it's not built in to Rails. Fortunately, it's not hard to add.
Let's suppose your search results are [2, 1, 3]. In SQL, we can retrieve them in order like this:
SELECT * FROM movies
WHERE id IN (2, 1, 3)
ORDER BY CASE id
WHEN 2 THEN 0
WHEN 1 THEN 1
WHEN 3 THEN 2
ELSE 3 END;
To make this compatible with ActiveRecord, we can add a class method to Movie:
app/models/movie.rb
class Movie < ActiveRecord::Base
# ...
def self.order_by_ids_array(ids)
order_clause = "CASE id "
ids.each_with_index do |id, index|
order_clause << sanitize_sql_array(["WHEN ? THEN ? ", id, index])
end
order_clause << sanitize_sql_array(["ELSE ? END", ids.length])
order(order_clause)
end
end
Now your ActiveAdmin scope uses both where and order_by_ids_array:
scope :movie_name_search, -> (term) {
movie_ids = elasticSearch(term, :name).map(&id)
where(id: movie_ids).order_by_ids_array(movie_ids) unless movie_ids.nil?
}
Reference:
http://www.justinweiss.com/articles/how-to-select-database-records-in-an-arbitrary-order/
Edit II: A real hack
Note: This requires a recent version of ActiveAdmin that uses Ransack.
The issue we're having is that filters don't play well with sorting. So here's
the new plan: let's add another column to our index table that shows the search
rank of each movie. This column will only appear when we've filtered by movie
name, and it will be sortable. This way there will be no "default" sort, but
you can sort by anything you want, including search ranking.
The trick is to insert a computed column into the query using a CASE like
above, but in the SELECT clause. We'll call it search_rank and it can be
accessed on any returned movie as movie.attributes['search_rank'].
app/admin/movies.rb
ActiveAdmin.register Movie do
filter :movie_search, as: string, label: 'Movie Name'
index do
# only show this column when a search term is present
if params[:q] && params[:q][:movie_search]
# we'll alias this column to `search_rank` in our scope so it can be sorted by
column :search_rank, sortable: 'search_rank' do |movie|
movie.attributes['search_rank']
end
end
end
end
Now in our model, we need to define the movie_search scope (as a class method)
and mark it as ransackable.
app/models/movie.rb
class Movie < ActiveRecord::Base
def self.ransackable_scopes(opts)
[:movie_search]
end
def self.movie_search(term)
# do search here
ids = elasticSearch(term, :name).map(&id)
# build extra column
rank_col = "(CASE movies.id "
ids.each_with_index do |id, index|
rank_col << "WHEN #{ id } THEN #{ index } "
end
rank_col << 'ELSE NULL END) AS search_rank'
select("movies.*, #{ rank_col }").where(id: ids)
end
end

active_admin config.sort for two columns

I have an active_admin table called shows that acts of a list of rsvps for bike riders and bike shows that the riders will compete in. The following code correctly sorts the table alphabetically by rider_last_name:
config.sort_order = 'rider_last_name_asc'
Now when a rider is attending multiple bike shows, I want the table to first sort by rider_last_name and then within that rider sort by an attribute of shows called start_time. start_time is a DateTime. According to this stackoverflow article, the following should work:
config.sort_order = 'rider_last_name_asc, start_time_asc'
but it doesn't. In fact, it undoes the sorting by rider_last_name. How do I sort by both columns?
You may try refine apply_sorting method for collections, like this:
controller do
def apply_sorting(chain)
params[:order] ? chain : chain.reorder(rider_last_name: :asc, start_time: :asc)
end
end
ActiveAdmin suggests overwrite the find_collection method, which returns an ActiveRecord::Relation. But I prefer add the order to it's output.
ActiveAdmin.register Show do
controller do
def find_collection(options = {})
super.reorder(rider_last_name: :asc, start_time: :asc)
end
end
...
end
Although it works, this overwrite the user option of click on a column.
Alternative
You can do the same with scoped_collection, which is called at the start of find_collection, but it does not work unless you add active_admin_config.sort_order = '' to the controller. This way:
controller do
active_admin_config.sort_order = ''
def scoped_collection
super.reorder(rider_last_name: :asc, start_time: :asc)
end
end
Now, if we want to reorder before and, after that take care of the user params (and do not overwrite them). This is the way.
Note: I did use active_admin_config.sort_order and not config.sort_order.
Also, the sort_order option, ends as a param of OrderClause.new call, which expect for only one sort field, see here and here.
you can try to pass an array to it as such:
config.sort_order = [:rider_last_name_asc, :start_time_asc]

grouping with a non-primary key in postgres / activerecord

I have a model Lap:
class Lap < ActiveRecord::Base
belongs_to :car
def self.by_carmodel(carmodel)
scoped = joins(:car_model).where(:car_models => {:name => carmodel})
scoped
end
def self.fastest_per_car
scoped = select("laps.id",:car_id, :time, :mph).group("laps.id", :car_id, :time, :mph).order("time").limit(1)
scoped
end
end
I want to only return the fastest lap for each car.
So, I need to group the Laps by the Lap.car_id and then only return the fastest lap time based on that car, which would determined by the column Lap.time
Basically I would like to stack my methods in my controller:
#corvettes = Lap.by_carmodel("Corvette").fastest_per_car
Hopefully that makes sense...
When trying to run just Lap.fastest_per_car I am limiting everything to 1 result, rather than 1 result per each Car.
Another thing I had to do was add "laps.id" as :id was showing up empty in my results as well. If i just select(:id) it was saying ambiguous
I think a decent approach to this would be to add a where clause based on an efficient SQL syntax for returning the single fastest lap.
Something like this correlated subquery ...
select ...
from laps
where id = (select id
from laps laps_inner
where laps_inner.car_id = laps.car_id
order by time asc,
created_at desc
limit 1)
It's a little complex because of the need to tie-break on created_at.
The rails scope would just be:
where("laps.id = (select id
from laps laps_inner
where laps_inner.car_id = laps.car_id
order by time asc,
created_at desc
limit 1)")
An index on car_id would be pretty essential, and if that was a composite index on (car_id, time asc) then so much the better.
You are using limit which will return you one single value. Not one value per car. To return one car value per lap you just have to join the table and group by a group of columns that will identify one lap (id is the simplest).
Also, you can have a more ActiveRecord friendly friendly with:
class Lap < ActiveRecord::Base
belongs_to :car
def self.by_carmodel(carmodel)
joins(:car_model).where(:car_models => {:name => carmodel})
end
def self.fastest_per_car
joins(:car_model)
.select("laps.*, MIN(car_models.time) AS min_time")
.group("laps.id")
.order("min_time ASC")
end
end
This is what I did and its working. If there is a better way to go about these please post your answer:
in my model:
def self.fastest_per_car
select('DISTINCT ON (car_id) *').order('car_id, time ASC').sort_by! {|ts| ts.time}
end

Resources