Say for example you have two users looking at a page. Both click to delete the same record. Is there a 'rails way' to handle these types of edge cases?
I haven't been able to find out how rails handles this but I know that, by default, whoever clicks later will end up with a record not found error. How can you catch this and know it came from a destroy action on a record that was already deleted?
Any approach that checks for the record's existence will be subject to a race condition. Eg.
#record = Record.find_by_id(params[:id])
if #record
# I got there first! Or did I?...
#record.destroy!
If the record is destroyed by another request after the existence test, destroy! will raise an exception.
You could simply handle the exceptions as they occur:
def destroy
#record = Record.find(params[:id])
#record.destroy!
flash[:message] = "Record was destroyed"
rescue ActiveRecord::RecordNotFound
flash[:message] = "Record was already destroyed"
rescue ActiveRecord::RecordNotDestroyed
flash[:message] = "Record was not destroyed"
end
In practice, there generally isn't a reason to do this unless the destroy can actually fail in both requests, for example, if there is a callback that prevents the record from being destroyed. And in that case, you probably need more extensive error handling to communicate the reason for the failure back to the client.
For most applications I've seen, just routing the user back to a safe place in the event of a failure is enough.
or
def destroy
#record = Record.find_by(id:params[:id])
if #record
#record.destroy!
flash[:message] = "Record is destroyed"
else
flash[:message] = "Record not found!"
end
end
also you can have this in your application_controller.rb, to handle all such request, it will apply to all resource.
rescue_from ActiveRecord::RecordNotFound, :with => :record_not_found
private
def record_not_found
flash[:danger] = "Record not Found!"
redirect_to root_path
end
but again, now you should use find() to raise ActiveRecord::RecordNotFound if record not found
Related
I'm looking for a neater way to assign a variable and return if the value is found in the DB in a controller before_action method that does not require multiple DB calls:
if Plan.exists?(plan)
#plan = Plan.find(plan)
return
end
logger.error "User selected invalid plan."
redirect_to plan_path, warning: "Invalid plan selected!"
Would this be an appropriate place to use a rescue? E.g:
begin
#plan = Plan.find(plan)
rescue
logger.error "User selected invalid plan."
redirect_to plan_path, warning: "Invalid plan selected!"
end
It feels wrong... what is the best approach?
There is not a best approach I think. It's a matter of taste. I usually do something like your latter way in a Rails 5 API. Something like
def show
#book = Book.find(params[:id])
rescue ActiveRecord::RecordNotFound
render json: { error: "Book not found" }
end
And if everything goes fine I have a view for the happy path
Some consider it an anti-pattern to use exceptions for flow control. Personally, I tend to agree. But, I suppose it is a matter of style and preference.
How about something like:
def foo_before_action
unless #plan = Plan.find_by(id: params[:id])
redirect_to plan_path, warning: "Invalid plan selected!"
end
end
Or perhaps as a one-liner:
def foo_before_action
redirect_to plan_path, warning: "Invalid plan selected!" unless #plan = Plan.find_by(id: params[:id])
end
The former is, perhaps, a bit more readable.
Little background information: I have a project with authenticated users which can have multiple 'scopes' of data. A scope determines which records can and can't be accessed by a user. A logged in user is always subsribed to a single scope stored in the session.
A user can at any time switch between the scopes he has access to, regardless of which page he is on.
After a scope switch I would like to try to reload the same page by using redirect_to :back. This is not always possible, since the current scope might not allow access to the page that was previously being viewed.
In those cases, the controller in question will throw an ActiveRecord::RecordNotFound exception in the respective controller on the .find(params[:id]) call, which is very ugly. I would very much like to gracefully fallback to a redirect_to :root with an accompanying alert message.
Currently I have this, but this does not work:
def switch_scope
s = current_client.scopes.find(params[:scope_id])
set_scope(s.id)
begin
flash[:notice] = 'Scope switched succesfully.'
redirect_to :back # <= This might throw NotFoundException
rescue
flash[:alert] = 'Scope switched. Page could not be reloaded'
redirect_to :root
end
end
EDIT
For example, the redirect_to :back might go back to this show Product function:
def show
#product = Product.where(scope_id: session[:scope_id]).find(params[:id])
end
As you can see, it first filters all propducts based on the selected scope in the session. Then it does a find(ID) on that subset. It is very possible that after a scope switch the Product with said ID is no longer accessible, giving a RecordNotFoundException.
Now again, this is an example and many of these constructions exists in my application, so preferably I do not want to edit it on this side manually.
Add This in the application_controller.rb
rescue_from ActiveRecord::RecordNotFound do |exception|
if (params[:controller]=="your_controller_name" && params[:action] == "Your method")
flash[:alert] = "Access denied. You are not authorized to access the requested page."
redirect_to root_path
end
end
OR
def switch_scope
s = current_client.scopes.find(params[:scope_id])
set_scope(s.id)
begin
flash[:notice] = 'Scope switched succesfully.'
redirect_to :back # <= This might throw NotFoundException
rescue ActiveRecord::RecordNotFound => e
flash[:alert] = 'Scope switched. Page could not be reloaded'
redirect_to :root
end
end
I'm trying to stop A particular Admin from being removed from the database with ruby on rails
I have tried a few things but heres the code as it stands
Edit 2 changed User.name to #user.name
Model
after_destroy :can_not_destroy_super_admin
private
def can_not_destroy_super_admin
if #user.name == "super admin"
raise "Can't delete this admin"
end
end
I think its a problem with User.name, but I know its seeing this code because I've had errors raising issues with different code I've tried in here.
I'm aware that this is a relatively crude method for stopping deletion of an admin but it's a simple way of getting what I need done.
Any suggestions or help is very much appreciated.
Edit.1
Here is the destroy method
Controller
def destroy
#user = User.find(params[:id])
begin
#user.destroy
flash[:notice] = "User #{#user.name} deleted"
rescue Exception => e
flash[:notice] = e.message
end
respond_to do |format|
format.html { redirect_to users_url }
format.json { head :no_content }
end
end
I'm guessing your destroy action looks something like this?
def destroy
#user = user.find params[:id]
#user.destroy
end
If this is the case, the user you want to check against in your callback is #user.name, not User.name. You want to ensure that the actual user instance you called destroy on is the same one you're checking the name of.
Edit: As determined in the comments, the callback is actually on the model, I misinterpreted as being in the controller. In the model, to reference the objects name, only name is needed, not User.name or #user.name.
I'm working with an admin user in Chrome and a regular user in another browser. After I as admin destroy one of the regular users, I tried to reopen the application in the browser the destroyed user was using. However, I got this error message
Couldn't find Twitteruser with id=2
So the session's living on in the browser after the user's destroyed
The session's created like this
def create
twitteruser = Twitteruser.from_omniauth(env["omniauth.auth"])
session[:twitteruser_id] = twitteruser.id
redirect_to twitterquestions_url, notice: "Signed in!"
end
In the application_controller, it current user's created like this
def current_user
#current_user ||= Twitteruser.find(session[:twitteruser_id]) if session[:twitteruser_id]
end
This is the destroy action
def destroy
Twitteruser.find(params[:id]).destroy
flash[:success] = "User destroyed."
redirect_to users_url
end
Based on other SO answers I found, I tried to reset the session two different ways, but they both reset the session of the admin user, not the user who was destroyed
def destroy
Twitteruser.find(params[:id]).destroy
#1. session[:twitteruser_id] = nil destroys my own session, not deleted users
#2. reset_session #reset admin's session
flash[:success] = "User destroyed."
redirect_to twitterusers_url
end
I also tried to pass an argument to reset_session but it doesn't accept them.
Can anyone tell me how to clear the destroyed user's session? Thanks
It depends on what your using for backing your sessions. If the session is in the Cookie then there's nothing your Admin can do with it as there's nothing server side to work with. Irregardless, messing with someone else's session may not be possible as you won't know the session ID.
What you want to do is either catch the ActiveRecord::RecordNotFound that is thrown by find or use find_by_id which will return nil. When the user tries to access the site with the session referencing a deleted user, you can then kill the session.
def current_user
#current_user ||= Twitteruser.find(session[:twitteruser_id]) if session[:twitteruser_id]
rescue ActiveRecord::RecordNotFound
session[:twitteruser_id] = nil # or reset_session
end
or
def current_user
#current_user ||= fetch_user(session[:twitteruser_id])
end
def fetch_user(id)
Twitteruser.find_by_id(id) || reset_session unless id.nil?
end
This will work regardless of how a Twitteruser gets deleted. For example, imagine if you deleted the user from the rails console where there is no session.
I am trying to build a Rails 3.2 app and I have just a quick question when doing destroy.
First I make a find to find the user I need to delete but I do not want to make destroy
if it is not found.
This is my code and I feel something is missing on line 3 (if #user):
#user = User.find(params[:user_id])
if #user
#user.destroy
else
"User not found"
end
you can also do this with try:
if User.find_by_id(params[:user_id]).try(:destroy)
"User found and destroyed"
else
"User not found or was not successfully destroyed"
end
Your code will not work and will raise an exception, you should do:
#user = User.find_by_id(params[:user_id])
if #user
#user.destroy! #methods with bang raise an exception, I advise you to use them
#no flash msg?
else
flash[:error] = "User not found"
end
If the user is not found, you'd get an exception
If you don't want it, do :
#user = User.find_by_id(params[:user_id])
Then your test is correct