Nested comment thread – recursive rendering in view

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

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

  1. <li>
  2. <%= answer.body %></br>
  3. <%= link_to answer.user.first_name, answer.user %>
  4. <%= link_to answer.user.last_name, answer.user %>
  5. answered <%= time_ago_in_words(answer.created_at) %> ago.
  6. <div class="comments-container">
  7. <%= render partial: "answers/reply", locals: {commentable: answer.commentable, parent_id: answer.parent.id} %>
  8. </div>
  9. <ul> <%= render partial: "answers/answer", collection: answer.answers %> </ul>
  10. </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

  1. <h3><%= @question.title %></h3>
  2. <p> Asked by <%= link_to @question.user.email, @question.user %> <%= time_ago_in_words(@question.created_at) %> ago. </p></br>
  3. <span class="body"> <%= @question.body %> </span></br>
  4. <h5><strong><%= @question.answers.count %> Answers</strong></h5>
  5. <%= render @answers %></br>
  6. <%= render partial: "answers/form", locals: {commentable: @question} %></br>
  7. <%= paginate @answers %>

Answer model

  1. belongs_to :user
  2. belongs_to :parent, optional: true, class_name: 'Answer'
  3. belongs_to :commentable, polymorphic: true
  4. has_many :answers, as: :commentable, dependent: :destroy
  5. validates :body, presence: true
  6. validates :user, presence: true

Question model

  1. belongs_to :user
  2. has_many :answers, as: :commentable, dependent: :destroy
  3. validates :body, presence: true
  4. validates :title, presence: true
  5. validates :user, presence: true

AnswerController

  1. class AnswersController < ApplicationController
  2. before_action :set_answer, only: [:edit, :update, :destroy, :upvote, :downvote]
  3. before_action :find_commentable, only: [:create]
  4. def new
  5. @answer = Answer.new
  6. end
  7. def create
  8. @answer = @commentable.answers.new(answer_params)
  9. respond_to do |format|
  10. if @answer.save
  11. format.html { redirect_to @commentable }
  12. format.json { render :show, status: :created, location: @commentable }
  13. else
  14. format.html { render :new }
  15. format.json { render json: @answer.errors, status: :unprocessable_entity }
  16. end
  17. end
  18. end
  19. def destroy
  20. @answer = @commentable.answers.find(params[:id])
  21. @answer.discard
  22. respond_to do |format|
  23. format.html { redirect_to @commentable, notice: 'Answer was successfully destroyed.' }
  24. format.json { head :no_content }
  25. end
  26. end
  27. private
  28. def set_answer
  29. @answer = Answer.find(params[:id])
  30. end
  31. def answer_params
  32. params.require(:answer).permit(:body).merge(user_id: current_user.id, parent_id: params[:parent_id])
  33. end
  34. def find_commentable
  35. @commentable = Answer.find(params[:answer_id]) if params[:answer_id]
  36. @commentable = Question.find(params[:question_id]) if params[:question_id]
  37. end
  38. end

Question Controller

  1. class QuestionsController < ApplicationController
  2. before_action :set_question, only: [:show, :edit, :update, :destroy, :upvote, :downvote]
  3. def index
  4. @questions = Question.order('created_at desc').page(params[:page])
  5. end
  6. def show
  7. @answer = @question.answers.new
  8. @answers = if params[:answer]
  9. @question.answers.where(id: params[:answer])
  10. else
  11. @question.answers.where(parent_id: nil)
  12. end
  13. @answers = @answers.page(params[:page]).per(5)
  14. end
  15. def new
  16. @question = Question.new
  17. end
  18. def edit
  19. end
  20. def create
  21. @question = Question.new(question_params)
  22. respond_to do |format|
  23. if @question.save
  24. format.html { redirect_to @question, notice: 'You have successfully asked a question!' }
  25. format.json { render :show, status: :created, location: @question }
  26. else
  27. format.html { render :new }
  28. format.json { render json: @question.errors, status: :unprocessable_entity }
  29. end
  30. end
  31. end
  32. def update
  33. respond_to do |format|
  34. if @question.update(question_params)
  35. format.html { redirect_to @question, notice: 'Question successfully updated.' }
  36. format.json { render :show, status: :ok, location: @question }
  37. else
  38. format.html { render :edit }
  39. format.json { render json: @question.errors, status: :unprocessable_entity }
  40. end
  41. end
  42. end
  43. def destroy
  44. @question.discard
  45. respond_to do |format|
  46. format.html { redirect_to @questions_url, notice: 'Question successfully deleted.' }
  47. format.json { head :no_content }
  48. end
  49. end
  50. private
  51. def set_question
  52. @question = Question.find(params[:id])
  53. end
  54. def question_params
  55. params.require(:question).permit(:title, :body, :tag_list).merge(user_id: current_user.id)
  56. end
  57. 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

  1. &lt;li&gt;
  2. &lt;%= answer.body %&gt;&lt;/br&gt;
  3. &lt;%= link_to answer.user.first_name, answer.user %&gt;
  4. &lt;%= link_to answer.user.last_name, answer.user %&gt;
  5. answered &lt;%= time_ago_in_words(answer.created_at) %&gt; ago.
  6. &lt;div class=&quot;comments-container&quot;&gt;
  7. &lt;%= render partial: &quot;answers/reply&quot;, locals: {commentable: answer.commentable, parent_id: answer.parent.id} %&gt;
  8. &lt;/div&gt;
  9. &lt;ul&gt; &lt;%= render partial: &quot;answers/answer&quot;, collection: answer.answers %&gt; &lt;/ul&gt;
  10. &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

  1. &lt;h3&gt;&lt;%= @question.title %&gt;&lt;/h3&gt;
  2. &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;
  3. &lt;/br&gt;
  4. &lt;span class=&quot;body&quot;&gt; &lt;%= @question.body %&gt; &lt;/span&gt;
  5. &lt;/br&gt;
  6. &lt;h5&gt;&lt;strong&gt;&lt;%= @question.answers.count %&gt; Answers&lt;/strong&gt;&lt;/h5&gt;
  7. &lt;%= render @answers %&gt;&lt;/br&gt;
  8. &lt;%= render partial: &quot;answers/form&quot;, locals: {commentable: @question} %&gt; &lt;/br&gt;
  9. &lt;%= paginate @answers %&gt;

Answer model

  1. belongs_to :user
  2. belongs_to :parent, optional: true, class_name: &#39;Answer&#39;
  3. belongs_to :commentable, polymorphic: true
  4. has_many :answers, as: :commentable, dependent: :destroy
  5. validates :body, presence: true
  6. validates :user, presence: true

Question model

  1. belongs_to :user
  2. has_many :answers, as: :commentable, dependent: :destroy
  3. validates :body, presence: true
  4. validates :title, presence: true
  5. validates :user, presence: true

AnswerController

  1. class AnswersController &lt; ApplicationController
  2. before_action :set_answer, only: [:edit, :update, :destroy, :upvote, :downvote]
  3. before_action :find_commentable, only: [:create]
  4. def new
  5. @answer = Answer.new
  6. end
  7. def create
  8. @answer = @commentable.answers.new(answer_params)
  9. respond_to do |format|
  10. if @answer.save
  11. format.html { redirect_to @commentable }
  12. format.json { render :show, status: :created, location: @commentable }
  13. else
  14. format.html { render :new }
  15. format.json { render json: @answer.errors, status: :unprocessable_entity }
  16. end
  17. end
  18. end
  19. def destroy
  20. @answer = @commentable.answers.find(params[:id])
  21. @answer.discard
  22. respond_to do |format|
  23. format.html { redirect_to @commentable, notice: &#39;Answer was successfully destroyed.&#39; }
  24. format.json { head :no_content }
  25. end
  26. end
  27. private
  28. def set_answer
  29. @answer = Answer.find(params[:id])
  30. end
  31. def answer_params
  32. params.require(:answer).permit(:body).merge(user_id: current_user.id, parent_id: params[:parent_id])
  33. end
  34. def find_commentable
  35. @commentable = Answer.find(params[:answer_id]) if params[:answer_id]
  36. @commentable = Question.find(params[:question_id]) if params[:question_id]
  37. end
  38. end

Question Controller

  1. class QuestionsController &lt; ApplicationController
  2. before_action :set_question, only: [:show, :edit, :update, :destroy, :upvote, :downvote]
  3. def index
  4. @questions = Question.order(&#39;created_at desc&#39;).page(params[:page])
  5. end
  6. def show
  7. @answer = @question.answers.new
  8. @answers = if params[:answer]
  9. @question.answers.where(id: params[:answer])
  10. else
  11. @question.answers.where(parent_id: nil)
  12. end
  13. @answers = @answers.page(params[:page]).per(5)
  14. end
  15. def new
  16. @question = Question.new
  17. end
  18. def edit
  19. end
  20. def create
  21. @question = Question.new(question_params)
  22. respond_to do |format|
  23. if @question.save
  24. format.html { redirect_to @question, notice: &#39;You have successfully asked a question!&#39; }
  25. format.json { render :show, status: :created, location: @question }
  26. else
  27. format.html { render :new }
  28. format.json { render json: @question.errors, status: :unprocessable_entity }
  29. end
  30. end
  31. end
  32. def update
  33. respond_to do |format|
  34. if @question.update(question_params)
  35. format.html { redirect_to @question, notice: &#39;Question successfully updated.&#39; }
  36. format.json { render :show, status: :ok, location: @question }
  37. else
  38. format.html { render :edit }
  39. format.json { render json: @question.errors, status: :unprocessable_entity }
  40. end
  41. end
  42. end
  43. def destroy
  44. @question.discard
  45. respond_to do |format|
  46. format.html { redirect_to @questions_url, notice: &#39;Question successfully deleted.&#39; }
  47. format.json { head :no_content }
  48. end
  49. end
  50. private
  51. def set_question
  52. @question = Question.find(params[:id])
  53. end
  54. def question_params
  55. params.require(:question).permit(:title, :body, :tag_list).merge(user_id: current_user.id)
  56. end
  57. end

答案1

得分: 0

你在建模多态性方面有些问题。如果你想要一个真正的多态关联,应该这样建模:

  1. class Question
  2. has_many :answers, as: :answerable
  3. end
  4. class Answer
  5. belongs_to :answerable, polymorphic: true
  6. has_many :answers, as: :answerable
  7. end

这允许问题的“父级”可以是问题或答案,你不需要像 @question.answers.where(parent_id: nil) 这样做荒谬的事情。你只需使用 @answers = @question.answers,这将只包括第一代子级。

然而,多态性并不是完美的,尤其在构建树状层次结构时会显得不足。因为我们实际上必须从数据库中提取行才能知道要加入的位置,所以你不能有效地预加载树。多态性主要在父类数量很大或未知的情况下有用,或者你只是在原型设计阶段。

相反,你可以使用单表继承来设置关联:

  1. class CreateAnswers < ActiveRecord::Migration[6.0]
  2. def change
  3. create_table :answers do |t|
  4. t.string :type
  5. t.belongs_to :question, null: true, foreign_key: true
  6. t.belongs_to :answer, null: true, foreign_key: true
  7. # ... 更多列
  8. t.timestamps
  9. end
  10. end
  11. end

注意可空的外键列。与多态性不同,这些是真正的外键,因此数据库将确保引用完整性。还要注意 type 列,在ActiveRecord中具有特殊意义。

然后设置模型:

  1. class Question < ApplicationRecord
  2. has_many :answers, class_name: 'Questions::Answer'
  3. end
  4. class Answer < ApplicationRecord
  5. has_many :answers, class_name: 'Answers::Answer'
  6. end

以及Answer的子类:

  1. # app/models/answers/answer.rb
  2. module Answers
  3. class Answer < ::Answer
  4. belongs_to :answer
  5. has_one :question, through: :answer
  6. end
  7. end
  8. # app/models/questions/answer.rb
  9. module Questions
  10. class Answer < ::Answer
  11. belongs_to :question
  12. end
  13. end

现在我们可以预加载第一代和第二代:

  1. Question.eager_load(answers: :answer)

我们还可以继续:

  1. Question.eager_load(answers: { answers: :answer })
  2. 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:

  1. class Question
  2. has_many :answers, as: :answerable
  3. end
  4. class Answer
  5. belongs_to :answerable, polymorphic: true
  6. has_many :answers, as: :answerable
  7. 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:

  1. class CreateAnswers &lt; ActiveRecord::Migration[6.0]
  2. def change
  3. create_table :answers do |t|
  4. t.string :type
  5. t.belongs_to :question, null: true, foreign_key: true
  6. t.belongs_to :answer, null: true, foreign_key: true
  7. # ... more columns
  8. t.timestamps
  9. end
  10. end
  11. 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:

  1. class Question &lt; ApplicationRecord
  2. has_many :answers, class_name: &#39;Questions::Answer&#39;
  3. end
  4. class Answer &lt; ApplicationRecord
  5. has_many :answers, class_name: &#39;Answers::Answer&#39;
  6. end

And the subclasses of Answer:

  1. # app/models/answers/answer.rb
  2. module Answer
  3. class Answer &lt; ::Answer
  4. belongs_to :answer
  5. has_one :question, through: :answer
  6. end
  7. end
  8. # app/models/questions/answer.rb
  9. module Questions
  10. class Answer &lt; ::Answer
  11. belongs_to :question
  12. end
  13. end

Pretty cool. Now we can eager load to the first and second generation with:

  1. Question.eager_load(answers: :anser)

And we can keep going:

  1. Question.eager_load(answers: { answers: :answer })
  2. 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:

确定