Refactoring a large method with many conditions - Ruby - ruby-on-rails

I have this method:
method:
def unassigned_workers?(users)
assigned_users = []
unassigned_users = []
users.each do |user|
if user.designated_to_assignment?(self)
assigned_users << user
else
unassigned_users << user
end
end
if unassigned_users.count > 0
true
else
false
end
end
It's in my Assignment model. The assignment model has many Users, and basically what this method is trying to do is check if the user is designated to the assignment based on another relationship I have setup. It checks if the user is assigned and pushes it on the correct array. Does anybody know how I can refactor this to be smaller and more readable?

How about using any?
assigned_users not necessarily required.
def unassigned_workers?(users)
users.any? { |user| !user.designated_to_assignment?(self) }
end

not sure why you have assigned_users at all
try:
def unassigned_workers?(users)
users.reject { |user| user.designated_to_assignment?(self) }.count > 0
end
reject removes elements from a collection that match a predicate.
Moreover passing a self in a model as an argument is a code smell, maybe the dependencies are reversed

Related

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.

Interpolating an attribute's key before save

I'm using Rails 4 and have an Article model that has answer, side_effects, and benefits as attributes.
I am trying to create a before_save method that automatically looks at the side effects and benefits and creates links corresponding to another article on the site.
Instead of writing two virtually identical methods, one for side effects and one for benefits, I would like to use the same method and check to assure the attribute does not equal answer.
So far I have something like this:
before_save :link_to_article
private
def link_to_article
self.attributes.each do |key, value|
unless key == "answer"
linked_attrs = []
self.key.split(';').each do |i|
a = Article.where('lower(specific) = ?', i.downcase.strip).first
if a && a.approved?
linked_attrs.push("<a href='/questions/#{a.slug}' target=_blank>#{i.strip}</a>")
else
linked_attrs.push(i.strip)
end
end
self.key = linked_attrs.join('; ')
end
end
end
but chaining on the key like that gives me an undefined method 'key'.
How can I go about interpolating in the attribute?
in this bit: self.key you are asking for it to literally call a method called key, but what you want, is to call the method-name that is stored in the variable key.
you can use: self.send(key) instead, but it can be a little dangerous.
If somebody hacks up a new form on their browser to send you the attribute called delete! you don't want it accidentally called using send, so it might be better to use read_attribute and write_attribute.
Example below:
def link_to_article
self.attributes.each do |key, value|
unless key == "answer"
linked_attrs = []
self.read_attribute(key).split(';').each do |i|
a = Article.where('lower(specific) = ?', i.downcase.strip).first
if a && a.approved?
linked_attrs.push("<a href='/questions/#{a.slug}' target=_blank>#{i.strip}</a>")
else
linked_attrs.push(i.strip)
end
end
self.write_attribute(key, linked_attrs.join('; '))
end
end
end
I'd also recommend using strong attributes in the controller to make sure you're only permitting the allowed set of attributes.
OLD (before I knew this was to be used on all attributes)
That said... why do you go through every single attribute and only do something if the attribute is called answer? why not just not bother with going through the attributes and look directly at answer?
eg:
def link_to_article
linked_attrs = []
self.answer.split(';').each do |i|
a = Article.where('lower(specific) = ?', i.downcase.strip).first
if a && a.approved?
linked_attrs.push("<a href='/questions/#{a.slug}' target=_blank>#{i.strip}</a>")
else
linked_attrs.push(i.strip)
end
end
self.answer = linked_attrs.join('; ')
end

Return all user profile except current users'

I am iterating over all user profiles to get their country. I wish not to include the current user's profile. If I have a total of 10 users, I want only 9 user's country:
# helper function
def method_name
Profile.all.select do |m|
n = m.country.class == String # Because most countries will be nil. A shorter way to not include nil profiles?
return n.reject! {|x| x == current_user.profile.user_id }
end
end
The method above should return all user's profile except for the current user.
The error I got is
undefined method `reject!' for false:FalseClass
The reason? In my views I could:
<%= method_name.map do |p| p.country end %>
Beware of using Profile.all here, because you will potentially use a lot of memory instantiating a profile object for every row in your database.
I think the best way to do this is to create a method within your Profile model, which performs the query, and uses pluck() to return an array containing just the countries you want.
def self.countries_except_for(user)
where.not(user_id: user.id, country: nil).pluck(:country)
end
Now you can just call:
Profile.countries_except(current_user)
To get the array of countries. This approach is much more efficient than looping through as in your question.
The line n = m.country.class == String sets n to true or false, thefore the next line fails, because you try to call reject! {|x| x == current_user.profile.user_id } on that boolean value.
One way to solve the problem might be to fix your code like this:
def method_name
Profile.all.select do |p|
p.country.present? && p != current_user.profile
end
end
Another way - and my preferred one - would be to only load matching record from the database:
def method_name
Profile.where.not(user_id: current_user.id).where.not(country: nil)
end

Rails How can one query association definitions

I have a lot of dynamic code which keeps complex relations in a string.
ex:
"product.country.continent.planet.galaxy.name"
How can I check if these relations exist?
I want a method like the following:
raise "n00b" unless Product.has_associations?("product.country.planet.galaxy")
How could I implement this?
Try this:
def has_associations?(assoc_str)
klass = self.class
assoc_str.split(".").all? do |name|
(klass = klass.reflect_on_association(name.to_sym).try(:klass)).present?
end
end
If these are active record associations, here's how you can do it:
current_class = Product
has_associations = true
paths = "country.planet.galaxy".split('.')
paths.each |item|
association = current_class.reflect_on_association( item )
if association
current_class = association.klass
else
has_associations = false
end
end
puts has_association
And this will tell you if this specific path has all the associations.
If indeed you are storing the AR associations in a string like that, this code placed in an initializer should let you do what you want. For the life of me I can't quite figure out why you'd want to do this, but I trust you have your reasons.
class ActiveRecord::Base
def self.has_associations?(relation_string="")
klass = self
relation_string.split('.').each { |j|
# check to see if this is an association for this model
# and if so, save it so that we can get the class_name of
# the associated model to repeat this step
if assoc = klass.reflect_on_association(j.to_sym)
klass = Kernel.const_get(assoc.class_name)
# alternatively, check if this is a method on the model (e.g.: "name")
elsif klass.instance_method_already_implemented?(j)
true
else
raise "Association/Method #{klass.to_s}##{j} does not exist"
end
}
return true
end
end
With this you'll need to leave off the initial model name, so for your example it would be:
Product.has_associations?("country.planet.galaxy")

Rails validate association only when loaded

I have an activity model which has_many participants and I'd like to ensure that a participant always exists when updating an activity and its participants. I have the following method in my activity model which does the trick:
def must_have_participant
if self.participants.size == 0 || self.participants.size == self.participants.to_ary.find_all{ |p| p.marked_for_destruction? }.count
self.errors[:base] << I18n.t(:msg_activity_must_have_participant)
end
end
The problem is that the participants are lazy loaded if I'm simply updating the activity on its own which I'd like to avoid. I've tried the following alternative, however, loaded? returns false when removing all participants using the :_destroy flag.
def must_have_participant
if self.new_record? || self.participants.loaded?
if self.participants.size == 0 || self.participants.size == self.participants.to_ary.find_all{ |p| p.marked_for_destruction? }.count
self.errors[:base] << I18n.t(:msg_activity_must_have_participant)
end
end
end
Is there an alternative to loaded? that I can use to know whether the participants are going to be updated?
I did something like this in a recent validation that I created. I searched for the original record and checked the original value against the new value. No guarantees my code will work for you but here is my code for your application:
orig_rec = self.find(id)
if participant_ids.size != orig_rec.participant_ids.size
Note that I checked the size of participant_ids instead of fetching all the participant records and checking the size of them. That should be more efficient.
I don't know if there is some kind of built in function to do this or not in ruby, I'll be curious to see what someone who is more rails specific may suggest.
For reference I've amended the method like so:
def must_have_participant
if self.new_record? || self.association(:participants).loaded?
if self.participants.size == 0 || self.participants.size == self.participants.select{ |p| p.marked_for_destruction? }.size
self.errors[:base] << I18n.t(:msg_must_have_participant)
end
end
end

Resources