Best way to handle 404 in Rails3 controllers with a DataMapper get - ruby-on-rails

It's very simple, I want to handle a normal [show] request with a call to DataMapper like I did in Merb.
With ActiveRecord I could have done this:
class PostsController
def show
#post = Post.get(params[:id])
#comments = #post.comments unless #post.nil?
end
end
and it handles the 404 by catching the resource's exceptions.
DataMapper instead doesn't do this automatically so right now I'm solving it with this solution:
[moved in the answers]
It is possible to tell the controller to halt inside the not_found function?

I like to use exception throwing, and then use ActionController's rescue_from.
Example:
class ApplicationController < ActionController::Base
rescue_from DataMapper::ObjectNotFoundError, :with => :not_found
def not_found
render file => "public/404.html", status => 404, layout => false
end
end
class PostsController
def show
#post = Post.get!(params[:id]) # This will throw an DataMapper::ObjectNotFoundError if it can't be found
#comments = #post.comments
end
end

Done 'the old Merb way':
class ApplicationController
def not_found
render file: "public/404.html", status: 404, layout: false
end
end
class PostsController
def show
#post = Post.get(params[:id])
not_found; return false if #post.nil?
#comments = #post.comments
end
end
again: It is possible to tell the controller to halt inside the not_found function instead of explicitly calling 'return false' in the show action?
edit: thanx to Francois that found a better solution:
class PostsController
def show
#post = Post.get(params[:id])
return not_found if #post.nil?
#comments = #post.comments
end
end

As DM documentation says, you can use #get!

Related

Dynamically set current_object and avoid using before_filter

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

How to handle error when ID is not found?

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

How to render json for all actions from the after_action filter in ApplicationController?

Is it possible to create an after_filter method in the Rails ApplicationController that runs on every action and renders to JSON? I'm scaffolding out an API, and I'd like to render output to JSON for every action in the controller.
clients_controller.rb
def index
#response = Client.all
end
application_controller.rb
...
after_action :render_json
def render_json
render json: #response
end
The after_action is never executed, and the code aborts with:
Template is missing. Missing template clients/index, ...
If the render json: #response is moved into the controller action, it works correctly.
Is there a filter that will allow me to DRY up the controllers and move the render calls to the base controller?
You can't render after_action/ after_filter. The callback after_action is for doing stuff after rendering. So rendering in after_action is too late.
But your exception is just because you miss the JSON template. I recommend using RABL (which offers a lot of flexibility to your JSON responses and there is also a Railscast about it). Then your controller could look like:
class ClientsController < ApplicationController
def index
#clients = Client.all
end
def show
#client = Client.find params[:id]
end
end
And don't forget to create your rabl templates.
e.g. clients/index.rabl:
collection #clients, :object_root => false
attributes :id
node(:fancy_client_name) { |attribute| attribute.client_method_generating_a_fancy_name }
But in the case you still want to be more declarative you can take advantage of the ActionController::MimeResponds.respond_to like:
class ClientsController < ApplicationController
respond_to :json, :html
def index
#clients = Client.all
respond_with(#clients)
end
def show
#client = Client.find params[:id]
respond_with(#client)
end
end
Btw. beware if you put code in an after_action, this will delay the whole request.

Rails RABL respond_with error template

Using RABL in Rails 3.2.x, given the following controller action:
respond_to :html, :json
def create
#foo = Foo.create(params[:foo])
respond_with #foo
end
Assuming the validation fails, how do you get respond_with to use a RABL template instead of the standard JSON hash of errors -- IE. I would like other model attributes besides the validation error message sent back along with the request.
Suggestions?
I found this one out the hard way. You should create a custom responder for your application controller, or at least your individual response. See Three reasons to love ActionController::Responder for more details.
My solution:
# app/responders/api_responder.rb
class ApiResponder < ActionController::Responder
def to_format
case
when has_errors?
controller.response.status = :unprocessable_entity
when post?
controller.response.status = :created
end
default_render
rescue ActionView::MissingTemplate => e
api_behavior(e)
end
end
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
#...
self.responder = ApiResponder
#...
end
You could also use respond_with #foo, responder: ApiResponder instead.
Thanks to haxney for sending me in the right direction.
I guess, you need to remove the respond_to call at the top of the controller and remove the respond_with call within the action to get rabl render your rabl template.
Just add a respond_to block at the end of each action where you don't need RABL.

How to avoid multiple redirect_to when called inside a method

I've got a little issue with rails, I want to be able to do something like this to avoid multiple redirections :
def render_not_found
not_found
end
private
def not_found
redirect_to website_url(#website), :status => 301 and return return
end
return return doesn't work of course!
Using: rails 3.2.0
There's a few ways to do this. One way is to define and raise a custom error, and have a handler that redirects when that happens.
application_controller.rb
Class ApplicationController < ActionController::Base
around_filter :catch_errors
def catch_errors
yield
rescue SiteNotFoundError
redirect_to website_url(#website), :status => 301
rescue ActiveRecord::RecordNotFound
render 404
rescue ...
...
...
end
end
class SiteNotFoundError < StandardError; end
in your controller
def your_action
raise SiteNotFoundError if (some condition)
end
or in a before filter
before_filter :ensure_valid_site
def ensure_valid_site
raise SiteNotFoundError if ....
end
I usually put my redirections for errors in before_filters.
But if you really want to do this, you can do it like this... but I'm warning you
it ain't pretty.
def render_not_found
not_found(binding)
end
private
def not_found(b)
redirect_to website_url(#website), :status => 301
b.eval('return')
end

Resources