I am trying to make an app with Rails 4.
I use simple form for forms.
I am trying to follow this tutorial so that my polymorphic comments model can be used to add comments to articles. https://gorails.com/episodes/comments-with-polymorphic-associations?autoplay=1
I have models for article and comment as follows:
article.rb
has_many :comments, as: :commentable
accepts_nested_attributes_for :comments
comment.rb
belongs_to :user
belongs_to :commentable, :polymorphic => true
The comment controllers have been setup as shown in the video tutorial:
comments_controller.rb
Article/comments_controller.rb
The article controller has:
def new
#article = Article.new
#article.comments.build
end
def article_params
params[:article].permit(:user_id, :body, :title, :image, :tag_list,
comment_attributes: [:opinion])
end
The article show page has:
<div class="col-xs-12">
<%= render :partial => 'comments/form', locals: {commentable: #article} %>
</div>
The comments form partial has:
<%= simple_form_for [commentable, Comment.new] do |f| %>
<%= f.error_notification %>
<div class="form-inputs">
<%= f.input :opinion, as: :text, :label => "Add your thoughts", :input_html => {:rows => 4} %>
</div>
<div class="form-actions">
<%= f.button :submit, "Submit", :class => 'formsubmit' %>
</div>
<% end %>
The routes are:
resources :articles do
collection do
get 'search'
end
resources :comments, module: :articles
end
When I save all of this and try to render the articles show page, I get this error:
undefined method `new' for #
The error points to the comments controller create action:
def create
#comment = #commentable.new(comment_params)
#comment.user = current_user
respond_to do |format|
if #comment.save
format.html { redirect_to #commentable }
format.json { render :show, status: :created, location: #comment }
else
format.html { render :new }
format.json { render json: #comment.errors, status: :unprocessable_entity }
end
end
end
I don't know what the problem is. I can't understand why this isn't working or what this error message means. I'm wondering if its because comment belongs to both user and commentable.
In fact when I push this and try to see it in production mode, I get a failure and heroku logs show this error:
Exiting
2016-01-02T02:27:59.274318+00:00 app[web.1]: /app/app/controllers/Articles/comments_controller.rb:1:in `<top (required)>': uninitialized constant Articles (NameError)
The entire Article/comments controller has:
class Articles::CommentsController < CommentsController
before_action :set_commentable#, only: [:show, :edit, :update, :destroy]
private
# Use callbacks to share common setup or constraints between actions.
def set_commentable
#commentable = Article.find(params[:article_id])
end
end
So this now works on new articles, but only for 1 comment. If I try and add a second comment to a single article, I get this error:
PG::UniqueViolation: ERROR: duplicate key value violates unique constraint "index_comments_on_commentable_type_and_commentable_id" DETAIL: Key (commentable_type, commentable_id)=(Article, 4) already exists. : INSERT INTO "comments" ("opinion", "user_id", "commentable_id", "commentable_type", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5, $6) RETURNING "id"
Try:
def create
#comment = Comment.new(comment_params)
#comment.user = current_user
#comment.commentable = #commentable
respond_to do |format|
if #comment.save
format.html { redirect_to #comment }
format.json { render :show, status: :created, location: #comment }
else
format.html { render :new }
format.json { render json: #comment.errors, status: :unprocessable_entity }
end
end
end
Lots of issues.
This is how it should look:
--
#config/routes.rb
resources :articles do
get :search, on: :collection
resources :comments, module: :articles #-> url.com/articles/:article_id/comments
end
#app/controllers/articles/comments_controller.rb #-> this is the heroku error
class Articles::CommentsController < ApplicationController
before_action :set_article
respond_to :js, :json, :html
def create
#comment = #article.comments.new comment_params
respond_with #comment
end
private
# Use callbacks to share common setup or constraints between actions.
def set_article
#article = Article.find params[:article_id]
end
def comment_params
params.require(:comment).permit(:opinion).merge(user: current_user)
end
end
You should be creating comments as their own entity (not as a nested attribute of articles). You should do this using the following:
#app/views/articles/show.html.erb
<%= content_tag :div, render(partial: 'comments/form', locals: { article: #article }), class: "col-xs-12" %>
#app/views/comments/_form.html.erb
<%= simple_form_for [article, article.comments.new] do |f| %>
<%= f.input :opinion, as: :text, label: "Add your thoughts", input_html: {rows: 4} %>
<%= f.submit %>
<% end %>
This should create a standalone comment on the back of the Article that's been invoked.
Related
I have a Poll app with 3 models.
Poll.rb
class poll < ApplicationRecord
validates_presence_of :user, :title
belongs_to :user
has_many :questions, dependent: :destroy
has_many :options, through: :questions
accepts_nested_attributes_for :questions
end
Question.rb
class Question < ApplicationRecord
validates_presence_of :poll_id, :question_id, :title
belongs_to :poll
has_many :options
accepts_nested_attributes_for :options, reject_if: proc { |attributes| attributes['title'].blank? }
end
Option.rb
class Option < ApplicationRecord
validates_presence_of :question_id, :title
belongs_to :question
belongs_to :poll
end
I want the question form to have a field for adding options so I've added this to the question _form.
<%= form.fields_for :option do |o| %>
<div>
<%= o.label "Option", style: "display: block" %>
<%= o.text_field :title, placeholder: "Enter Option here" %>
</div>
<% end %>
I can now see an option block which is good. Although I wish to have 3 possbile options so in the questions_controller.rb I've added the following:
def new
#question = #poll.questions.build
3.times { #question.options.build } # 3 different options
end
Despite this I'm only seeing one option block instead of the 3. Why is this the case and how do i fix? Additionally I'm not seeing new entries into the options postgresql table.
Full questions_controller.rb
class QuestionsController < ApplicationController
before_action :set_question, only: %i[ show edit update destroy ]
before_action :set_poll
# GET /questions or /questions.json
def index
#questions = Question.all
end
# GET /questions/1 or /questions/1.json
def show
end
# GET /questions/new
def new
# #question = Question.new
#question = #poll.questions.build
3.times { #question.options.build } # 5 different options
end
# GET /questions/1/edit
def edit
end
# POST /questions or /questions.json
def create
#question = Question.new(question_params)
respond_to do |format|
if #question.save
format.html { redirect_to polls_question_url(#question), notice: "Question was successfully created." }
format.json { render :show, status: :created, location: #question }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: #question.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /questions/1 or /questions/1.json
def update
respond_to do |format|
if #question.update(question_params)
format.html { redirect_to polls_question_url(#question), notice: "Question was successfully updated." }
format.json { render :show, status: :ok, location: #question }
else
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: #question.errors, status: :unprocessable_entity }
end
end
end
# DELETE /questions/1 or /questions/1.json
def destroy
poll_id = Question.find_by(params[:poll_id])
session[:return_to] ||= request.referer
#question.destroy
respond_to do |format|
format.html { redirect_to session.delete(:return_to), notice: "Question was successfully destroyed." }
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_question
#question = Question.find(params[:id])
end
# Only allow a list of trusted parameters through.
def question_params
params.require(:question).permit(:poll_id, :question_type, :title, :description, :randomize_selection, :voter_abstain, { option_attributes: [:question_id, :poll_id, :party_id, :title, :description] } )
end
def set_poll
#poll = poll.find_by(params[:poll_id])
end
end
routes.rb
resources :users do
resources :polls
end
resource :polls do
resources :questions
end
resource :questions do
resources :options
end
Edit:
Here is my questions form partial.
_form.html.erb
<%= form_with(model: [#Poll, question] ) do |form| %>
<% if question.errors.any? %>
<div style="color: red">
<h2><%= pluralize(question.errors.count, "error") %> prohibited this question from being saved:</h2>
<ul>
<% question.errors.each do |error| %>
<li><%= error.full_message %></li>
<% end %>
</ul>
</div>
<% end %>
<div>
<%= form.hidden_field :poll_id %>
</div>
<div>
<%= form.label :question_type, style: "display: block" %>
<%= form.text_field :question_type %>
</div>
<div>
<%= form.label :title, style: "display: block" %>
<%= form.text_field :title %>
</div>
<div>
<%= form.label :description, style: "display: block" %>
<%= form.text_area :description %>
</div>
<div>
<%= form.label :randomize_selection, style: "display: block" %>
<%= form.check_box :randomize_selection %>
</div>
<div>
<%= form.label :voter_abstain, style: "display: block" %>
<%= form.check_box :voter_abstain %>
</div>
<div>
<%= form.fields_for :options do |o| %>
<div>
<%= o.label "Option", style: "display: block" %>
<%= o.text_field :title, placeholder: "Enter Option here" %>
</div>
<% end %>
</div>
<div>
<%= form.submit %>
</div>
<% end %>
Here is the poll's show where I am rendering the forms.
show.html.erb
<p style="color: green"><%= notice %></p>
<p>
<strong>Poll Title:</strong>
<%= #poll.title %>
<%= render #poll %>
</p>
<div>
<%= link_to "Edit this poll", edit_user_poll_path(#poll) %> |
<%= link_to "Back to polls", user_polls_path %> |
<%= link_to "Destroy this poll", user_poll_path(#poll), method: :delete %>
</div>
<% if #poll.questions.any? %>
<hr>
<h2>Questions:</h2>
<%= render #poll.questions %>
<% end %>
<hr>
<h2>Add a new Question:</h2>
<%= render "questions/form", question: #poll.questions.build %>
The argument you pass to fields_for has to match the name of the assocation on the model:
<%= form.fields_for :options do |o| %>
<div>
<%= o.label "Option", style: "display: block" %>
<%= o.text_field :title, placeholder: "Enter Option here" %>
</div>
<% end %>
Pay very careful attention to plurization in Rails. Its a huge part of getting Convention over Configuration to work for you instead of against you.
However there are a quite a few other problems with this code.
Constants should always be CamelCase or UPPERCASE in Ruby - you need to change class poll to class Poll and fix all the references to the class. This isn't just a matter of style since the interpreter treats identifiers that start with an uppercase letter completely differently.
You're not nesting it properly. You have a nested route but you're still treating it like a non-nested resource in your controller and docstrings.
You're passing the parent id in your params whitelist. :poll_id and :question_id should not be whitelisted. Do not pass the parent id with a hidden input. The question id is assigned by Rails - you should not trust the user to pass it.
The option should not need a poll_id. Use an indirect has_one assocation to go up the tree. This could cause a edge case where a question and its options belong to different polls.
First lets fix the models:
class Poll < ApplicationRecord
# belongs_to assocations are required by default
# adding validations will just cause duplicate error messages
validates_presence_of :title
belongs_to :user
has_many :questions, dependent: :destroy
has_many :options, through: :questions
accepts_nested_attributes_for :questions
end
class Question < ApplicationRecord
validates_presence_of :title
belongs_to :poll
has_many :options
accepts_nested_attributes_for :options, reject_if: proc { |attributes| attributes['title'].blank? }
end
class Option < ApplicationRecord
validates_presence_of :title
belongs_to :question
has_one :poll, through: :question
end
Then I would recommend that you use shallow nesting
resource :polls do
resources :questions, shallow: true
end
This creates the questions member routes (show, edit, delete) without the /polls/:poll_id prefix while the collection routes (index, create, new) are nested.
And that you set controller up as:
class QuestionsController < ApplicationController
before_action :set_question, only: %i[ show edit update destroy ]
before_action :set_poll, only: %i[ new create index ]
# GET /polls/1/questions or /polls/1/questions.json
def index
#questions = #poll.questions.all
end
# GET /questions/1 or /polls/1/questions/1.json
def show
end
# GET /polls/1/questions/new
def new
# build is just an alias of new for legacy compatibility with Rails 2...
# its about time that we ditch it
#question = #poll.questions.new
3.times { #question.options.new } # 5 different options
end
# GET /questions/1/edit
def edit
end
# POST /polls/1/questions or /polls/1/questions.json
def create
#question = #poll.questions.new(question_params)
respond_to do |format|
if #question.save
format.html { redirect_to #question, notice: "Question was successfully created." }
format.json { render :show, status: :created, location: #question }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: #question.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /questions/1 or /questions/1.json
def update
respond_to do |format|
if #question.update(question_params)
format.html { redirect_to #question, notice: "Question was successfully updated." }
format.json { render :show, status: :ok, location: #question }
else
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: #question.errors, status: :unprocessable_entity }
end
end
end
# DELETE /questions/1 or /questions/2.json
def destroy
session[:return_to] ||= request.referer
#question.destroy
respond_to do |format|
format.html { redirect_to session.delete(:return_to), notice: "Question was successfully destroyed." }
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_question
#question = Questions.find(params[:id])
end
# Only allow a list of trusted parameters through.
def question_params
# do not write this in a single unreadable line
params.require(:question).permit(
:question_type,
:title,
:description,
:randomize_selection,
:voter_abstain,
# do not wrap hash arguments in brackets
# as it will break if/when the `permit` method is changed to use real keyword arguments
# for has_many assocations the key naming convention is also plural_attributes
options_attributes: [
:party_id,
:title,
:description
]
)
end
def set_poll
#poll = Poll.find_by(params[:poll_id])
end
end
The key difference here is that you should look up the poll by the parameter in the URL for the nested routes and create the question off the poll instance (which sets poll_id).
Added:
You're not actually using the model you initialized in your controller. If you want to render the form from a completely different action you need to initialize the instance variable there:
class PollsController < ApplicationController
def show
#question = #poll.questions.new
3.times { #question.options.new } # 5 different options ???
end
# ...
end
<%= render "questions/form", question: #question %>
And in your partial you have a sneaky little bug. Ruby is case sensitive so #poll and #Poll are actually different variables.
irb(main):049:0> #foo = "bar" => "bar"
irb(main):050:0> #Foo
=> nil
Since instance variables are auto-vivified you're just get an unexpected nil instead of an error. What you actually want is:
<%= form_with(model: [#poll, question] ) do |form| %>
I'm struggling to figure out how to set up my rails evaluation model so that users can use it to leave feedback on other users.
I outlined the key part of my problem in this post:
Rails - feedback on specific users, how to set up the form to identify relevant users
The suggestion I received from that was to set up the model as follows:
User.rb
has_many :given_evaluations, foreign_key: :evaluator_id, dependent: :destroy, class_name: Evaluation
has_many :received_evaluations, foreign_key: :evaluatee_id, dependent: :destroy, class_name: Evaluation
Evaluation.rb
belongs_to :evaluator, foreign_key: :evaluator_id, class_name: User
belongs_to :evaluatee, foreign_key: :evaluatee_id, class_name: User
Evaluation Controller
class EvaluationsController < ApplicationController
before_action :set_evaluation, only: [:show, :edit, :update, :destroy]
# before_filter :get_user, only: [:show, :edit, :update, :destroy]
# GET /evaluations
# GET /evaluations.json
def index
# #evaluations = Evaluation.all
#given_evaluations = current_user.given_evaluations
#received_evaluations = current_user.received_evaluations
end
# GET /evaluations/1
# GET /evaluations/1.json
def show
# #received_evaluations = #user.received_evaluations
#evaluation = current_user.received_evaluations.find_by(id: params[:id]) || current_user.given_evaluations.find(params[:id])
# #received_evaluation = current_user.received_evaluations.find params[:id]
end
# GET /evaluations/new
def new
#evaluation = Evaluation.new
end
# GET /evaluations/1/edit
def edit
end
# POST /evaluations
# POST /evaluations.json
def create
# #evaluation = Evaluation.new(evaluation_params)
#evaluation = current_user.given_evaluations.build(evaluation_params)
respond_to do |format|
if #evaluation.save
format.html { redirect_to #evaluation, notice: 'Evaluation was successfully created.' }
format.json { render :show, status: :created, location: #evaluation }
else
format.html { render :new }
format.json { render json: #evaluation.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /evaluations/1
# PATCH/PUT /evaluations/1.json
def update
current_user.given_evaluations.find(params[:id])
respond_to do |format|
if #evaluation.update(evaluation_params)
format.html { redirect_to #evaluation, notice: 'Evaluation was successfully updated.' }
format.json { render :show, status: :ok, location: #evaluation }
else
format.html { render :edit }
format.json { render json: #evaluation.errors, status: :unprocessable_entity }
end
end
end
# DELETE /evaluations/1
# DELETE /evaluations/1.json
def destroy
current_user.given_evaluations.find(params[:id])
#evaluation.destroy
respond_to do |format|
format.html { redirect_to evaluations_url, notice: 'Evaluation was successfully destroyed.' }
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_evaluation
#evaluation = Evaluation.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def evaluation_params
params[:evaluation].permit(:overall_score, :project_score, :personal_score, :remark, :work_again?, :continue_project?, :evaluatee_id)
end
end
Evaluation form
<%= simple_form_for(#evaluation) do |f| %>
<%= f.error_notification %>
<div class="form-inputs">
<%= f.select :evaluatee_id, User.all.map{|u| [u.formal_name, u.id]} %>
<%= f.input :overall_score, collection: 1..10, autofocus: true, :label => "How do you rate this project experience (1 being did not meet expectations - 10 being met all expectations) ?" %>
<%= f.input :continue_project?, as: :boolean, checked_value: true, unchecked_value: false, :label => "Do you intend to continue working on the project?" %>
<%= f.input :remark, as: :text, :label => "Evaluate your experience", :input_html => {:rows => 10} %>
</div>
<div class="form-actions">
<%= f.button :submit %>
Evaluation show view
<% #received_evaluations.each do |receval| %>
<div id="portfolioFiltering" class="masonry-wrapper row">
<%= receval.remark %>
<%#= eval.personal_score %>
<small><%= receval.created_at %></small>
</div>
<% end %>
Alternative attempt at evaluation show view
<% #given_evaluations.each do |receval| %>
<div id="portfolioFiltering" class="masonry-wrapper row">
<%= receval.remark %>
<%#= eval.personal_score %>
<small><%= receval.created_at %></small>
</div>
<% end %>
The problem I'm having now is that regardless of whether I try to show given evaluation or received evaluation in the show, I get an error message that says:
undefined method `each' for nil:NilClass
I can't figure out how to setup the model so that a user can evaluate another user. I want to show each user's received evaluation on their respective show page. i can't figure out what's going wrong. I can see from the console that a user has received evaluations as:
=> #<Evaluation id: 8, evaluatee_id: 34, overall_score: 4, project_score: 5, personal_score: 5, remark: "jhjkhjhjkhkjhjkhjhkhjhkj", work_again?: nil, continue_project?: nil, created_at: "2016-06-12 21:52:53", updated_at: "2016-06-12 21:52:53", evaluator_id: 34>
There is an entry for the user I'm working with. However, I can't find a way to show that evaluation.
The most immediate problem I could see is that the instance variables: #given_evaluations and #received_evaluations are not being set in your show actions and the commented portions are using the ActiveRecord#find method, which would return one instance therefore the reason you couldn't loop through the evaluations.
I think a better place to show all your user's evaluation is in the index action, as you're doing already, you may move the current logic for the show to the index view.
To show each user's received evaluation on the show action, you would do:
#evaluation = current_user.received_evaluations.find(params[:id])
Then in your show view, you should have something like:
<div id="portfolioFiltering" class="masonry-wrapper row">
<%= #evaluation.remark %>
<%= #evaluation.personal_score %>
<small><%= #evaluation.created_at %></small>
</div>
I am building an app that allows a user to create a contest. Each contest has many questions and each contests has many entries. Each entry has many answers and each question has many answers. Here are my models:
class Answer < ActiveRecord::Base
belongs_to :entry
belongs_to :question
end
class Contest < ActiveRecord::Base
has_many :entries
has_many :questions
end
class Entry < ActiveRecord::Base
belongs_to :contest
has_many :answers
accepts_nested_attributes_for :answers, allow_destroy: true
end
class Question < ActiveRecord::Base
has_many :answers
belongs_to :contest
end
Everything works except for when I try to create an entry. I get a "param is missing or the value is empty: entry" error. Here is my controller:
class EntriesController < ApplicationController
before_action :set_entry, only: [:show, :edit, :update, :destroy]
before_action :set_contest
# GET /entries
# GET /entries.json
def index
#entries = Entry.all
end
# GET /entries/1
# GET /entries/1.json
def show
end
# GET /entries/new
def new
#entry = Entry.new
end
# GET /entries/1/edit
def edit
end
# POST /entries
# POST /entries.json
def create
#entry = Entry.new(entry_params)
#entry.contest = #contest
respond_to do |format|
if #entry.save
format.html { redirect_to #entry, notice: 'Entry was successfully created.' }
format.json { render :show, status: :created, location: #entry }
else
format.html { render :new }
format.json { render json: #entry.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /entries/1
# PATCH/PUT /entries/1.json
def update
respond_to do |format|
if #entry.update(entry_params)
format.html { redirect_to #entry, notice: 'Entry was successfully updated.' }
format.json { render :show, status: :ok, location: #entry }
else
format.html { render :edit }
format.json { render json: #entry.errors, status: :unprocessable_entity }
end
end
end
# DELETE /entries/1
# DELETE /entries/1.json
def destroy
#entry.destroy
respond_to do |format|
format.html { redirect_to entries_url, notice: 'Entry was successfully destroyed.' }
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_entry
#entry = Entry.find(params[:id])
end
def set_contest
#contest = Contest.find(params[:contest_id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def entry_params
params.require(:entry).permit(:contest_id, answers_attributes: [:id, :content, :entry_id, :question_id, :_destroy])
end
end
And here is my entry form:
<%= simple_form_for([#contest, #entry]) do |f| %>
<%= f.error_notification %>
<div class="form-inputs">
<h3>Questions</h3>
<%= simple_fields_for :answers do |ff| %>
<% #contest.questions.each do |question| %>
<h4><%= question.content %></h4>
<%= ff.input :content, input_html: {class: 'form-control'} %>
<% end %>
<% end %>
</div>
<div class="form-actions">
<%= f.button :submit %>
</div>
<% end %>
I am still working on the logic but am perplexed as to why the entry form is giving me this error. Any help would be appreciated!
UPDATE
In the Rails Guide example they show the new action as:
def new
#person = Person.new
2.times { #person.addresses.build}
end
Do I need to build the answer objects in my new action? I'm not sure... I tried it but it didn't work. I feel like that can't be the problem though as the error is coming from the entry_params method
You should be adding this line to your new action.
#entry.answers.build
And change this line
<%= simple_fields_for :answers do |ff| %>
to
<%= f.simple_fields_for :answers do |ff| %>
I'm getting an error raised of "undefined method 'comments_path' for..."
At this code in app/views/comments/_form.html.erb (line 1).
I recently tried to implement nestable comments via polymorphic associations on a website I'm building; however, I'm running into a problem that's not allowing me to move forward.
I'm trying to implement nested comments on a 'commentable' model (ie the blog in this case) and then when show is clicked, all the nested comments are shown and the individual can comment on the blog or reply to a comment (and hence result in nested comments) without leaving the page.
I've included a few other files to show the setup, but if I've missed one that's necessary, please let me know and I'll promptly add it. Any help is much appreciated. I've been stumped for several hours and I'm sure its something simple.
<%= form_for [#commentable, #comment] do |f| %>
<%= f.hidden_field :parent_id %></br>
<%= f.label :content, "New Comment" %></br>
<%= f.text_area :body, :rows => 4 %></br>
<%= f.submit "Submit Comment" %>
<% end %>
app/views/blogs/show.html.erb
<div class="content">
<div class="large-9 columns" role="content">
<h2>Comments</h2>
<div id="comments">
<%= nested_comments #comments %>
<%= render "comments/form" %>
</div>
</div>
</div>
app/controllers/comments_controller
class CommentsController < ApplicationController
def new
#parent_id = params.delete(:parent_id)
#commentable = find_commentable
#comment = Comment.new( :parent_id => #parent_id,
:commentable_id => #commentable.id,
:commentable_type => #commentable.class.to_s)
end
def create
#commentable = find_commentable
#comment = #commentable.comments.build(params[:comment])
if #comment.save
flash[:notice] = "Successfully created comment."
redirect_to #commentable
else
flash[:error] = "Error adding comment."
end
end
private
def find_commentable
params.each do |name, value|
if name =~ /(.+)_id$/
return $1.classify.constantize.find(value)
end
end
nil
end
def comment_params
params.require(:comment).permit(:parent_id, :body, :commentable_type, :commentable_id)
end
end
app/controller/blogs_controller.rb
class BlogsController < ApplicationController
before_filter :authenticate, :except => [ :index, :show ]
before_action :set_blog, only: [:show, :edit, :update, :destroy]
include MyModules::Commentable
# GET /blogs
# GET /blogs.json
def index
#blogs = Blog.all.order('created_at DESC')
respond_to do |format|
format.html
format.json
format.atom
end
end
# GET /blogs/1
# GET /blogs/1.json
def show
#blog = Blog.find(params[:id])
end
# GET /blogs/new
def new
#blog = Blog.new
end
# GET /blogs/1/edit
def edit
#blog = Blog.find(params[:id])
end
# POST /blogs
# POST /blogs.json
def create
#blog = Blog.new(blog_params)
respond_to do |format|
if #blog.save
flash[:success] = "Blog was sucessfuly created"
format.html { redirect_to #blog }
format.json { render :show, status: :created, location: #blog }
else
format.html { render :new }
format.json { render json: #blog.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /blogs/1
# PATCH/PUT /blogs/1.json
def update
respond_to do |format|
if #blog.update(blog_params)
flash[:success] = "Blog was successfully updated."
format.html { redirect_to #blog }
format.json { render :show, status: :ok, location: #blog }
else
format.html { render :edit }
format.json { render json: #blog.errors, status: :unprocessable_entity }
end
end
end
# DELETE /blogs/1
# DELETE /blogs/1.json
def destroy
#blog.destroy
respond_to do |format|
flash[:success] = "Blog was successfully deleted."
format.html { redirect_to blogs_url }
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_blog
#blog = Blog.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def blog_params
params.require(:blog).permit(:title, :body, :image, :image_cache, :remote_image_url)
end
private
def authenticate
authenticate_or_request_with_http_basic do |name, password|
name == "admin" && password == "runfast"
end
end
end
my routes file looks like this...
Rails.application.routes.draw do
resources :blogs do
resources :comments
end
resources :applications
resources :reviews
resources :properties
root to: 'blogs#index'
end
the blog and comment models...
class Blog < ActiveRecord::Base
validates_presence_of :body, :title
has_many :comments, :as => :commentable, :dependent => :destroy
mount_uploader :image, ImageUploader
end
class Comment < ActiveRecord::Base
has_ancestry
belongs_to :commentable, :polymorphic => true
validates_presence_of :body
end
and finally the commentable module in lib/my_modules/commentable.rb
require 'active_support/concern'
module MyModules::Commentable
extend ActiveSupport::Concern
included do
before_filter :comments, :only => [:show]
end
def comments
#Commentable = find_commentable
#comments = #Commentable.comments.arrange(:order => :created_at)
#comment = Comment.new
end
private
def find_commentable
return params[:controller].singularize.classify.constantize.find(params[:id])
end
end
#blog
#<Blog id: 8, title: "New Blog Post about Databases.... Again", body: "RDM, the database management system, was designed ...", created_at: "2015-03-01 22:28:07", updated_at: "2015-03-03 00:11:07", image: "IMG_2210.JPG">
#commentable
#<Blog id: 8, title: "New Blog Post about Databases.... Again", body: "RDM, the database management system, was designed ...", created_at: "2015-03-01 22:28:07", updated_at: "2015-03-03 00:11:07", image: "IMG_2210.JPG">
app/helpers/comments_helper.rb
module CommentsHelper
def nested_comments(comments)
comments.map do |comment, sub_comments|
content_tag(:div, render(comment), :class => "media")
end.join.html_safe
end
end
#_params instance variable in better errors
{"utf8"=>"✓", "authenticity_token"=>"OH2tDdI5Kp54hf5J78wXHe//Zsu+0jyeXuG27v1REqjdAec7yBdlrVPLTZKEbLZxgR2L7rGwUwz5BlGTnPcLWg==", "comment"=>{"parent_id"=>"", "body"=>"Hello!\r\n"}, "commit"=>"Submit Comment", "controller"=>"comments", "action"=>"create", "blog_id"=>"8"}
You're getting the error in this view: app/views/blogs/show.html.erb.
The data for this view has been prepared in blogs#show. So in your view, you should have <%= form_for [#blog, #comment] do |f| %>, since you set #blog, not #commentable.
You should also do #comment = Comment.new. Not sure where you set this one...
Do <% raise #commentable.inspect %> in your view (app/views/blogs/show.html.erb). If it's nil, then that's why you're getting the error.
I'm working on an app that allows users to comment on a single "work" (think blog post). The associations in the models are as follows:
class User < ActiveRecord::Base
has_many :works
has_many :comments
class Work < ActiveRecord::Base
belongs_to :user
has_many :comments
class Comment < ActiveRecord::Base
belongs_to :user
belongs_to :post
belongs_to :work
There's a form on the Works show page that allows users to post a comment:
<%= form_for(#comment) do |f| %>
<%= render 'shared/error_messages', object: f.object %>
<div class="field">
<%= f.text_area :content, placeholder: "Post a comment!" %>
</div>
<%= f.submit "Post", class: "btn btn-small btn-primary" %>
<% end %>
The Works controller is as follows. Note that I'm adding the build comment functionality here so that the form on the Works page functions:
class WorksController < ApplicationController
#before_filter :current_user, only: [:edit, :update]
def index
#works = Work.all
#comment = #work.comments.build(params[:comment])
#comment.user = current_user
respond_to do |format|
format.html # index.html.erb
format.xml { render :xml => #works }
end
end
def create
#work = current_user.works.create(params[:work])
redirect_to current_user
end
def edit
#work = current_user.works.find(params[:id])
end
def new
#work = current_user.works.new
end
def destroy
#work = current_user.works.find(params[:id]).destroy
flash[:success] = "Work deleted"
redirect_to current_user
end
def update
#work = current_user.works.find(params[:id])
if #work.update_attributes(params[:work])
flash[:success] = "Profile updated"
redirect_to #work
else
render 'edit'
end
end
def show
#work = Work.find(params[:id])
#comment = #work.comments.build
#comment.user = current_user
#activities = PublicActivity::Activity.order("created_at DESC").where(trackable_type: "Work", trackable_id: #work).all
#comments = #work.comments.order("created_at DESC").where(work_id: #work ).all
respond_to do |format|
format.html # show.html.erb
format.xml { render :xml => #work }
end
end
end
And lastly, here is the Comments controller:
class CommentsController < ApplicationController
before_filter :authenticate_user!
def index
#comments = Comment.all
end
def show
#comment = Comment.find(params[:id])
#activities = PublicActivity::Activity.order("created_at DESC").where(trackable_type: "Comment", trackable_id: #comment).all
respond_to do |format|
format.html # show.html.erb
format.xml { render :xml => #comment }
end
def update
#comment = current_user.comments.find(params[:id])
if #comment.update_attributes(params[:comment])
flash[:success] = "Comment updated"
redirect_to #comment
end
end
def create
#work = Work.find(params[:id])
#comment = #work.comments.build(params[:comment])
#comment.user = current_user
if #comment.save
#flash[:success] = "Post created!"
redirect_to #work
else
render 'home#index'
end
end
end
end
When I attempt to submit a comment using the comment form on the works show view page, I get the following error:
Activerecord::RecordNotFound in CommentsController#create
Couldn't find Work without an ID
Why can't the application find the Work so that it can associate the comment to it?
EDIT 1:
Thanks to the answers below I edited the comment form:
<%= form_for(#work, #comment) do |f| %>
<%= render 'shared/error_messages', object: f.object %>
<div class="field">
<%= f.text_area :content, placeholder: "Post feedback or contribute content
to this work!" %>
</div>
<%= f.submit "Post", class: "btn btn-small btn-primary" %>
<% end %>
I'm still getting the same error after making the change to the form and adding the nested route.
I edited the routes file to include a nest for work comments:
authenticated :user do
root :to => 'activities#index'
end
root :to => "home#index"
devise_for :users
resources :users do
member do
get :following, :followers, :posts, :comments
end
end
resources :works do
resources :comments
end
resources :relationships, only: [:create, :destroy]
resources :posts
resources :activities
resources :comments
Rake routes shows the following for Comments#create:
POST /comments(.:format)
The POST URL (where the error shows up) is appURL/works/1/comments
Doesn't seem right. What do I need to change? Thank you so much for the help so far!!
Your form needs to be form_for([#work, #comment]) so that Rails knows to build a URL like /works/123/comments. Right now it would just be posting to /comments.
Check your rake routes to see the route for your CommentsController#create action. You might also need to tweak the controller to read params[:work_id] instead of params[:id].
The view helper form_for(#comment) will post to '/comments' by default. You can specify a url (see the guides) that includes the :id of the work record. The typical approach is to use form_for([#work, #comment]) and Rails will do this for you so long as you've set up your routes with comments as a nested resource of work.