I'm working on a project with a goal of POSTing a string and returning every third character of that string. The project is built in Ruby/Rails.
I have the (hacky) logic down for getting that third element out, putting it in a new string, and saving this new string it with the new object in the database, but my issue at the moment is that all of this logic is done in the Controller. I'd really like to follow MVC, so my goal is to get all of this logic into the Model.
Currently, the *params and create methods in my Controller look like this:
Class TestController < ApplicationController
...
def create
new_output_string = params[:input_string].split("")
final_output_string_array = []
counter = 1
new_output_string.each do |letter|
if counter % 3 == 0
final_output_string_array.append(letter)
end
counter = counter + 1
end
final_output_string = final_output_string_array.join("")
#test = Test.create(
input_string: params[:input_string],
output_string: final_output_string
)
render json: #test
end
def test_params
params.require(:test).permit(:input_string, :output_string)
end
So you can see that all of the logic for manipulating this data is in the Controller. I tried writing a method make_output_string in the model:
Class Test < ApplicationRecord
def self.make_output_string()
new_output_string = params[:input_string].split("")
final_output_string_array = []
counter = 1
new_output_string.each do |letter|
if counter % 3 == 0
final_output_string_array.append(letter)
end
counter = counter + 1
end
#final_output_string = final_output_string_array.join("")
end
I hoped that this would manipulate the data before the create action (I tried using before_create etc.) calling before_create, and referencing the method in the controller's create method, but I either get no result or an undefined method error.
How can I move my logic into the Model so that it can handle the manipulation/creation of the parameters?
You should create a custom setter for input_string on the Model and make it save both input_string and output_string.
Class Test < ApplicationRecord
def input_string=(input_string)
super(input_string)
self.output_string = generate_output(input_string)
end
private
def generate_output(input_string)
new_output_string = input_string.split("")
final_output_string_array = []
counter = 1
new_output_string.each do |letter|
if counter % 3 == 0
final_output_string_array.append(letter)
end
counter = counter + 1
end
final_output_string_array.join("")
end
end
Then your controller would be like the following.
Class TestController < ApplicationController
def create
#test = Test.create(input_string: params[:input_string])
render json: #test
end
def test_params
params.require(:test).permit(:input_string, :output_string)
end
end
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
i just want to calculate the total sum of all active events that users have paid to attend. If you could advise me i could be grateful as i am very unsure. Many thanks
event.rb
has_many :payments
payment
belongs_to :event
in the event.rb i tried the below method but no success
def self.active_events
active_events = live_events.open_events
active_events.all.each do |event|
event.price * event.payments.count
end
end
You can do this simply in following way,
total = 0
Event.live_events.open_events.find_each { |e| total += e.price * e.payments.count }
In Event.rb place it in a method with meaningful name.
This will work for you.
def self.total_price_for_active_events
total = 0
Event.live_events.open_events.find_each { |e| total += e.price * e.payments.count }
total
end
Most optimized way
def self.total_price_for_active_events
Event.live_events.open_events.joins(:payments).sum("events.price")
end
You're off too a good start! Unfortunately, what you have there is only the beginning; you're generating an array that contains the total sum for each event. All that remains is to add them together:
def self.active_events
active_events = live_events.open_events
costs = active_events.all.each do |event|
event.price * event.payments.count
end
costs.reduce(0) do |sum,x|
sum + x
end
end
You could also get real fancy and simply use:
costs.reduce(0, :+)
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
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: