I was handed a project that another developer worked on, without leaving any documentation behind. The code fetches some purchases from a shopping website, looks for a price and notifies the user.
The app may encounter errors like "no results found" and then I raise a standarderror.
I want to redirect the user to the error page and notify them about it but I can't do that because it isn't a controller, so the redirect_to option doesn't work.
services/purchase_checker.rb is called once an hour:
def call
user.transaction do
store_purchase
if better_purchase?
update_purchase
end
end
rescue MyError=> e
store_error(e)
end
def store_error(error)
user.check_errors.create!(error_type: error.class.name, message: error.message)
end
services/my_error.rb:
class MyError< StandardError
def initialize(error_type, error_message)
super(error_message)
#error_type = error_type
end
attr_reader :error_type
end
services/purchase_fetcher.rb:
def parse_result_page
raise purchase_form_page.error if purchase_form_page.error.present?
offer = purchase_page.map{|proposal_section|
propose(proposal_section, purchase) }
.min_by(&:price)
offer or raise MyError.new("No results", "No results could be found")
end
you should create another err class, eg NotFoundError:
offer or raise NotFoundError.new("No results", "No results could be found")
then in your controller:
begin
parse_result_page
rescue NotFoundError => e
redirect_to err_page, :notice => e.message
end
Since this is running in a job, the best way to notify the user would be by email, or some other async notification method. When an error is detected, an email is sent.
If that's not an option for some reason, you can check if a user has check_errors in any relevant controllers. Looking at the store_error(error) method that is called when an error is found, it seems it's creating a new record in the Database to log the error. You should be able to check if a user has any error logged via the user.check_errors relationship.
You could do it like this, for example:
class SomeController < ActionController::Base
# ...
before_action :redirect_if_check_errors
# ...
def redirect_if_check_errors
# Assuming you're using Devise or something similar
if current_user && current_user.check_errors.exists?
redirect_to some_error_page_you_create_for_this_path
end
end
end
This will check for these errors in every action of SomeController and redirect the user to an error page you should create, where you render the errors in the user.check_errors relationship.
There are multiple ways to do this, but I still think sending an email from the Job is a better option if you want to actively notify the user. Or perhaps add an interface element that warns the user whenever user.check_errors has stuff there, for example.
I propose that you do this synchronously so that the response can happen directly in the request/response cycle. Perhaps something like this:
# controller
def search
# do your searching
# ...
if search_results.blank?
# call model method, but do it synchrously
purchase_check = PurchaseChecker.call
end
if purchase_check.is_a?(MyError) # Check if it's your error
redirect_to(some_path, flash: { warning: "Warn them"})
end
end
# model, say PurchaseChecker
def call
# do your code
rescue MyError => e
store_error(e)
e # return the error so that the controller can do something with it
end
Related
I am using Mailchimp (via the Gibbon gem) to add email addresses to my Mailchimp mailing list, and I want to handle any errors that are returned by Mailchimp and display them in my view.
Here is my Pages controller:
class PagesController < ApplicationController
def subscribe
email = subscriber_params[:email]
if email.empty?
flash[:error] = 'Please provide an email.'
redirect_to root_path
else
subscriber = Mailchimp.new.upsert(email)
if subscriber
flash[:success] = 'You\'re in!'
redirect_to root_path(subscribed: :true)
else
# Return error coming from Mailchimp (i.e. Gibbon::MailChimpError)
end
end
end
end
And here is the app > services > mailchimp.rb file I set up to separate out the Mailchimp logic:
class Mailchimp
def initialize
#gibbon = Gibbon::Request.new(api_key: Rails.application.credentials.mailchimp[:api_key])
#list_id = Rails.application.credentials.mailchimp[:list_id]
end
def upsert(email)
begin
#gibbon.lists(#list_id).members.create(
body: {
email_address: email,
status: "subscribed"
}
)
rescue Gibbon::MailChimpError => e #This is at the bottom of the Gibbon README
raise e.detail
end
end
end
What I'm trying to figure out is how to return/send Gibbon::MailChimpError back to my Pages#subscribe action. I see it being outputted as a RuntimeError in my console, but I'm not sure the right way to access/pass it along.
And please let me know if there's a better practice for this kind of implementation.
You could move the begin/rescue block to the subscribe action inside your controller to handle the error from there, or even better, you can use rescue_from in your controller like this:
class PagesController < ApplicationController
rescue_from Gibbon::MailChimpError do |e|
# Handle the exception however you want
end
def subscribe
# ....
end
end
I have an application which uses Devise as authentication. I don't want user to be able to changed there email address. I've done this by setting the email attribute to read only in the User model.
class User < ActiveRecord::Base
attr_readonly :email
end
This works fine: Rails rollback the transaction. Devise however thinks that the update was succesful and displays a message succesfull message.
"Your account has been updated successfully."
I've tried several things including creating my onw methode that would return a flash notice but it keeps saying that the account has been succesfully updated.
Is there a way to raise an error when the record is not saved succesfully?
Edit after Ashvin's anwser. This is what I have in my model:
def email=(address)
begin
if new_record?
write_attribute(:email, address)
end
rescue Exception => error
flash[:alert] = error.message
end
end
I dont know I got your question or not, But from what I get following is solution. You can use exception handling while record is rollback
begin
# do some stuff here
rescue Exception => e
flash[:notice] = e.message
end
Suppose I have a scenario where we have Users and each user can create their own Projects.
I'm trying to limit the Show action of my Rails controller to only allow admin or the owner of the project to be able to go through Show action.
The problem I am facing is, perhaps I'm misunderstanding on how to use Scopes in Pundit.
My Show action looks like this:
def show
project = policy_scope(Project).find_by({id: project_params[:id]})
if project
render json: project
else
render json: { error: "Not found" }, status: :not_found
end
end
My Pundit Scope class looks like this:
class Scope < Scope
def resolve
if #user.admin?
scope.all
else
# obviously, if non-matching user id, an ActiveRelation of
# empty array would be returned and subsequent find_by(...)
# would fail causing my controller's 'else' to execute
# returning 404 instead of 403
scope.where(user_id: #user.id)
end
end
end
In my Rails test, I am trying to assert that non-project owner should receive a 403 forbidden:
test "show project should return forbidden if non admin viewing other user's project" do
# "rex" here is not the owner of the project
get project_path(#project.id), headers: #rex_authorization_header
assert_response :forbidden
end
My test is failing. I am getting the error:
Failure:
ProjectsControllerTest#test_show_project_should_return_forbidden_if_non_admin_viewing_other_user's_project [/Users/zhang/App_Projects/LanceKit/Rails_Project/LanceKit/test/controllers/projects_controller_test.rb:40]:
Expected response to be a <403: forbidden>, but was a <404: Not Found>.
Expected: 403
Actual: 404
I don't quite feel like I'm using Pundit correctly.
Should I be using Pundit's authorize project instead of using policy_scope(Project)... for the Show action?
I was expecting the scope.where(...) to detect the incorrect user id and return some error saying 'you are not authorized to view this resource' rather than returning results.
From what my test results are indicating to me, using scope for show action is wrong.
My finding is telling me Pundit scope are only used for filtering a set of data to only return those that matches a condition, it does NOT check whether the current_user is the owner of the resource. Pundit scope does NOT raise a 403 Forbidden error.
In other words, using scoping only in the show action will lead to a semantic bug that's saying this project with id 3 does not exist in the database for example instead of saying you are not authorized to view this project because it belongs to a different user.
A summary for myself:
use policy_scope for index action
use authorize for show, create, update, delete
use authorize AND policy_scope if you're not resource owner and trying to access some funky plural resource route like
get "/user/1/projects" => "Project.index"
in case you want to check if user is say a "project manager" or "collaborator" who is allowed to view your project. In this case, you would probably need to modify your scope code with an extra elsif clause.
In relation to my above question, I modified my project to use authorize inside my show action:
def show
project = Project.find_by({id: project_params[:id]})
authorize project
if project
render json: project
else
render json: { error: "Not found" }, status: :not_found
end
end
This then raises the expected 403 Forbidden error that my tests is expecting and thus my test passes.
Pundits docs regarding scopes state that you can indeed use them for the show action:
def index
#posts = policy_scope(Post)
end
def show
#post = policy_scope(Post).find(params[:id])
end
Just using authorize may not be enough if a User (manually) opens a URL with a id param of an instance, that she should not be able to view.
To avoid a RecordNotFound error, I used the recommended NilClassPolicy:
class NilClassPolicy < ApplicationPolicy
class Scope < Scope
def resolve
raise Pundit::NotDefinedError, "Cannot scope NilClass"
end
end
def show?
false # Nobody can see nothing
end
end
I have a method in my application that finds a photo from the og:image tag of a link that is submitted. In my create action, I use the method photo_form_url, described below.
def photo_from_url(url)
if !Nokogiri::HTML(open(url)).css("meta[property='og:image']").blank?
photo_url = Nokogiri::HTML(open(url)).css("meta[property='og:image']").first.attributes["content"]
self.photo = URI.parse(photo_url)
self.save
end
end
However, this produces an error if a bad url is entered.
I tried to rescue as below, but this gives me an "undefined method redirect_to"
def photo_from_url(url)
begin
if !Nokogiri::HTML(open(url)).css("meta[property='og:image']").blank?
photo_url = Nokogiri::HTML(open(url)).css("meta[property='og:image']").first.attributes["content"]
self.photo = URI.parse(photo_url)
self.save
end
rescue OpenURI::HTTPError
redirect_to :back, notice: 'link's broken!'
end
end
What am I doing wrong?
According to your answer to my comment, your function photo_from_url is defined in the model. Trying to redirect a user within a model is not possible as shown by the error you are facing.
Bear in mind that your model can be called outside of a browsing session environment. EG:
tests
rake task
You should thus never, ever put any code that has to do with manipulating the user browser, or the user session within your models. This is the job of the controller.
So what you need to do is simply raise an exception or return a specific value in your model when you are encountering a bad url. And react to that exception / return value in your controller by redirecting the user. This ensure that anything that has to do with the user browser stays in the controller, and that you could implement a different behavior in a rake task if encountering the same error.
So, your model should do stuff, and raise errors when it can't :
# Link.rb
def photo_from_url(url)
if !Nokogiri::HTML(open(url)).css("meta[property='og:image']").blank?
photo_url = Nokogiri::HTML(open(url)).css("meta[property='og:image']").first.attributes["content"]
self.photo = URI.parse(photo_url)
self.save
end
end
Your controller should ask your model to do stuff, and deal with the user if there is a problem :
# link_controller
# in create
begin
link.photo_from_url(url)
rescue OpenURI::HTTPError
redirect_to :back, notice: 'link's broken!'
end
I am using Authlogic-connect to connect various service providers. There is a method in user.rb
def complete_oauth_transaction
token = token_class.new(oauth_token_and_secret)
old_token = token_class.find_by_key_or_token(token.key, token.token)
token = old_token if old_token
if has_token?(oauth_provider)
self.errors.add(:tokens, "you have already created an account using your #{token_class.service_name} account, so it")
else
self.access_tokens << token
end
end
When a service provider is already added it gives the error as stated in the has_token? method and the page breaks. I need to redirect the app to the same page and flash the error. How do i do this? I have overridden the method in my own user.rb so that I can change the code.
Hmm, well you could put a method that handles the error that has_token? throws, and tell your controller to redirect that exact error. something like this in your controller:
rescue_from OauthError::RecordNotFound, :with => :deny_access
then you can put
def deny_access
redirect_to your_view_path, :alert => "Too bad sucker" #some flash message
end
Or you could do something like this in the controller:
if complete_oauth_transaction.errors.present?
redirect_to your_view_path
else
# continue on with the normal code here
end
This is how you could generically handle errors. Your exact code will vary, as this is all we have to go off of.