I'm continuing to tweak the Rails Getting Started project to get the authentication behavior I want.
I want to control what is allowed and not allowed at the level of specific actions rather than for a whole controller. For example, you don't need to be signed in to view posts (index / show), but you must be signed in to access the form to submit a new post (new) and to get a submitted post processed (create).
Since I would like people to be redirected to sign-in if they're not signed-in, and I'll be using that snippet over and over again in a million places, I put this in the application controller:
def authcheck
unless user_signed_in?
redirect_to new_user_session_path
end
end
For new posts, this seems to work:
def new
authcheck #see application controller
#post = Post.new
end
But for the case where I have two tabs open and I have the first one on the new post form, but I log out on the second one, then try to submit the post on the first form, I get an error about the user being null even though it seems to me like it should have been redirected:
def create
authcheck
#when not signed in, causes error "undefined method 'posts' for nil:NilClass"
#post = current_user.posts.new(post_params)
if #post.save
redirect_to #post
else
render 'new'
end
end
Actually, I was getting an exception page originally but I changed exception to null_session in the application controller's protect_from_forgery with: :exception line.
Basically: Why isn't it redirecting to the sign-in page like it was when it was just about showing the form for a new post? And, from there, what might you suggest I should do about it?
I think I was labouring under a false impression. I thought that a redirect_to was the end of execution. But it doesn't seem to work that way - after a redirect line I could, for instance, tie up the server in an attempt to square the circle, even if it's not tied to spitting out a webpage.
I modified my "authcheck" to return true or false, then put everything in if blocks. The first example didn't fail the old way because it was just instantiating a new post but it didn't matter if it was actually saved or not.
Change to application controller routine:
def authcheck
unless user_signed_in?
redirect_to new_user_session_path
return false
end
return true
end
Changes to Posts controller routines:
def new
if authcheck #see application controller
#post = Post.new
end
end
def create
if authcheck
#post = current_user.posts.new(post_params)
if #post.save
redirect_to #post
else
render 'new'
end
end
end
I suspect I'm not really doing things the accepted way, so I'll hold out a few days before accepting my own answer. :-)
Related
I have some extra, non-Restful actions in a restful controller. One action responds to a GET, with a form, the form does a POST/PATCH (I've tried both), to another action. If the requirements aren't met, then the method that responds to a POST adds error messages to the model, and renders the first action, exactly like edit and update, or new and create. But...
If the second action has to render back to the first action I see in my logs that it does it right, but then immediately afterwards, there's a request to GET the second action, which obviously fails because there's no GET path to the second action. The end result is that I don't get errors passed down to the form. Why is this? What could be causing it? It's driving me nuts.
Here's the code
def finish_subscription
unless user_signed_in?
store_location_for :user,request.path
redirect_to new_user_session_path
else
#user ||=User.find_by(subscribe_token: params[:token])
authorize! :finish_subscription,#user
#user.accepted_t_and_c = false
end
end
def confirm_subscription
#user=User.find_by(subscribe_token: params[:token])
authorize! :confirm_subscription,#user
if #user.update_attributes(user_params)
o = #user.orders.create
o.build_a_default_subscription
o.save
session[:active_order_id] = o.id
#user.subscribe_token = nil
#user.save
redirect_to pay_via_braintree_path
else
# I've tried all combinations of render action: 'finish_subscription', render :finish_subscription & etc
render 'finish_subscription'
end
end
This is what I see in my logs;-
Started POST "/users/confirm_subscription/d6bb1ffbe56207b6c6864e739e9a455b"
When that fails on validation errors it gets to the line
render 'finish_subscription'
which responds with ;-
Rendered users/finish_subscription.html.haml within layouts/application (2.4ms)
Which is what I expect. But then, immediately afterwards ...
Started GET "/users/confirm_subscription/d6bb1ffbe56207b6c6864e739e9a455b"
WTF!!! Of course that fails because there's no GET route for that action.
Why why why is this happening? And why is it happening on a sunny Sunday afternoon when I've got better things to do?
My new and edit pages depend on an #important_data instance variable that is not used in the create and update actions.
As a result my page can't render the new page upon failure.
def create
#my_object = MyObject.new(params[:my_object])
if #my_object.save
redirect_to root_path
else
render action: "new"
#this can't render because the page asks for an #important_data variable that's not defined.
end
end
Which of the two solutions below should I choose?
What are the advantages/disadvantages of each?
OPTION 1: declare #important_data prior to render
def create
#my_object = MyObject.new(params[:my_object])
if #my_object.save
redirect_to root_path
else
#important_data = ImportantData.all
render action: "new"
end
end
OPTION 2: Redirect
def create
#my_object = MyObject.new(params[:my_object])
if #my_object.save
redirect_to root_path
else
redirect_to new_my_object_path
end
end
When you use render, you're using #my_object with the attributes updated from params[:my_object]. Most of the case, this is what you want. When you show the page to the user, you want to preserve the changes they made to the form and show them the errors.
When you use redirect, you're doing a different and additional request so the parameters submitted from the form are not preserved (unless you pass them in your call to redirect and build them up in the controller action).
So in most cases, you'll definitely want to declare #important_data when the validation fails. I can't think of a case where you'd want to redirect.
Offcourse, the Option1 will work best, since you are rendering new in case of errors only. Also, redirection li'l bit mess up the user experience, it will take a bit long to render the page again with same query #important_data = .... running again.
I think you should use OPTION1
So the place where redirect_to should be used is when you're doing a HTTP POST request and you don't want the user to resubmit the request when it's done (which may cause duplicate items and other problems).
In Rails, when a model fails to be saved, render is used to redisplay the form with the same entries that was filled previously. This is simpler because if you use redirect, you'll have to pass the form entries either using parameters or session. The side effect is that if you refresh the browser, it will try to resubmit the previous form entries. This is acceptable because because it will probably fail the same way, or if it's successful now, it was what the user should expect in the first place anyway.
The above answer is referenced from: Are redirect_to and render exchangeable?
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.
There are many posts on SO about this ( respond_with redirect with notice flash message not working Why is :notice not showing after redirect in Rails 3, among others) , I've read at least 4 and still can't solve this issue.
I've got a portion of my site that lets people do some things before they create an account. I prefer this from a UX perspective. So they're allowed to do X and Y then they get redirected to the "Create account" page (uses Devise).
The redirect looks like:
if userIsNew
... stow information in a cookie to be retrieved later ...
redirect_to "/flash", flash[:notice]
=> "Ok, we'll get right on that after you sign up (we need your email)."
and return # this has to be here, since I'm terminating the action early
end
So "/flash" is a plain page that I made to test this. It doesn't do anything, has no markup of its own, just has the basic html from the application.html, which has this line in the body:
<% if flash[:notice] %>
<p><%= notice %></p>
<% else %>
No notice!
<% end %>
It says 'No notice' every time.
I have tried:
adding in a flash.keep to my before_filter in the static controller
using :notice => instead of flash[:notice] =>
putting the notice in a cookie and pulling that text out of the cookie and into a flash in the before_filter of my application controller
redirect_to :back with the flash[:notice] =>
It's either
flash[:notice] = 'blablabla'
redirect_to foo_url
or
redirect_to foo_url, notice: 'blablabla'
I'm overriding ApplicationController#redirect_to to call flash.keep so that any messages are persisted on redirect without having to explicitly call flash.keep in my controller actions. Works well so far. Haven't had a scenario yet where unwanted messages are persisted.
class ApplicationController < ActionController::Base
def redirect_to(*args)
flash.keep
super
end
end
Let me know if there are any scenarios where this isn't a good solution.
I have been fighting with the same problem for some time and none of the posts seemed to help.
It turns out that - like usually it happens - the the problem was in my code. I did have a "redirect_to" that I forgot about, which was clearing the flash.
Namely, "root_path" for me was served by the StaticPagesController's home method. "home" was doing some checks and then redirecting you to the user_path.
In my code I had in numerous places
redirect_to root_path, :flash => {error: #error}
These redirects were never displaying the flash because my hidden "home" controller serving the "root_path" was making another redirect that cleared the flash.
Therefore my problem was solved when i added the "flash.keep" in my "home" controller method
def home
if current_user
#user = current_user
flash.keep
redirect_to #user unless #user.no_role?
end
end
Faced the same problem, flash just disappeared after any redirect, nothing helped, then, I found that it was switched off...
Check your /config/application.rb for this:
config.middleware.delete ActionDispatch::Flash
I have a simple method in the ApplicationController that, when called, may set a 'flash[:notice]' then redirect to the root_url.
The problem is that even though that method is only called once, the root URL renders that flash[:notice] TWICE.
Here's a the method (which is a before_filter used in other controllers, and is defined in the ApplicationController) :
def authenticate
if params[:id].try(:size) == 40
company = Company.find_by_hash_identifier(params[:id])
if company
session[:editable_companies] ||= []
session[:editable_companies] << company.id
session[:editable_companies].compact!.uniq!
end
end
unless session[:editable_companies].try('&', [company.try(:id), params[:id]])
flash[:notice]= "You are not permitted to edit this company.<br />Please check the URL from the email we sent you, and try again."
flash.keep[:notice]
redirect_to root_url and return
end
end
In the root_url view, I get two flashes like so:
You are not permitted to edit this company.You are not permitted to edit this company.
Get rid of the flash.keep(:notice) line.
You don't (at least shouldn't) need to call flash.keep(:notice) to store the flash across a redirect. A value in the flash hash only gets auto-deleted on a render.
Turns out it was a problem in the view. :-(
I actually had flash[:notice] twice.