Rolling back Model-changes from action - ruby-on-rails

I have the action, in where I assign groups to the members that apply. Basically, I just get a list of emails, from the view in a form, and then I have the action to catch it:
What I'm wondering is, that if I can rollback the changes that's been made already, if say the second member doesn't exist, or has a group already, how can I rollback these?
def group_create
#group = Group.new
params[:member].each { |m|
v = Volunteer.find_by_email(m[1])
raise "#{m[1]} doesn't exist" unless v.present?
raise "#{v.email} is already in a group" if v.group.present?
v.group = #group
v.save!
}
#group.save
rescue => error
flash[:error] = "Error: #{error}"
ensure
respond_to do |format|
unless flash[:error].present?
flash[:notice] = 'Group Application succeded.'
flash[:joined] = true
format.html { redirect_to apply_group_path }
else
flash.discard
format.html { render :group }
end
end
end
What I've thought about already, was move the v.save and #group.save to the end, and make another loop of params[:member].each...., but that would be quite a waste of ressources, to do the find_by_email-method twice as many times, as needed.

I suggest move your non-controller logic into model and wrap it in ActiveRecord::Base.transaction:
ActiveRecord::Base.transaction do
#group = Group.new
params[:member].each { |m|
v = Volunteer.find_by_email(m[1])
raise "#{m[1]} doesn't exist" unless v.present?
raise "#{v.email} is already in a group" if v.group.present?
v.group = #group
v.save!
}
#group.save
end
Use transactions as a protective wrapper around SQL statements to ensure changes to the database only occur when all actions succeed together.

Related

if one object fails all objects rollback

begin
ActiveRecord::Base.transaction do
installations.each do |installation|
id = installation.id
installation = current_user.installations.find_by(:id=> id)
#ticket = installation.tickets.new(ticket_params)
unless #ticket.save
raise ActiveRecord::Rollback
end
end
end
redirect_to '/tickets', notice: "done"
rescue ActiveRecord::Rollback
render action: "new", notice: "problem" and return
end
Imagine that we have two tickets, the first ticket is valid and the second is invalid. In this code the first ticket will be saved. But I want to rollback all the tickets when one fail.
How can I do that ?
You could build a collection of #ticket.save's return values, call .all? on them, and if you get a false raise the rollback:
ActiveRecord::Base.transaction do
installations.each do |installation|
tickets_saved = []
id = installation.id
installation = current_user.installations.find_by(:id=> id)
ticket = installation.tickets.new(ticket_params)
tickets_saved << ticket.save
end
unless tickets_saved.all?
raise ActiveRecord::Rollback
end
end

Refactoring related nested if

I'm trying to create 2 related models inside my create method, where the 2nd model is created using model1.model2s.build. The model*_params is just Rails 4 strong parameters.
So I have this set of code in my create method:
def create
#model1 = current_user.model1.build(model1_params)
if #model1.save
#model2 = #model1.model2s.build(model2_params)
if #model2.save
redirect_to model1_path(#model1)
else
render 'new'
end
else
render 'new'
end
end
As you can see there's an ugly nested if in the method, and it's not DRY as I'm forced to repeat render 'new' in order to capture save failures. This has been the only way I can get model2 to save, because it requires a relation to model1, and model1 must save first, in order for the id of model1 to be propagated to the build method.
My question therefore, is how can I refactor this set of code so that it doesn't require a nested if?
def create
#model1 = current_user.model1.build(model1_params)
return render("new") unless #model1.save
#model2 = #model1.model2s.build(model2_params)
return render("new") unless #model2.save
redirect_to model1_path(#model1)
end
You can easily make it a single if/else:
if #model1.save && #model1.model2s.build(model2_params).save
redirect_to #model1
else
render 'new'
end
Alternatively, exceptions:
begin
#model1 = current_user.model1.build(model1_params)
#model1.save!
#model2 = #model1.model2s.build(model2_params)
#model2.save!
redirect_to #model1
rescue ActiveRecord::RecordInvalid => e
render 'new'
end

Stop deletion of admin where name like 'string_name'

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.

Render failing to render correct template in rescue_from ActiveRecord::Rollback method

I'm building the checkout page for an e-commerce site, and I have a fairly long transaction that creates a new User model and a new Order model. I wrapped the creation of these models in a transaction so that if validation for one fails, the other isn't hanging around in the database. Here's the trimmed-down code in my OrdersController:
rescue_from ActiveRecord::Rollback, with: :render_new
def render_new
render action: 'new'
end
ActiveRecord::Base.transaction do
#user = User.new params[:user]
unless #user.save
raise ActiveRecord::Rollback
end
//More stuff
...
#order = Order.new params[:order]
...
unless #order.save
raise ActiveRecord::Rollback
end
end
The error I'm seeing is this:
Missing template orders/create, application/create with {:locale=>[:en], :formats=>[:html], :handlers=>[:erb, :builder, :coffee]}
I'm confused as to why its trying to render the templates orders/create and application/create instead of rendering orders/new.
Is there a better way to force the transaction to fail so that the rollback will occur?
I think the intention is a bit clearer when wrapping the transaction in a begin/rescue block.
def create
begin
ActiveRecord::Base.transaction do
#user = User.new params[:user]
unless #user.save
raise ActiveRecord::Rollback
end
//More stuff
...
#order = Order.new params[:order]
...
unless #order.save
raise ActiveRecord::Rollback
end
end
rescue ActiveRecord::Rollback
render action: "new" and return
end
end
You need to return in the create method, otherwise it's execution will continue to the end of the method and Rails default render will occur (in this case it means attempting to find a create.___ template).
If you don't like the begin/rescue block you can just add an and return to the raise lines
raise ActiveRecord::Rollback and return
Above answer is correct but some modification is required for rendering action.
Do it like this:-
def create
is_project_saved = false
is_proposal_saved = false
ActiveRecord::Base.transaction do
is_project_saved = #project.save
is_proposal_saved = #proposal.save
if is_project_saved && is_proposal_saved
# Do nothing
else
raise ActiveRecord::Rollback
end
end
if is_project_saved && is_proposal_saved
# You can add more nested conditions as per you need.
flash[:notice] = "Proposal Created Successfully."
redirect_to project_show_path(:job_id => #project.job_id)
else
render :new
end
end
ActiveRecord::Rollback will not be caught by resque. So it needs to be done outside transaction block.
You can also use save_point using :requires_new => true in nested ActiveRecord::Base.transaction.
You need to raise the ActiveRecord::Rollback and manage the render/redirect as you desire. As #WasimKhan said, the ActiveRecord::Rollback will not be caught by rescue.
def create
#user = User.new params[:user]
ActiveRecord::Base.transaction do
if #user.save
#order = Order.new params[:order]
if #order.save
redirect_to :index
else
raise ActiveRecord::Rollback
end
else
render :new
end
end
render :new if #user.id.nil?
end

Rails: how can I optimize this action

The action bellow creates a new comment.
A user has many statuses
A status has many comments
How can optimize this action so that head 401 and return is not repeated many times.
def create
#user = User.where(id: params[:user_id]).first
if #user
if current_user.friend_with?(#user) or current_user == #user
#status = #user.statuses.where(id: params[:status_id]).first
if #status
#comment = #status.comments.build(params[:comment])
#comment.owner = current_user
if #comment.valid?
#comment.save
current_user.create_activity(:comment_status, #comment, #user)
else
head 401 and return
end
else
head 401 and return
end
else
head 401 and return
end
else
head 401 and return
end
end
Thank you.
When do you want to return 401?
when a user has not been found
when a user is not a current user or is not a friend of that user
when a status has not been found
when new comment has not been successfully created
Instead of using so many conditionals, you can use methods that raise exceptions. When you do so, you can rescue from that exceptions with the desired behavior (rendering 401).
So my suggestions for listed conditions are:
use find! instead of where and then first.
raise something, preferably custom exception (NotAFriendError)
same as 1., use find!
use create!, it's an equivalent to new and then save! which will raise ActiveRecord::RecordInvalid exception if it fails on validation.
Here's the result:
def create
begin
#user = User.find!(params[:user_id])
raise unless current_user.friend_with?(#user) || current_user == #user
#status = #user.statuses.find!(params[:status_id])
#comment = #status.comments.
create!(params[:comment].merge(:owner => current_user))
rescue ActiveRecord::RecordNotFound, ActiveRecord::RecordInvalid
head 401
end
# everything went well, no exceptions were raised
current_user.create_activity(:comment_status, #comment, #user)
end
You have a lot of excessive checking and branching in your code, so it can be simplified to this:
def create
success = false
#user = User.find(params[:user_id])
current_user_is_friend = current_user.friend_with?(#user) || current_user == #user
if #user && current_user_is_friend && #status = #user.statuses.find(params[:status_id])
#comment = #status.comments.build(params[:comment])
#comment.owner = current_user
if #comment.save
current_user.create_activity(:comment_status, #comment, #user)
success = true
end
end
render(status: 401, content: '') unless success
end
A few things I did:
Combine a lot of the if conditions, since there was no need for them to be separate.
Change where(id: ...).first to find(...) since they're the same. Note that, if the find fails, it will give a 404. This may make more sense, though (I think it does)
Don't call #comment.valid? right before #comment.save, since save returns false if the object wasn't valid.
Use || instead of or for boolean logic (they're not the same).
Use render(status: ..., content: '') instead of head ... and return.
Use a boolean variable to track the success of the method.
I would advise that you try and pull some of this logic out into models. For example, User#friend_with should probably just return true if it's passed the same User.
def create
#user = User.where(id: params[:user_id]).first
if #user
if current_user.friend_with?(#user) or current_user == #user
#status = #user.statuses.where(id: params[:status_id]).first
if #status
#comment = #status.comments.build(params[:comment])
#comment.owner = current_user
if #comment.valid?
#comment.save
current_user.create_activity(:comment_status, #comment, #user)
everythingOK = true
end
end
end
end
head 401 and return unless everythingOK
end

Resources