Rails/Ruby extract if block to helper or guard - ruby-on-rails

In my Rails app I need to implement authentication for web app, I need to use an external resource to make it work. To do so I'm using custom Devise Strategies. After a tremendous amount of work, I finally managed to implement a code that covers all scenarios - the code is working but unfortunately my eyes bleed when I see the code below:
module Devise
module Strategies
class AwareLogin < Authenticatable
def authenticate!
# some logic
# (...)
if login.valid_password?(password) && aware_response.success?
success!(login)
elsif login.valid_password?(password) && !aware_response.success?
success!(login)
elsif login.id.nil? && aware_response.success?
login.set_user_tokens(aware_response)
success!(login)
elsif !login.valid_password?(password) && !aware_response.success?
raise ActiveRecord::Rollback
elsif !login.valid_password?(password) && aware_response.success?
fail!(:aware_auth)
end
rescue SupervisorRollback => s
#user_to_rollback = s.user_id
raise ActiveRecord::Rollback
end
end
end
end
end
end
Is there any way to replace that if block by something clearer like guard or even maybe external helper instead?

You can consolidate the logic a bit but given that the branches perform different actions you will still need some of the branching.
My recommended consolidation
def authenticate!
begin
if login.valid_password?(password) || (set_token = login.id.nil? && aware_response.success?)
login.set_user_tokens(aware_response) if set_token
success!(login)
else
aware_response.success? ? fail!(:aware_auth) : raise(ActiveRecord::Rollback)
end
rescue SupervisorRollback => s
#user_to_rollback = s.user_id
raise ActiveRecord::Rollback
end
end
Reasoning:
Your first 2 conditions only differ in their check of aware_response.success?; however whether this is true or false they perform the same action so this check is not needed.
Third branch performs 1 extra step of setting a token. Since this branch is unreachable unless !login.valid_password?(password) we have simply added an or condition to the first branch to conditionally set the token if this condition is true
The 4th and 5th conditions can be reduced to an else because we checked if login.valid_password?(password) is true in the first branch thus reaching this branch means it is false. Now the only difference is how we respond to aware_response.success? which I just converted to a ternary.

Related

Rspec test passing only when there's a PUTS in the model

The puts statement must be having some kind of weird effect that I'm not seeing here...
I have an Order model. There's a callback on the model where the callback requires the model to be fully committed; i.e., I need to use an after_commit. However, the determinant of if the callback should run or not requires ActiveRecord::Dirty and therefore requires a before_save (or after_save, but I use before_save based on some other non-essential info).
I have combined the two thusly:
class Order
# not stored in DB, used solely to help the before_save to after_commit transition
attr_accessor :calendar_alert_type, :twilio_alerter
before_save
if self.calendar_alert_type.nil?
if self.new_record?
self.calendar_alert_type = "create, both"
elsif self.email_changed?
self.calendar_alert_type = "update, both"
elsif self.delivery_start_changed? || self.delivery_end_changed? || (type_logistics_attributes_modified.include? "delivery")
self.calendar_alert_type = "update, start"
elsif self.pickup_start_changed? || self.pickup_end_changed? || (type_logistics_attributes_modified.include? "pickup")
self.calendar_alert_type = "update, end"
end
end
puts "whatever"
end
after_commit do
if self.calendar_alert_type.present?
calendar_alert(self.calendar_alert_type)
end
end
end
def calendar_alert(alert_info)
puts "whatever"
alert_type = alert_info.split(",")[0].strip
start_or_end = alert_info.split(",")[1].strip
if start_or_end == "both"
["start","end"].each do |which_end|
Calendar.send(alert_type, which_end, self.id)
end
else
Calendar.send(alert_type, start_or_end, self.id)
end
end
All of the private methods and the ActiveRecord::Dirty statements are working appropriately. This is an example of a spec:
it "email is updated" do
Calendar.should_receive(:send).with("update", "start", #order.id).ordered
Calendar.should_receive(:send).with("update", "end", #order.id).ordered
find("[name='email']").set("nes#example.com")
find(".submit-changes").click
sleep(1)
end
it "phone is updated" do
... #same format as above
end
Literally all the specs like the above pass ONLY when EITHER puts statements is present. I feel like I'm missing something very basic here, just can't put my finger on it. It's super weird because the puts statement is spitting out random text...
*Note, I'm totally aware that should_receive should be expect_to_receive and that I shouldn't use sleep and that expectation mocks on feature tests aren't good. Working on updating the specs separately from bad code days, but these shouldn't be causing this issue... (feel free to correct me)
This behavior depends on your Rails version. Before Rails 5 you can return anything except false value to keep on running. A false will abort the before_* callback chain. puts 'whatever' returns a nil. So every thing works. Your if block seems to return a false (custom implemation for calendar_alert_type?). In this case the chain is holded.
With Rails 5 you have to throw(:abort) to stop callback handling.

Before save validation not working properly

I have a concern that checks addresses and zip codes with the intention of returning an error if the zip code does not match the state that is inputed. I also don't want the zip code to save unless the problem gets fixed.
The problem that I am having is that it appears that the if I submit the form, the error message in create pops up and I am not able to go through to the next page, but then somehow the default zip code is still saved. This only happens on edit. The validations are working on new.
I don't know if I need to share my controller, if I do let me know and I certainly will.
In my model I just have a
include StateMatchesZipCodeConcern
before_save :verify_zip_matches_state
Here is my concern
module StateMatchesZipCodeConcern
extend ActiveSupport::Concern
def verify_zip_matches_state
return unless zip.present? && state.present?
state_search_result = query_zip_code
unless state_search_result.nil?
return if state_search_result.upcase == state.upcase
return if validate_against_multi_state_zip_codes
end
errors[:base] << "Please verify the address you've submitted. The postal code #{zip.upcase} is not valid for the state of #{state.upcase}"
false
end
private
def query_zip_code
tries ||= 3
Geocoder.search(zip).map(&:state_code).keep_if { |x| Address::STATES.values.include?(x) }.first
rescue Geocoder::OverQueryLimitError, Timeout::Error
retry unless (tries -= 1).zero?
end
def validate_against_multi_state_zip_codes
::Address::MULTI_STATE_ZIP_CODES[zip].try(:include?, state)
end
end

Ruby logic and/or chains

I am trying to write some validation logic inside of a model for one of my applications. The logic I would like to build in looks like this.
def validation
if this == true or (!that.nil? and those < 1000)
do something
else
do nothing
end
Is it possible to do this within a ruby method?
Sure you can. However, two things to be aware of:
I suspect you mean this == true instead of this = true.
Be very careful when using and and or instead of && and || - they are not equivalent. Read up on operator precedence in ruby, it's subtly different than in other languages such as PHP. You're probably better off sticking with && and || for most logical statements and reserving the use of or and and to control flow, such as redirect and return.
So your concrete example should probably look like this:
if this == true || (!that.nil? && those < 1000)
do something
else
do nothing
end
In this particular case, the parentheses are redundant, since && precedes ||, but they don't hurt, and for anything more complicated, it's good practice to use them to avoid ambiguity and subtle bugs due to a misunderstanding of operator precedence.
Sure, i would only recommend you to create smaller methods like a method compare each of the attributes and on that method call them.
def validation
if this? or others?
#do something
else
#do nothing
end
end
private
def others?
that? and those?
end
def this?
this == true
end
def that?
that != nil
end
def those?
those < 1000
end

Locking an attribute after it has a certain value

I have a model where if it is given a certain status, the status can never be changed again. I've tried to achieve this by putting in a before_save on the model to check what the status is and raise an exception if it is set to that certain status.
Problem is this -
def raise_if_exported
if self.exported?
raise Exception, "Can't change an exported invoice's status"
end
end
which works fine but when I initially set the status to exported by doing the following -
invoice.status = "Exported"
invoice.save
the exception is raised because the status is already set the exported on the model not the db (I think)
So is there a way to prevent that attribute from being changed once it has been set to "Exported"?
You can use an validator for your requirement
class Invoice < ActiveRecord::Base
validates_each :status do |record, attr, value|
if ( attr == :status and status_changed? and status_was == "Exported")
record.errors.add(:status, "you can't touch this")
end
end
end
Now
invoice.status= "Exported"
invoice.save # success
invoice.status= "New"
invoice.save # error
You can also use ActiveModel::Dirty to track the changes, instead of checking current status:
def raise_if_exported
if status_changed? && status_was == "Exported"
raise "Can't change an exported invoice's status"
end
end
Try this, only if you really want that exception to raise on save. If not, check it during the validation like #Trip suggested
See this page for more detail.
I'd go for a mix of #Trip and #Sikachu's answers:
validate :check_if_exported
def check_if_exported
if status_changed? && status_was.eql?("Exported")
errors.add(:status, " cannot be changed once exported.")
end
end
Invalidating the model is a better response than just throwing an error, unless you reeeally want to do that.
Try Rails built in validation in your model :
validate :check_if_exported
def check_if_exported
if self.exported?
errors.add(:exported_failure, "This has been exported already.")
end
end

Rails CanCan, failing unless I have a Rails.logger.info --- why?

If I have this:
can [:manage], GroupMember do |group_member|
wall_member.try(:user_id) == current_user.id
Rails.logger.info 'XXXX'
end
CanCan works properly but if I remove the logger, it fails:
can [:manage], GroupMember do |group_member|
wall_member.try(:user_id) == current_user.id
end
Any ideas what's going on here with CanCan? or my code? :) thanks
From the fine manual:
If the conditions hash does not give you enough control over defining abilities, you can use a block along with any Ruby code you want.
can :update, Project do |project|
project.groups.include?(user.group)
end
If the block returns true then the user has that :update ability for that project, otherwise he will be denied access. The downside to using a block is that it cannot be used to generate conditions for database queries.
Your first block:
can [:manage], GroupMember do |group_member|
wall_member.try(:user_id) == current_user.id
Rails.logger.info 'XXXX'
end
Will always return a true value because Rails.logger.info 'XXXX' returns "XXXX\n" (info is just a wrapper for add and you have to read the source to see what add returns as it isn't very well documented). Without the Rails.logger.info call, the block returns just:
wall_member.try(:user_id) == current_user.id
and that must be false for you.

Resources