So I've started getting this error after I tried to implement AJAX comments in my rails app:
ActionController::InvalidAuthenticityToken in CommentsController#create
ActionController::InvalidAuthenticityToken
def handle_unverified_request
raise ActionController::InvalidAuthenticityToken
end
end
end
Here are all the codes from the relevant files:
comments_controller.rb
class CommentsController < ApplicationController
before_action :find_post
def create
#comment = #post.comments.build(comment_params)
#comment.user_id = current_user.id
if #comment.save
respond_to do |format|
format.html { redirect_to root_path }
format.js
end
else
flash[:alert] = "Check the comment form, something went horribly wrong."
render root_path
end
end
Add comments form:
= form_for([post, post.comments.build], remote: true) do |f|
= f.text_field :content, placeholder: 'Add a comment...', class: "comment_content", id: "comment_content_#{post.id}"
views/comments/create.js.erb
$('#comments_<%= #post.id %>').append("<%=j render 'comments/comment', post: #post, comment: #comment %>");
$('#comment_content_<%= #post.id %>').val('')
comment.rb
class Comment < ActiveRecord::Base
belongs_to :user
belongs_to :post
end
I have no idea what's causing this error as it worked fine before the introduction of AJAX. I looked up answers to similar problems on stackoverflow and added protect_from_forgery at the top of comments_controller.rb to no avail. I don't get the InvalidAuthenticityToken error alright, but instead, it gives me a different error:
NoMethodError in CommentsController#create
undefined method `id' for nil:NilClass
def create
#comment = #post.comments.build(comment_params)
#comment.user_id = current_user.id #highlighted line
if #comment.save
respond_to do |format|
Solution to my problem was to put on first line inside controller:
skip_before_action :verify_authenticity_token, :only => [:create,:inquire_enterprise]
As you can see I am escaping 2 actions which produced the error.
You have to send an authenticity token with your forms, it should be generated in your form_for, so I guess your ajax is just not sending it.
In case it's not automatically generated, you can do it manually: <%= hidden_field_tag :authenticity_token, form_authenticity_token %>
Unless config.action_view.embed_authenticity_token_in_remote_forms is set to true (the default is false), Rails won't generate the hidden input containing the csrf token if the form is a remote one.
This is because ajax powered forms have another mechanism for getting the token & this change means you can now fragment cache html containing this form because it no longer contains content that changes for every user.
This mechanism is adding the csrf tag to the page's meta tags, which the rails javascript can read and add to the ajax request. There is a helper, csrf_meta_tags that does this for you - just add a call to it in the <head> of the html you are rendering (this will usually be in your layout file).
Related
I have a very straight-forward task to fulfil --- just to be able to write comments under posts and if the comments fail validation display error messages on the page.
My comment model uses a gem called Acts_as_commentable_with_threading, which creates a comment model after I installed.
On my post page, the logic goes like this:
Posts#show => display post and a form to enter comments => after the comment is entered, redisplay the Post#show page which has the new comment if it passes validation, otherwise display the error messages above the form.
However with my current code I can't display error messages if the comment validation fails. I think it is because when I redisplay the page it builds a new comment so the old one was erased. But I don't know how to make it work.
My codes are like this:
Comment.rb:
class Comment < ActiveRecord::Base
include Humanizer
require_human_on :create
acts_as_nested_set :scope => [:commentable_id, :commentable_type]
validates :body, :presence => true
validates :first_name, :presence => true
validates :last_name, :presence => true
# NOTE: install the acts_as_votable plugin if you
# want user to vote on the quality of comments.
#acts_as_votable
belongs_to :commentable, :polymorphic => true
# NOTE: Comments belong to a user
belongs_to :user
# Helper class method that allows you to build a comment
# by passing a commentable object, a user (could be nil), and comment text
# example in readme
def self.build_from(obj, user_id, comment, first_name, last_name)
new \
:commentable => obj,
:body => comment,
:user_id => user_id,
:first_name => first_name,
:last_name => last_name
end
end
PostController.rb:
class PostsController < ApplicationController
before_action :authenticate_user!, except: [:index, :show]
def show
#post = Post.friendly.find(params[:id])
#new_comment = Comment.build_from(#post, nil, "", "", "")
end
end
CommentsController:
class CommentsController < ApplicationController
def create
#comment = build_comment(comment_params)
respond_to do |format|
if #comment.save
make_child_comment
format.html
format.json { redirect_to(:back, :notice => 'Comment was successfully added.')}
else
format.html
format.json { redirect_to(:back, :flash => {:error => #comment.errors}) }
end
end
end
private
def comment_params
params.require(:comment).permit(:user, :first_name, :last_name, :body, :commentable_id, :commentable_type, :comment_id,
:humanizer_answer, :humanizer_question_id)
end
def commentable_type
comment_params[:commentable_type]
end
def commentable_id
comment_params[:commentable_id]
end
def comment_id
comment_params[:comment_id]
end
def body
comment_params[:body]
end
def make_child_comment
return "" if comment_id.blank?
parent_comment = Comment.find comment_id
#comment.move_to_child_of(parent_comment)
end
def build_comment(comment_params)
if current_user.nil?
user_id = nil
first_name = comment_params[:first_name]
last_name = comment_params[:last_name]
else
user_id = current_user.id
first_name = current_user.first_name
last_name = current_user.last_name
end
commentable = commentable_type.constantize.find(commentable_id)
Comment.build_from(commentable, user_id, comment_params[:body],
first_name, last_name)
end
end
comments/form: (this is on the Posts#show page)
<%= form_for #new_comment do |f| %>
<% if #new_comment.errors.any? %>
<div id="errors">
<h2><%= pluralize(#new_comment.errors.count, "error") %> encountered, please check your input.</h2>
<ul>
<% #new_comment.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
<% end %>
I would instead use nested routes to create a more restful and less tangled setup:
concerns :commentable do
resources :comments, only: [:create]
end
resources :posts, concerns: :commentable
This will give you a route POST /posts/1/comments to create a comment.
In your controller the first thing you want to do is figure out what the parent of the comment is:
class CommentsController < ApplicationController
before_action :set_commentable
private
def set_commentable
if params[:post_id]
#commentable = Post.find(params[:post_id])
end
end
end
This means that we no longer need to pass the commentable as form parameters. Its also eliminates this unsafe construct:
commentable = commentable_type.constantize.find(commentable_id)
Where a malicous user could potentially pass any class name as commentable_type and you would let them find it in the DB... Never trust user input to the point where you use it to execute any kind of code!
With that we can start building our create action:
class CommentsController < ApplicationController
before_action :set_commentable
def create
#comment = #commentable.comments.new(comment_params) do |comment|
if current_user
comment.user = current_user
comment.first_name = current_user.first_name
comment.last_name = current_user.last_name
end
end
if #comment.save
respond_to do |format|
format.json { head :created, location: #comment }
format.html { redirect_to #commentable, success: 'Comment created' }
end
else
respond_to do |format|
format.html { render :new }
format.json { render json: #comment.errors, status: 422 }
end
end
end
private
# ...
def comment_params
params.require(:comment).permit(:first_name, :last_name, :body, :humanizer_answer, :humanizer_question_id)
end
end
In Rails when the user submits a form you do not redirect the user back to the form - instead you re-render the form and send it as a response.
While you could have your CommentsController render the show view of whatever the commentable is it will be quite brittle and may not even provide a good user experience since the user will see the top of the post they where commenting. Instead we would render app/views/comments/new.html.erb which should just contain the form.
Also pay attention to how we are responding. You should generally avoid using redirect_to :back since it relies on the client sending the HTTP_REFERRER header with the request. Many clients do not send this!
Instead use redirect_to #commentable or whatever resource you are creating.
In your original code you have totally mixed up JSON and HTML responses.
When responding with JSON you do not redirect or send flash messages.
If a JSON POST request is successful you would either:
Respond with HTTP 201 - CREATED and a location header which contains the url to the newly created resource. This is preferred when using SPA's like Ember or Angular.
Respond with HTTP 200 - OK and the resource as JSON in the response body. This is often done in legacy API's.
If it fails do to validations you should respond with 422 - Unprocessable Entity - usually the errors are rendered as JSON in the response body as well.
Added.
You can scrap your Comment.build_from method as well which does you no good at all and is very idiosyncratic Ruby.
class PostsController < ApplicationController
before_action :authenticate_user!, except: [:index, :show]
def show
#post = Post.friendly.find(params[:id])
#new_comment = #post.comments.new
end
end
Don't use line contiuation (\) syntax like that - use parens.
Don't:
new \
:commentable => obj,
:body => comment,
:user_id => user_id,
:first_name => first_name,
:last_name => last_name
Do:
new(
foo: a,
bar: b
)
Added 2
When using form_for with nested resources you pass it like this:
<%= form_for([commentable, comment]) do |f| %>
<% end %>
This will create the correct url for the action attribute and bind the form to the comment object. This uses locals to make it resuable so you would render the partial like so:
I'm assuming your form_for submits a POST request which triggers the HTML format in CommentsController#create:
def create
#comment = build_comment(comment_params)
respond_to do |format|
if #comment.save
make_child_comment
format.html
format.json { redirect_to(:back, :notice => 'Comment was successfully added.')}
else
format.html
format.json { redirect_to(:back, :flash => {:error => #comment.errors}) }
end
end
end
So, if #comment.save fails, and this is an HTML request, the #create method renders create.html. I think you want to render Posts#show instead.
Keep in mind that if validations fail on an object (Either by calling save/create, or validate/valid?), the #comment object will be populated with errors. In other words calling #comment.errors returns the relevant errors if validation fails. This is how your form is able to display the errors in #new_comment.errors.
For consistency, you'll need to rename #new_comment as #comment in the posts#show action, otherwise you'll get a NoMethodError on Nil::NilClass.
TL;DR: You're not rendering your form again with your failed #comment object if creation of that comment fails. Rename to #comment in posts, and render controller: :posts, action: :show if #comment.save fails from CommentsController#create
I have figured out the answer myself with the help of others here.
The reason is that I messed up with the JSON format and html format (typical noobie error)
To be able to display the errors using the code I need to change two places ( and change #comment to #new_comment as per #Anthony's advice).
1.
routes.rb:
resources :comments, defaults: { format: 'html' } # I set it as 'json' before
2.
CommentsController.rb:
def create
#new_comment = build_comment(comment_params)
respond_to do |format|
if #new_comment.save
make_child_comment
format.html { redirect_to(:back, :notice => 'Comment was successfully added.') }
else
commentable = commentable_type.constantize.find(commentable_id)
format.html { render template: 'posts/show', locals: {:#post => commentable} }
format.json { render json: #new_comment.errors }
end
end
end
In my Rails 4 app, I have a calendar and a post models: a calendar has_many posts and a post belong_to a calendar.
In Calendars#Show, I display all the posts that belong to this calendar.
In this very same view, I use the following link to update the approval custom attribute of a post:
<%= link_to post_path(:id => #post.id, "post[approval]" => "ok"), :method => :patch %>
Problem, once I click this link, I am taken to the Posts#Show view of this post.
What I would like instead, is to remain on the same Calendars#Show view.
I thought of implementing a redirect_to :back in my Posts#Update action in the controller, but this is not possible, since this would redirect me to the Posts#Edit view when I update a post from this very same Posts#Edit view.
So, I am looking for a solution to implement a redirect_to specifically for my link_to helper with remote: true from the Calendars#Show to the Calendars#Show.
Is that possible?
with remote: true
If you're using remote (your code snippet does not have it), you won't be redirected anyway:
<%= link_to post_path(:id => #post.id, "post[approval]" => "ok"), :method => :patch, remote :true %>
The remote: true functionality invokes ajax, which is an XML call (through JS) to your server. By virtue of the call being out of "scope" (IE not a standard "request"), it will not cause any changes to your current browser state, unless you trigger them yourself:
#app/controllers/posts_controller.rb
class PostsController < ApplicationController
def update
#post = Post.find params[:id]
respond_to do |format|
if #post.update
format.json #-> fired only when json is datatype
format.js #-> invokes app/views/posts/update.js.erb
format.html { redirect_to #post } #-> only fires on http request
end
end
end
end
#app/views/posts/update.js.erb
## do what you want here
from the Calendars#Show to the Calendars#Show
If you want to refresh your page, you could use:
#app/views/posts/update.js.erb
window.location.reload(true);
--
To give you some more context, ajax stands for asynrhconous javascript and xml. This means that each of the requests you send with it are considered to be asynchronous -- or out of scope of the "normal" HTTP request.
Ajax requests are meant to be used to apply changes / updates to an already loaded page. A good example would be a chat system or something.
Your question of how to redirect after an Ajax response, although not wrong, is definitely against convention of how it's considered to work. If you wanted to create a redirect as you are attempting to, I would highly recommend using a standard HTTP request with some conditions in your action:
#app/controllers/posts_controller.rb
class PostsController < ApplicationController
def update
#post = Post.find params[:id]
#post.update post_params
redirect_to posts_path if #post.approved.changed?
redirect_to root_path if ....
end
end
Since this is an AJAX request, in your Post controller you need to respond to js:
def update
# ...
respond_to do |format|
if #post.update(post_params)
format.js { head :ok } # see here no redirects
# ... other formats
else
# ...
end
end
end
The problem in brief: I'm working on a rails 4 app (4.1.8) and I'm trying to get flash[:notice] and flash[:alert] to show up under a form.
Two controllers: landingpage_controller and contacts_controller. The landingpage_controller serves a static landingpage through its show action and the contacts_controller has new and create actions, to store the contacts in a db table.
On the static landingpage, a modal with id="contact-modal" contains a partial with a simple_form_for #contact (see below). Upon submittal of the form, a db-entry is not created if the fields are not all filled out and a db-entry is created if the fields are filled out. However, no flash messages are displayed.
Wanted output:
Ideally the partial would re-load without leaving/closing the modal, with either: a success message and an empty form or a alert message and the form as it was upon submittal. How do I do this?
The controller: app/controllers/contacts_controller.rb
class ContactsController < ApplicationController
def new
#contact = Contact.new
render layout: "contact"
end
def create
#contact = Contact.new
respond_to do |format|
if #contact.save
flash[:notice] = "Success"
format.js
else
flash[:alert] = "Error"
format.js
end
end
end
private
def contact_params
params.require(:contact).permit(:email, :structure, :message_content)
end
end
The form: app/views/contacts/_new.html.haml
= simple_form_for #contact, html: { id: "contact-form"} do |c|
= c.input :email
= c.input :structure
= c.input :message_content
= c.button :submit
.messages-container
= if flash[:notice]
%p
= flash[:notice]
= if flash[:alert]
%p
= flash[:alert]
Routes:
resources :contacts, only: [:new, :create]
I'm aware that a partial reload probably involves AJAX. I've read several StackOverflow questions on this but have not been able to figure it out. See here, here and these two blog-posts: jetthoughts, ericlondon.
Your help is very much appreciated
There are several problems in your code:
views, that start with underscore are called partials and are not full actions, but just parts of reusable view code (you don't redirect to them, instead you use render since you usually don't want a full page reload.
1.1 Rename your _new.html.haml to _form.html.haml
2.1 Create a new view new.html.erb(I guess you have that already, otherwise your new action might not work properly) with content = render 'form'
From what I understand you don't want the modal to close, just to render a form after successful submission or if there is an error.
In that case:
1.create a create.js.erb file in your views/contacts folder
create.js.erb
$("#your_modal_id").html("<%= j( render 'form') %>")
2. change your create action
def create
#contact = Contact.new(contact_params)
respond_to do |format|
if #contact.save
flash[:notice] = "Success"
format.js
else
flash[:alert] = "Error"
format.js
end
end
end
to your form add remote: true
WARNING: This will leave your form filled in even if it is successful.
More about this topic see:
http://guides.rubyonrails.org/v4.1.8/working_with_javascript_in_rails.html#form-for
Hope it helps, and I hope I didn't forget anything
I have a resource called Books. It's listed as a resource properly in my routes file.
I have a new action, which gives the new view the standard:
#book = Book.new
On the model, there are some attributes which are validated by presence, so if a save action fails, errors will be generated.
In my controller:
#book = Book.create
... # some logic
if #book.save
redirect_to(#book)
else
render :new
end
This is pretty standard; and the rationale for using render:new is so that the object is passed back to the view and errors can be reported, form entries re-filled, etc.
This works, except every time I'm sent back to the form (via render :new), my errors show up, but my URL is the INDEX URL, which is
/books
Rather than
/books/new
Which is where I started out in the first place. I have seen several others posts about this problem, but no answers. At a minimum, one would assume it would land you at /books/create, which I also have a view file for (identical to new in this case).
I can do this:
# if the book isn't saved then
flash[:error] = "Errors!"
redirect_to new_book_path
But then the #book data is lost, along with the error messages, which is the entire point of having the form and the actions, etc.
Why is render :new landing me at /books, my index action, when normally that URL calls the INDEX method, which lists all the books?
It actually is sending you to the create path. It's in the create action, the path for which is /books, using HTTP method POST. This looks the same as the index path /books, but the index path is using HTTP method GET. The rails routing code takes the method into account when determining which action to call. After validation fails, you're still in the create action, but you're rendering the new view. It's a bit confusing, but a line like render :new doesn't actually invoke the new action at all; it's still running the create action and it tells Rails to render the new view.
I just started with the Rails-Tutorial and had the same problem.
The solution is just simple: If you want the same URL after submitting a form (with errors), just combine the new and create action in one action.
Here is the part of my code, which makes this possible (hope it helps someone^^)
routes.rb (Adding the post-route for new-action):
...
resources :books
post "books/new"
...
Controller:
...
def create
#book = Book.new(book_params)
if #book.save
# save was successful
print "Book saved!"
else
# If we have errors render the form again
render 'new'
end
end
def new
if book_params
# If data submitted already by the form we call the create method
create
return
end
#book = Book.new
render 'new' # call it explicit
end
private
def book_params
if params[:book].nil? || params[:book].empty?
return false
else
return params.require(:book).permit(:title, :isbn, :price)
end
end
new.html.erb:
<%= form_for #book, :url => {:action => :new} do |f| %>
<%= f.label :title %>
<%= f.text_field :title %>
<%= f.label :isbn %>
<%= f.text_field :isbn %>
<%= f.label :price %>
<%= f.password_field :price %>
<%= f.submit "Save book" %>
<% end %>
Just had the very same question, so maybe this might help somebody someday. You basically have to make 3 adjustments in order for this thing to work, although my solution is still not ideal.
1) In the create action:
if #book.save
redirect_to(#book)
else
flash[:book] = #book
redirect_to new_book_path
end
2) In the new action:
#book = flash[:book] ? Book.new(flash[:book]): Book.new
3) Wherever you parse the flash hash, be sure to filter out flash[:book].
--> correct URL is displayed, Form data is preserved. Still, I somehow don't like putting the user object into the flash hash, I don't think that's it's purpose. Does anyboy know a better place to put it in?
It doesn't land you at /books/new since you are creating resource by posting to /books/. When your create fails it is just rendering the new action, not redirecting you to the new action. As #MrYoshiji says above you can try redirecting it to the new action, but this is really inefficient as you would be creating another HTTP request and round trip to the server, only to change the url. At that point if it matters you could probably use javascript change it.
It can be fixed by using same url but different methods for new and create action.
In the routes file following code can be used.
resources :books do
get :common_path_string, on: :collection, action: :new
post :common_path_string, on: :collection, action: :create
end
Now you new page will render at url
books/common_path_string
In case any errors comes after validation, still the url will be same.
Also in the form instead using
books_path
use
url: common_path_string_books_path, method: :post
Choose common_path_string of your liking.
If client side validation fails you can still have all the field inputs when the new view is rendered. Along with server side errors output to the client. On re-submission it will still run create action.
In books_controller.rb
def new
#book = current_user.books.build
end
def create
# you will need to have book_params in private
#book = current_user.books.build(book_params)
if #book.save
redirect_to edit_book_path(#book), notice: "Book has been added successfully"
# render edit but you can redirect to dashboard path or root path
else
redirect_to request.referrer, flash: { error: #book.errors.full_messages.join(", ") }
end
end
In new.html.erb
<%= form_for #book, html: {class: 'some-class'} do |f| %>
...
# Book fields
# Can also customize client side validation by adding novalidate:true,
# and make your own JS validations with error outputs for each field.
# In the form or use browser default validation.
<% end %>
I have a Post that has_many :comments and a Comment that belongs_to :post.
On /posts/:id (the post show method) I render a form where users can leave comments.
It all works, validations, tests and posting is just fine. Only thing missing is how to re-render the POSTed data on validation errors.
The (simplified) code for this is:
#app/controllers/posts_controller.rb
class PostsController < ApplicationController
def index
#posts = Post.all_published(params[:page])
#title = "Blog"
end
def show
#post = Post.where({:published => true}).find(params[:id])
#comment = Comment.new(:post => #post)
#title = #post.title
end
end
#app/controllers/comments_controller.rb
class CommentsController < ApplicationController
def create
#comment = Comment.new(params[:comment])
puts #comment
if #comment.save
flash[:notice] = 'Comment was successfully created.'
redirect_to(#comment.post)
else
flash[:notice] = "Error creating comment: #{#comment.errors}"
redirect_to(#comment.post)
end
end
end
#app/views/posts/show.haml
.html renders Post contents.
- form_for #comment do |f|
= f.hidden_field :post_id
= f.text_area :body
= f.text_field :name
.some more fields.
I expect the solution to be either in some magical declaration in the comments_controller.rb, part
else
flash[:notice] = "Error creating comment: #{#comment.errors}"
redirect_to(#comment.post)
end
Or in the PostsController.show where I prepare the #comment. Should I set that #comment conditional and fill it with some magic variable on errors?
Or did I make some entirely different mistake?
If you redirect, that data is usually lost, thats why in most cases in create create actions you would have noticed that in the false scenario, render not redirect_to.
So instead you could just try,
flash[:notice] = ""Error creating comment: #{#comment.errors}"
render :template => "posts/show"
#post = #comment.post
# you may need to pre-populate the instance variables used inside PostsController#show