crud actions into a module causing stack level too deep error - ruby-on-rails

I am trying to create a module which houses the standard crud functions. whether this can be done, is wise, stand practice, or not, i would like to find out for myself. So far I have created the standard get requests no problem. However I am trying to implement a create action and am encountering a 'stack level too deep error'.
class FlagsController < ApplicationController
include CrudConcern
before_action lambda { crud_index(Flag.all) }, only: :index
before_action lambda { crud_new(Flag.new) }, only: :new
before_action lambda { crud_create(Flag.new, flags_path) }, only: :create
def create
end
end
crud module
def crud_create(model, route)
variable = model(params)
if variable.save
flash[:notice] = "Saved!"
redirect_to route
else
flash[:error] = "Try again"
render :new
end
end
Why would this occur? Is there a way around it? There is a Gem called Crudify which offers this so i assume it can be done.
Thanks

It looks like you are passing in an instance of a model, rather than the model class your method expects. I think you mean the following instead:
# in controller
before_action lambda { crud_create(Flag, flags_path) }, only: :create
# in crud module
def crud_create(model, route)
variable = model.new(params) # change is here
if variable.save
flash[:notice] = "Saved!"
redirect_to route
else
flash[:error] = "Try again"
render :new
end
end
UPDATE
It may be also a redirect loop. flags_path may be hitting the same create method (as opposed to the index), which will continually hit the crud_create before_action, causing the stack level too deep error. Try changing the redirect to test :)
To avoid hitting create again, you may have to set the status to 303. From the APIDock entry for redirect_to:
If you are using XHR requests other than GET or POST and redirecting
after the request then some browsers will follow the redirect using
the original request method. This may lead to undesirable behavior
such as a double DELETE. To work around this you can return a 303 See
Other status code which will be followed using a GET request.
example: redirect_to route, status: 303

Related

Rails: NoMethodError (undefined method `update' for nil:NilClass)

I am following this tutorial in YT:
https://www.youtube.com/watch?v=oyjzi837wME&list=PLWqjhA7WxVW6L7AWzQElmYfXV3NUv_Lbs&index=1
def update
airline = Airline.find_by(slug: params[:slug])
if airline.update(airline_params)
render json: AirlineSerializer.new(airline, options).serialized_json
else
render json: {error: "airline not "}, status: 422
end
end
But I am getting this error:
NoMethodError (undefined method `update' for nil:NilClass)
Any help would be greatly appreciated.
In controllers you almost always want to use find or find_by! so that an ActiveRecord::RecordNotFound exception is raised and you bail early instead of getting a nil error:
def update
airline = Airline.find_by!(slug: params[:slug])
if airline.update(airline_params)
render json: AirlineSerializer.new(airline, options).serialized_json
else
render json: { errors: airline.errors.full_messages }, status: 422
end
end
After all if the record can't even be found there is no reason to continue processing - which is something the tutorial author didn't consider or test for.
This exception is rescued by Rails by default which sends a 404 response but you can override it on a per controller basis by using rescue_from:
class AirlinesController < ApplicationController
rescue_from ActiveRecord::RecordNotFound, with: :not_found
# ...
private
def not_found
render json: { error: "Oh noes" }, status: :not_found
end
end
On a side note params[:slug] is a bit of an antipattern. Your routes should just stick with the conventional /airlines/:id. :id in this case just means a unique identifier for the resource and not the id column. This avoids the need to refactor if you want to look records up by id or slug.
There are also some other problems with the tutorial code:
Authentication is opt-in as evidenced by before_action :authenticate, only: %i[create update destroy]. Use a opt-out secure by default approach instead so that you don't inadvertantly leave security holes. You do this by adding before_action :authenticate in your ApiController (or whatever the base class is) and then using skip_before_action :authenticate, ... to opt out on endpoints that should not require authentication.
There is no validation of slugs and no guarentee of uniqueness for either the slug or name in the form of unique indexes. Just validating the name doesn't actually guarentee the uniqueness of the slug as the name can be changed but the slug is generated when the record is first created.
parameterize is a quite naive solution to the problem of slugging. For real world use cases you need a library like stringex. Or use FriendlyID if you want to avoid reinventing the wheel.
I have spotted my mistake in following the tutorial. The code in the route should be as below:
namespace :api do
namespace :v1 do
resources :airlines, param: :slug
resources :reviews, only: [:create, :destroy]
end
end
Instead I have put resources :airlines, params: :slug with an s in the param.
So I am not very sure why there is no warning about this error when I tried to run it.

How to redirect from /:id to /:friendly_id

Is is possible to force a 301 redirect when someone attempts to browse to a page using the old /:id URL, rather than than the preferred /:friendly_id link?
Apparently such redirections help to tell Google that you have updated the link.. so it stops displaying the old non-friendly link.
With the latest version of friendly_id (5.0.3 at the time of writing this answer) and Rails 4, I do this in the controller:
class ItemsController < ApplicationController
before_action :set_item, only: [:show, :edit, :update, :destroy]
...
private
def set_item
#item = Item.friendly.find(params[:id])
redirect_to action: action_name, id: #item.friendly_id, status: 301 unless #item.friendly_id == params[:id]
end
end
Here's a description of the redirect_to line, broken down piece by piece:
action: action_name retains the action that you're connecting to (which can be show, edit, update, or destroy based on the before_action that's in place) so that if you're accessing /items/1/edit you will be redirected to /items/pretty-url/edit
id: #item.friendly_id ensures that the URL you're being redirected to is the pretty URL
status: 301 sets the redirect to the status of 301, for SEO
unless #item.friendly_id == params[:id] makes sure that we're not redirecting people who access #item through its pretty URL
just defined the redirection inside the routes file
get '/:old_id', to: redirect {|params, req| "/#{X.find(params[:old_id]).friendly_id}" }
While James Chevalier's answer is correct, you can extract this method to the ApplicationController in order to use with any model that uses FriendlyId:
def redirect_resource_if_not_latest_friendly_id(resource)
# This informs search engines with a 301 Moved Permanently status code that
# the show should now be accessed at the new slug. Otherwise FriendlyId
# would make the show accessible at all previous slugs.
if resource.friendly_id != params[:id]
redirect_to resource, status: 301
end
end
As you can see it's also unnecessary to pass a specific action key to redirect_to. Passing a Rails model to redirect_to will automatically attempt to access the show action on the associated collection resource route (assuming it's set up that way). That also means it's unnecessary to pass an id key since FriendlyId always returns the latest slug in the model's #to_param.
Not being a huge fan of unless (confusing semantics) I tend to shy away from it but that's more my personal preference.
Routes
I don't think your routes are the problem here
The problem is the backend handling of the route (I.E whether it uses friendly_id or not). All Google will see is this:
domain.com/users/45
domain.com/users/your_user
If both of those routes work, Google will be happy. I think you're alluding to the idea that if you change the routes to only handle your_user, you'll need to be able to get Google to appreciate the redirects
Redirects
Considering you can handle both id and slug in the backend (we have code for this if you want), I'd handle redirects using the ActionDispatch::Routing::Redirection class:
#config/routes.rb
begin
User.all.each do |u|
begin
get "#{u.id}" => redirect("#{u.slug}")
rescue
end
end
rescue
end
Yes it is possible, you need to define both routes on your config/routes.rb
get 'path/:id' => 'controller#action'
get 'path/:friendly_id' => 'controller#action_2'
then in your legacy action method you need to provide a
return redirect_to controller_action_2_path(friendly_id: friendly_id),
status: :moved_permanently
this will generate a 301 response code. Which will eventually make bots start hitting your new pattern, without losing any of your traffic or indexing (SEO).

Ruby on Rails controller design

When I look at examples of Rails controllers, I usually see something like this:
class WidgetController < ActionController::Base
def new
#widget = Widget.new
end
def create
#widget = Widget.new(params[:id])
if #widget.save
redirect_to #widget
else
render 'new'
end
end
end
This works, but there's a couple problems:
Routes
If I add widgets to my routes.rb file:
Example::Application.routes.draw do
resources :widgets
end
GET /widgets/new will route to new and POST /widgets will route to create.
If the user enters incorrect information on the new widget page and submits it, their browser will display a URL with /widgets, but the new template will be rendered. If the user bookmarks the page and returns later or refreshes the page, the index action will be called instead of the new action, which isn't what the user expects. If there's no index action or if the user doesn't have permission to view it, the response will be a 404.
Duplication of code
As a contrived example, let's say I had some tricky logic in my new method:
def new
#widget = Widget.new
do_something_tricky()
end
Using the current approach, I'd duplicate that logic in new and create. I could call new from create, but then I'd have to modify new to check if #widget is defined:
def new
#widget ||= Widget.new
do_something_tricky()
end
Plus, this feels wrong because it reduces the orthogonality of the controller actions.
What to do?
So what's the Rails way of resolving this problem? Should I redirect to new instead of rendering the new template? Should I call new inside of create? Should I just live with it? Is there a better way?
I don't think this is a problem in "the rails way" and there is no builtin functionality to allow this without getting your hands dirty. What does a user expects when bookmarking a form they just submitted and had errors? Users don't know better, and they shouldn't bookmark a failed form.
I think redirecting to new_widget_path is the cleanest solution. Yet, you should keep the errors and display them on the form. For this I recommend you keep the params in session (which I expect to be smaller than a serialized Widget object).
def new
#widget = widget_from_session || Widget.new
end
def widget_from_session
Widget.new(session.delete(:widget_params)) if session[:widget_params].present?
end
private :widget_from_session
# Before the redirect
session[:widget_params] = params
The code is self explanatory, Widget.new will only be called when widget_from_session returns nil, this is when session[:widget_params] is present. Calling delete on a hash will return de deleted value and delete it from the original hash.
UPDATE Option 2
What about submitting the form using ajax? Your controller could benefit from:
respond_to :html, :json
...
def create
#widget = Widget.new params[:widget]
#widget
respond_with #widget, location: nil
end
Based on the response code (which is set by Rails: 201 Created or 422 Unprocessable Entity), you could show the errors (available in the body of the response when validations fail) or redirect the user to #widget
This is how StackOverflow does it: https://stackoverflow.com/questions/ask. They submit the form asynchronously.
In general, I think the Rails way of solving the problem would be to put the tricky method onto the model or as a helper method, so the controller stays "thin" and you don't have to make sure to add custom behavior to both #new and #create.
EDIT: For further reading, I'd recommend the "Rails AntiPatterns" book, as they go through a lot of these common design issues and give potential solutions.
you put do_something_tricky() in its own method and call it inside the create action (but only when you're rendering the new template, ie when validation fails).
As for the bookmark issue, I don't know a good way to prevent that but to modify the routes and set the create action to the new action but using POST
get '/users/new' => 'users#new'
post '/users/new' => 'users#create'
UPDATE: using resources
resources :platos, except: :create do
post '/new' => 'plates#create', on: :collection, as: :create
end
then you can use create_platos_path in your forms
You don't need to write same function in two action , use before_filter instead.
If you want to have "widget_new_url" after incorrect submission then in your form add url of new widget path something like :url => widget_new_path .
Rails takes the url from Form .
I have this problem before, so I use edit action instead.
Here is my code.
Routes:
resources :wines do
collection do
get :create_wine, as: :create_wine
end
end
Controller:
def create_wine
#wine = Wine.find_uncomplete_or_create_without_validation(current_user)
redirect_to edit_wine_path(#wine)
end
def edit
#wine = Wine.find(params[:id])
end
def update
#wine = Wine.find(params[:id])
if #wine.update_attributes(params[:wine])
redirect_to #wine, notice: "#{#wine.name} updated"
else
render :edit
end
end
Model:
def self.find_uncomplete_or_create_without_validation(user)
wine = user.wines.uncomplete.first || self.create_without_validation(user)
end
def self.create_without_validation(user)
wine = user.wines.build
wine.save(validate: false)
wine
end
View:
= simple_form_for #wine, html: { class: 'form-horizontal' } do |f|
= f.input :complete, as: :hidden, input_html: { value: 'true' }
What I did is create a new action 'create_wine' with get action.
If user request 'create_wine', it will create a new wine without validation and redirect to edit action with a update form for attributes and a hidden field for compele .
If user has create before but gave up saving the wine it will return the last uncompleted wine.
Which means whether use save it or not, the url will be the same to /wines/:id.
Not really good for RESTful design, but solve my problem. If there is any better solution please let me know.

Simple respond_with in rails that avoids 204 from PUT

I want to PUT to rails and avoid getting a 204. I am using this pattern:
class SomeController < ApplicationController
respond_to :json
def update
# ...
respond_with(some_object)
end
end
However, when I do a put to update, I get a 204 back. I realize this is completely valid etc, but I explicitly want the content back. I can override it to some extent like this:
def update
respond_with(some_object) do |format|
format.json{render json: some_object}
end
end
but this seems a bit too hands-on for rails. Is there any more idiomatic way of avoiding a 204 and requesting the full content to be sent back? This is Rails 3.2.
In summary: I want maximally idiomatic rails that avoids a 204.
I made a custom responder which always returns my JSON encoded resource even on PUT/POST.
I put this file in lib/responders/json_responder.rb. Your /lib dir should be autoloaded.
module Responders::JsonResponder
protected
# simply render the resource even on POST instead of redirecting for ajax
def api_behavior(error)
if post?
display resource, :status => :created
# render resource instead of 204 no content
elsif put?
display resource, :status => :ok
else
super
end
end
end
Now, explicitly modify the controller which requires this behavior, or place it in the application controller.
class ApplicationController < ActionController::Base
protect_from_forgery
responders :json
end
You should now get JSON encoded resources back on PUT.
As a less invasive alternative, you can pass a json: option to the respond_with method invocation inside your controller update action, like this:
def update
# ...
respond_with some_object, json: some_object
end
Granted it seems a bit unDRY having to repeat the object twice in the arguments, but it'll give you what you want, the json representation of the object in the response of a PUT request, and you don't need to use the render json: way, which won't give you the benefits of responders.
However, if you have a lot of controllers with this situation, then customizing the responders, as jpfuentes2 showed in the accepted anwser, is the way to go. But for a quick single case, this alternative may be easier.
Source: https://github.com/plataformatec/responders/pull/115#issuecomment-72517532
This behavior seems intentional to fall in line with the HTTP spec, and "ideally" you should be firing off an additional GET request to see the results. However, I agree in the real world I'd rather have it return the JSON.
#jpfuentes2's solution above should do the trick (it's very similar to the pull request below), but I'm hesitant to apply anything that's patching rails internals, as it could be a real pain to upgrade between major versions, especially if you don't have tests for it (and let's face it, developers often skimp on controller tests).
References
https://github.com/rails/rails/issues/9862
https://github.com/rails/rails/pull/9887
Just to clarify, you do not need the responders gem to do this... You can just do:
config/initializers/responder_with_put_content.rb
class ResponderWithPutContent < ActionController::Responder
def api_behavior(*args, &block)
if put?
display resource, :status => :ok
else
super
end
end
end
and then either (for all updates actions to be affected):
class ApplicationController < ActionController::Base
def self.responder
ResponderWithPutContent
end
end
or in your action:
def update
foo = Foo.find(params[:id])
foo.update_attributes(params[:foo])
respond_with foo, responder: ResponderWithPutContent
end
What's wrong with simply doing:
def update
some_object = SomeObject.update()
render json: some_object
end
Not a big fan of this behavior. To get around it, I had to avoid using the respond_with method:
class SomeController < ApplicationController
respond_to :json
def update
# ...
respond_to do |format|
format.json { render(json: some_object, status: 200) }
end
end
end

How do I check if a controller action is already redirecting?

OK, as is often the case, I have a controller action in my app that is protected from unauthorized access by a before_filter. The only thing is that I need to redirect this action if another condition is true:
class Payment < ApplicationController
before_filter login_required
def new
redirect_to some_other_path if #order.is_free?
#payment = Payment.new
end
end
In my testing, I check to make sure that the action is correctly protected, however, it is also the case that the #order.is_free statement is true. When this is the case, I get the following error:
`render_with_no_layout': Can only render or redirect once per action
Is there any way of checking to make sure I'm not already redirecting or to override an existing redirect?
I am assuming that the login_required method performs a redirect if the user is not logged in. In which case:
Your before filter should return false after calling redirect. This will prevent the new action from ever being called. Later versions of rails automatically do this if you call render or redirect in a before_filter, so maybe you are using an older version.
Also you should return after the call to redirect in the new handler, unless you want to always create a new Payment object.
Your class should be PaymentController, not Payment. The reason for this is so the controller class and model class do not clash.
I don't think your before filter is what causes the double render error. Take a look at this example:
class PostsController < ApplicationController
before_filter :perform_a_redirect, :except => [:wtf]
def index
redirect_to 'http://google.com'
end
def wtf
render :text => 'wtf'
end
private
def perform_a_redirect
redirect_to :action => 'wtf'
end
end
When visiting /posts, I get redirected to /posts/wtf. No double render error. Assuming your 'login_required' method only redirects/renders once, I suspect that the code you are posting here is not the problem, but that something else is causing this.
The before filter is a red herring. When #order_is_free? the code is both setting up a redirect and falling through to a rendering of new. The redirect statement doesn't control the flow of the method. Add a return statement after the redirect, or you can even return the redirect, as in return(redirect_to :action => :show, :id => #order, :controller => :free_orders)

Resources