Adding comments to article in Rails 5 (deeply nested resources) - ruby-on-rails

I have a Rails app that has three main models: Qn, Ans, and Comments. I've been doing okay with 1-level-deep nested resources, but these three resources are nested deeply (Comments is shallowly nested) and they are all displayed in one single view, which makes it very confusing.
In a url like: http://localhost:3000/questions/2, the user can see all #question.answers displayed using a loop. In each of those answers, the user can see the answer.comments displayed using a loop. Below each answer, the user can also submit a new comment.
But after attempting several times to implement a 1) loop displaying all comments and 2) form for new comment, I always get some error along the lines of:
undefined method `model_name' for {:url=>"/questions/4/answers/2/comments/new"}:Hash
So I tried to pass in params #commentable instead of the answer, or point to the specific controller and action and so on, but none of these methods worked. I am guessing that I have an issue with my controllers to begin with, but I cannot seem to figure out what.
routes.rb (top ommited)
# Resources
resources :sessions
resources :users
resources :bookmarks # to be implemented later
resources :questions do
resources :answers do
resources :comments, shallow: true
end
end
Question model
class Question < ApplicationRecord
has_many :answers
has_many :bookmarks #later
end
Answer model:
class Answer < ApplicationRecord
belongs_to :question
has_many :comments, as: :commentable
has_many :likes, as: :likeable
validates :answercontent, length: {minimum: 50}
end
Comment model:
class Comment < ApplicationRecord
belongs_to :commentable, polymorphic: true
end
The show.html.erb (of QuestionsController)
<% #question.answers.each do |answer| %>
// ommited
<!-- Comments -->
<% answer.comments.each do |comment| %>
<%= comment.content %>
<br>
<% end %>
<!-- Submit new comment -->
<%= form_for(url: new_question_answer_comment_path, comment: {answer_id: answer.id, question_id: #question.id}) do |f| %>
<%= f.text_area :content %>
<%= f.submit "Submit" %>
<% end %>
<% end %>
QuestionsController (new, create, destroy ommited for brevity)
class QuestionsController < ApplicationController
def index
#questions = Question.all
end
def show
#question = Question.find(params[:id])
#answers = Answer.all
# Delete only appears when num_ans is 0
#deletable = (current_user== User.find(#question.user_id)) && (#question.answers.all.size==0)
end
private
def question_params
params.require(:question).permit(:picture_url, :country, :educational_level, :topic)
end
end
AnswersController (edit, update, destroy ommited for brevity)
class AnswersController < ApplicationController
def create
#question = Question.find(params[:question_id])
#answer = #question.answers.create(answer_params)
#answer.question_id = #question.id
#answer.user_id = current_user.id
if #answer.save
redirect_to #question
else
render :new
end
end
private
def answer_params
params.require(:answer).permit(:user_id, :question_id, :answercontent)
end
end
CommentsController
class CommentsController < ApplicationController
before_filter: load_commentable
def index
#commentable = Answer.find(params[:answer_id])
#comments = #commentable.comments
end
def new
#comment = #commentable.comments.new
end
def create
#comment = #commentable.comments.new(params[:comment])
if #comment.save
redirect_to #commentable
else
render :new
end
end
# From RailsCast ep.154
private
def load_commentable
resource, id = request.path.split('/')[1,2]
#commentable = resource.singularize.classify.constantize.find(id)
end
end
The routes
are quite messy right now so I will just post where the comments are:
question_answer_comments GET /questions/:question_id/answers/:answer_id/comments(.:format) comments#index
POST /questions/:question_id/answers/:answer_id/comments(.:format) comments#create
new_question_answer_comment GET /questions/:question_id/answers/:answer_id/comments/new(.:format) comments#new
edit_comment GET /comments/:id/edit(.:format) comments#edit
comment GET /comments/:id(.:format) comments#show
PATCH /comments/:id(.:format) comments#update
PUT /comments/:id(.:format) comments#update
DELETE /comments/:id(.:format) comments#destroy
Thanks in advance for the help.
Update:
To give you more info on what solutions I attempted:
1. Passing in two params like:
<%= form_for([answer, #comment], url: new_question_answer_comment_path(answer.id, #question.id)) do |f| %>
Gave me:
First argument in form cannot contain nil or be empty
Using #commentable (which is basically the answer) gives me an error saying that 'the id in #commentable.id doesn't exist as #commentable is nil'.
So I think the problem is that answer or #commentable is nil. But I specified it in the loop and in the controller too. So what else may I try?

form_for expects record as first argument, in your case it should be a comment instance. Also new_question_answer_comment_path expects values for question_id and answer_id keys, as you are creating a new comment, the route should be question_answer_comments not new_question_answer_comment so your form_for should be
<%= form_for Comment.new,url: question_answer_comments_path(#question,answer) do |f| %>
<%= f.text_area :content %>
<%= f.submit "Submit" %>
<% end %>
or just
<%= form_for [Comment.new,#question,answer] do |f| %>
<%= f.text_area :content %>
<%= f.submit "Submit" %>
<% end %>

Related

How do I get a destroy method to work for nested resources in Rails?

Currently learning Ruby on Rails and creating a simple blog app with comments. I have a Comment model and an Article model. Comment is polymorphic and both models have many comments.
I'm trying to come up with a destroy method that's able to delete both the comments that belong to Comment and the ones that belong to Article (and that remain as [deleted] without destroying their children, much like in Reddit, although I haven't even gotten to that part).
I have tried different paths but I haven't got it right yet. Nested paths still confuse me a little and I'm not sure on how to pass the params that the path requests when creating the link_to.
These are my files:
routes.rb:
Rails.application.routes.draw do
get 'comments/new'
get 'comments/create'
get 'articles/index'
get 'articles/show'
root 'articles#index'
resources :articles do
resources :comments
end
resources :comments do
resources :comments
end
end
article.rb:
class Article < ApplicationRecord
has_many :comments, as: :commentable
end
comment.rb:
class Comment < ApplicationRecord
belongs_to :commentable, polymorphic: :true
has_many :comments, as: :commentable
end
comments_controller.rb:
class CommentsController < ApplicationController
before_action :find_commentable
def new
#comment = Comment.new
end
def create
#comment = #commentable.comments.new(comment_params)
if #comment.save
redirect_back(fallback_location: root_path)
else
redirect_back(fallback_location: root_path)
end
end
def destroy
#comment = #commentable.comments.find(params[:id])
#comment.destroy
redirect_back(fallback_location: root_path)
end
private
def comment_params
params.require(:comment).permit(:body)
end
def find_commentable
if params[:article_id]
#commentable = Article.find_by_id(params[:article_id])
elsif params[:comment_id]
#commentable = Comment.find_by_id(params[:comment_id])
end
end
end
show.html.erb, where the form for commments that belong to Article.rb is:
<h1> <%= #article.title %> </h1>
<p> <%= #article.body %> </p>
<small>Submitted <%= time_ago_in_words(#article.created_at) %> ago </small> <br/>
<h3>Comments</h3>
<%= form_for [#article, Comment.new] do |f| %>
<%= f.text_area :body, placeholder: "Say something!" %> <br/>
<%= f.submit "Submit" %>
<% end %>
<ul class="parent-comment">
<%= render partial: 'comments/comment', collection: #article.comments %>
</ul>
<%= link_to "Index", articles_path %>
And the partial _comment.html.erb , which displays the comments that belong to the article as well as those that belong to other comments, and where I'm trying to integrate the link_to:
<p> <%= comment.body %> </p>
<small>Submitted <%= time_ago_in_words(comment.created_at) %> ago </small> <br/>
<%= form_for [comment, Comment.new] do |f| %>
<%= f.text_area :body, placeholder: "Add a reply!" %><br/>
<%= f.submit "Reply" %>
<%= link_to "Delete", comment_path(comment), method: :delete %>
<% end %>
<ul>
<%= render partial: 'comments/comment', collection: comment.comments %>
</ul>
Whenever I do seem to get the path right, NoMethodError in CommentsController#destroy — undefined method `comments' for nil:NilClass comes up. Why would the controller show it as undefined? It worked in the new method, as far as I can see.
Could you give some guidance as to what I should do or what I should fix? I'm not sure how to delete the parent comments, either, and I haven't managed to find information that suits this case. If you know where to point me to, I'm all eyes.
Thank you.
Because of your design model structure.
Your view
<%= link_to "Delete", comment_path(comment), method: :delete %>
Your find_commentable
elsif params[:comment_id]
#commentable = Comment.find_by_id(params[:comment_id])
end
#commentable will be a Comment class, so it won't have .comments methods as your Article class
check carefully to destroy the method
def destroy
#comment = #commentable.comments.find(params[:id])
#comment.destroy
redirect_back(fallback_location: root_path)
end
use #comment = #commentable.comments.find_by(id: params[:id]) and check whether #comment has some value or not?
just add one condition like this and it won't throw the error:
#comment.destroy if #comment
if #comment is nil and trying to destroy then it will throw the error.

Routing issue after update

So I'm still a Rails noob so I may be completely going at this wrong but I have two controllers. A Question Controller and an Answer Controller. I am trying to build a grading function that allows an admin user to assign points to essay questions. I am using the /answer/:id to be where the :id is the id of the question and then rendering a partial to iterate through all of the answers for that id. Clear as mud I'm sure...
My problem: Within the partial where the user's answer is displayed, I have a form that allows the admin to fill out the number of points for that answer and submit. Ideally, I'd like it to move to the next page (using will_paginate), but at a minimum, I'd like to stay on the same page. I am able to get the form working but it keeps going to /answers/:id but where :id is the id of the individual answer, so not what I'm hoping.
answers_controller.rb
class AnswersController < ApplicationController
def index
#user = current_user
#questions = Question.all
end
def show
#user = current_user
#question = Question.find(params[:id])
#answers = Answer.where("question_id = ?", #question.id).paginate(:page => params[:page], :per_page => 1)
#answer = Answer.where("question_id =? AND user_id = ?", #question.id, #user.id)
end
def edit
#answer = Answer.find(params[:id])
end
def update
#answer = Answer.find(params[:id])
if #answer.update_attributes(grade_params)
flash[:success] = "Answer Graded"
else
flash[:warning] = "Not Graded"
end
end
private
def grade_params
params.require(:answer).permit(:points_earned)
end
end
_essay_grades.html.erb (partial that is being rendered on the show page that contains the form)
<% #answers.each do |answer| %>
<p>User: <%= answer.user_id %></p>
<%= answer.answer %><br>
<%= #question.value %>
<br>
<%= form_for(answer) do |f| %>
<%#= f.radio_button :points_earned, #question.value %><br>
<%#= f.radio_button :points_earned, 0 %> <br>
<%= f.text_field :points_earned %> Points<br>
<br>
<%= f.submit "Award Points" %>
<% end %>
<% end %>
<br>
<br>
<%= will_paginate #answers, renderer: BootstrapPagination::Rails %>
routes.rb
Rails.application.routes.draw do
resources :admins, :answers, :static_pages, :questions
devise_for :users, :controllers => { registrations: 'registrations' },
:path => '', :path_names =>
{ :sign_in => "login", :sign_up => "register" }
root "static_pages#index"
end
I'm sure there's a simple solution here (or maybe it's changing how I have things set up...). Any help is greatly appreciated!
AFTER FEEDBACK:
Added the grades model and set up a through relationship with questions.
answer_controller.rb
class AnswersController < ApplicationController
def show
#user = current_user
#question = Question.find(params[:id])
#answers = Answer.where("question_id = ?", #question.id).paginate(:page => params[:page], :per_page => 1)
#answer = Answer.where("question_id =? AND user_id = ?", #question.id, #user.id)
end
def update
#user = current_user
#question = Question.find(params[:question_id])
#answer = #question.answers.find(params[:id])
#grade = #question.grades.new(grade_params)
if #grade.save
flash[:success] = "Answer Graded"
redirect_to #question
end
end
private
def grade_params
params.require(:grade).permit(:user_id, :answer_id, :points_earned, :graded_by, :comment)
end
end
_answer.html.erb
<%= answer.user_id %>
<%= form_tag [#question, answer], method: :put do %>
<%= hidden_field_tag :graded_by, current_user.id %>
<%= hidden_field_tag :answer_id, answer.id %>
<%= number_field_tag :points_earned %>
<%= submit_tag "Submit Grade" %>
<% end %>
routes.rb
Rails.application.routes.draw do
resources :questions do
resources :answers, only: [:update]
end
resources :admins, :static_pages
questions/show.html.erb
...
<h3>Show answers</h3>
<%= render #answers, locals: {question: #question} %>
<%= will_paginate #answers, renderer: BootstrapPagination::Rails %>
You will have to use the following in your form so the update does not load a new page, but still submits your update. Use Chrome / Firefox developer tools to view requests / responses.
<%= form_for(answer), :remote => true do |f| %>
Then, alter the update action in the answers controller to load the 'next unrated answer':
def update
rated_answer = Answer.find(params[:id])
if rated_answer.update_attributes(grade_params)
flash[:success] = "Answer Graded"
else
flash[:warning] = "Not Graded"
end
#answer = get_next_unrated_answer(rated_answer.question_id)
end
private
def get_next_unrated_answer(question_id)
# I am making a couple of assumptions on your model here, but get an answer that has not been rated yet for this question
next_answer = Answer.where("question_id = ? and points_earned = ?", question.id, nil)
#returned automatically
end
Then you will have to create app/views/answers/update.js.erb to load the new answer to your page with the following line:
$('#main_div').html('<%= escape_javascript(render partial: 'whatever_partial_you_have_created_to_display_the_next_unrated_answer') %>');
Just go and create a new partial that displays your answer and form correctly for the next unrated answer. Or ideally load your initial 'show.html.erb' with the relevant partials and reuse them.
This is the simple way to do it, but if I were you I would probably rename these new functions to not use 'update' or 'show' but rather call it something like 'rate' and even 'rate_show' so you can use update and show in its original form (for updating and answer or showing an answer) if required later in your project.
From what I understood of your question, I think you'd be best looking into nested routes:
#config/routes.rb
resources :questions do
resources :answers, only: [:update]
end
#app/controllers/questions_controller.rb
class QuestionsController < ApplicationController
def show
#question = Question.find params[:id]
end
end
#app/views/questions/show.html.erb
<%= #question.title %>
<%= render #question.answers, locals: {question: #question} %>
#app/views/questions/_answer.html.erb
<%= answer.title %>
<%= form_tag [question, answer], method: :put do %>
<%= text_field_tag :grade %>
<%= submit_tag %>
<% end %>
The above will give you what you have already (just to clarify your "clearly mad" remark is not the case at all!).
-
The following is where the nested resources come in.
At the moment, it seems you're having a problem associating an answer to a question:
where :id is the id of the individual answer, so not what I'm hoping
A remedy for this is as follows:
#app/controllers/answers_controller.rb
class AnswersController < ApplicationController
def update
#question = Question.find params[:question_id]
#answer = #question.answers.find params[:id]
#grade = #answer.grades.new grade_params
redirect_to #question if #grade.save
end
private
def grade_params
params.permit(:points_earned) #-> will probably have to refactor this
end
end
This will create a new grade (which you should have in an associated model), for that specific answer. Because the answer has been associated to a question, it will allow you to use the nested routes to load both.
In terms of your setup, I'd personally add a Grade model, so that you can have multiple grades per answer. This is against your current schema, but works well to ensure you have the functionality necessary to facilitate multiple grades:
#app/models/grade.rb
class Grade < ActiveRecord::Base
belongs_to :answer
belongs_to :user
end
#app/models/answer.rb
class Answer < ActiveRecord::Base
has_many :grades
end

Allowing user to favorite a post

I'm working on allowing a user to favorite a post.
I've created a model called favorite.
belongs_to :user
belongs_to :post
it stores the user_id and post_id.
I've also created a FavoritesController
class FavoritesController < ApplicationController
def create
#post = Post.find(params[:post_id])
current_user.favorite(#post)
end
def destroy
#post = Post.find(params[:id])
current_user.unfavorite(#post)
end
end
the form I have on my Posts#index is:
<%= form_for current_user.favorites.build do |favorite| %>
<%= hidden_field_tag :post_id, f.id %>
<%= favorite.button do %>
<i class="fa fa-star-o"></i>
<% end %>
<% end %>
my user model looks like this:
# Favorites a post.
def favorite(post)
favorite.create(post_id: post)
end
# Unfavorites a post.
def unfavorite(post)
favorite.find_by(post_id: post).destroy
end
when I try to click on favorite I get:
wrong number of arguments (0 for 1)
Parameters:
{"utf8"=>"✓",
"authenticity_token"=>"HjiANQUqTQVEqy0yzfLFMlnC8RsTiY5kVlvIUnD5OSIaSYSi4ELSuC95vRMIBA/6W+KvzCWMMXQ==",
"post_id"=>"7",
What am I doing wrong here? Also is there a better way to do this?
If you're just trying to get a 'voting/favoriting' system working, I suggest using a gem like https://github.com/ryanto/acts_as_votable or something of the like.

How to link_to correct show page from feed?

If a user clicks on:
activities/valuations/_create.html.erb
<%= link_to valuation_path(activity) do %>
<%= activity.trackable.name %>
<% end %>
he is directed to, for example, the error:
Couldn't find Valuation with 'id'=24
because the valuation is actually 7. This is because this line of code is trying to find the valuation show page by looking at the id number as an activity instead of the id number as a valuation.
To fix it I tried:
<%= link_to valuation_path(#valuation) do %>
<%= activity.trackable.name %>
<% end %>
but that gives the error:
ActionController::UrlGenerationError in Activities#index
No route matches {:action=>"show", :controller=>"valuations",
:id=>nil} missing required keys: [:id]
How do we rewrite the code to take the user to the correct valuations show page?
activities_controller
class ActivitiesController < ApplicationController
def index
#activities = Activity.order("created_at desc").paginate(:page => params[:page])
end
def show
redirect_to(:back)
end
def like
#activity = Activity.find(params[:id])
#activity_like = current_user.activity_likes.build(activity: #activity)
if #activity_like.save
#activity.increment!(:likes)
flash[:success] = 'Thanks for liking!'
else
flash[:error] = 'Two many likes'
end
redirect_to(:back)
end
end
activity.rb
class Activity < ActiveRecord::Base
self.per_page = 20
has_many :notifications
belongs_to :user
belongs_to :trackable, polymorphic: true
def conceal
trackable.conceal
end
def page_number
(index / per_page.to_f).ceil
end
private
def index
Activity.order(created_at: :desc).index self
end
end
activities/index
<% #activities.each do |activity| %>
<%= link_to activity.user.name, activity.user %></b>
<%= render "activities/#{activity.trackable_type.underscore}/#{activity.action}", activity: activity %>
<% end %>
it seems like you want to do a nested resource,
when you have activities/valuations/_create.html.erb , that means you should have a nested route
#routes.rb
resources :activities do
resources :valuations
end
So your link should be something like
<%= link_to 'link', activities_valuataions_path(activity, #validation) %>
if you could post your routes
rake routes
it should be easy to track down the problem

Rails: Routing Error, comment model to micropost model

I made a commenting system and I am trying to get it to post under a micropost but I constantly get this routing error. Any suggestions? All help is much appreciated!
Routing Error
No route matches [POST] "/microposts/comments"
Form
<div class="CommentField">
<%= form_for ([#micropost, #micropost.comments.new]) do |f| %>
<%= f.text_area :content, :class => "CommentText", :placeholder => "Write a Comment..." %>
<div class="CommentButtonContainer">
<%= f.submit "Comment", :class => "CommentButton b1" %>
</div>
<% end %>
</div>
comment controller
class CommentsController < ApplicationController
def create
#micropost = Micropost.find(params[:micropost_id])
#comment = #micropost.comments.build(params[:comment])
#comment.user_id = current_user.id
#comment.save
respond_to do |format|
format.html
format.js
end
end
end
routes
resources :microposts do
resources :comments
end
Micropost Model
class Micropost < ActiveRecord::Base
attr_accessible :title, :content, :view_count
acts_as_voteable
belongs_to :user
has_many :comments
has_many :views
accepts_nested_attributes_for :comments
end
User Controller
class UsersController < ApplicationController
def show
#user = User.find(params[:id])
#school = School.find(params[:id])
#micropost = Micropost.new
#comment = Comment.new
#comment = #micropost.comments.build(params[:comment])
#microposts = #user.microposts.paginate(:per_page => 10, :page => params[:page])
end
end
The reason you are getting the error is that you are trying to build a form for a comments of a micropost that does not exist yet in the database.
the form, there is a -
form_for ([#micropost, #micropost.comments.new]) do |f|
And in UsersController you have -
#micropost = Micropost.new
comment is a sub-resource of micropost, so a url that creates a comment should look like /micropost/:id/comments where :id is the id of micropost. That is possible only after the micropost is saved.
So I believe your action should assign #micropost to an existing post, or create one right there to have the form working. Something like -
#micropost = Micropost.last || Micropost.create
would at least get rid of the error.
I'll try this again (deleted my other answer since, as Marc Talbot pointed out, was not the correct response to your issue).
Perhaps the issue is as simple as making :microposts be :micropost instead (to reflect your model's name).
resources :micropost do
resources :comments
end

Resources