英文:
Nested comment thread - recursive rendering in view
问题
I am trying to create a question and answer thread in Rails 6 where a User can answer on a question, and then other users can comment on the answer - similar to a reddit or even stackoverflow.
I created a polymorphic association on my Answer model with a 'parent_id' and I am able to post answers on the initial question. However the nested answers do not render below the initial answer, but rather below the main question. I think I have isolated the problem to the corresponding partial view seen below:
Answer View
<li>
<%= answer.body %></br>
<%= link_to answer.user.first_name, answer.user %>
<%= link_to answer.user.last_name, answer.user %>
answered <%= time_ago_in_words(answer.created_at) %> ago.
<div class="comments-container">
<%= render partial: "answers/reply", locals: {commentable: answer.commentable, parent_id: answer.parent.id} %>
</div>
<ul> <%= render partial: "answers/answer", collection: answer.answers %> </ul>
</li>
From my understanding, the last line should render the answers to the answer, however the answers render underneath the initial question, and not the answer. Any ideas on what im doing wrong?
Should I be using a gem like Ancestry to do this? If so how would that work?
For completeness, here are the other components
Question View
<h3><%= @question.title %></h3>
<p> Asked by <%= link_to @question.user.email, @question.user %> <%= time_ago_in_words(@question.created_at) %> ago. </p></br>
<span class="body"> <%= @question.body %> </span></br>
<h5><strong><%= @question.answers.count %> Answers</strong></h5>
<%= render @answers %></br>
<%= render partial: "answers/form", locals: {commentable: @question} %></br>
<%= paginate @answers %>
Answer model
belongs_to :user
belongs_to :parent, optional: true, class_name: 'Answer'
belongs_to :commentable, polymorphic: true
has_many :answers, as: :commentable, dependent: :destroy
validates :body, presence: true
validates :user, presence: true
Question model
belongs_to :user
has_many :answers, as: :commentable, dependent: :destroy
validates :body, presence: true
validates :title, presence: true
validates :user, presence: true
AnswerController
class AnswersController < ApplicationController
before_action :set_answer, only: [:edit, :update, :destroy, :upvote, :downvote]
before_action :find_commentable, only: [:create]
def new
@answer = Answer.new
end
def create
@answer = @commentable.answers.new(answer_params)
respond_to do |format|
if @answer.save
format.html { redirect_to @commentable }
format.json { render :show, status: :created, location: @commentable }
else
format.html { render :new }
format.json { render json: @answer.errors, status: :unprocessable_entity }
end
end
end
def destroy
@answer = @commentable.answers.find(params[:id])
@answer.discard
respond_to do |format|
format.html { redirect_to @commentable, notice: 'Answer was successfully destroyed.' }
format.json { head :no_content }
end
end
private
def set_answer
@answer = Answer.find(params[:id])
end
def answer_params
params.require(:answer).permit(:body).merge(user_id: current_user.id, parent_id: params[:parent_id])
end
def find_commentable
@commentable = Answer.find(params[:answer_id]) if params[:answer_id]
@commentable = Question.find(params[:question_id]) if params[:question_id]
end
end
Question Controller
class QuestionsController < ApplicationController
before_action :set_question, only: [:show, :edit, :update, :destroy, :upvote, :downvote]
def index
@questions = Question.order('created_at desc').page(params[:page])
end
def show
@answer = @question.answers.new
@answers = if params[:answer]
@question.answers.where(id: params[:answer])
else
@question.answers.where(parent_id: nil)
end
@answers = @answers.page(params[:page]).per(5)
end
def new
@question = Question.new
end
def edit
end
def create
@question = Question.new(question_params)
respond_to do |format|
if @question.save
format.html { redirect_to @question, notice: 'You have successfully asked a question!' }
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
def update
respond_to do |format|
if @question.update(question_params)
format.html { redirect_to @question, notice: 'Question successfully updated.' }
format.json { render :show, status: :ok, location: @question }
else
format.html { render :edit }
format.json { render json: @question.errors, status: :unprocessable_entity }
end
end
end
def destroy
@question.discard
respond_to do |format|
format.html { redirect_to @questions_url, notice: 'Question successfully deleted.' }
format.json { head :no_content }
end
end
private
def set_question
@question = Question.find(params[:id])
end
def question_params
params.require(:question).permit(:title, :body, :tag_list).merge(user_id: current_user.id)
end
end
英文:
I am trying to create a question and answer thread in Rails 6 where a User can answer on a question, and then other users can comment on the answer - similar to a reddit or even stackoverflow.
I created a polymorphic association on my Answer model with a 'parent_id' and I am able to post answers on the initial question. However the nested answers do not render below the initial answer, but rather below the main question. I think I have isolated the problem to the corresponding partial view seen below:
Answer View
<li>
<%= answer.body %></br>
<%= link_to answer.user.first_name, answer.user %>
<%= link_to answer.user.last_name, answer.user %>
answered <%= time_ago_in_words(answer.created_at) %> ago.
<div class="comments-container">
<%= render partial: "answers/reply", locals: {commentable: answer.commentable, parent_id: answer.parent.id} %>
</div>
<ul> <%= render partial: "answers/answer", collection: answer.answers %> </ul>
</li>
From my understanding, the last line should render the answers to the answer, however the answers render underneath the initial question, and not the answer. Any ideas on what im doing wrong?
Should I be using a gem like Ancestry to do this? If so how would that work?
For completeness, here are the other components
Question View
<h3><%= @question.title %></h3>
<p> Asked by <%= link_to @question.user.email, @question.user %> <%= time_ago_in_words(@question.created_at) %> ago. </p>
</br>
<span class="body"> <%= @question.body %> </span>
</br>
<h5><strong><%= @question.answers.count %> Answers</strong></h5>
<%= render @answers %></br>
<%= render partial: "answers/form", locals: {commentable: @question} %> </br>
<%= paginate @answers %>
Answer model
belongs_to :user
belongs_to :parent, optional: true, class_name: 'Answer'
belongs_to :commentable, polymorphic: true
has_many :answers, as: :commentable, dependent: :destroy
validates :body, presence: true
validates :user, presence: true
Question model
belongs_to :user
has_many :answers, as: :commentable, dependent: :destroy
validates :body, presence: true
validates :title, presence: true
validates :user, presence: true
AnswerController
class AnswersController < ApplicationController
before_action :set_answer, only: [:edit, :update, :destroy, :upvote, :downvote]
before_action :find_commentable, only: [:create]
def new
@answer = Answer.new
end
def create
@answer = @commentable.answers.new(answer_params)
respond_to do |format|
if @answer.save
format.html { redirect_to @commentable }
format.json { render :show, status: :created, location: @commentable }
else
format.html { render :new }
format.json { render json: @answer.errors, status: :unprocessable_entity }
end
end
end
def destroy
@answer = @commentable.answers.find(params[:id])
@answer.discard
respond_to do |format|
format.html { redirect_to @commentable, notice: 'Answer was successfully destroyed.' }
format.json { head :no_content }
end
end
private
def set_answer
@answer = Answer.find(params[:id])
end
def answer_params
params.require(:answer).permit(:body).merge(user_id: current_user.id, parent_id: params[:parent_id])
end
def find_commentable
@commentable = Answer.find(params[:answer_id]) if params[:answer_id]
@commentable = Question.find(params[:question_id]) if params[:question_id]
end
end
Question Controller
class QuestionsController < ApplicationController
before_action :set_question, only: [:show, :edit, :update, :destroy, :upvote, :downvote]
def index
@questions = Question.order('created_at desc').page(params[:page])
end
def show
@answer = @question.answers.new
@answers = if params[:answer]
@question.answers.where(id: params[:answer])
else
@question.answers.where(parent_id: nil)
end
@answers = @answers.page(params[:page]).per(5)
end
def new
@question = Question.new
end
def edit
end
def create
@question = Question.new(question_params)
respond_to do |format|
if @question.save
format.html { redirect_to @question, notice: 'You have successfully asked a question!' }
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
def update
respond_to do |format|
if @question.update(question_params)
format.html { redirect_to @question, notice: 'Question successfully updated.' }
format.json { render :show, status: :ok, location: @question }
else
format.html { render :edit }
format.json { render json: @question.errors, status: :unprocessable_entity }
end
end
end
def destroy
@question.discard
respond_to do |format|
format.html { redirect_to @questions_url, notice: 'Question successfully deleted.' }
format.json { head :no_content }
end
end
private
def set_question
@question = Question.find(params[:id])
end
def question_params
params.require(:question).permit(:title, :body, :tag_list).merge(user_id: current_user.id)
end
end
答案1
得分: 0
你在建模多态性方面有些问题。如果你想要一个真正的多态关联,应该这样建模:
class Question
has_many :answers, as: :answerable
end
class Answer
belongs_to :answerable, polymorphic: true
has_many :answers, as: :answerable
end
这允许问题的“父级”可以是问题或答案,你不需要像 @question.answers.where(parent_id: nil)
这样做荒谬的事情。你只需使用 @answers = @question.answers
,这将只包括第一代子级。
然而,多态性并不是完美的,尤其在构建树状层次结构时会显得不足。因为我们实际上必须从数据库中提取行才能知道要加入的位置,所以你不能有效地预加载树。多态性主要在父类数量很大或未知的情况下有用,或者你只是在原型设计阶段。
相反,你可以使用单表继承来设置关联:
class CreateAnswers < ActiveRecord::Migration[6.0]
def change
create_table :answers do |t|
t.string :type
t.belongs_to :question, null: true, foreign_key: true
t.belongs_to :answer, null: true, foreign_key: true
# ... 更多列
t.timestamps
end
end
end
注意可空的外键列。与多态性不同,这些是真正的外键,因此数据库将确保引用完整性。还要注意 type
列,在ActiveRecord中具有特殊意义。
然后设置模型:
class Question < ApplicationRecord
has_many :answers, class_name: 'Questions::Answer'
end
class Answer < ApplicationRecord
has_many :answers, class_name: 'Answers::Answer'
end
以及Answer的子类:
# app/models/answers/answer.rb
module Answers
class Answer < ::Answer
belongs_to :answer
has_one :question, through: :answer
end
end
# app/models/questions/answer.rb
module Questions
class Answer < ::Answer
belongs_to :question
end
end
现在我们可以预加载第一代和第二代:
Question.eager_load(answers: :answer)
我们还可以继续:
Question.eager_load(answers: { answers: :answer })
Question.eager_load(answers: { answers: { answers: :answers }})
但是在某个时候,你可能想放弃并开始使用像Reddit那样的ajax。
英文:
You kind of failed at modeling polymorphism. If you want a true polymorphic association you would model it as so:
class Question
has_many :answers, as: :answerable
end
class Answer
belongs_to :answerable, polymorphic: true
has_many :answers, as: :answerable
end
This lets the "parent" of a question be either a Question or a Answer and you don't need to do ridiculous stuff like @question.answers.where(parent_id: nil)
. You can just do @answers = @question.answers
and this will only include the first generation children.
However polymorphism isn't all its cracked up to be and that will be especially apparent when building a tree hierarchy. Since we actually have to pull the rows out of the database to know where to join you can't eager load the tree effectively. Polymorphism is mainly useful if the number of parent classes in large or unknown or you're just prototyping.
Instead you can use Single Table Inheritance to setup the associations:
class CreateAnswers < ActiveRecord::Migration[6.0]
def change
create_table :answers do |t|
t.string :type
t.belongs_to :question, null: true, foreign_key: true
t.belongs_to :answer, null: true, foreign_key: true
# ... more columns
t.timestamps
end
end
end
Note the nullable foreign key columns. Unlike with polymophism these are real foreign keys so the db will ensure referential integrity. Also note the type
column which has a special significance in ActiveRecord.
Then lets setup the models:
class Question < ApplicationRecord
has_many :answers, class_name: 'Questions::Answer'
end
class Answer < ApplicationRecord
has_many :answers, class_name: 'Answers::Answer'
end
And the subclasses of Answer:
# app/models/answers/answer.rb
module Answer
class Answer < ::Answer
belongs_to :answer
has_one :question, through: :answer
end
end
# app/models/questions/answer.rb
module Questions
class Answer < ::Answer
belongs_to :question
end
end
Pretty cool. Now we can eager load to the first and second generation with:
Question.eager_load(answers: :anser)
And we can keep going:
Question.eager_load(answers: { answers: :answer })
Question.eager_load(answers: { answers: { answers: :answers }})
But at some point you'll want to call it quits and start using ajax like reddit does.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论