嵌套评论线程——视图中的递归渲染
Nested comment thread - recursive rendering in view
我正在尝试在 Rails 6 中创建一个问答线程,用户可以在其中回答问题,然后其他用户可以对答案发表评论 - 类似于 reddit 甚至 Whosebug。
我用 'parent_id' 在我的 Answer 模型上创建了一个多态关联,我能够 post 回答初始问题。但是,嵌套答案不会呈现在初始答案下方,而是呈现在主要问题下方。我想我已经将问题隔离到下面看到的相应部分视图:
回答查看
<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>
根据我的理解,最后一行应该呈现答案的答案,但答案呈现在初始问题下方,而不是答案下方。关于我做错了什么的任何想法?
我应该使用 Ancestry 之类的 gem 来执行此操作吗?如果是这样,那将如何工作?
为了完整起见,这里是其他组件
问题查看
<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 %>
答案模型
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
问题模型
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
问题控制器
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
你在多态性建模方面有点失败。如果你想要一个真正的多态关联,你可以这样建模:
class Question
has_many :answers, as: :answerable
end
class Answer
belongs_to :answerable, polymorphic: true
has_many :answers, as: :answerable
end
这让问题的 "parent" 成为问题或答案,您不需要做像 @question.answers.where(parent_id: nil)
这样荒谬的事情。你可以只做 @answers = @question.answers
这将只包括第一代 children.
然而,多态性并不像吹嘘的那样,在构建树层次结构时尤其如此。parent。由于我们实际上必须将行从数据库中拉出才能知道在哪里加入,因此您不能急于有效地加载树。如果 parent 类 的数量很大或未知,或者您只是在制作原型,多态性主要有用。
相反,您可以使用单一 Table 继承来设置关联:
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
注意可为空的外键列。与多态性不同,这些是真正的外键,因此数据库将确保参照完整性。另请注意 type
列,它在 ActiveRecord 中具有特殊意义。
然后让我们设置模型:
class Question < ApplicationRecord
has_many :answers, class_name: 'Questions::Answer'
end
class Answer < ApplicationRecord
has_many :answers, class_name: 'Answers::Answer'
end
以及答案的子类:
# 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
很酷。现在我们可以通过以下方式预先加载到第一代和第二代:
Question.eager_load(answers: :anser)
我们可以继续前进:
Question.eager_load(answers: { answers: :answer })
Question.eager_load(answers: { answers: { answers: :answers }})
但在某些时候你会想要退出并开始使用 ajax 就像 reddit 一样。
我正在尝试在 Rails 6 中创建一个问答线程,用户可以在其中回答问题,然后其他用户可以对答案发表评论 - 类似于 reddit 甚至 Whosebug。
我用 'parent_id' 在我的 Answer 模型上创建了一个多态关联,我能够 post 回答初始问题。但是,嵌套答案不会呈现在初始答案下方,而是呈现在主要问题下方。我想我已经将问题隔离到下面看到的相应部分视图:
回答查看
<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>
根据我的理解,最后一行应该呈现答案的答案,但答案呈现在初始问题下方,而不是答案下方。关于我做错了什么的任何想法? 我应该使用 Ancestry 之类的 gem 来执行此操作吗?如果是这样,那将如何工作?
为了完整起见,这里是其他组件
问题查看
<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 %>
答案模型
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
问题模型
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
问题控制器
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
你在多态性建模方面有点失败。如果你想要一个真正的多态关联,你可以这样建模:
class Question
has_many :answers, as: :answerable
end
class Answer
belongs_to :answerable, polymorphic: true
has_many :answers, as: :answerable
end
这让问题的 "parent" 成为问题或答案,您不需要做像 @question.answers.where(parent_id: nil)
这样荒谬的事情。你可以只做 @answers = @question.answers
这将只包括第一代 children.
然而,多态性并不像吹嘘的那样,在构建树层次结构时尤其如此。parent。由于我们实际上必须将行从数据库中拉出才能知道在哪里加入,因此您不能急于有效地加载树。如果 parent 类 的数量很大或未知,或者您只是在制作原型,多态性主要有用。
相反,您可以使用单一 Table 继承来设置关联:
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
注意可为空的外键列。与多态性不同,这些是真正的外键,因此数据库将确保参照完整性。另请注意 type
列,它在 ActiveRecord 中具有特殊意义。
然后让我们设置模型:
class Question < ApplicationRecord
has_many :answers, class_name: 'Questions::Answer'
end
class Answer < ApplicationRecord
has_many :answers, class_name: 'Answers::Answer'
end
以及答案的子类:
# 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
很酷。现在我们可以通过以下方式预先加载到第一代和第二代:
Question.eager_load(answers: :anser)
我们可以继续前进:
Question.eager_load(answers: { answers: :answer })
Question.eager_load(answers: { answers: { answers: :answers }})
但在某些时候你会想要退出并开始使用 ajax 就像 reddit 一样。