Moving business rules into model - ruby-on-rails

I asked a question earlier which elicited some great responses.
Here's the earlier question
On the back of some advice given there, I've tried moving the following controller logic
if params[:concept][:consulted_legal] == 0 && params[:concept][:consulted_marketing] == 1
#concept.attributes = {:status => 'Awaiting Compliance Approval'}
elsif params[:concept][:consulted_marketing] == 0 && params[:concept][:consulted_legal] == 1
#concept.attributes = {:status => 'Awaiting Marketing Approval'}
elsif params[:concept][:consulted_marketing] == 0 && params[:concept][:consulted_legal] == 0
#concept.attributes = {:status => 'Awaiting Marketing & Legal Approval'}
else
#concept.attributes = {:status => 'Pending Approval'}
end
into the model, as so:
def set_status
if status.blank?
if (consulted_legal == true) && (consulted_marketing == true)
status = "Pending Approval"
elsif (consulted_legal == true) && (consulted_marketing == false)
status = "Awaiting Marketing Approval"
elsif (consulted_legal == false) && (consulted_marketing == true)
status = "Awaiting Legal Approval"
elsif (consulted_legal == false) && (consulted_marketing == false)
status = "Awaiting Marketing & Legal Approval"
end
end
true # Needs to return true for the update to go through
end
I am calling that from a before_save callback.
As a default, both the consulted_legal and consulted_marketing attributes are set to false and not null, which is why I am testing for == false or true here, instead of asking
if consulted_legal?
for instance.
However, this logic doesn't seem to be working. If I inspect the object, status is not being set to anything, ever. Can anyone spot why this might be happening? Have I got how attributes are accessed wrong in models, for instance?
TIA

Instead of status = try self.status =. I've found that I needed to use self. to change a model's attribute within the model.
It's also much better to have errors.empty? at the end instead of true, so if you ever use errors.add_to_base in the future, your set_status method is ready to abort a save.
Edit:
You may also want to check out acts_as_state_machine. It looks like a plugin for exactly what you're doing.

Are you setting the parameters from user input?
If they're not defined as boolean database columns, then you'll be assigning a string to them, which will never be equal to true.

Related

Simplifying method

I have this method which works fine but I'm thinking that it may be improved, either for readability and/or efficience.
def default_something
something =
Legal::Something.find_for_A_in_placea('xx', claim.blabla.identifier) ||
Legal::Something.find_for_B_in_placeb('xx', claim.eligible.first.bleble.indivcode) ||
Legal::Something.find_for_B_in_placeb('xx', claim.eligible.last.blublu.indivcode) ||
Legal::Something.find_by(id: DEFAULT_SOMETHING_ID)
{
'name' => something.name,
'day' => something.meta_data[:day],
'hour' => something.meta_data[:hour],
}
end
I can "beautify it" by creating some more methods like:
def default_something
something = def A || def B || (etc)
end
def A
Legal::Something.find_for_A_in_placea('xx', claim.blabla.identifier)
end
def B
Legal::Something.find_for_B_in_placeb('xx', claim.eligible.first.bleble.indivcode) ||
Legal::Something.find_for_B_in_placeb('xx', claim.eligible.last.blublu.indivcode)
end
In addition I should say:
find_for_B part only retrieves a value when claim.eligible.first.bleble.indivcode || claim.eligible.last.blublu.indivcode = 'ASL'
Is the "beautified" version the way to go?
And/or should I add an if statement regarding
Legal::Something.find_for_B_in_placeb('xx', claim.eligible.first.bleble.indivcode) ||
Legal::Something.find_for_B_in_placeb('xx', claim.eligible.last.blublu.indivcode)
to improve efficiency and readability, stating it happens only when "indivcode" = "ASL"?
What else can I do?

Sort membership tiers by names on index page

So I have a drivers (user table) which has a relationship with the subscriptions table. There are 3 different tiers available: Gold, Silver and a Free tier. What i want to do is group and order by tiers, so I'd have the golds together, silvers together etc in descending order.
What i have now in my controller:
class DriversController < ApplicationController
def index
order_subs = Driver.order_by_subs.all
def gold_drivers
Driver.select { |driver| driver.subscriptions.stripe_plan == 'price_xxx' || driver.subscriptions.stripe_plan == 'price_xxx'}
end
def silver_drivers
Driver.select { |driver| driver.subscriptions.stripe_plan == 'price_xxx' || driver.subscriptions.stripe_plan == 'price_xxx'}
end
def free_drivers
Driver.select { |driver| driver.subscriptions == 'null' || driver.subscriptions == ''}
end
#pagy, #drivers = pagy(
Driver.joins(:profile).select(
'drivers.*',
'(profiles.no_races + profiles.no_poles + profiles.no_podiums + profiles.no_wins) AS score'
).reorder(gold_drivers, silver_drivers, free_drivers, score),
page: params[:page],
items: 16
)
end
end
So my thoughts were I could select the records under a variable i.e gold_drivers and then add them as I would in the reorder section in the #pagy pagination section .reorder(gold_drivers, silver_drivers, free_drivers, score) At the moment when i run the page I get the error undefined method stripe_plan' for nil:NilClass` so i'm guessing it can't find the column. If it's a free user, they won't have a record in the subscription table. Thanks
EDIT: driver model
scope :is_gold, -> { where("drivers.subscriptions.stripe_plan == 'price_xxx'") || where("drivers.subscriptions.stripe_plan == 'price_xxx'") }
scope :is_silver, -> { where("drivers.subscriptions.stripe_plan == 'price_xxx'") || where("drivers.subscriptions.stripe_plan == 'price_xxx'") }
scope :is_null, -> { where("drivers.subscriptions.stripe_plan == ''") || where("drivers.subscriptions.stripe_plan" == null) }
scope :order_by_subs, -> { reorder(:is_gold, :is_silver, :is_null) }
I have to be honest your code is incredibly confusing. It looks like you can order by subs, but I guess you can't, I'm not sure what is working and what isn't looking at your code. I see what you are doing from our previous conversation and this is off. Like I said in my comment, I would focus on understanding the basics here before you dive into some of this stuff. I'm going to fix your method and explain along the way so it hopefully makes some more sense and either works, or shows you a path to getting it to work.
class DriversController < ApplicationController
def index
# because there is no # on this it is a local variable, it will not be available in the view. Wondering also why you didn't just use this for the select below. Also, if you can already order by subs why would you even need to select them, wasn't that the reason for selecting them like this in the first place?
order_subs = Driver.order_by_subs.all
# it is my understanding you want these in the view so we add the # symbol which makes it an instance variable and you access those variables in the view now
#gold_drivers = order_subs.select { |driver| driver.subscriptions.stripe_plan == 'price_xxx' || driver.subscriptions.stripe_plan == 'price_xxx'}
#silver_drivers = order_subs.select { |driver| driver.subscriptions.stripe_plan == 'price_xxx' || driver.subscriptions.stripe_plan == 'price_xxx'}
#free_drivers = order_subs.select { |driver| driver.subscriptions == 'null' || driver.subscriptions == ''}
# Not sure where your local variable 'score' is coming from, do you need to set that first?
#pagy, #drivers = pagy(
Driver.joins(:profile).select(
'drivers.*',
'(profiles.no_races + profiles.no_poles + profiles.no_podiums + profiles.no_wins) AS score'
).reorder(#gold_drivers, #silver_drivers, #free_drivers, score),
page: params[:page],
items: 16
)
end
end
O.k so now you are setting the drivers values as instance variables which can be accessed in your view.

How to use multiple #select blocks in Ruby?

Let's say I have a list of records like:
transactions = Transaction.all
And I have the following instance methods #currency, #geo, #industry. I want to select records which has the following criteria:
Select all transactions that has field currency which equals to #currency unless #currency is nil and in this case we'll ignore the condition (currency would mean all currencies when it's nil)
Select all transactions that has field geo which equals to #geo unless #geo is nil.
Select all transactions that has field industry which equals to #industry unless #industry is nil.
I tried multiple #select but with no luck something like:
transactions.select{ |i| (i.currency == #currency) unless #currency.nil? }.
.select{ |i| (i.geo == #geo) unless #geo.nil? }.
.select{ |i| (i.industry == #industry) unless #industry.nil? }
The problem with your example is the unless #currency.nil? will return nil (which is falsey) if #currency is nil, which is the opposite of what you intended.
You should use || instead:
transactions.select{ |i| (i.currency == #currency) || #currency.nil? }.
select{ |i| (i.geo == #geo) || #geo.nil? }.
select{ |i| (i.industry == #industry) || #industry.nil? }
In this case, if #currency is nil, the first condition will return true, and all elements will pass the select box to the next one...
Another option would be to run the select block only is the parameter is not nil. In this case, you'd like to break the line into separate blocks:
transactions.select!{ |i| (i.currency == #currency) } unless #currency.nil?
transactions.select!{ |i| (i.geo == #geo) } unless #geo.nil?
transactions.select!{ |i| (i.industry == #industry) } unless #industry.nil?
transactions.select do |t|
(#currency.nil? || t.currency == #currency) &&
(#geo.nil? || t.geo == #geo) &&
(#industry.nil? || t.industry == #industry)
end
this should do the job.
Or, if you are into dynamics:
[:currency, :geo, :industry].all? do |field|
(ivar = instance_variable_get("##{field}")).nil? || t.send(field) == ivar
end
Use AR/SQL instead of Ruby processing when possible:
transactions.where(currency: #currency, geo: #geo, industry: #industry)
Multiple use of select is superfluous in this situation. You can use && and || logical operators:
transactions.select do |transaction|
(#currency.nil? || transaction.currency == #currency) &&
(#geo.nil? || transaction.geo == #geo) &&
(#industry.nil? || transaction.industry == #industry)
end

Ruby: Looping through models to calculate ratios within nested hashes

I'm building a spam filter for a job app (think tinder for jobs). I'm currently helping to build a spam filter. To achieve that goal, a signal for users who are "over-applying" for jobs is their apply-to-rejection ratio per day.
To inform our threshold, I've come up with a solution to gather that data from the db by using a nested hash i.e. {user1 =>{date1=>0.33, date2=>0.66}}. My problem now is that the ratios are all 1.0, because i think i'm looping up until either rejections or applications are all gone through so the calculation is always the same number divided by itself.
Here's what i got so far. Appreciate the help.
users = User.all
ratio_hash = Hash.new
users.each do |user|
if user.job_applications.count > 0 && user.job_rejections.count > 0
ratio_hash[user.name] = Hash.new
apply_array = []
reject_array = []
user.job_rejections.each do |reject|
user.job_applications.each do |apply|
if (apply.user_id.present? && reject.user_id.present?) || rej.user_id.present?
if (apply.user_id == user.id && reject.user_id == user.id) || rej.user_id == user.id
if (apply.created_at.present? && reject.created_at.present?) || reject.created_at.present?
date = (apply_array && reject.created_at.to_date)
if (apply.created_at.to_date == reject.created_at.to_date) || reject.created_at.to_date == date
apply_array << apply.created_at.to_date
reject_array << reject.created_at.to_date
ratio_hash[user.name][(apply.created_at.to_date || reject.created_at.to_date)] = (apply_array.length.round(2)/reject_array.length)
end
end
end
end
end
end
end
end

strange nil object result

I have a module:
module Voteable
def has_up_vote_of user
return ! self.votes.select{|v| v.user.id == user.id && v.value == 1}.empty?
end
def has_down_vote_of user
return ! self.votes.select{|v| v.user.id == user.id && v.value == -1}.empty?
end
end
Which is mixed into a model:
class Comment < ActiveRecord::Base
include Voteable
end
In a controller code, there is a check:
has_up_vote = #voteable.has_up_vote_of #user
has_down_vote = #voteable.has_down_vote_of #user
#voteable and #user are existing model items, found in a DB.
Suppose, voteable item has up-vote of user. After executing the code, has_up_vote will be equal to true, and has_down_vote will be nil.
Why nil, instead of false ?
I have used several variations of methods, but the problem is the same. Even this gives me the same effect:
def has_up_vote_of user
has = self.votes.select{|v| v.user.id == user.id && v.value == 1}.empty?
return !has.nil? && has
end
Posssible, i'm misunderstanding something, but this behavior is strange
Update
I've noticed very strange behaviour.
When i change methods to trivial:
def has_up_vote_of user
return false
end
def has_down_vote_of user
return false
end
They both returns nil, when i debug the app.
But, from console, they returns false.
It's more stange, because i cannot do anything with these results. These code is not working:
has_up_vote = false if has_up_vote.nil?
has_down_vote = false if has_down_vote.nil?
I think that the debugging environment you're running in is interfering with the actual value of has_down_votes. The select method should never return nil as defined.
Instead of !{}.empty? you could use {}.present?
Its more readable and the output will always be true/false only
I know this doesn't get to the root cause of your strange problem, but it should give you the results you want. Instead of
return ! self.votes.select{|v| v.user.id == user.id && v.value == -1}.empty?
try
return !!self.votes.select{|v| v.user.id == user.id && v.value == -1}.any?
The double exclamation point is intentional -- it will cause nil to become false. (!arr.empty? is equivalent to arr.any? which is equivalent to !!arr.any? -- except the last one converts the nil to false)

Resources