Active Record: find records by ceratin condition - ruby-on-rails

My goal is to find three doctors with more than 1 review and with average rating >= 4
At the moment I'm using this service
class RatingCounterService
def get_three_best_doctors
doctors = find_doctors_with_reviews
sorted_doctors = sort_doctors(doctors)
reversed_hash = reverse_hash_with_sorted_doctors(sorted_doctors)
three_doctors = get_first_three_doctors(reversed_hash)
end
private
def find_doctors_with_reviews
doctors_with_reviews = {}
Doctor.all.each do |doctor|
if doctor.reviews.count > 0 && doctor.average_rating >= 4
doctors_with_reviews[doctor] = doctor.average_rating
end
end
doctors_with_reviews
end
def sort_doctors(doctors)
doctors.sort_by { |doctor, rating| rating }
end
def reverse_hash_with_sorted_doctors(sorted_doctors)
reversed = sorted_doctors.reverse_each.to_h
end
def get_first_three_doctors(reversed_hash)
reversed_hash.first(3).to_h.keys
end
end
Which is very slow.
My Doctor model:
class Doctor < ApplicationRecord
has_many :reviews, dependent: :destroy
def average_rating
reviews.count == 0 ? 0 : reviews.average(:rating).round(2)
end
end
Review model:
class Review < ApplicationRecord
belongs_to :doctor
validates :rating, presence: true
end
I can find all doctors with more than 1 review with this request
doctors_with_reviews = Doctor.joins(:reviews).group('doctors.id').having('count(doctors.id) > 0')
But how can I find doctors with an average rating >= 4 and order them by the highest rating if the "average rating" is an instance method?

Thanks to this answer :highest_rated scope to order by average rating
My final solution is
Doctor.joins(:reviews).group('doctors.id').order('AVG(reviews.rating) DESC').limit(3)

Related

Display Similar Items With Having Distinct Count Rails 5.1

I'm trying to display a list of gins that have a similar minimum number of botanicals on my show page. I feel I'm close, but the current output is not right. It's actually just printing the name of the gin a number of times.
Gin Load (1.6ms) SELECT "gins".* FROM "gins" INNER JOIN
"gins_botanicals" ON "gins_botanicals"."gin_id" = "gins"."id" INNER
JOIN "botanicals" ON "botanicals"."id" =
"gins_botanicals"."botanical_id" WHERE "botanicals"."id" IN (4, 10, 3)
AND ("gins"."id" != $1) GROUP BY gins.id HAVING (COUNT(distinct
botanicals.id) >= 3) [["id", 2]]
I have three models; two resources with a joins table:
gin.rb
class Gin < ApplicationRecord
belongs_to :distillery, inverse_of: :gins
accepts_nested_attributes_for :distillery, reject_if: lambda {|attributes| attributes['name'].blank?}
acts_as_punchable
has_many :gins_botanical
has_many :botanicals, through: :gins_botanical
botanical.rb
class Botanical < ApplicationRecord
has_many :gins_botanical
has_many :gins, through: :gins_botanical
gins_botanical.rb
class GinsBotanical < ApplicationRecord
belongs_to :gin
belongs_to :botanical
gins_controller
def show
#gin = Gin.friendly.find(params[:id])
#gin.punch(request)
#meta_title = meta_title #gin.name
#similiar_gins = Gin.joins(:botanicals).where("botanicals.id" => #gin.botanical_ids).where.not('gins.id' => #gin.id).having("COUNT(distinct botanicals.id) >= 3").group("gins.id")
end
so in #similar_gins i am trying to count how many matching botanicals does the current #gin have compared to all the other #gins and if >= 3 return the values.
And in my view:
show.html.erb
<% #similiar_gins.each do |gin| %>
<%= #gin.name %>
<% end %>
I'm suspecting my where is not correct...
Yes, I have the similar feature but I have implemented like below
#gin = Gin.find(params[:id])
if #gin.botanicals.count > 1
#botanicals = #gin.botanical_ids
#gin_ids = Botanical.select('distinct gin_id').where('gin_id IN (?)', #botanicals).limit(10)
#ids = #gin_ids.map(&:gin_id)
#similiar_gins = Gin.where('id IN (?)', #ids).where.not(id: #gin) #=> similar all without current gin
end
This code is converted from my code which is relation is category and jobs, if you need to see my code for showing the similar jobs then it is
def show
#job = Job.find(params[:id])
if #job.categories.count > 1
#category = #job.category_ids
#jobs = JobCategory.select('distinct job_id').where('category_id IN (?)', #category).limit(10)
ids = #jobs.map(&:job_id)
#releted_jobs = Job.where('id IN (?)', ids).where.not(id: #job)
end
end
Hope it helps

Rails association scope by method with aggregate

I'm trying to retrieve association records that are dependent on their association records' attributes. Below are the (abridged) models.
class Holding
belongs_to :user
has_many :transactions
def amount
transactions.reduce(0) { |m, t| t.buy? ? m + t.amount : m - t.amount }
end
class << self
def without_empty
includes(:transactions).select { |h| h.amount.positive? }
end
end
class Transaction
belongs_to :holding
attributes :action, :amount
def buy?
action == ACTION_BUY
end
end
The problem is my without_empty method returns an array, which prevents me from using my pagination.
Is there a way to rewrite Holding#amount and Holding#without_empty to function more efficiently with ActiveRecord/SQL?
Here's what I ended up using:
def amount
transactions.sum("CASE WHEN action = '#{Transaction::ACTION_BUY}' THEN amount ELSE (amount * -1) END")END")
end
def without_empty
joins(:transactions).group(:id).having("SUM(CASE WHEN transactions.action = '#{Transaction::ACTION_BUY}' THEN transactions.amount ELSE (transactions.amount * -1) END) > 0")
end

Custom Rails Dashboard, how to optimize data retrieval to display in view?

I am making a custom dashboard for a school application that requires me to calculate some KPIs, the way am doing it right now is calling several class methods from the Opportunity class in the dashboard/index action from the controller, and storing each method result in a variable that is going to be used in a tile. So each variable is a different tile of the dashboard.
The methods belong to the Opportunity class shown below:
class Opportunity < ApplicationRecord
belongs_to :organization
belongs_to :opportunity_status
has_many :tasks, dependent: :destroy
has_many :opportunity_status_logs, dependent: :destroy
before_create :create_status_log
after_update :create_status_log, if: :opportunity_status_id_changed?
validates :name, :description, :revenue, :opportunity_status_id, :closing_date, presence: true
validates :name, :description, format: { with: /\A[[:alpha:]a-zA-Z0-9ñÑ#()\-.,\s]+\z/ }
validates :revenue, numericality: true
validates :closing_date, inclusion: { in: (Time.zone.today..Time.zone.today+5.years) }
def create_status_log
OpportunityStatusLog.create(opportunity_id: self.id, opportunity_status_id: self.opportunity_status_id)
end
def status_updated_by(user)
#status_log = self.opportunity_status_logs.last
#status_log.user_id = user.id
#status_log.save!
end
def self.actives
self.where.not(opportunity_status_id: [11,12])
end
def self.won
self.where(opportunity_status_id: 11)
end
def self.lost
self.where(opportunity_status_id: 12)
end
def self.average_revenue
self.won.average(:revenue)
end
def self.minimum_revenue
self.won.minimum(:revenue)
end
def self.maximum_revenue
self.won.maximum(:revenue)
end
def self.filter_by_status(status_id)
self.where(opportunity_status: status_id)
end
def self.relative_percentage(item_amount, total)
item_amount * 100 / total
end
def self.conversion_rate
self.won.count / self.all.count.to_f * 100
end
def self.potential_revenue
self.actives.sum(:revenue)
end
end
and this is the way the controller is structured:
class DashboardController < ApplicationController
before_action :authenticate_user!
def index
#opportunities = Opportunity.includes(:opportunity_status).all
#actives = Opportunity.actives.count
#won = Opportunity.won.count
#lost = Opportunity.lost.count
#average_revenue = Opportunity.average_revenue
#minimum_revenue = Opportunity.minimum_revenue
#maximum_revenue = Opportunity.maximum_revenue
#in_appreciation = Opportunity.filter_by_status(6).count
#in_value_proposition = Opportunity.filter_by_status(7).count
#in_management_analysis = Opportunity.filter_by_status(8).count
#in_proposal = Opportunity.filter_by_status(9).count
#in_review = Opportunity.filter_by_status(10).count
#app_perc = Opportunity.relative_percentage(#in_appreciation, #opportunities.count)
#vp_perc = Opportunity.relative_percentage(#in_value_proposition, #opportunities.count)
#ma_perc = Opportunity.relative_percentage(#in_management_analysis, #opportunities.count)
#pp_perc = Opportunity.relative_percentage(#in_proposal, #opportunities.count)
#rw_perc = Opportunity.relative_percentage(#in_review, #opportunities.count)
#conversion_rate = '%.2f' % [Opportunity.conversion_rate]
#potential_revenue = Opportunity.potential_revenue
end
end
Even though it works as expected, it looks like the controller is a bit too fat and I feel that with the current approach if the app scales it will be very slow due to the amount of queries that are being done. So, is there a way to refactor this in order to optimize the data retrieval and the displaying of the KPIs?
Thanks in advance
You can try implementing Facade Pattern in Rails. It will make your controller skinny but on the query part you will still be needing to make those queries, there is no way to skip that.
You can try to optimize db by adding index and creating sql views in future when you get performance lag, at this time it will be like premature optimization

Calculation in model or controller

I'm builing a weight loss app. For this in my app each user has_one :profile and has_many :weights. Each profile belongs_to :pal. For my app to work I need a value called SMR which basically is a formula that takes as variables the user's size, age and gender (all from profiles table), the user's current weight (from weights table) as well as a float number from pal table.
I am able to calculate SMR in profiles_controller.rb show action and show it in the profiles show.html.erb.
I have two questions now:
Is it correct to do this calculation in the profiles_controller.rb show action or should I do it in the profile.rb model? If I should do it in the model: how can I do it (how should the code look like)?
I will need the SMR value later on in my app as a variable for other calculations as well. How can I achieve this (if it is calculated in the profile controller/model but needed somewhere else later on)?
I'm fairly new to the Rails world so maybe my questions are really noob questions.
profile.rb
class Profile < ActiveRecord::Base
belongs_to :user
belongs_to :pal
belongs_to :goal
def age
if birthdate != nil
now = Time.now.utc.to_date
now.year - birthdate.year - (birthdate.to_date.change(:year => now.year) > now ? 1 : 0)
else
nil
end
end
end
weight.rb
class Weight < ActiveRecord::Base
belongs_to :user
end
pal.rb
class Pal < ActiveRecord::Base
has_many :profiles
end
profiles_controller.rb (show action only)
def show
#pal = #profile.pal
#goal = #profile.goal
#current_weight = Weight.where(:user_id => current_user.id).order(:day).last
if #profile.gender == 0
#smr = (10*#current_weight.kilograms+6.25*#profile.size-5*#profile.age+5)*#pal.value
elsif #profile.gender == 1
#smr = (10*#current_weight.kilograms+6.25*#profile.size-5*#profile.age-161)*#pal.value
else
nil
end
end
I think you should create a separate class or you can do on profile model as well
class SmrCalculator
def initialize(profile, user)
#profile = profile
#user = user
end
def get_smr
#pal = #profile.pal
#goal = #profile.goal
#current_weight = Weight.where(:user_id => #user.id).order(:day).last
if #profile.gender == 0
#smr = (10*#current_weight.kilograms+6.25*#profile.size-5*#profile.age+5)*#pal.value
elsif #profile.gender == 1
#smr = (10*#current_weight.kilograms+6.25*#profile.size-5*#profile.age-161)*#pal.value
else
nil
end
#smr
end
end
And call this class on your controller show method like this:
#smr_calculator = SmrCalculator.new(#profile, current_user)
#smr = #smr_calculator.get_smr
And add this class as smr_calculator.rb in models folder
so anywhere in the app you need #smr you can call this class with profile and current user
You can create a services directory inside app folder.And inside that you can create your class as CalculatorService.
Example:
class CalculatorService
def initialize(profile, user)
#profile = profile
#user = user
end
def smr_value
#pal = #profile.pal
#goal = #profile.goal
#current_weight = Weight.users_weight(#user.id)
#smr = if #profile.gender == 0
(10*#current_weight.kilograms + 6.25*#profile.size-5*#profile.age+5)*#pal.value
elsif #profile.gender == 1
(10*#current_weight.kilograms + 6.25*#profile.size-5*#profile.age-161)*#pal.value
else
nil
end
#smr
end
end
class Weight < ActiveRecord::Base
scope :users_weight, ->(user_id) { where(:user_id => user_id).order(:day).last}
end
And call this service in your controller like this:
#smr_calculator = CalculatorService.new(#profile, current_user)
#smr = #smr_calculator.smr_value

rails 4 - sort by number of votes

So... I have images. and those images have votes.
I currently have image.rb
class Image < ActiveRecord::Base
belongs_to :user
belongs_to :event
has_many :image_votes, dependent: :destroy
default_scope { order(ci_lower_bound) }
def taken_by? (photographer)
self.user == photographer
end
def self.ci_lower_bound
pos = image_votes.where(value: 1).size
n = image_votes.size
if n == 0
return 0
end
z = 1.96
phat = 1.0*pos/n
(phat + z*z/(2*n) - z * Math.sqrt((phat*(1-phat)+z*z/(4*n))/n))/(1+z*z/n)
end
end
I've been playing around with this... the only way to get default scope to work is to use a method with self. I found that formula at http://www.evanmiller.org/how-not-to-sort-by-average-rating.html - how would I call this to make it work??
I'd create a new scope called by_votes, include sum() and order by this new column:
scope :by_votes, -> { select("#{Image.table_name}.*, sum(#{ImageVote.table_name}.votes) AS votes").order("votes DESC") }

Resources