I have a rails model with a start_time timestamp, which should obviously be in the future, which is what my validation should ensure.
appointment.rb
validate :start_time_in_future, :on => :create
private
def start_time_in_future
errors.add(:base, 'Start time must be in the future') unless self.start_time > Time.now
end
appointments_controller.rb
around_filter :start_time_in_future
def create
#appointment = Appointment.create(foo_params)
redirect_to #appointment
end
private
def start_time_in_future
begin
yield
rescue ActiveRecord::RecordInvalid => e
redirect_to request.referrer, :alert => e.message
end
end
And it all works just fine, but its so over the top. Can't I just have a custom validation that fails with a message instead of an exception?
I think you can do that by changing your create method like this
def create
#appointment = Appointment.new(foo_params)
if #appointment.save
redirect_to #appointment
else
render "new"
end
end
In your new template, just show error messages by retrieving it from #appointment object like this
#appointment.errors.full_messages
This is my fault and I feel like an idiot.
I made a class method called .confirm! that uses .save! with a bang.
If you want exceptions, use bang methods, if you don't, use .save and .create()
Related
I have a method, create, which calls a private method get_title:
def create
#article = #user.articles.new(article_params)
#article.title = get_title(#article.link)
if #article.save
redirect_to user_path, success: "Article Saved."
else
flash.now[:danger] = "Failed to save article."
end
end
The get_title method works so long as there are no errors in the get request:
def get_title(url)
request = HTTParty.get(url)
document = Nokogiri::HTML(request.body)
title = document.at_css "title"
return title.text
end
I can catch the error with a begin/rescue before the Nokogiri call:
begin
HTTParty.get(url)
rescue HTTParty::Error
...
rescue StandardError
...
else
request = HTTParty.get(url)
end
I don't know how to stop the method and return the error to the create method.
How do I return an error from the rescue statement to the create method and display that error message in a flash?
What you probably want to do is "harden" this method so it doesn't explode unless something really unusual happens:
class TitleFetchError < StandardError
end
def get_title(url)
request = HTTParty.get(url)
Nokogiri::HTML(request.body).at_css('title').text
rescue HTTParty::Error => error
raise TitleFetchError, error.to_s
rescue StandardError => error
raise TitleFetchError, error.to_s
rescue ... (some other type of error, e.g. Nokogiri parse errors)
end
Where in my opinion that should actually be a method on the #article model, like #article.fetch_title which would do the same thing, using its own link property.
One thing to note is this pattern for writing controller actions helps simplify things:
def create
#article = #user.articles.new(article_params)
#article.title = get_title(#article.link)
#article.save!
redirect_to user_path, success: "Article Saved."
rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved
redirect_to user_path, danger: "Failed to save article."
rescue TitleFetchError => error
redirect_to user_path, danger: "Failed to save article. Error: #{error}"
end
Where you have a nice clean "normal" path, and then your exceptions are handled on the side if/when that becomes necessary. This keeps the two flows separated and avoids additional nesting from the if.
Suppose we have a rails API. In many controllers methods I need to set my current_object thanks to params from the request. I can then set a before_action like:
def set_current_object
if My_object.exists? params[:id]
#current_object = My_object.find params[:id]
else
render json: {error: 'Object not found'}.to_json, status:404
end
end
This is ok. But I would like to set current_object dynamically in my controllers methods. Imagine I have a show method in one controller where I need to use my current_object like:
def show
render json: {object_name: current_object.name}.to_json, status: 200
end
current_object would be a helper method like:
def current_object
if My_object.exists? params[:id]
return My_object.find params[:id]
else
render json: {error: 'Object not found'}.to_json, status:404
end
end
Then, if My_object.exists? params[:id] is false I would like to send a 404 and to stop my controller method. Like written here, it is obviously not working. Any suggestion?
You're on the right track. Typically you would implement this sort of "lazy-loading" as a method which memoizes its return value using the ||= idiom.
You simply need to modify your current_object helper so that it can trigger a 404 error when it's unable to return a valid value. Typically you would do this by raising a recognizable exception such as an ActiveRecord::RecordNotFound, and handling this in your controller with a rescue_from clause.
class ApplicationController
def current_object
if My_object.exists? params[:id]
# memozie the value so subsequent calls don't hit the database
#current_object ||= My_object.find params[:id]
else
raise ActiveRecord::RecordNotFound
end
end
rescue_from ActiveRecord::RecordNotFound with: :show_404
def show_404
render json: {error: 'Object not found'}.to_json, status:404
end
end
Now, because you're following a pretty standard Rails convention of handling ActiveRecord::RecordNotFound at the top-level of your controller hierarchy, you can now clean up your current_object method considerably. Instead of checking for the presence of a record, just try to find the record by id. If it doesn't exist, ActiveRecord will automatically raise the exception for you. In fact, your entire current_object method should be a single line of code:
class ApplicationController
def current_object
#current_object ||= My_object.find(params[:id])
end
rescue_from ActiveRecord::RecordNotFound with: :show_404
def show_404
render json: {error: 'Object not found'}.to_json, status:404
end
end
Assuming My_object is a model, if you simply use find, then a params[:id] that doesn't exist in the database will raise an ActiveRecord::RecordNotFound error, and Rails' ActionController::Base will catch the exception and render a 404 by default:
def current_object
My_object.find params[:id]
end
What is the best way to handle the error then ID is not found?
I have this code in my controller:
def show
#match = Match.find(params[:id])
end
I was thinking about something like this:
def show
if #match = Match.find(params[:id])
else
render 'error'
end
end
But I still get:
ActiveRecord::RecordNotFound in MatchesController#show
Couldn't findMatch with 'id'=2
Why?
What is the correct solution?
Rescue it in the base controller and leave your action code as simple as possible.
You don't want to deal not found exception in every action, do you?
class ApplicationController < ActionController::Base
rescue_from ActiveRecord::RecordNotFound, :with => :render_404
def render_404
render :template => "errors/error_404", :status => 404
end
end
By default the find method raises an ActiveRecord::RecordNotFound exception. The correct way of handling a not found record is:
def show
#match = Match.find(params[:id])
rescue ActiveRecord::RecordNotFound => e
render 'error'
end
However, if you prefer an if/else approach, you can use the find_by_id method that will return nil:
def show
#match = Match.find_by_id(params[:id])
if #match.nil? # or unless #match
render 'error'
end
end
You can use find_by_id method it returns nil instead of throwing exception
Model.find_by_id
There is two approaches missing:
One is to use a Null-Object (there I leave research up to you)
Te other one was mentioned, but can be placed more reusable and in a way more elegantly (but it is a bit hidden from you action code because it
works on a somewhat higher level and hides stuff):
class MyScope::MatchController < ApplicationController
before_action :set_match, only: [:show]
def show
# will only render if params[:id] is there and resolves
# to a match that will then be available in #match.
end
private
def set_match
#match = Match.find_by(id: params[:id])
if !#match.present?
# Handle somehow, i.e. with a redirect
redirect_to :back, alert: t('.match_not_found')
end
end
end
I want to save two models in one controller action, or save neither, and return with the validation errors.
Is there a better way than this?
def update
#job = Job.find(params[:id])
#location = #job.location
#job.assign_attributes(job_params)
#location.assign_attributes(location_params)
#job.save unless #job.valid? # gets validation errors
#location.save unless #location.valid? # gets validation errors
if #job.valid? && #location.valid?
#job.save
#location.save
flash[:success] = "Changes saved."
redirect_to edit_job_path(#job)
else
render 'edit'
end
end
New version:
def update
#job = Job.find(params[:id])
#location = #job.location
begin
Job.transaction do
#job.assign_attributes(job_params)
#job.save!(job_params)
#location.assign_attributes(location_params)
#location.save!(location_params)
end
flash[:success] = "Changes saved."
redirect_to edit_job_path(#job)
rescue ActiveRecord::RecordInvalid => invalid
render 'edit'
end
end
Have a look at Active Record Nested Attributes.
Using Nested attributes, you can save associated record attributes through parent.If parent record fails, associated records won't be saved.!
the first thing you'd want to do is delete these two lines
#job.save unless #job.valid? # gets validation errors
#location.save unless #location.valid? # gets validation errors
and only keep the #save in the if statement. because if one of them is valid, but the other isn't, you'll still save the valid one to the db.
To answer your second question, is there a better way to do this? At first blush, it looks like a job for #accepts_nested_attributes_for. However, accepts_nested_attributes_for is somewhat notorious for being difficult to get working (really it just takes a fare amount of tinkering) and what you're currently doing should get you where you're trying to go, so it's up to you.
You can use validates_associated rails helper:
class Model < ActiveRecord::Base
has_one :location
validates_associated :location
end
Then:
if #job.save
#blah
else
#blah
end
Is enough without having to mess with ActiveRecord#Nested_attributes. It's fastest, but less cleaner. Your choice.
Reference:
http://apidock.com/rails/ActiveRecord/Validations/ClassMethods/validates_associated
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