I have a calculated field in my model as follows.
class Products < ApplicationRecord
attr_reader :days
def days
(Date.today - self.created_at).to_i
end
end
When I try to sort it with, I receive an error.
#products = Product.all.order("days").paginate(:page => params[:page], :per_page => 15)
Error:
PG::UndefinedColumn: ERROR: column "days" does not exist
I will appreciate if anyone can show me how to sort based on a calculated field?
Rails order clause parameter columb should be exist in the table, does not support for user defined custom attribute in the model. So, you have to use the ruby sort_by method for custom defined attributes like below,
Product.all.sort_by(&:days)
And also you have to change your method to like below,
def days
DateTime.now.to_i - self.created_at.to_i
end
It will just work but this is not a best practice to sort the records based on custom user defined custom attribute. So, you have to move this logic to sql query itself like below,
Product.all.order("now() - created_at")
It works on postgres, not sure about mysql, please check alternate in mysql if not working.
the problem for your code above is attr_reader :days, meanwhile days you declared as method not variable
here is my idea for your problem
in your model
class Products < ApplicationRecord
def total_days
(Date.today - self.created_at).to_i
end
def self.sorted_by_days
Product.all.sort_by(&:total_days).reverse
# just delete .reverse incase you want ascending
# I put reverse in case you want to sort descending
end
end
in your controller
#products = Product.sorted_by_days.paginate(:page => params[:page], :per_page => 15)
I am not sure how you are running this piece of code:
(Date.today - self.created_at).to_i
Because it expects a numeric value with the - sign. I was able to do it like this:
((Time.zone.now - self.created_at) / 1.day).to_i
But the main point is I think you want to order the records by created_at date. So by default it is ascending and you want to display the records which has been recently created first so you can do it like this directly:
Product.all.order(:created_at)
and if you want it in descending order then you can do it like this:
Product.all.order(created_at: :desc)
Still if you need to sort it by an attr_reader the other answers show how you can do it. Just the issue will be that the pagination works on ActiveRecord::Collection rather than array so for will_paginate you may refer here:
Ruby on Rails will_paginate an array
Hope this helps.
Related
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]
I have a model:
class Event < ActiveRecord::Base
default_scope :order => 'date_begin'
end
There is a sort link in a view file:
= sort_link #search, :date_begin
When I'm trying to order date_begin as DESC nothing happens because the SQL query is:
SELECT * FROM events ORDER BY date_begin, date_begin DESC
How to make MetaSearch reorder this column? (I know that there is a "reorder" method in ActiveRecord but I don't know how to apply it to MetaSearch)
You can use unscoped method when you decided to use meta_search:
#search = Event.unscoped.search(params[:search])
I also wanted to use a default sort order, and didn't figure out any other way than to enforce a default order in the controller, not using any ordering scope in the model:
search = {"meta_sort" => "created_at.desc"}.merge(params[:search] || {})
#search = Photo.search(search)
The default sort order is created_at DESC, but it will be overwritten if a new sort order is received in the params. Seems to work for me.
#search = if params[:q] && params[:q][:s]
# Ransack sorting is applied - cancel default sorting
Event.reorder(nil).search(params[:q])
else
# Use default sorting
Event.search(params[:q])
end
Benefits:
1) only cancels :order scope - useful if you have .where(:deleted_at => nil).order(:date_begin) default scope.
2) uses default ordering when Ransack sorting is not applied.
How do I select a single random record for each user, but order the Array by the latest record pr. user.
If Foo uploads a new painting, I would like to select a single random record from foo. This way a user that uploads 10 paintings won't monopolize all the space on the front page, but still get a slot on the top of the page.
This is how I did it with Rails 2.x running on MySQL.
#paintings = Painting.all.reverse
first_paintings = []
#paintings.group_by(&:user_id).each do |user_id, paintings|
first_paintings << paintings[rand(paintings.size-1)]
end
#paintings = (first_paintings + (Painting.all - first_paintings).reverse).paginate(:per_page => 9, :page => params[:page])
The example above generates a lot of SQL query's and is properly badly optimized. How would you pull this off with Rails 3.1 running on PostgreSQL? I have 7000 records..
#paintings = Painting.all.reverse = #paintings = Painting.order("id desc")
If you really want to reverse the order of the the paintings result set I would set up a scope then just use that
Something like
class Painting < ActiveRecord::Base
scope :reversed, order("id desc")
end
Then you can use Painting.reversed anywhere you need it
You have definitely set up a belongs_to association in your Painting model, so I would do:
# painting.rb
default_scope order('id DESC')
# paintings_controller.rb
first_paintings = User.includes(:paintings).collect do |user|
user.paintings.sample
end
#paintings = (first_paintings + Painting.where('id NOT IN (?)', first_paintings)).paginate(:per_page => 9, :page => params[:page])
I think this solution results in the fewest SQL queries, and is very readable. Not tested, but I hope you got the idea.
You could use the dynamic finders:
Painting.order("id desc").find_by_user_id!(user.id)
This is assuming your Paintings table contains a user_id column or some other way to associate users to paintings which it appears you have covered since you're calling user_id in your initial code. This isn't random but using find_all_by_user_id would allow you to call .reverse on the array if you still wanted and find a random painting.
I'm using Ruby on Rails. I have a couple of models which fit the normal order/order lines arrangement, i.e.
class Order
has_many :order_lines
end
class OrderLines
belongs_to :order
belongs_to :product
end
class Product
has_many :order_lines
end
(greatly simplified from my real model!)
It's fairly straightforward to work out the most popular individual products via order line, but what magical ruby-fu could I use to calculate the most popular combination(s) of products ordered.
Cheers,
Graeme
My suggestion is to create an array a of Product.id numbers for each order and then do the equivalent of
h = Hash.new(0)
# for each a
h[a.sort.hash] += 1
You will naturally need to consider the scale of your operation and how much you are willing to approximate the results.
External Solution
Create a "Combination" model and index the table by the hash, then each order could increment a counter field. Another field would record exactly which combination that hash value referred to.
In-memory Solution
Look at the last 100 orders and recompute the order popularity in memory when you need it. Hash#sort will give you a sorted list of popularity hashes. You could either make a composite object that remembered what order combination was being counted, or just scan the original data looking for the hash value.
Thanks for the tip digitalross. I followed the external solution idea and did the following. It varies slightly from the suggestion as it keeps a record of individual order_combos, rather than storing a counter so it's possible to query by date as well e.g. most popular top 10 orders in the last week.
I created a method in my order which converts the list of order items to a comma separated string.
def to_s
order_lines.sort.map { |ol| ol.id }.join(",")
end
I then added a filter so the combo is created every time an order is placed.
after_save :create_order_combo
def create_order_combo
oc = OrderCombo.create(:user => user, :combo => self.to_s)
end
And finally my OrderCombo class looks something like below. I've also included a cached version of the method.
class OrderCombo
belongs_to :user
scope :by_user, lambda{ |user| where(:user_id => user.id) }
def self.top_n_orders_by_user(user,count=10)
OrderCombo.by_user(user).count(:group => :combo).sort { |a,b| a[1] <=> b[1] }.reverse[0..count-1]
end
def self.cached_top_orders_by_user(user,count=10)
Rails.cache.fetch("order_combo_#{user.id.to_s}_#{count.to_s}", :expiry => 10.minutes) { OrderCombo.top_n_orders_by_user(user, count) }
end
end
It's not perfect as it doesn't take into account increased popularity when someone orders more of one item in an order.
I want to give users the ability to page through my blog posts in random order.
I can't implement it like this:
#posts = Post.paginate :page => params[:page], :order => 'RANDOM()'
since the :order parameter is called with every query, and therefore I risk repeating blog posts.
What's the best way to do this?
RAND accepts a seed in MySQL:
RAND(N)
From the MySQL docs:
RAND(), RAND(N)
Returns a random floating-point value
v in the range 0 <= v < 1.0. If a
constant integer argument N is
specified, it is used as the seed
value, which produces a repeatable
sequence of column values. In the
following example, note that the sequences of values produced by RAND(3) is the same both places where it occurs.
Other databases should have similar functionality.
If you use the SAME seed each time you call RAND, the order will be consistent across requests and you can paginate accordingly.
You can then store the seed in the user's session - so each user will see a set of results unique to them.
To avoid each page (generated from a new request) potentially having a repeated post you'll need to store the order of posts somewhere for retrieval over multiple requests.
If you want each user to have a unique random order then save the order in a session array of IDs.
If you don't mind all users having the same random order then have a position column in the posts table.
You could :order => RANDOM() on your original query that populates #posts, and then when you paginate, don't specify the order.
Create a named scope on your Post model that encapsulates the random behaviour:
class Post < ActiveRecord::Base
named_scope :random, :order => 'RANDOM()'
.
.
.
end
Your PostsController code then becomes:
#posts = Post.random.paginate :page => params[:page]