I am following ruby-on-rails instruction guide to creating a simple blog web application: https://guides.rubyonrails.org/getting_started.html#generating-a-controller
All my project files are pretty much the same as the ones in the guide.
app/views/articles/show.html.erb
<p>
<strong>Title:</strong>
<%= #article.title %>
</p>
<p>
<strong>Text:</strong>
<%= #article.text %>
</p>
<h2>Add a comment:</h2>
<%= render 'comments/form' %>
<h2>Comments (<%= #article.comments.count %>)</h2>
<%= render 'comment_section' %>
<%#= render #article.comments %>
<%= link_to 'Edit', edit_article_path(#article) %> |
<%= link_to 'Delete', article_path(#article),
method: :delete,
data: {confirm: 'Are you sure?'} %> |
<%= link_to 'Back', articles_path %>
app/views/comments/_form.html.erb
<%= form_with(model: [#article, #article.comments.build], local: true) do |form| %>
<p>
<%= form.label :commenter %><br>
<%= form.text_field :commenter %>
</p>
<p>
<%= form.label :body %><br>
<%= form.text_area :body %>
</p>
<p>
<%= form.submit %>
</p>
<% end %>
app/views/articles/_comment_section.html.erb
<% if #article.comments.count > 0 %>
<%= render #article.comments %>
<% else %>
<p>There are no comments yet!</p>
<% end %>
app/views/comments/_comment.html.erb
<p>
<strong>Commenter:</strong>
<%= comment.commenter %>
</p>
<p>
<strong>Comment:</strong>
<%= comment.body %>
</p>
<p>
<%= link_to 'Delete comment', [comment.article, comment],
method: :delete,
data: {confirm: 'Are you sure you want to delete this comment?'}
%>
A simple article with no comments works as expected:
However, when showing an article with some actual comments, an extra empty comment gets displayed at the end:
When I try to delete that comment I get the following error (11 in the path is the article_id):
Deleting other comments works fine.
Rest of the files that I think might be relevant:
app/config/routes.rb
Rails.application.routes.draw do
get 'welcome/index'
resources :articles do
resources :comments
end
root 'welcome#index'
end
app/models/article.rb
class Article < ApplicationRecord
has_many :comments, dependent: :destroy
validates :title, presence: true, length: {minimum: 5}
end
app/models/comment.rb
class Comment < ApplicationRecord
belongs_to :article
end
app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
def index
#articles = Article.all
end
def show
#article = Article.find(params[:id])
end
def new
#article = Article.new
end
def edit
#article = Article.find(params[:id])
end
def create
#article = Article.new(article_params)
if #article.save
redirect_to #article
else
render 'new'
end
end
def update
#article = Article.find(params[:id])
if #article.update(article_params)
redirect_to #article
else
render 'edit'
end
end
def destroy
#article = Article.find(params[:id])
#article.destroy
redirect_to articles_path
end
private
def article_params
params.require(:article).permit(:title, :text)
end
end
app/controllers/comments_controller.rb
class CommentsController < ApplicationController
def create
#article = Article.find(params[:article_id])
#comment = #article.comments.create(comment_params)
redirect_to article_path(#article)
end
def destroy
#article = Article.find(params[:article_id])
#comment = #article.comments.find(params[:id])
#comment.destroy
redirect_to article_path(#article)
end
private
def comment_params
params.require(:comment).permit(:commenter, :body)
end
end
I'm using:
ruby 2.6.5p114
Rails 6.0.0
sqlite3 3.8.7.2
RubyMine 2019.2.3
I'm developing on Windows
The reason why this is happening is this line:
<%= form_with(model: [#article, #article.comments.build], local: true) do |form| %>
The part that says #article.comments.build is building an empty comment on the article. If there are no comments on the article and you were to print out #article.comments.count it would be zero. It does this because #article.comments.count runs a query, and since the blank comment isn't saved yet, it doesn't count it against the comments count.
As a side note, #article.comments.size would return 1, since in this case it returns the size of the relation with the blank comment. This is why you don't get a blank comment when the article has no comments.
However if you were to already have a comment and print out #article.comments.count, it would be 1 because now you have a saved comment in the database. This renders your comments out on the page now. The thing is that there is a blank comment inside of the #article.comments return value. This gets printed out to the screen, and since it doesn't have an id, the route for delete gets rendered like this /article/11/comments without a comment id. This route does not exist, so you get an error.
One possible way to fix this would be to change this line in your comment_section partial from this:
<%= render #article.comments %>
to this:
<%= render #article.comments.select { |comment| comment.persisted? %>
UPDATE:
I think that arieljuod's solution is even cleaner, to change this:
<%= form_with(model: [#article, #article.comments.build], local: true) do |form| %>
To this:
<%= form_with(model: [#article, Comment.new], local: true) do |form| %>
in your views/comments/_comment.html.erb
change
<%= link_to 'Delete comment', [comment.article, comment],
method: :delete,
data: {confirm: 'Are you sure you want to delete this comment?'} %>
to
<%= link_to 'Delete comment', comment_path(comment),
method: :delete,
data: {confirm: 'Are you sure you want to delete this comment?'} %>
Related
I'm implementing a voting system where when the course is already upvoted/downvoted, clicking downvote/upvote will switch to that vote respectively and clicking again will delete that vote (similar to Reddit).
While voting and destroying vote perform correctly, I'm having trouble trying to update the vote with form_for and patch method, where the value of the vote_type returns nil
My Votes_Controller
class VotesController < ApplicationController
def create
#vote = Vote.new(secure_params)
#vote.course = Course.find(params[:course_id])
if #vote.save
respond_to do |format|
format.html { redirect_to :courses }
format.js
end
end
def update
vote = Vote.find_by(user: current_user)
vote.update_attribute(:vote_type, update_vote_params)
redirect_to :courses
end
def destroy
vote = Vote.find_by(user: current_user)
vote.destroy
redirect_to :courses, :notice => 'Unvoted!'
end
private
def secure_params
params.require(:vote).permit( :user_id, :vote_type )
end
def update_vote_params
params.require(:vote).permit(:vote_type)
end
end
My index.html.erb
<% if Vote.exists?(user: current_user) && Vote.find_by(user: current_user).vote_type.equal?(-1) %>
<%= form_for course.votes.build, url: course_vote_path(course, Vote.find_by(user: current_user).id), method: :patch do |f| %>
<%= f.hidden_field :vote_type, value: 1 %>
<%= f.submit 'Upvote', class: 'btn btn-default' %>
<% end %>
<% elsif Vote.exists?(user: current_user) && Vote.find_by(user: current_user).vote_type.equal?(1) %>
<%= form_for course.votes.build, url: course_vote_path(course, Vote.find_by(user: current_user).id), method: :delete do |f| %>
<%= f.submit "Upvote", class: 'btn btn-default' %>
<% end %>
<% else %>
<%= form_for course.votes.build, url: course_votes_path(course) do |f| %>
<%= f.hidden_field :user_id, value: current_user.id %>
<%= f.hidden_field :vote_type, value: 1 %>
<%= f.submit 'Upvote', class: 'btn btn-default' %>
<% end %>
<% end %>
My routes.rb
Rails.application.routes.draw do
get 'vote/create'
get 'vote/destroy'
get 'courses/new'
get 'users/new'
resources :courses do
resources :votes, only: [:create, :destroy, :update]
end
resources :users
end
I expect the param to pass 1 for vote_type, however, the actual output is nil
I have an app where Question model has_many relationship with Option. I also have a button to add options while creating a question. Every question has only one correct answer. So when I create a question and click on Add Option button, new option is created but the new radio button associated with it has different name. In fact the name of radio button is of the form question[options_attributes][i][is_answer] where i is id. As far as I know radio buttons should have the same name to work as a collection or group. So how can I make it work as a group even if I create any number of options for a single question?
html.erb
<%= form_for #question do |form| %>
<div class="field">
<%= form.label :body %>
<%= form.text_area :body %>
</div>
<%= form.fields_for :options, question.options.each do |a| %>
<div class="field">
<%= a.label :options %>
<%= a.text_area :body %>
<%= a.radio_button :is_answer, "options" %>
<%= a.check_box :_destroy %>
<%= a.label :_destroy, 'delete' %>
</div>
<% end %>
<%= form.submit 'Add option', :name => "add_option" %>
<%= form.submit 'Delete options', :name => "remove_option" %>
<div class="actions">
<%= form.submit %>
</div>
<% end %>
controller.rb
class QuestionsController < ApplicationController
def new
#question = Question.new
#question.options.build
end
def create
#question = Question.new(question_params)
#question.user = current_user
if params[:add_option]
#question.options.build
else
respond_to do |format|
if #question.save
format.html { redirect_to #question, notice: 'Question was successfully created.' and return }
format.json { render :show, status: :created, location: #question }
else
format.html { render :new }
format.json { render json: #question.errors, status: :unprocessable_entity }
end
end
end
render :action => 'new'
end
private
# Use callbacks to share common setup or constraints between actions.
def set_question
#question = Question.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def question_params
params.require(:question).permit(:body, options_attributes: [:id, :body, :question_id, :created_at, :updated_at, :is_answer])
end
end
There are two options:
Using JavaScript on the client-side to uncheck the radio buttons.
Using radio buttons with the same name. It this case you will have to change the way you pass the :is_answer parameter and manually assign the value in options_attributes.
Method 1 details:
See this question radio different names - only check one
Method 2 details:
Instead of passing :is_answer parameter for each option you can pass a single parameter for the question having chosen answer id as the value. Lets name it "answer_id". We want this parameter to be in the params[question]
hash in the controller, so the whole name will be "question[answer_id]". Although radio buttons are generated for each option, only the chosen one will be sent to the server as they all have the same name.
<%= form.fields_for :options, question.options.each do |a| %>
<div class="field">
<%= a.label :options %>
<%= a.text_area :body %>
<%= radio_button_tag "question[answer_id]", a.object.id, a.object.is_answer? %>
<%= a.check_box :_destroy %>
<%= a.label :_destroy, 'delete' %>
</div>
<% end %>
https://apidock.com/rails/v4.2.7/ActionView/Helpers/FormTagHelper/radio_button_tag
In the controller you will have to manually assign the option's is_answer parameter based on the answer_id value.
def question_params
result = params.require(:question).permit(:body, :answer_id, options_attributes: [:id, :body, :question_id])
answer_id = result.delete(:answer_id)
result[:options_attributes].values.each do |option_attrs|
option_attrs[:is_answer] = option_attrs[:question_id] == answer_id
end
result
end
If you need further details please let me know. I will update the answer to give more information.
I'm currently working on my Rails Blog. While the development is coming along nicely, I've hit a roadblock in the comment.
Every time I try to leave a test comment, I always get "rollback transaction" just before the comment gets saved.
Here's what happens when I create a comment.
Here's the code for looking at blog entry:
post/show.html.erb
<h1 class="title">
<%= #post.title %>
</h1>
<h2>Authored by:
<%= #post.user.username%>
</h2>
<p>
<%=raw #post.content %>
</p>
<div class="comments">
<h3 class="is-3">Comments</h3>
<%if #post.comments.count == 0%>
<strong>There are no comments on this post. Feel free to send one!</strong><br/>
<% else %>
<%= render #post.comments %>
<% end %>
<%if !user_signed_in? %>
<strong>Yo, if you wanna comment on my site, either <%= link_to 'Sign up', new_user_registration_path %> or <%= link_to 'Log in', new_user_session_path %></strong>
<% else %>
<%= render partial: "comments/form", locals: {comment: #post.comments.new} %>
<% end %>
</div>
</div>
<br>
<% if user_signed_in? %>
<div class="functions">
<%= link_to "Update", edit_post_path(#post) if policy(#post).update? do%>
<i class="fa fa-edit editpage fa-3x"></i>
<% end %>
<%= link_to "Delete", #post, :confirm => "Are you sure you want to delete this post?", :method => :delete if policy(#post).destroy? do%>
<i class="fa fa-trash deletepage fa-3x"></i>
<% end %>
</div>
<% end %>
<%= link_to 'Back to the main page', root_path %>
In case you're curious, here's what the comment partial looks like:
<strong><%= #post.comment.user.username %> says:</strong><br>
<%=#post.comment.content %>
<p>
<=time_ago_in_words(comment.created_at) %>
</p>
Can anyone help me figure out why is it that when I create a comment, it rolls back the transaction? I'll provide more information if required.
Edit: Here's the Comments Controller for my rails blog.
class CommentsController < ApplicationController
before_action :set_post
def create
set_post
# Create associated model, just like we did in the console before
#comment = #post.comments.create(comment_params)
# We want to show the comment in the context of the Post
#comment.post_id = #post.post_id
#comment.user_id = current_user.user_id
#comment.save
redirect_to #post
end
def update
#comment = set_comment
#comment.update(comment_params)
redirect_to #post
end
def destroy
#comment = set_comment
#comment.destroy
redirect_to #post
end
private
def comment_params
params.require(:comment).permit(:content)
end
def set_post
#post = Post.friendly.find(params[:post_id])
end
def set_comment
#comment = Comment.find(params[:id])
end
end
Comment Model:
class Comment < ApplicationRecord
belongs_to :post
belongs_to :user
end
Also, the Comment form
<strong><%= #post.comment.user.username %> says:</strong><br>
<%=#post.comment.content %>
<p><= time_ago_in_words(comment.created_at) %></p>
Yes, I already checked here. It didn't work.
Rails: 5.0.2
Ruby: 2.4.0
I have a collection for comments and every time it is called, it renders one time too many and an empty comment always appears below others and when no comments exist, one empty one still renders.
Here is the code:
View
<h2>Add a comment:</h2>
<%= render 'comments/form' %>
<h2>Comments</h2>
<%= render #video.comments || "There are no comments yet." %>
Form partial
<%= form_for([#video, #video.comments.new]) do |f| %>
<p>
<%= f.label :name %><br>
<%= f.text_field :commenter %>
</p>
<p>
<%= f.label :body %><br>
<%= f.text_area :body %>
</p>
<p>
<%= f.submit %>
</p>
<% end %>
Comment partial
<p>
<strong>Name:</strong>
<%= comment.commenter %>
</p>
<p>
<strong>Comment:</strong>
<%= comment.body %>
</p>
<p>
<%= link_to 'Destroy Comment', [comment.video, comment],
method: :delete,
data: { confirm: 'Are you sure?' } %>
</p>
Controller
def create
#video = Video.find(params[:video_id])
#comment = #video.comments.create(comment_params)
redirect_to video_path(#video)
end
def destroy
#video = Video.find(params[:video_id])
#comment = #video.comments.find(params[:id])
#comment.destroy
redirect_to video_path(#video)
end
private
def comment_params
params.require(:comment).permit(:commenter, :body)
end
Does anyone know why this would render the partial an extra time?
You may try to call .scope on comments association:
<%= render #video.comments.scope || "There are no comments yet." %>
I am using rails 4 and have a subject and comment models. Subject is a one to many relationship with comments. I want a simple page that can add comments to many subjects on the same page. So in my form I know how to submit a comment to create but I dont know how to find the right subject in my controller to add it to. Any advice?
class CommentsController < ApplicationController
def create
comment = Comment.create(comment_params)
if comment.save
# The line below is incorrect, I dont know what to do
Subject.find(params[:subject_id]).comments << comment
redirect_to(:controller => 'static_pages', action: 'home')
end
end
def new
end
private
def comment_params
params.require(:comment).permit(:text, :user_name)
end
end
StaticPages#home Find me in
app/views/static_pages/home.html.erb
<% #subjects.each do |subject| %>
<div class="subjects <%= cycle('odd', 'even') %>">
<h1><%= subject.name %></h1>
<h3><%= subject.description %></h3>
<% subject.comments.each do |comment|%>
<div class="comment">
<h4><%= comment.user_name%></h4>
<%= comment.text %>
</div>
<% end %>
<%= form_for(#comment) do |f| %>
<%= f.label :user_name %>
<%= f.text_field :user_name %>
<%= f.label :text %>
<%= f.text_field :text %>
<%= f.submit('Create comment', subject_id: subject.id) %>
<% end %>
</div>
<% end %>
The simplest way would be to populate the subject_id attribute of your #comment form, like this:
<%= form_for(#comment) do |f| %>
<%= f.label :user_name %>
<%= f.text_field :user_name %>
<%= f.label :text %>
<%= f.text_field :text %>
<%= f.hidden_field :subject_id, value: subject.id %>
<%= f.submit('Create comment', subject_id: subject.id) %>
<% end %>
This will populate the subject_id attribute of your new Comment object, which will essentially associate it through Rails' backend:
#app/controllers/your_controller.rb
Class YourController < ApplicationController
def create
#comment = Comment.new comment_params
#comment.save
end
private
def comment_params
params.require(:comment).permit(:subject_id, :text, :user_name)
end
end
--
foreign_keys
This works because of the Rails / relational database foreign_keys structure
Every time you associate two objects with Rails, or another relational database system, you basically have a database column which links the two. This is called a foreign_key, and in your case, every Comment will have the subject_id foreign_key column, associating it with the relevant subject
So you may have many different forms using the same #comment variable - the trick is to populate the foreign_key for each one