Calculation in model or controller - ruby-on-rails

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

Related

How to list in order of the point

I want to list users in order of points. But I didn't migrate point column, I define the point in the model like below.
class User < ApplicationRecord
def my_point(user)
point = user.posts.count * 10 + user.followers.count * 2
end
end
How can I arrange users in the view?
Remember that you can reference self to reference the instance, removing the argument from your method.
class User < ApplicationRecord
def points
(self.posts.count * 10) + (self.followers.count * 2)
end
end
#users = User.includes(:posts, :followers).all.sort_by { |user| user.points }

Active Record: find records by ceratin condition

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)

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

Virtual Column to count record

First, sorry for my English, I am totally new in ruby on rails even in very basic thing, so I hope you all can help me.
I have table Role and RoleUser
table Role have has_many relationship to RoleUser with role_id as foreign key
in table RoleUser is contain user_id, so I can call it 1 role have many users
and I want is to show all record in Role with additional field in every record called total_users,
total_users is in every record have role_id and count the user_id for every role, and put it in total_users,
I know this is must use the join table, but in rails I absolutely knew nothing about that, can you all give me a simple example how to do that.
and one more, same with case above, can I do for example Role.all and then the total_users in include in that without added it in database? is that use virtual column?
anyone have a good source of link to learn of that
I have following code in model
def with_filtering(params, holding_company_id)
order = []
if params[:sort].present?
JSON.parse(params[:sort]).each do |data|
order << "#{data['property']} #{data['direction']}"
end
end
order = 'id ASC' if order.blank?
if self.column_names.include? "holding_company_id"
string_conditions = ["holding_company_id = :holding_company_id"]
placeholder_conditions = { holding_company_id: holding_company_id.id }
else
string_conditions = []
placeholder_conditions = {}
end
if params[:filter].present?
JSON.parse(params[:filter]).each do |filter|
if filter['operation'] == 'between'
string_conditions << "#{filter['property']} >= :start_#{filter['property']} AND #{filter['property']} <= :end_#{filter['property']}"
placeholder_conditions["start_#{filter['property']}".to_sym] = filter['value1']
placeholder_conditions["end_#{filter['property']}".to_sym] = filter['value2']
elsif filter['operation'] == 'like'
string_conditions << "#{filter['property']} ilike :#{filter['property']}"
placeholder_conditions["#{filter['property']}".to_sym] = "%#{filter['value1']}%"
else
string_conditions << "#{filter['property']} = :#{filter['property']}"
placeholder_conditions["#{filter['property']}".to_sym] = filter['value1']
end
end
end
conditions = [string_conditions.join(' AND '), placeholder_conditions]
total_count = where(conditions).count
if params[:limit].blank? && params[:offset].blank?
data = where(conditions).order(order)
else
data = where(conditions).limit(params[:limit].to_i).offset(params[:offset].to_i).order(order)
end
return data, total_count.to_s
end
And I have follwing code in controllers
def crud_index(model)
data, total = Role.with_filtering(params, current_holding_company)
respond_to do |format|
format.json { render json: { data: data, total_count: total }.to_json, status: 200 }
end
end
My only purpose is to add virtual field called total_users, but i want added it in model and combine it with data in method with_filtering
If you have the models like this:
Class Role < ActiveRecord::Base
has_many :role_users
end
Class RoleUser < ActiveRecord::Base
belong_to :role
end
You could use select and joins to generate summary columns, but all the Role's attributes should be include in group.
roles = Role.select("roles.*, count(role_users.id) as total_users")
.joins(:role_users)
.group("roles.id")
Type those scripts in Rails console, Rails will generate a sql like :
SELECT roles.id, count(role_users.id) as total_users
FROM roles
INNER JOIN role_users
ON roles.id = role_users.role_id
GROUP BY roles.id
Then you can use roles.to_json to see the result. The summary column total_users can be accessed in every member of roles.
And there are many other way can match your requirement. Such as this. There is a reference of counter cache.
My suggestion is after searching, you can test those method by rails console, it's a useful tool.
UPDATE
According to OP's update and comment, seems you have more works to do.
STEP1: move with_filtering class method to controller
with_filtering handle a lot of parameter things to get conditions, it should be handled in controller instead of model. So we can transfer with_filtering into conditions and orders in controller.
class RolesController < ApplicationController
def conditions(params, holding_company_id)
if self.column_names.include? "holding_company_id"
string_conditions = ["holding_company_id = :holding_company_id"]
placeholder_conditions = { holding_company_id: holding_company_id.id }
else
string_conditions = []
placeholder_conditions = {}
end
if params[:filter].present?
JSON.parse(params[:filter]).each do |filter|
if filter['operation'] == 'between'
string_conditions << "#{filter['property']} >= :start_#{filter['property']} AND #{filter['property']} <= :end_#{filter['property']}"
placeholder_conditions["start_#{filter['property']}".to_sym] = filter['value1']
placeholder_conditions["end_#{filter['property']}".to_sym] = filter['value2']
elsif filter['operation'] == 'like'
string_conditions << "#{filter['property']} ilike :#{filter['property']}"
placeholder_conditions["#{filter['property']}".to_sym] = "%#{filter['value1']}%"
else
string_conditions << "#{filter['property']} = :#{filter['property']}"
placeholder_conditions["#{filter['property']}".to_sym] = filter['value1']
end
end
end
return [string_conditions.join(' AND '), placeholder_conditions]
end
def orders(params)
ord = []
if params[:sort].present?
JSON.parse(params[:sort]).each do |data|
ord << "#{data['property']} #{data['direction']}"
end
end
ord = 'id ASC' if ord.blank?
return ord
end
end
STEP2: update action crud_index with conditions and orders to get total_count of Roles.
class AnswersController < ApplicationController
def crud_index(model)
total = Role.where(conditions(params, current_holding_company)).count
if params[:limit].blank? && params[:offset].blank?
data = Role.where(conditions(params, current_holding_company)).order(orders(params))
else
data = Role.where(conditions(params, current_holding_company)).limit(params[:limit].to_i).offset(params[:offset].to_i).order(orders(params))
end
respond_to do |format|
format.json { render json: { data: data, total_count: total }.to_json, status: 200 }
end
end
end
STEP3: update action crud_index to get total_users by every role.
Make sure the two previous steps is pass the test.
class AnswersController < ApplicationController
def crud_index(model)
total = Role.where(conditions(params, current_holding_company)).count
if params[:limit].blank? && params[:offset].blank?
data =
Role.select(Role.column_names.map{|x| "Roles.#{x}"}.join(",") + " ,count(role_users.id) as total_users")
.joins(:role_users)
.group(Role.column_names.map{|x| "Roles.#{x}"}.join(","))
.where(conditions(params, current_holding_company))
.order(orders(params))
else
data =
Role.select(Role.column_names.map{|x| "Roles.#{x}"}.join(",") + " ,count(role_users.id) as total_users")
.joins(:role_users)
.group(Role.column_names.map{|x| "Roles.#{x}"}.join(","))
.where(conditions(params, current_holding_company))
.order(orders(params))
.limit(params[:limit].to_i)
.offset(params[:offset].to_i).order(orders(params))
end
respond_to do |format|
format.json { render json: { data: data, total_count: total }.to_json, status: 200 }
end
end
end
NOTE: step3 may need you to modify conditions and orders method to generate column_name with table_name prefix to avoid column name ambiguous error
If you can make these steps through, I suggest you can try will_paginate to simplify the part of your code about total_count ,limit and offset.
With what you explained, you could do something like this:
class Role < ActiveRecord::Base
has_many :role_users
has_many :users
def total_users
self.users.count
end
end
So you just need to call the total_users method on roles object which should get you what you desire. Something like this:
Role.first.total_users
# this will give you the total users for the first role found in your database.
Hope it helps
You might want to watch this Railscast too:
#app/models/role.rb
Class Role < ActiveRecord::Base
has_many :role_users
has_many :users, -> { select "users.*", "role_users.*", "count(role_users.user_id) as total_users" }, through: :role_users
end
This will allow you to call:
#roles = Role.find params[:id]
#roles.users.each do |role|
role.total_users
end
You can see more about how this works with a question I wrote some time ago - Using Delegate With has_many In Rails?
--
It's where I learnt about Alias columns, which Ryan Bates uses to count certain values:

How do I return all non deleted messages of a current thread in ruby on rails?

Having a little trouble figuring out a way to display messages for users message threads depending on which ones have been deleted and not. Deleted messages "sender_status or recipient_status" will change from 0 to 1 on deletion of a message. Currently to display a list of users message threads in their inbox I use:
e.g.
current_user = User.find(2)
current_user.message_threads
This grabs all their message_threads with 0 sender or recipient statuses.
class User < ActiveRecord::Base
has_many :messages
has_many :message_threads
Nice and easy but what I would like to do now is some how set up something that enables me to be able to grab messages with 0 sender or recipient statuses from a current thread. So if I was to type:
one_thread = current_user.message_threads.first
From this thread I'd want to be able to easily grab the only messages I needed. Would like to put something in the message_thread model so I could eventually type out:
current_user.message_threads.first.messages
#or
one_thread.messages
and have only the messages I needed loaded and the ones with "1" statuses ignored.
User Model:
class User < ActiveRecord::Base
has_many :messages
has_many :message_threads
def message_threads
MessageThread.where([ '(sender_id = ? AND sender_status = 0) OR (recipient_id = ? AND recipient_status = 0)', self.id, self.id ])
end
def messages
Message.where([ '(sender_id = ? AND sender_status = 0) OR (recipient_id = ? AND recipient_status = 0)', self.id, self.id ])
end
MessageThread model:
class MessageThread < ActiveRecord::Base
has_many :messages
Message model:
class Message < ActiveRecord::Base
acts_as_tree
has_one :message_thread
I tried experimenting with joins but this didn't work out well for me.
def messages
joins(:messages).merge( Message.where([ '(parent_id = ? AND sender_status = 0) OR (parent_id = ? AND recipient_status = 0)',self.message_id, self.message_id ]) )
end
Help would be much appreciate.
Kind regards
I'm guessing the problem is that one_thread is returning a MessageThread object, and then you're trying to get the user id by calling self.id, but it's being called on the wrong model.
I haven't tried this code but it might work:
class User < ActiveRecord::Base
def self.threads_in_inbox
message_threads.where('(sender_id = ? AND sender_status = 0) OR (recipient_id = ? AND recipient_status = 0)', self.id, self.id)
end
end
class MessageThread < ActiveRecord::Base
def self.inbox_messages
messages.where('(sender_id = ? AND sender_status = 0) OR (recipient_id = ? AND recipient_status = 0)', self.user.id, self.user.id)
# this hits the database 1 extra time however due to self.user.id
# you could change it to inbox_messages(user_id) and pass in the id manually
end
end
first_thread = current_user.threads_in_inbox.first
first_thread.inbox_messages
In your question it seems like you're overriding the association queries generated by rails (message_threads and messages) - not sure if it's on purpose but I changed it above.
Also, I think you can remove the duplication and do the following:
where('(sender_id = ?) AND (sender_status = 0 OR recipient_status = 0)', self.id)

Resources