around_action for render view in controller rails - ruby-on-rails-4

I have the following method in my controller
around_action :wrap_in_transaction_and_begin, only: :update
def wrap_in_transaction_and_begin
ActiveRecord::Base.transaction do
begin
render json: yield
rescue => e
ActiveRecord::Rollback
render json: { error: e.to_s }
end
end
end
but the problem is raising an error:
Missing template topics/update...
because rails expected "render" in method update itself, not in action_around.
But I need use render json with the returned value of my method update.
exemple:
def update
hash = { foo: :bar }
topic.update(hash)
hash
end
I want render in json the hash object.
how can I do that?
thx.

There's a bunch of things wrong here, mostly because you're diverging from how Rails is supposed to work into really unsupported work-flows.
First, the return value from an action isn't useful. Rails doesn't expect your action to return anything, and it doesn't do anything with the return value. Rails expects output from your action to be either a render or a redirect, and if you don't do either of those things, you're implicitly asking Rails to do the rendering for you. This is why Rails is attempting to render topics/update.
You're also misusing around filters. Their purpose is to transform the output of your action, not to by the sole generator of output.
The way you should be rendering JSON is using respond_with from your action. First, use respond_to to define the kinds of data that your controller can output...
class MyController < ApplicationController
respond_to :json
end
Then, respond with the value you want to respond with.
def update
hash = { foo: :bar }
topic.update(hash)
respond_with hash
end

Related

What is the best way to redirect 404 if request url contains invalid parameters in Rails?

I want to redirect to 404 if request url contains invalid parameters, but I did not know what is the best way to achieve that.
For example:
The request url: http://example.com/a_path/?invalid_parameter=foo
In this case, I want rails return 404. My stupid way is iterating params and check if contains invalided keys:
def redirect_404_if_invalid_params
valid_params = [:valid_key_1, :valid_key_2, :valid_key_3]
params.each do |key, value|
if !valid_params.include?(key)
redirect 404
end
end
end
But is there any better way to do this trick?
Thanks.
You can use Strong Parameters to filter out the parameters you don't want in an action. To do this, you would have to set:
ActionController::Parameters.action_on_unpermitted_parameters = :raise
so Strong Parameters fails whenever you receive parameters that are not explicitly allowed. Then, specify the parameters you do want to be available, like:
def valid_params
params.require(:param_a).permit(:attr_a, :attr_b)
end
And consume your parameters like valid_params[:attr_a], instead of referencing params directly.
You can catch ActionController::UnpermittedParameters and throw your 404 accordingly or do anything else you may need, though I don't suggest you to this. 404 is the HTTP error for "Not Found", having invalid parameters sound more like 422, "Unprocessable Entity".
Assuming that you want to do exactly that for some reason, here's a compact way.
valid_params = [:valid_key_1, :valid_key_2, :valid_key_3]
if (params.keys - valid_params.map(&:to_s)).present?
not_found
end
Take all params, remove all known valid params. If there's something left, it must be invalid keys. In which case, raise.
BTW, not_found in code above is defined as shown in this answer.
Although, for most cases this is not needed and strong parameters should be used instead. With strong parameters it's another approach: we don't care about unknown parameters, we extract only known good parameters and discard the rest.
I had this same problem and researched extensively. This was my solution:
In my application_controller.rb I did this:
rescue_from ActiveRecord::RecordNotFound, with: :not_found
rescue_from Exception, with: :not_found
rescue_from ActionController::RoutingError, with: :not_found
def raise_not_found
raise ActionController::RoutingError.new("No route matches # {params[:unmatched_route]}")
end
def not_found
respond_to do |format|
format.html { render :file => "#{Rails.root}/public/404", :layout => false, :status => :not_found }
format.xml { head :not_found }
format.any { head :not_found }
end
end
And then in your routes.rb file do this:
get '*unmatched_route', to: 'application#raise_not_found'
What does the code do.
You probably have an idea what the rescue_from code line of code does.
raise_not_found if for the line of code you put in the routes file. This method triggers the RoutingError and when this happens, not_found is called.
Any url that does not exit in your routes file would yield the 404.html page.

undefined method `model_name' for #<User:XXXXXXXX>

I have a dashboard containing 2 partials. One partial works fine and does what its suppose to (bill). The other partial is setup similar to bill but wont work(request). When I look at my log it shows that the tenant(user) is being queried, also, the 1st partial is queried but the 2nd partial doesn't query. when I went to my dashboard controller and changed the instance of the partial to (Request.new) it works but I can't seem to get it to work right thru the controller. I don't want to have the method in the model. I am using mongoid.
SO here is my render in the dashboard...
<%= render partial: "request", locals: {request: #request} %>
In the partial I have...
<%= form_for [:tenants, request] do |f| %>
And on the dashboard controller I have...
def show
#dashboard = current_tenant
#bill = current_tenant.bill || current_tenant.build_bill
#request = current_tenant.request || current_tenant.build_request
end
(if I change #request = Request.new it works fine but I know that's not right)
The bill partial works and the dashboard finds the tenant but I keep getting
"undefined method `request' for #
Any idea of what I am missing? I compared the bill controller to the request controller and I cant find any differences. When I put the Model.new into the dashboard controller it works but I know this isn't right, its as if the app wont recognize the Request controller...
The error is saying it doesn't recognize "request" method.
Also here is my controller for request...
class Tenants::RequestsController < ApplicationController
before_filter :authenticate_tenant!
def index
#requests = Request.all
end
def show
#request = Request.find(params[:id])
end
def create
if #request = current_tenant.create_request(authorization_params)
redirect_to tenants_dashboard_path, :notice => "#{request.manager_name} has been Authorized!"
else
redirect_to tenants_dashboard_path, :error => "#{request.manager_name} has NOT been Authorized, please try again."
end
end
def edit
end
def update
if current_tenant.request.update_attributes(authorization_params)
redirect_to tenants_dashboard_path, :notice => "You have approved #{request.manager_name} to review your report"
else
redirect_to tenants_dashboard_path, :notice => "#{request.manager_name} is NOT allowed to review your report"
end
end
def destroy
#request = Request.find(params[:request_id])
name = #request.name
if #request.destroy
flash[:notice] = "\"#{name}\" was successfully removed from your profile."
redirect_to #dashboard
else
flash[:error] = "There was an error deleting this managers access."
render :show
end
end
Well it looks like
current_tenant.request has an issue. That means that the method is not available. Assuming you're not trying to access the http request , then you have an issue with the request method.
So your issue is with how you defined the request method (maybe in your model). e.g. is it a class method or a instance method etc.
Without knowing your goal, that's the general answer I can give you. Creating a Request.new could be right depending on your goal, but if your goal is to call the request method, you must make it available to current_tenant
One controller shouldn't be calling your other controller as you have suggested...

Expected format for validation errors in Ember Data (using ActiveModel::Serializers)

Unfortunately, ActiveModel::Serializers does not currently support validation errors, though they're scheduled for 1.0. Until then, I've got to hack a solution of my own. The big problem? I have no idea what format Ember Data's ActiveModelAdapter expects these errors to be in. I tried simply passing in the errors property, but Ember Data didn't pick up on it:
class MySerializer < ActiveModel::Serializer
attributes :errors
end
So what should I pass in instead?
I use this method to render validation errors (note that you don't use the serializer at all):
def render_validation_errors errors
render json: {errors: errors.to_h}, status: 422
end
You would use it like this:
def create
model = Model.new model_params
if model.save
respond_with model
else
render_validation_errors model.errors
end
end
The format expected by ActiveModelAdapter is:
{"errors":{"title":"should begin with a capital letter"}}

Does ActiveModel::Serializer require an explicit render call?

I know that when using view templates (html, rabl), I don't need an explicit render call in my controller action because by default, Rails renders the template with the name corresponding to the controller action name. I like this concept (not caring about rendering in my controller code) and therefore wonder whether this is possible as well when using ActiveModel::Serializers?
Example, this is code from a generated controller (Rails 4.1.0):
class ProductsController < ApplicationController
before_action :set_product, only: [:show, :edit, :update, :destroy]
#other actions
# GET /products/1
# GET /products/1.json
def show
end
end
and this is the serializer:
class ProductSerializer < ActiveModel::Serializer
attributes :id, :name, :description, :url, :quantity, :price
end
Hitting /products/1.json, I would expect two things to happen:
Fields not listed in the serializer to be ommited,
Whole JSON object to be incapsulated within a 'product' top level field.
However, this does not happen, whole serializer is ignored. But then if I modify the Show method to the following:
# GET /products/1
# GET /products/1.json
def show
#product = Product.find(params[:id])
respond_to do |format|
format.html
format.json { render json: #product }
end
end
And now it is all fine, but I have lost the benefit of the before_action filter (and it seems to me that I have some redundant code).
How should this really be done?
Without an explicit render or respond_with or respond_to Rails will look for a matching template. If that template does not exist Rails throws an error.
However, you can create your own resolver to bypass this. For instance, suppose you created app\models\serialize_resolver.rb and put this into it:
class SerializeResolver < ActionView::Resolver
protected
def find_templates(name, prefix, partial, details)
if details[:formats].to_a.include?(:json) && prefix !~ /layout/
instance = prefix.to_s.singularize
source = "<%= ##{instance}.active_model_serializer.new(##{instance}).to_json.html_safe %>"
identifier = "SerializeResolver - #{prefix} - #{name}"
handler = ActionView::Template.registered_template_handler(:erb)
details = {
format: Mime[:json],
updated_at: Date.today,
virtual_path: "/#{normalize_path(name, prefix)}"
}
[ActionView::Template.new(source, identifier, handler, details)]
else
[]
end
end
def normalize_path(name, prefix)
prefix.present? ? "#{prefix}/#{name}" : name
end
end
And then, in either your application controller (or in an individual controller) place:
append_view_path ::SerializeResolver.new
With that you should be able to do what you want. If it is a json request, it will create an erb template with the right content and return it.
Limitations:
This is a bit clunky because it relies on erb, which is not needed. If I have time I will create a simple template handler. Then we can invoke that without erb.
This does wipe out the default json response.
It relies on the controller name to find the instance variable (/posts is converted to #post.)
I've only tested this a little. The logic could probably be smarter.
Notes:
If a template is present, it will be used first. That allows you to override this behavior.
You can't simply create a new renderer and register it, because the default process doesn't hit it. If the template is not found, you get an error. If the file is found, it goes straight to invoking the template handler.
The 'redundant code' we see in the second one, is this line only:
#product = Product.find(params[:id])
And I believe this is the same logic as your before_action. You don't need this line, just remove it. Now the duplication is removed.
To the remaining part. An action needs to know what to render. By default if the action is empty or absent, the corresponding 'action_name'.html.erb (and other formats specified by respond_to) will be looked up and rendered.
This is why what the Rails 4 generator created works: it creates the show.html.erb and show.json.jbuilder which get rendered.
With ActiveModel::Serializer, you don't have a template. If you leave the action empty, it doesn't have a clue what to render. Thus you need to tell it to render the #product as json, by either:
render json: #product
or
respond_with #product

Why Ruby template handler isn't called in Rails 4?

What could be the reason that the following Ruby template handler isn't called in my Rails 4 application? The returned json contains all the fields that a Job has, instead of just id and date fields.
# app/controllers/jobs_controller.rb
...
def index
#jobs = Job.all
render json: #jobs
end
...
# app/views/jobs/index.json.ruby
jobs = #jobs.map |job| do
{
id: job.id,
date: job.created_at
}
end
jobs.to_json
I use Rails 4.0.0.rc1 and ruby 1.9.3p0.
You are actually not calling a template handler. Instead, what Rails does here is call #jobs.as_json which returns a Hash containing with the attributes of that model. On this hash, it then calls to_json and return the result to the client.
The normal course of action is to override as_json on your model instead of using a template. As said above, as_json should return a hash that contains only the information you'd like to return to the user. Rails will then just serialize this hash as JSON.