Query nested count - ruby-on-rails

I have two models, a User and an Exercise where a user has_many :exercises. There is a cache_counter on the User model, exercise_count
I want to make a leaderboard based on a user's exercise count in the current month. I want to display the 2 users with the highest exercise counts of the month along with the count. If there are other users with the same exercise count as the 2nd place user I want to display them too. (Display 2+ users)
The current query I have,
# User model
scope :exercises_this_month, -> { includes(:exercises).references(:exercises)
.where("exercise_count > ? AND exercises.exercise_time > ? AND exercises.exercise_time < ?",
0 , Time.now.beginning_of_month, Time.now.end_of_month)
}
def User.leaders
limit = User.exercises_this_month.where("exercise_count > ?", 0).order(exercise_count: :desc).second.exercise_count
User.exercises_this_month.where("exercise_count > ? AND exercise_count >= ?", 0, limit)
end
will return the top 2+ user object for all time. I want to limit this to the current month.

You can't use the counter_cache here as it stores the number from all time. Another issue you have is that if you have multiple players having the same highest number of exercises, you'll miss the ones with the second highest number.
# Exercise.rb
scope :exercises_this_month, -> {
where("exercises.exercise_time > ? AND exercises.exercise_time <= ?",
Time.now.beginning_of_month, Time.now.end_of_month)
}
#User.rb
players_this_month = User.joins(:exercises).group(:user_id).merge(Exercise.exercises_this_month).count ##
sorted_counts = players_this_month.values.uniq.sort
if sorted_counts.present?
second_place_counts = sorted_counts[-2] || sorted_counts[-1] #in case all have the same number
top_two_ids = players_this_month.map { |k,v| k if v >= second_place_counts } ##user ids with top two numbers
end

gem 'groupdate'
model
def self.by_month(month_num)
self.where("cast(strftime('%m', created_at) as int) = #
{Date.today.strftime("%m")}")
end
controller
def report
#posts =Post.by_month(Date.today.strftime("%m"))
end
view
some think like this
= #posts.group_by_month(:created_at).count
I am not sure what you want but this will be big help hopefully enjoy :)

Related

Rails query with condition in count

I'm having a little trouble with a query in Rails.
Actually my problem is:
I want to select all users which do not have any user_plans AND his role.name is equals to default... OR has user_plans and all user_plans.expire_date are lower than today
user has_many roles
user has_many user_plans
users = User.where(gym_id: current_user.id).order(:id)
#users = []
for u in users
if u.roles.pluck(:name).include?('default')
add = true
for up in u.user_plans
if up.end_date > DateTime.now.to_date
add = false
end
end
if add
#users << u
end
end
end
This code up here, is doing exactly what I need, but with multiple queries.
I don't know how to build this in just one query.
I was wondering if it is possible to do something like
where COUNT(user_plans.expire_date < TODAY) == 0
User.joins(:user_plans, :roles).where("roles.name = 'default' OR user_plans.expire_date < ?", Date.today)
Should work, not tested, but should give you some idea you can play with (calling .distinct at the end may be necessary)
There is also where OR in Rails 5:
User.joins(:user_plans, :roles).where(roles: { name: 'default' }).or(
User.joins(:user_plans).where('user_plans.expire_date < ?', Date.today)
)
FYI: Calling .joins on User will only fetch those users who have at least one user_plan (in other words: will not fetch those who have no plans)

Rails 4: Selecting records between current date and given specific date/number of days in past

I have a Customer table in my Rail app. In the customer table one field is customer_expiry_date.
I want to add a filter in my application, that means customers, who all are expires within the following criteria:
Expire within one(1) day.
Expire within seven(7) days.
Expire within one(1) month.
How should I write my query using where clause to achieve this ?
My current query is like:
#customers = Customer.where.not(customer_expiry_date: nil)
How to select customers into three collection based on my requirement ?
#customers_exp_1day = ?
#customers_exp_1week = ?
#customers_exp_1month = ?
Just write a scope in your model:
class Customer < ActiveRecord::Base
# your code
scope :expired_in, -> (from_now) {
where.not(customer_expiry_date: nil).where('customers."customer_expiry_date" >= ? AND customers."customer_expiry_date" <= ?', Time.now, Time.now + from_now)
}
end
Then use it:
#customers_exp_1day = Customer.expired_in(1.day)
#customers_exp_1week = Customer.expired_in(1.week)
#customers_exp_1month = Customer.expired_in(1.month)
Now it will work with the latest update.
#customers_exp_1day = Customer.where("customer_expiry_date <= ?",
Date.tomorrow)
#customers_exp_1week = Customer.where("customer_expiry_date <= ?",
Date.today + 1.week)
#customers_exp_1month = Customer.where("customer_expiry_date <= ?",
Date.today + 1.month)
You had "Customers" in your question and I used "Customer" here, assuming that your model name is actually singular.

How to return the next record?

In my Rails app I have a function next which I am using in my show templates to link to the next product (ordered by price):
class Product < ActiveRecord::Base
belongs_to :user
def next
Product.where("user_id = ? AND price > ?", user_id, price).order("price ASC").first
end
end
The problem is that this function works only when all products have different prices. The moment there are multiple products with the same price, the next function picks one product randomly (?) thereby skipping all the others.
Is there a way to make this function return the next product, even though it has the same price? (if the price is the same, the products could be ordered simply by ID or date)
I tried replacing > with >= but that didn't work.
Thanks for any help.
You have to use id in where clause and in order as well
def next
Product.where("user_id = ? AND price >= ? AND id > ?", user_id, price, id).order("total, id").first
end

What's the most efficient way to keep track of an accumulated lifetime sales figure?

So I have a Vendor model, and a Sale model. An entry is made in my Sale model whenever an order is placed via a vendor.
On my vendor model, I have 3 cache columns. sales_today, sales_this_week, and sales_lifetime.
For the first two, I calculated it something like this:
def update_sales_today
today = Date.today.beginning_of_day
sales_today = Sale.where("created_at >= ?", today).find_all_by_vendor_id(self.id)
self.sales_today = 0
sales_today.each do |s|
self.sales_today = self.sales_today + s.amount
end
self.save
end
So that resets that value everytime it is accessed and re-calculates it based on the most current records.
The weekly one is similar but I use a range of dates instead of today.
But...I am not quite sure how to do Lifetime data.
I don't want to clear out my value and have to sum all the Sale.amount for all the sales records for my vendor, every single time I update this record. That's why I am even implementing a cache in the first place.
What's the best way to approach this, from a performance perspective?
I might use ActiveRecord's sum method in this case (docs). All in one:
today = Date.today
vendor_sales = Sale.where(:vendor_id => self.id)
self.sales_today = vendor_sales.
where("created_at >= ?", today.beginning_of_day).
sum("amount")
self.sales_this_week = vendor_sales.
where("created_at >= ?", today.beginning_of_week).
sum("amount")
self.sales_lifetime = vendor_sales.sum("amount")
This would mean you wouldn't have to load lots of sales objects in memory to add the amounts.
You can use callbacks on the create and destroy events for your Sales model:
class SalesController < ApplicationController
after_save :increment_vendor_lifetime_sales
before_destroy :decrement_vendor_lifetime_sales
def increment_vendor_lifetime_sales
vendor.update_attribute :sales_lifetime, vendor.sales_lifetime + amount
end
def decrement_vendor_lifetime_sales
vendor.update_attribute :sales_lifetime, vendor.sales_lifetime - amount
end
end

Possible override the way count works, or finding a better way, altogether to do this

I have this scope in my artist model that gives me the artists, in the order of their popularity within a certain time period. popularity in the popularity_caches table is computed every day.
scope :by_popularity, lambda { |*args|
options = (default_popularity_options).merge(args[0] || {})
select("SUM(popularity) AS popularity, artists.*").
from("popularity_caches FORCE INDEX (popularity_cache_group), artists FORCE INDEX (index_artists_on_id_and_genre_id)").
where("popularity_caches.target_type = 'Artist'").
where("popularity_caches.target_id = artists.id").
where("popularity_caches.time_frame = ?", options[:time_frame]).
where("popularity_caches.started_on > ?", options[:started_on]).
where("popularity_caches.started_on < ?", options[:ended_on]).
group("artists.id").
order("popularity DESC")
}
This seems to work except when I want to get the count: Artist.by_popularity.count. I get a funky hash in return (probably the count of artists that have popularity_caches within that period):
#<OrderedHash {295954=>1, 20143=>1, 157532=>1, 181291=>1, 300086=>1, 50100=>1, 262898=>1, 293888=>1, 130158=>2, 279943=>1, 336758=>1, 100201=>1, 134290=>2, 22726=>3, 144620=>2, 62497=>2 # snip
This is the SQL I probably want in return:
SELECT COUNT(DISTINCT(artists.id)) AS count_all
FROM popularity_caches FORCE INDEX (popularity_cache_group), artists FORCE INDEX (index_artists_on_id_and_genre_id)
WHERE (popularity_caches.target_type = 'Artist')
AND (popularity_caches.target_id = artists.id)
AND (popularity_caches.time_frame = 'week')
AND (popularity_caches.started_on > '2011-02-28 16:00:00')
AND (popularity_caches.started_on < '2011-10-05')
ORDER BY popularity DESC
To get the count, I had to make a separate method that pretty much does the same thing, except the SQL is formed differently. It kinds sucks through, because when I want to paginate, I have to pass two things:
#artists = Artists.by_popularity(some args).paginate(
:total_entries => Artist.count_by_popularity(pass in the same args here as in Artist.by_popularity),
:per_page => 5,
page => ...
)
That smells to me because it's very brittle.
Is there a way to do this in ARel? Maybe override how it counts things (distinct artists.id) and removing the group by so it doesn't return a hash for the count?
Thanks!
Solved with the amazing scuttle.io:
PopularityCach.select(
Arel::Nodes::Group.new(Artist.arel_table[:id]).count.as('count_all')
).where(
PopularityCach.arel_table[:target_type].eq('Artist').and(
PopularityCach.arel_table[:target_id].eq(Artist.arel_table[:id]).and(
PopularityCach.arel_table[:time_frame].eq('week').and(
PopularityCach.arel_table[:started_on].gt('2011-02-28 16:00:00').and(
PopularityCach.arel_table[:started_on].lt('2011-10-05')
)
)
)
)
).order(:popularity).reverse_order

Resources