Access to old association inside Rails/ActiveRecord callback - ruby-on-rails

In my Rails callback I have something like this:
private
def update_points
if user_id_changed?
user_was = User.find(user_id_was)
user_was.points -= points_was
user_was.save
user.points += points
user.save
end
end
Is that the proper way to do this with user_was? I initially just assumed user_was was already defined (or could be defined on the spot) because user_id_was existed.

It's not clear to me from the context what exactly you're doing (maybe the 2nd points is supposed to be points_was?). For mildly improved clarity, depending on who you ask, and with fewer lines:
...
user_was = User.find(user_id_was)
user_was.update_column :points, user_was.points - points_was
user.update_column :points, user.points + points
...

Related

Rails validations and initializations with interdependent fields

After coding in Rails for a couple of years, I still don't understand the Rails Way for more advanced concepts. The Rails Way is so specific about convention over configuration, however when you get into Enterprise coding, all rules go out the window and everyone writes blogs with non-standard way of doing things. Here's another one of those situations: contextual validations where the context is a little more complex (fields depend on each other). Basically in a shipping application, I need to initialize an AR object with request params and with some calculated values. The calculated values are dependent on the request params, and I'm not sure how to initialize and validate my member variables.
table mailpieces
mail_class
weight
sort_code
t_indicator_id
end
class Mailpiece
validates_presence_of: :mail_class
validates_presence_of: :weight
validates_presence_of: :sort_code
validates_presence_of: :t_indicator_id
def some_kind_of_initializer
if mail_class == 'Priority'
sort_code = '123'
elsif mail_class == 'Express'
if weight < 1
sort_code = '456'
else
sort_code = '789'
end
end
t_indicator = ndicator.find_by(name: 'blah')
if sort_code = '456'
t_indicator = Indicator.find_by(name: 'foobar')
end
end
end
mailpiece = Mailpiece.new(
mail_class: params[:mail_class],
weight: params[:weight])
#mailpiece.some_kind_of_initializer ?!
raise 'SomeError' if !mailpiece.valid?
What should some_kind_of_initializer be?
Override of ActiveRecord initialize? That's not good practice.
after_initialize. More Rails Way-sy.
Custom method called after
Mailpiece.new (e.g. mailpiece.some_kind_of_initializer)
Whichever of the above choices, the problem is that the initialization of sort_code and t_indicator depends on mail_class and weight being valid. Given that mail_class and weight should be not null before I enter some_kind_of_initializer, how should I write my validations?
Extract all validations into a json schema validation. More complex business rules around mail_class and weight are difficult to write in a json schema.
Extract all validations into some type of Data Transfer Object validation class. Moves away from the Rails Way of doing things. Feels like I'm writing in .NET/Java and I'm afraid that Rails will kick my azz later (in validations, testing, etc.).
Assign sort_code only if mail_class and weight have been initialized. This seems to be most Rails Way to write things, but it's tough. So many if/else. This is just a simple example, but my mailpiece has references that have references and they all do these type of validations. If this is the right answer, then I'm getting a gut feeling that it might be easier to move ALL validations and ALL initializations to an external class/module - perhaps getting close to option #2.
Option 3 code rewrite
def some_kind_of_initializer
if mail_class && weight
if (mail_class == 'Priority')
sort_code = '123'
elsif (mail_class == 'Express')
if weight < 1
sort_code = '456'
else
sort_code = '789'
end
end
end
if sort_code
t_indicator = Indicator.find_by(name: 'blah')
if sort_code = '456'
t_indicator = Indicator.find_by(name: 'foobar')
end
end
end
I'd love to hear your opinion on this. It seems to me this is such a popular use case of AR and I'm not sure what to do. Again, this is a just a simple case. My Mailpiece model has many other references that have dependencies on the mailpiece object properties and their own interdependencies in the same style as above.
I'm not sure if it helps or I understood your question correctly, but maybe using "case" instead of if/elsif/else, the ternary operator, some lazy assignment and some extra validations can give your code a better "rails" feeling.
With lazy assignment I mean that you could wait to initialize sort_code and t_indicator until you actually need to use it or save it.
def sort_code
return self[:sort_code] if self[:sort_code].present?
sort_code = case mail_class
when 'Priority' then '123'
when 'Express'
weight < 1 ? '456' : '789' if weight
end
self[:sort_code] = sort_code
end
That way, sort_code gets initialized right before you need to use it the first time so you can do Mailpiece.new(mail_class: 'Something') and forget about initializing sort_code right away.
The problem would come when you need to save the object and you never called mailpiece.sort_code. You could have a before_validation callback that justs calls sort_code to initialize it in case it wasn't already.
The same can be done for t_indicator.
I would also add some context in your validations
validates_presence_of :weight, if: Proc.new{|record| 'Express' == record.mail_class} #you could add some helper method "is_express?" so you can just do "if: :is_express?"
validates_presence_of :sort_code, if: Proc.new{|record| 'Priority' == record.mail_class or 'Express' == record.mail_class && record.weight.present?}
Sorry if I missunderstood your question, I didn't even use your options haha.
How about:
class Mailpiece
validates_presence_of: :mail_class
validates_presence_of: :weight
validates_presence_of: :sort_code
validates_presence_of: :t_indicator_id
def default_mail_class
'Priority'
end
def default_sort_code
mail_class = mail_class || default_mail_class
if mail_class == 'Priority'
'123'
elsif mail_class == 'Express'
weight < 1 ? '456' : '789'
end
end
end
Then, when you needed to figure out t_indicator, just do it as-needed:
def is_foobar_indicator?
sort_code || default_sort_code == '456'
end
def t_indicator
indicator_params = {name: is_foobar_indicator? ? 'foobar' : 'blah'}
Indicator.find_by(indicator_params)
end
So you can still have the validation on sort_code (assuming that is user provided) but still use defaults when looking at t_indicator. I don't know how complicated the rest of your model is, but I would suggest not looking up values until you need them. Also, after_initialize is risky because it runs your code exactly where it claims - after every initialize, so if you run a query for N items but do nothing but find on them, or wind up not using the defaults you're setting, after_initialize runs N times for nothing.

Rails 5 - iterate until field matches regex

In my app that I am building to learn Rails and Ruby, I have below iteration/loop which is not functioning as it should.
What am I trying to achieve?
I am trying to find the business partner (within only the active once (uses a scope)) where the value of the field business_partner.bank_account is contained in the field self_extracted_data and then set the business partner found as self.sender (self here is a Document).
So once a match is found, I want to end the loop. A case exists where no match is found and sender = nil so a user needs to set it manually.
What happens now, is that on which ever record of the object I save (it is called as a callback before_save), it uses the last identified business partner as sender and the method does not execute again.
Current code:
def set_sender
BusinessPartner.active.where.not(id: self.receiver_id).each do |business_partner|
bp_bank_account = business_partner.bank_account.gsub(/\s+/, '')
rgx = /(?<!\w)(#{Regexp.escape(bp_bank_account)})?(?!\‌​w)/
if self.extracted_data.gsub(/\s+/, '') =~ rgx
self.sender = business_partner
else
self.sender = nil
end
end
end
Thanks for helping me understand how to do this kind of case.
p.s. have the pickaxe book here yet this is so much that some help / guidance would be great. The regex works.
Using feedback from #moveson, this code works:
def match_with_extracted_data?(rgx_to_match)
extracted_data.gsub(/\s+/, '') =~ rgx_to_match
end
def set_sender
self.sender_id = matching_business_partner.try(:id) #unless self.sender.id.present? # Returns nil if no matching_business_partner exists
end
def matching_business_partner
BusinessPartner.active.excluding_receiver(receiver_id).find { |business_partner| sender_matches?(business_partner) }
end
def sender_matches?(business_partner)
rgx_registrations = /(#{Regexp.escape(business_partner.bank_account.gsub(/\s+/, ''))})|(#{Regexp.escape(business_partner.registration.gsub(/\s+/, ''))})|(#{Regexp.escape(business_partner.vat_id.gsub(/\s+/, ''))})/
match_with_extracted_data?(rgx_registrations)
end
In Ruby you generally want to avoid loops and #each and long, procedural methods in favor of Enumerable iterators like #map, #find, and #select, and short, descriptive methods that each do a single job. Without knowing more about your project I can't be sure exactly what will work, but I think you want something like this:
# /models/document.rb
class Document < ActiveRecord::Base
def set_sender
self.sender = matching_business_partner.try(:id) || BusinessPartner.active.default.id
end
def matching_business_partners
other_business_partners.select { |business_partner| account_matches?(business_partner) }
end
def matching_business_partner
matching_business_partners.first
end
def other_business_partners
BusinessPartner.excluding_receiver_id(receiver_id)
end
def account_matches?(business_partner)
rgx = /(?<!\w)(#{Regexp.escape(business_partner.stripped_bank_account)})?(?!\‌​w)/
data_matches_bank_account?(rgx)
end
def data_matches_bank_account?(rgx)
extracted_data.gsub(/\s+/, '') =~ rgx
end
end
# /models/business_partner.rb
class BusinessPartner < ActiveRecord::Base
scope :excluding_receiver_id, -> (receiver_id) { where.not(id: receiver_id) }
def stripped_bank_account
bank_account.gsub(/\s+/, '')
end
end
Note that I am assigning an integer id, rather than an ActiveRecord object, to self.sender. I think that's what you want.
I didn't try to mess with the database relations here, but it does seem like Document could include a belongs_to :business_partner, which would give you the benefit of Rails methods to help you find one from the other.
EDIT: Added Document#matching_business_partners method and changed Document#set_sender method to return nil if no matching_business_partner exists.
EDIT: Added BusinessPartner.active.default.id as the return value if no matching_business_partner exists.

Rails optimistic locking update within a loop appears to work until I check from outside of the loop

I'm using optimistic locking on a Rails model. Inside of a loop, I update and save this model (or, rather, many instances of this model).
From inside the loop, I output the "before" and "after" values, and the field appears to be updated correctly. But afterward, when I find the models by ID, I see that the field is not updated. Can anyone spot my error?
class Usage::LeadDistributionWeight < ActiveRecord::Base
attr_accessible :agent_id, :tag_value_id, :weight, :premium_limit, :countdown, :lock_version, :tag_value
def increment_countdown!
self.countdown = self.countdown + self.weight
save
rescue ActiveRecord::StaleObjectError
attempts_to_crement_countdown ||= 0
attempts_to_crement_countdown += 1
self.increment_countdown! unless attempts_to_crement_countdown > 5
false
end
def self.increment_countdowns parent_id, lead_type_id
if lead_type_id.present?
joins(:agent)
.where("#{reflect_on_association(:agent).table_name}.parent_id = ?", parent_id)
.where(tag_value_id:lead_type_id)
.all(readonly:false).each { |weight|
prev = weight.countdown
if weight.increment_countdown!
puts "#{prev} differs from #{weight.countdown}"
else
puts "no difference!"
end
}
end
end
end

Why this application controller won't return correct value to controller who called?

Why do I get empty when I execute this? Asumming User's point is 2500. It should return 83
posts_controller
#user = User.find(params[:id])
#user.percentage = count_percentage(#user)
#user.save
application_controller
def count_percentage(user)
if user
if user.point > 2999
percentage = ((user.point / 5000.0) * 100 ).to_i
elsif user.point > 1999
percentage = ((user.point / 3000.0) * 100 ).to_i
end
return percentage
end
end
It looks like there is some lack of basic understanding, so I try to focus on a few points here.
count_percentage belongs to your model. It is a method which does things that are tied to your User records. In your example, the code can only be used with a User record. Therefore it belongs to your User class!
class User < ActiveRecord::Base
def percentage
if self.point > 2999
return ((self.point / 5000.0) * 100 ).to_i
elsif user.point > 1999
return ((self.point / 3000.0) * 100 ).to_i
else
# personally, I'd add a case for points range 0...3000
end
end
As you said, you want to put that value into your "side menu", so this allows to e.g #user.percentage there which gives you the desired percentage.
According to your description (if I understood it the right way) you want to store the calculated value in your user model. And here we go again: Model logic belongs into your model class. If you want to keep your percentage value up to date, you'd add a callback, like:
class User < ActiveRecord::Base
before_save :store_percentage
def precentage
# your code here
end
private
def store_percentage
self.my_percentage_field_from_my_database = self.percentage
end
end
To your actual problem: user.point may be NULL (db) / nil (ruby), so you should convert it to_i before actually calculating with it. Also that's why I've suggested an else block in your condition; To return a value wether or not your user.point is bigger than 1999 (if it even is an integer.. which I doubt in this case).
To answer on this question you should try to examine user.point, percentage value in the count_percentage method.
I strongly recommend pry. Because it's awesome.

How to format values before saving to database in rails 3

I have a User model with Profit field. Profit field is a DECIMAL (11,0) type. I have a masked input on the form which allows user to input something like $1,000. I want to format that value and remove everything except numbers from it so i will have 1000 saved. Here is what i have so far:
class User < ActiveRecord::Base
before_save :format_values
private
def format_values
self.profit.to_s.delete!('^0-9') unless self.profit.nil?
end
end
But it keeps saving 0 in database. Looks like it is converting it to decimal before my formatting function.
Try this:
def profit=(new_profit)
self[:profit] = new_profit.gsub(/[^0-9]/, '')
end
First of all, this:
def format_values
self.profit.to_s.delete!('^0-9') unless self.profit.nil?
end
is pretty much the same as this:
def format_values
return if(self.profit.nil?)
p = self.profit
s = p.to_s
s.delete!('^0-9')
end
So there's no reason to expect your format_values method to have any effect whatsoever on self.profit.
You could of course change format_values to assign the processed string to self.profit but that won't help because your cleansing logic is in the wrong place and it will be executed after '$1,000' has been turned into a zero.
When you assign a value to a property, ActiveRecord will apply some type conversions along the way. What happens when you try to convert '$1,000' to a number? You get zero of course. You can watch this happening if you play around in the console:
> a = M.find(id)
> puts a.some_number
11
> a.some_number = 'pancakes'
=> "pancakes"
> puts a.some_number
0
> a.some_number = '$1,000'
=> "1,000"
> puts a.some_number
0
> a.some_number = '1000'
=> "1000"
> puts a.some_number
1000
So, your data cleanup has to take place before the data goes into the model instance because as soon as AR gets its hands on the value, your '$1,000' will become 0 and all is lost. I'd put the logic in the controller, the controller's job is to mediate between the outside world and the models and data formatting and mangling certainly counts as mediation. So you could have something like this in your controller:
def some_controller
fix_numbers_in(:profit)
# assign from params as usual...
end
private
def fix_numbers_in(*which)
which.select { |p| params.has_key?(p) }.each do |p|
params[p] = params[p].gsub(/\D/, '') # Or whatever works for you
end
end
Then everything would be clean before ActiveRecord gets its grubby little hands on your data and makes a mess of things.
You could do similar things by overriding the profit= method in your model but that's really not the model's job.
class User < ActiveRecord::Base
before_save :format_values
private
def format_values
self.profit = profit.to_s.gsub(/\D/,'') if profit
end
end
def format_values
self.profit.to_d!
end
I recommend you to write custom setter for this particular instance variable #profit:
class User
attr_accessor :profit
def profit= value
#profit = value.gsub(/\D/,'')
end
end
u = User.new
u.profit = "$1,000"
p u.profit # => "1000"
I would suggest using the rails helper of number with precision. Below is some code.
Generic Example:
number_with_precision(111.2345, :precision => 1, :significant => true) # => 100
Rails code Example:
def profit=(new_profit)
number_with_precision(self[:profit], :precision => 1, :significant => true)
end

Resources