Nested comment thread – recursive rendering in view

huangapple go评论89阅读模式
英文:

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

&lt;li&gt;
&lt;%= answer.body %&gt;&lt;/br&gt;
&lt;%= link_to answer.user.first_name, answer.user %&gt; 
&lt;%= link_to answer.user.last_name, answer.user %&gt; 
answered &lt;%= time_ago_in_words(answer.created_at) %&gt; ago.
&lt;div class=&quot;comments-container&quot;&gt;
&lt;%= render partial: &quot;answers/reply&quot;, locals: {commentable: answer.commentable, parent_id: answer.parent.id} %&gt;  
&lt;/div&gt;
&lt;ul&gt; &lt;%= render partial: &quot;answers/answer&quot;, collection: answer.answers %&gt; &lt;/ul&gt;
&lt;/li&gt;

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

&lt;h3&gt;&lt;%= @question.title %&gt;&lt;/h3&gt;
&lt;p&gt; Asked by &lt;%= link_to @question.user.email, @question.user %&gt; &lt;%= time_ago_in_words(@question.created_at) %&gt; ago. &lt;/p&gt;
&lt;/br&gt;
&lt;span class=&quot;body&quot;&gt; &lt;%= @question.body %&gt; &lt;/span&gt;
&lt;/br&gt;
&lt;h5&gt;&lt;strong&gt;&lt;%= @question.answers.count %&gt; Answers&lt;/strong&gt;&lt;/h5&gt;
&lt;%= render @answers %&gt;&lt;/br&gt;
&lt;%= render partial: &quot;answers/form&quot;, locals: {commentable: @question} %&gt;	&lt;/br&gt;
&lt;%= paginate @answers %&gt;

Answer model

belongs_to :user
belongs_to :parent, optional: true, class_name: &#39;Answer&#39;
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 &lt; 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: &#39;Answer was successfully destroyed.&#39; }
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 &lt; ApplicationController
before_action :set_question, only: [:show, :edit, :update, :destroy, :upvote, :downvote]
def index
@questions = Question.order(&#39;created_at desc&#39;).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: &#39;You have successfully asked a question!&#39; }
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: &#39;Question successfully updated.&#39; }
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: &#39;Question successfully deleted.&#39; }
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 &lt; 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 &lt; ApplicationRecord
has_many :answers, class_name: &#39;Questions::Answer&#39;
end
class Answer &lt; ApplicationRecord
has_many :answers, class_name: &#39;Answers::Answer&#39;
end

And the subclasses of Answer:

# app/models/answers/answer.rb
module Answer
class Answer &lt; ::Answer
belongs_to :answer
has_one :question, through: :answer
end
end
# app/models/questions/answer.rb
module Questions
class Answer &lt; ::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.

huangapple
  • 本文由 发表于 2020年1月3日 17:44:58
  • 转载请务必保留本文链接:https://go.coder-hub.com/59576248.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定