I have the following code on a model, which works, but I'd love to find a way to refactor it to be something a little smaller and neater. I'd also love to only run it under the condition that the state field actually changes. Can someone point me in the right direction. This is rails 3 BTW.
What it does is look at the "state" field on a model called subscription, and if that changes from one condition to another, then makes an update in salesforce. I'm looking for a way to optimise this code.
after_save :update_salesforce
def update_salesforce
if self.state_changed? && salesforce_client
salesforce_client.materialize("Opportunity")
o = Opportunity.find_by_breatheHR_id__c(self.subscriber_id)
if o
old_state = self.state_was
new_state = self.state
#update salesforce when accounts go active
if (old_state =='trial' || old_state == 'suspended') && new_state =='active'
o.update_attribute(:Stage__c, "Active Account")
end
#update salesforce when accounts go active
if old_state =='trial' && new_state =='suspended'
o.update_attribute(:Stage__c, "Trial Suspended")
end
#update salesforce when accounts go active
if old_state =='active' && new_state =='suspended'
o.update_attribute(:Stage__c, "Account Suspended")
end
#update salesforce when accounts go active
if old_state =='active' && new_state =='inactive'
o.update_attribute(:Stage__c, "Account Cancelled")
end
#update salesforce when accounts go active
if old_state =='trial' && new_state =='inactive'
o.update_attribute(:Stage__c, "Trial Cancelled")
end
end_of_day
end
end
In terms of only running it, I tried
after_save :update_salesforce, :if => self.state_changed?
but it doesn't recognise "self" at this point.
Here are several refactorings that you can apply:
Remove unnecessary self
You don't need to write self every time, only when setting values, because otherwise Ruby will create a local variable instead of calling the setter method. In your case, all occurrences of self are superfluous and just add noise.
Flatten nested conditionals
You need to get rid of unnecessary nesting, it makes the code much easier to read and understand. Generally speaking, you can return early when preconditions fail. For example:
def update_salesforce
if state_changed? && salesforce_client
o = Opportunity.find_by_breatheHR_id__c(subscriber_id)
if o
# perform hard work
end
end
end
# becomes
def update_salesforce
return unless state_changed? && salesforce_client
o = Opportunity.find_by_breatheHR_id__c(subscriber_id)
return unless o
# perform hard work
end
Use ActiveRecord's :if option
In the special case of the check whether the state has changed, you can use the :if option on the after_save callback. You were very close, but you either need to supply the method name as symbol, or use a proc. For a simple situation like this one, you can use the symbol form:
after_save :update_salesforce, :if => :state_changed?
Use statement modifiers
The other case is also special in that you can tack the return at the end of the line with a statement modifier or. This is actually the intended use of or, whereas you should always use || for boolean operations. The transformation is
x = do_something
return unless x
# becomes
x = do_something or return
Replace repetitive conditionals with hash access
When you look at the method, most lines repeat over and over again with only slight variation. What you can do about this is to extract the state transition logic into a separate method. In this method you can pull off a trick and put the varying states into a hash, from which you select the appropriate value. The keys are arrays representing the transition, and the values are the new values for the Stage__c field. When nil is returned, Stage__c must not be updated. For example:
def new_stage_from_state_transition(old_state, new_state)
{
['suspended', 'active' ] => 'Active Account',
['trial' , 'active' ] => 'Active Account',
['trial' , 'suspended'] => 'Trial Suspended',
['active' , 'suspended'] => 'Account Suspended',
['active' , 'inactive' ] => 'Account Cancelled',
['trial' , 'inactive' ] => 'Trial Cancelled'
}[[old_state, new_state]]
end
Put everything together
All of this results in much cleaner and less repetitive code:
after_save :update_salesforce, :if => :state_changed?
def update_salesforce
return unless salesforce_client
o = Opportunity.find_by_breatheHR_id__c(subscriber_id) or return
new_stage = new_stage_from_state_transition(state_was, state) or return
o.update_attribute(:Stage__c, new_stage)
end
def new_stage_from_state_transition(old_state, new_state)
{
['suspended', 'active' ] => 'Active Account',
['trial' , 'active' ] => 'Active Account',
['trial' , 'suspended'] => 'Trial Suspended',
['active' , 'suspended'] => 'Account Suspended',
['active' , 'inactive' ] => 'Account Cancelled',
['trial' , 'inactive' ] => 'Trial Cancelled'
}[[old_state, new_state]]
end
#update salesforce when accounts go active
if new_state =='active'
o.update_attribute(:Stage__c, "Active Account")
else
what = old_state == 'active' ? "Account" : "Trial"
change = new_state == 'inactive' ? "Canceled" : "Suspended"
o.update_attribute(:Stage__c, "#{what} #{change}" )
end
Related
I have a few counters getting stored in REDIS that get updated upon state changes in customer.rb. The things I need to store are:
1) count of customers associated with a user (user has_many customers)
2) count of customers that have a state of (using aasm_state) 'open' or 'claimed'
3) count of customers that have a state of (using aasm_state) 'open
Whenever a customer's state changes, I increment/decrement the redis counters accordingly. However, no matter what i've tried, the counts seems to always be off after a certain time period.
I'm using Sidekiq but I don't think it's a concurrency issue since REDIS shouldn't be subject to problems of concurrency, right?
Here's my count updater method:
def reset_stats
if aasm_state_was == 'open' && aasm_state == 'claimed' # open => assigned
# update company and user
user.redis_increment_my_customers_length
company.redis_decrement_open_customers_length
elsif user_id_changed? && aasm_state_was == 'claimed' && aasm_state == 'claimed' # assigned => assigned
# update users (assigner and assignee)
user_was = User.find(user_id_was)
user.redis_increment_my_customers_length
user_was.redis_decrement_my_customers_length
elsif aasm_state_was == 'claimed' && aasm_state == 'closed' # assigned => closed
# update company and user
user_was = User.find(user_id_was)
user_was.redis_decrement_my_customers_length
company.redis_decrement_all_customers_length
elsif aasm_state_was == 'closed' && aasm_state == 'claimed' # closed => assigned
# update company and user
user.redis_increment_my_customers_length
company.redis_increment_all_customers_length
elsif aasm_state_was == 'closed' && aasm_state == 'open' # closed => open
# update company
company.redis_increment_all_customers_length
company.redis_increment_open_customers_length
elsif aasm_state_was == 'open' && aasm_state == 'closed' # open => closed
# update company
company.redis_decrement_all_customers_length
company.redis_decrement_open_customers_length
end
and in user.rb:
def redis_length_key
"my_customers_length_for_#{id}"
end
def set_my_customers_length(l)
RED.set(redis_length_key, l)
l.to_i
end
def redis_increment_my_customers_length
RED.get(redis_length_key) ? RED.incr(redis_length_key) : my_customers_length
end
def redis_decrement_my_customers_length
RED.get(redis_length_key) ? RED.decr(redis_length_key) : my_customers_length
end
def my_customers_length
if l = RED.get(redis_length_key)
l.to_i
else
set_my_customers_length(my_customers.length)
end
end
and in company.rb:
def open_customers
customers.open
end
def redis_open_length_key
"open_customers_length_for_#{id}"
end
def set_open_customers_length(l)
RED.set(redis_open_length_key, l)
l.to_i
end
def redis_increment_open_customers_length
RED.get(redis_open_length_key) ? RED.incr(redis_open_length_key) : open_customers_length
end
def redis_decrement_open_customers_length
RED.get(redis_open_length_key) ? RED.decr(redis_open_length_key) : open_customers_length
end
def open_customers_length
if l = RED.get(redis_open_length_key)
return l.to_i
else
set_open_customers_length(open_customers.length)
end
end
def redis_all_length_key
"all_customers_length_for_#{id}"
end
def set_all_customers_length(l)
RED.set(redis_all_length_key, l)
l
end
def redis_increment_all_customers_length
RED.get(redis_all_length_key) ? RED.incr(redis_all_length_key) : all_customers_length
end
def redis_decrement_all_customers_length
RED.get(redis_all_length_key) ? RED.decr(redis_all_length_key) : all_customers_length
end
def all_customers_length
if l = RED.get(redis_all_length_key)
l.to_i
else
set_all_customers_length(open_or_claimed_customers.length)
end
end
def open_or_claimed_customers
customers.open_or_claimed
end
Is there a better pattern for what I'm trying to accomplish? This has been extremely frustrating because the counts always seem to become incorrect after a while. Please help!
You have a race condition between the time you call set_my_customers_length(my_customers_length + 1) and the time you call RED.set(redis_open_length_key, l).
Two processes start.
my_customers_length is 5 when that first call is made for both processes.
First process makes the second call and sets Redis to 6.
Second process makes the second call and sets Redis to 6 again.
Redis value should in fact be 7.
Consider using Redis' INCR and DECR functions to atomically update the values.
http://redis.io/commands/incr
http://redis.io/commands/decr
http://redis.io/commands/incrby
http://redis.io/commands/decrby
You have a race condition here:
RED.get(redis_all_length_key) ? RED.incr(redis_all_length_key) : all_customers_length
You cannot do any logic between reading from and writing to Redis.
I have the following complex method. I'm trying to find and implement possible improvements. Right now I moved last if statement to Access class.
def add_access(access)
if access.instance_of?(Access)
up = UserAccess.find(:first, :conditions => ['user_id = ? AND access_id = ?', self.id, access.id])
if !up && company
users = company.users.map{|u| u.id unless u.blank?}.compact
num_p = UserAccess.count(:conditions => ['user_id IN (?) AND access_id = ?', users, access.id])
if num_p < access.limit
UserAccess.create(:user => self, :access => access)
else
return "You have exceeded the maximum number of alotted permissions"
end
end
end
end
I would like to add also specs before refactoring. I added first one. How should looks like others?
describe "#add_permission" do
before do
#permission = create(:permission)
#user = create(:user)
end
it "allow create UserPermission" do
expect {
#user.add_permission(#permission)
}.to change {
UserPermission.count
}.by(1)
end
end
Here is how I would do it.
Make the check on the Access more like an initial assertion, and raise an error if that happens.
Make a new method to check for an existing user access - that seems reusable, and more readable.
Then, the company limit is more like a validation to me, move this to the UserAccess class as a custom validation.
class User
has_many :accesses, :class_name=>'UserAccess'
def add_access(access)
raise "Can only add a Access: #{access.inspect}" unless access.instance_of?(Access)
if has_access?(access)
logger.debug("User #{self.inspect} already has the access #{access}")
return false
end
accesses.create(:access => access)
end
def has_access?(access)
accesses.find(:first, :conditions => {:access_id=> access.id})
end
end
class UserAccess
validate :below_company_limit
def below_company_limit
return true unless company
company_user_ids = company.users.map{|u| u.id unless u.blank?}.compact
access_count = UserAccess.count(:conditions => ['user_id IN (?) AND access_id = ?', company_user_ids, access.id])
access_count < access.limit
end
end
Do you have unit and or integration tests for this class?
I would write some first before refactoring.
Assuming you have tests, the first goal might be shortening the length of this method.
Here are some improvements to make:
Move the UserAccess.find call to the UserAccess model and make it a named scope.
Likewise, move the count method as well.
Retest after each change and keep extracting until it's clean. Everyone has a different opinion of clean, but you know it when you see it.
Other thought, not related to moving the code but still cleaner :
users = company.users.map{|u| u.id unless u.blank?}.compact
num_p = UserAccess.count(:conditions => ['user_id IN (?) AND access_id = ?', users, access.id])
Can become :
num_p = UserAccess.where(user_id: company.users, access_id: access.id).count
I have a class that I use to contain select menu options for property types. It works fine. However, I need to be able to verify the selection and perform specific logic based on the selected option. This needs to happen in my Ruby code and in JavaScript.
Here is the class in question:
class PropertyTypes
def self.[](id)
##types[id]
end
def self.options_for_select
##for_select
end
private
##types = {
1 => "Residential",
2 => "Commercial",
3 => "Land",
4 => "Multi-Family",
5 => "Retail",
6 => "Shopping Center",
7 => "Industrial",
8 => "Self Storage",
9 => "Office",
10 => "Hospitality"
}
##for_select = ##types.each_pair.map{|id, display_name| [display_name, id]}
end
What is the best way to verify the selection? I need to perform specific logic and display user interface elements based on each type of property type.
Since I am storing the id, I would be verifying that the id is a particular property type. Something like:
PropertyTypes.isResidential?(id)
Then this method would look like this:
def self.isResidential?(id)
##types[id] == "Residential"
end
But now I am duplicating the string "Residential".
For JavaScript, I assume I would make an ajax call back to the model to keep the verification code DRY, but this seems like over kill.
Do I need to manually create a verification method for each property type or can I use define_method?
This seems so basic yet I am confused and burned out on this problem.
Thanks
===
Here's my solution:
class << self
##types.values.each do |v|
# need to remove any spaces or hashes from the found property type
v = v.downcase().gsub(/\W+/, '')
define_method "is_#{v}?", do |i|
type_name = ##types[i]
return false if type_name == nil #in case a bogus index is passed in
type_name = type_name.downcase().gsub(/\W+/, '')
type_name == v
end
end
end
It sounds like you can benefit from some Ruby meta-programming. Try googling "ruby method_missing". You can probably do something quick & dirty along the lines of:
class PropertyTypes
def method_missing(meth, *args, &block)
if meth.to_s =~ /^is_(.+)\?$/
##types[args.first] == $1
else
super
end
end
end
On the ruby side you could also use something like this to define dynamically these methods:
class << self
##types.values.each do |v|
define_method "is_#{v}?", do |i|
##types[i] == v
end
end
end
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
How can I validate a number within a range dynamically using existing data?
For example - I have certain discounts on bulk ordering of products. If a customer buys 10-50 units they get X off and if they order 51-200 units Y off.
How can I validate this so that users can't put in quantity discounts over the same range?
I don't quite understand your question but I'm sure a custom validation would be one way to solve whatever you are trying to achieve. Simply add a validate method in your model like so:
def validate
self.errors.add(:amount, "is out of range") unless self.amount_in_allowed_range
end
private
def amount_in_allowed_range
# logic to return true or false
end
If I understand your question correctly then you are trying to avoid the creation of a discount range that overlaps an already existing one. The following code should do this for you
class QtyDiscount < ActiveRecord::Base
def validate
self.errors.add(:amount, "overlaps an existing range")
unless self.amount_in_allowed_range
end
def amount_in_allowed_range
# Check for overlapping ranges where our record either
# - overlaps the start of another
# - or overlaps the end of another
conditions = "
id != :id AND (
( min_value BETWEEN :min_value AND :max_value) OR
( max_value BETWEEN :min_value AND :max_value))"
puts "Conditions #{conditions}"
overlaps = QtyDiscount.find(:all, :conditions =>
[ conditions, { :id => self.id.nil? ? 0 : self.id,
:min_value => self.min_value,
:max_value => self.max_value} ])
overlaps.size == 0
end
end
EDITED
Removed an extraneous condition and added some checking for self.id to ensure we are not getting a false negative from our own record