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
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.