I have several actions in my hotel_controller where I call an API to get back data. I created different services to keep my API calls outside my controller logic. For every API calls I have got some "general response errors" like unauthorized or not found for instance. As these errors are common to all API calls, I wanted to create a private method to deal with them in my hotel controller:
private
def global_error_checking(response)
if response.message == "Unauthorized"
redirect_to unauthorized_path and return
elsif response.message == "Not Found"
redirect_to not_found_path and return
else
end
end
Then in every method of my controller where it's needed I would call the global_error_checking method before checking for specific errors. For instance :
def index
service = Hotels::GetHotelListService.new( account_id: params[:account_id],
user_email: session[:user_email],
user_token: session[:user_token]
)
#response = service.call
global_error_checking(#response)
if #response["hotels"].blank?
flash[:notice] = "You have not created any hotels yet !"
redirect_to account_path(params[:account_id])
else
#hotels = #response["hotels"]
#account = #response["account"]
end
end
The problem is that after executing global_error_checking, the action of the controller goes on and does not stop even though a condition of global_error_checking is satisfied.
1) How can I stop the execution of the whole controller method if a condition inside global_error_checking is satisfied ?
2) Is there maybe a better way to achieve this ?
I wouldn't name the parameter "response" since that's already being used by the controller.
The other thing I noticed is that you're accessing this "#response" in different ways which might be ok but it looks wrong. In your global_error_checking method you're accessing it's properties using dot syntax (response.message), however in your controller action you're accessing it as if it were a hash. Again, this might be ok depending on its data type.
If I were you, I would refactor this to look like:
class SomeController < ApplicationController
def index
#hotels = some_resource['hotels']
#account = some_resource['account']
end
private
def some_resource
#_some_resource ||= begin
service = Hotels::GetHotelListService.new({
account_id: params[:account_id],
user_email: session[:user_email],
user_token: session[:user_token]
})
result = service.call
if result['message'] == 'Unauthorized'
redirect_to unauthorized_path and return
elsif result['message'] == 'Not Found'
redirect_to unauthorized_path and return
else
result
end
end
end
end
You can use return statement:
return if global_error_checking in your controller
and case statement in private method with some changes:
private
#returns true after redirecting if error message "Unauthorized" or "Not Found"
def global_error_checking(response)
case response.message
when "Unauthorized"
redirect_to unauthorized_path and return true
when "Not Found"
redirect_to not_found_path and return true
end
end
Related
I got message AbstractController::DoubleRenderError in Admin::AdminsController#update
Render and/or redirect were called multiple times in this action.
Please note that you may only call render OR redirect, and at most
once per action. Also note that neither redirect nor render terminate
execution of the action, so if you want to exit an action after
redirecting, you need to do something like "redirect_to(...) and
return"
also redirect_to(...) and return not helped :(
How to fix that my problem? Help me, please? I want render error flash message and not update all stuff after if check_card_enabled? if in If return true. Thanks!
def update_resource(object, attributes)
if check_card_enabled?
redirect_to admin_admin_path(#admin)
flash[:alert] = I18n.t('errors.messages.please_add_gateway_info')
return false
end
update_method = if attributes.first[:password] == attributes.first[:password_confirmation] &&
attributes.first[:password].present? || attributes.first[:password_confirmation].present?
:update_attributes
else
:update_without_password
end
object.send(update_method, *attributes)
end
def check_card_enabled?
#admin.card_disabled? && (params[:admin][:card_disabled] == '0') && #admin.gateway_info.nil?
end
Anser:
before_filter :check_card_enabled, only: :update
def check_card_enabled
admin = Admin.find_by(email: params[:admin][:email])
if admin.card_disabled? && (params[:admin][:card_disabled] == '0') && admin.gateway_info.nil?
flash[:alert] = I18n.t('errors.messages.please_add_gateway_info')
redirect_to edit_admin_admin_url(admin)
end
end
Yes, you need to return from method when doing redirect. It actually only adds appropriate headers for the response object.
You can write more rubyish way:
if some_condition
return redirect_to(path_one)
end
so according to your code try :-
def update_resource(object, attributes)
if check_card_enabled?
flash[:alert] = I18n.t('errors.messages.please_add_gateway_info')
return redirect_to admin_admin_path(#admin),
end
....
....
I am making a rails json api which uses service objects in controllers actions and basing on what happend in service I have to render proper json. The example looks like this.
star_service.rb
class Place::StarService
def initialize(params, user)
#place_id = params[:place_id]
#user = user
end
def call
if UserStaredPlace.find_by(user: user, place_id: place_id)
return #star was already given
end
begin
ActiveRecord::Base.transaction do
Place.increment_counter(:stars, place_id)
UserStaredPlace.create(user: user, place_id: place_id)
end
rescue
return #didn't work
end
return #gave a star
end
private
attr_reader :place_id, :user
end
places_controller.rb
def star
foo_bar = Place::Star.new(params, current_user).call
if foo_bar == #sth
render json: {status: 200, message: "sth"}
elsif foo_bar == #sth
render json: {status: 200, message: "sth"}
else
render json: {status: 400, message: "sth"}
end
And my question is, if I should return plain text from service object or there is some better approach?
It'll be opinionated of course but still...
Rendering views with data, returning data, redirecting etc are the responsibilities of controllers. So any data, plain text and other things you have to handle in your controller.
Service object have to provide one single public method for any huge complex operation performing. And obviously that method has to return simple value which tells controller if operation was completed successfully or not. So it must be true or false. Maybe some recognizable result (object, simple value) or errors hash. It's the ideal use case of course but it's the point.
As for your use case your service may return the message or false. And then controller will render that message as json.
And your star method must live in your controller, be private probably and looks like that:
def star
foo_bar = Place::Star.new(params, current_user).call
if foo_bar
render json: {status: 200, message: foobar}
else
render json: {status: 400, message: "Failed"}
end
end
Your Service:
class Place::StarService
def initialize(params, user)
#place_id = params[:place_id]
#user = user
end
def call
if UserStaredPlace.find_by(user: user, place_id: place_id)
return "Message when star is already given"
end
begin
ActiveRecord::Base.transaction do
Place.increment_counter(:stars, place_id)
UserStaredPlace.create(user: user, place_id: place_id)
end
rescue
return false
end
return "Message if gave a star"
end
private
attr_reader :place_id, :user
end
I encountered an issue today I haven't seen before - I have a custom validation to check if a discount code has already been used in my Order model:
validate :valid_discount_code
def valid_discount_code
is_valid = false
code = nil
if discount_code.present?
if discount_code.try(:downcase) == 'xyz'
code = 'xyz'
is_valid = true
else
code = Coupon.find_by_coupon_code(discount_code)
is_valid = code.present?
end
if code.nil?
is_valid = false
errors.add(:discount_code, "is not a valid referral code")
elsif ( code.present? && Coupon.where(email: email, coupon_code: code).present? )
is_valid = false
errors.add(:discount_code, "has already been used.")
end
puts "------------"
puts errors.full_messages ## successfully 'puts' the correct error message into my console.
puts "------------"
if is_valid
.... do stuff.....
end
end
end
In my controller:
if current_order.update_attributes(discount_code: params[:coupon_code].downcase, another_attribute: other_stuff)
....
session[:order_id] = nil
render json: { charged: 'true' }.as_json
else
puts "==============="
puts current_order.id # shows the correct current order ID
puts current_order.errors.full_messages # shows that there are no errors present
puts "==============="
render json: { charged: 'false', errors: current_order.errors.full_messages }.as_json
end
So it looks like at update_attributes, it runs the validation, fails the validation, creates the error message, and then once it's back at my controller the error message is gone. I'm stumped as to what can be causing that issue.
EDIT:
Here is what current_order is:
In ApplicationController.rb:
def current_order
session[:order_id].present? ? Order.find(session[:order_id]) : Order.new
end
Looks like every time you call current_order it reruns the find method. You can confirm this in the logs, but try not to call that, or at least memoize it. In an instance variable, the same order will be used everytime.
def current_order
#current_order ||= (Order.find_by_id(session[:order_id]) || Order.new)
end
I am using has_secure_password with a rails 4.1.5 app. I wanted to decouple my login functionality from my SessionsController so I can reuse it to login any user from wherever I want in my app - for example logging in a user after registration, logging analytics events etc.
So I refactored my code into a LoginUser service object and I am happy with it.
The problem is that my controller still has some coupled logic after this refactoring. I am using a Form Object (via the reform gem) for form validation and then passing on the user, session and password to the LoginUser service.
Here is what the create method in my SessionsController looks like:
def create
login_form = Forms::LoginForm.new(User.new)
if login_form.validate(params[:user]) # validate the form
begin #find the user
user = User.find_by!(email: params[:user][:email])
rescue ActiveRecord::RecordNotFound => e
flash.now.alert = 'invalid user credentials'
render :new and return
end
else
flash.now.alert = login_form.errors.full_messages
render :new and return
end
user && login_service = LoginUser.new(user, session, params[:user][:password])
login_service.on(:user_authenticated){ redirect_to root_url, success: "You have logged in" }
login_service.execute
end
Everything is working as expected but the part I am not happy with is the tied up logic between validating the form and then finding the user before sending it to the service object. Also the multiple flash alerts feel..well..not right.
How would I make this method better by decoupling these two? It seems right now that one is carrying the other on it's back.
For your reference here is my LoginUser service object
class LoginUser
include Wisper::Publisher
attr_reader :user, :password
attr_accessor :session
def initialize(user, session, password)
#user = user
#session = session
#password = password
end
def execute
if user.authenticate(password)
session[:user_id] = user.id
publish(:user_authenticated, user)
else
publish(:user_login_failed)
end
end
end
What sticks out to me the most here is that create is a method with multiple responsibilities that can/should be isolated.
The responsibilities I see are:
validate the form
find the user
return validation error messages
return unknown user error messages
create LoginService object, setup after-auth behavior and do auth
The design goal to clean this up would be to write methods with a single responsibility and to have dependencies injected where possible.
Ignoring the UserService object, my first shot at a refactor might look like this:
def create
validate_form(user_params); return if performed?
user = find_user_for_authentication(user_params); return if performed?
login_service = LoginUser.new(user, session, user_params[:password])
login_service.on(:user_authenticated){ redirect_to root_url, success: "You have logged in" }
login_service.execute
end
private
def user_params
params[:user]
end
def validate_form(attrs)
login_form = Forms::LoginForm.new(User.new)
unless login_form.validate(attrs)
flash.now.alert = login_form.errors.full_messages
render :new
end
end
def find_user_for_authentication(attrs)
if (user = User.find_by_email(attrs[:email]))
user
else
flash.now.alert = 'invalid user credentials'
render :new
end
end
Of note, the return if performed? conditions will check if a render or redirect_to method has been called. If so, return is called and the create action is finished early to prevent double render/redirect errors.
I think this is a big improvement simply because the responsibilities have been divvied up into a few different methods. And these methods have their dependencies injected, for the most part, so that they can continue to evolve freely in the future as well.
How can I DRY the code below? Do I have to setup a bunch of ELSEs ? I usually find the "if this is met, stop", "if this is met, stop", rather than a bunch of nested ifs.
I discovered that redirect_to and render don't stop the action execution...
def payment_confirmed
confirm_payment do |confirmation|
#purchase = Purchase.find(confirmation.order_id)
unless #purchase.products_match_order_products?(confirmation.products)
# TODO notify the buyer of problems
return
end
if confirmation.status == :completed
#purchase.paid!
# TODO notify the user of completed purchase
redirect_to purchase_path(#purchase)
else
# TODO notify the user somehow that thigns are pending
end
return
end
unless session[:last_purchase_id]
flash[:notice] = 'Unable to identify purchase from session data.'
redirect_to user_path(current_user)
return
end
#purchase = Purchase.find(session[:last_purchase_id])
if #purchase.paid?
redirect_to purchase_path(#purchase)
return
end
# going to show message about pending payment
end
You can do the following to reduce the code.
1) Use
return redirect_to(..)
instead of
redirect_to(..)
return
2) Extract the flash and redirect_to code to a common method.
def payment_confirmed
confirm_payment do |confirmation|
#purchase = Purchase.find(confirmation.order_id)
return redirect_with_flash(...) unless #purchase.products_match_..(..)
return redirect_with_flash(...) unless confirmation.status == :completed
#purchase.paid!
return redirect_to(...)
end
return redirect_with_flash(...) unless session[:last_purchase_id]
#purchase = Purchase.find(session[:last_purchase_id])
return redirect_to(...) if #purchase.paid?
# going to show message about pending payment
end
Create a new method to redirect to a given url after showing a flash message.
def redirect_with_flash url, message
flash[:notice] = message
redirect_to(url)
end
Note I have truncated the code above in some places for readability.
Add and return false to the end of a redirect_to or render to halt execution at that point. That should help clean things up for you.
You could also factor out the steps into seperate methods. So the ending code would look something like:
def payment_confirmed
confirm_payment do |cnf|
confirmation_is_sane?(cnf) && purchase_done?(cnf)
return
end
has_last_purchase? && last_purchase_paid?
end
For a factoring looking like:
def confirmation_is_sane?(confirmation)
#purchase = Purchase.find(confirmation.order_id)
unless #purchase.products_match_order_products?(confirmation.products)
# TODO notify the buyer of problems and render
return false
end
true
end
def purchase_done?(confirmation)
if confirmation.status == :completed
#purchase.paid!
# TODO notify the user of completed purchase
redirect_to purchase_path(#purchase)
return false
else
# TODO notify the user somehow that thigns are pending and render
return true
end
end
def has_last_purchase?
unless session[:last_purchase_id]
flash[:notice] = 'Unable to identify purchase from session data.'
redirect_to user_path(current_user)
return false
end
#purchase = Purchase.find(session[:last_purchase_id])
return true
end
def last_purchase_paid?
if #purchase.paid?
redirect_to purchase_path(#purchase)
return false
end
# going to show message about pending payment
return true
end
This is basically just using true/falses with &&'s to do the early exiting rather than using return, but it seems to read easier, to me. You'd still have to call render in the other methods, but that shouldn't be too big of a deal.
The distinction between the confirmation order and the last purchase seems strange as well, but perhaps this is an artifact of the way confirm_payment works.