アンケートのRailsアプリDelegated type版

前回アンケートアプリのSTI版を作りました。
今回はDelegated type版を作ります。STIとの比較はこちらが参考になります。

環境
Ruby 3.3.0
Rails 7.1.3.2

モデルを生成します。
今回は派生クラスのテーブルも作ります。

rails new qrc_dt && cd $_

bin/rails g model user name

touch app/models/concerns/questionable.rb
bin/rails g model question content questionable_type questionable_id:integer
bin/rails g model question_radio
bin/rails g model radio content question_radio:belongs_to
bin/rails g model question_check
bin/rails g model check content question_check:belongs_to

touch app/models/concerns/answerable.rb
bin/rails g model answer user:belongs_to question:belongs_to answerable_type answerable_id:integer
bin/rails g model answer_radio radio:belongs_to
bin/rails g model answer_check
bin/rails g model choice answer_check:belongs_to check:belongs_to
bin/rails db:migrate

concernにQuestionableモジュールを作ります。

touch app/models/concerns/questionable.rb
# app/models/concerns/questionable.rb
module Questionable
  extend ActiveSupport::Concern

  included do
    has_one :question, as: :questionable, touch: true
  end
end

Questionにdelegated_typeを追加します。
Questionにaccepts_nested_attributes_forを追加します。

# app/models/question.rb
class Question < ApplicationRecord
  delegated_type :questionable, types: %w[ QuestionRadio QuestionCheck ], dependent: :destroy
end

QuestionRadioにモジュールをincludeします。

# app/models/question_radio.rb
class QuestionRadio < ApplicationRecord
  include Questionable

  has_many :radios

  def answer_class 
    AnswerRadio
  end
end

QuestionCheckにモジュールをincludeします。

# app/models/question_check.rb
class QuestionCheck < ApplicationRecord
  include Questionable

  has_many :checks

  def answer_class 
    AnswerCheck
  end
end

concernにAnswerableモジュールを作ります。

touch app/models/concerns/answerable.rb
# app/models/concerns/answerable.rb
module Answerable
  extend ActiveSupport::Concern

  included do
    has_one :answer, as: :answerable, touch: true
  end
end

Answerにdelegated_typeを追加します。
Answerにaccepts_nested_attributes_forを追加します。

# app/models/answer.rb
class Answer < ApplicationRecord
  belongs_to :user
  belongs_to :question

  delegated_type :answerable, types: %w[ AnswerRadio AnswerCheck ], dependent: :destroy
  accepts_nested_attributes_for :answerable
end

AnswerRadioにモジュールをincludeします。

# app/models/answer_radio.rb
class AnswerRadio < ApplicationRecord
  include Answerable

  belongs_to :radio
end

AnswerCheckにモジュールをincludeします。

# app/models/answer_check.rb
class AnswerCheck < ApplicationRecord
  include Answerable

  has_many :choices
  has_many :checks, through: :choices
end

初期データを生成します。
今回はログインユーザーの管理、管理者による質問の編集機能は省きます。

# db/seeds.rb
User.find_or_create_by!(name: "ユーザー1")
User.find_or_create_by!(name: "ユーザー2")

qr1 = Question.create! questionable: QuestionRadio.new, content: "好きな映画のジャンルは?"
["アクション", "コメディー", "ドラマ", "ホラー"].each do |content|
  Radio.find_or_create_by!(content: content, question_radio: qr1.questionable)
end

qr2 = Question.create! questionable: QuestionRadio.new, content: "好きな果物は?"
["いちご", "もも", "みかん", "りんご", "ぶどう"].each do |content|
  Radio.find_or_create_by!(content: content, question_radio: qr2.questionable)
end

qr3 = Question.create! questionable: QuestionCheck.new, content: "好きな映画のジャンルは?"
["アクション", "コメディー", "ドラマ", "ホラー"].each do |content|
  Check.find_or_create_by!(content: content, question_check: qr3.questionable)
end

qr4 = Question.create! questionable: QuestionCheck.new, content: "好きな果物は?"
["いちご", "もも", "みかん", "りんご", "ぶどう"].each do |content|
  Check.find_or_create_by!(content: content, question_check: qr4.questionable)
end
bin/rails db:seed

コントローラーを生成します。
質問の一覧と詳細ページと回答ページを作ります。

bin/rails g controller questions index show --skip-routes --skip-helper 
bin/rails g controller answer_radios --skip-routes --skip-helper 
bin/rails g controller answer_checks --skip-routes --skip-helper 
# config/routes.rb
Rails.application.routes.draw do
  resources :questions, only: %i[ index show ]
  resources :answer_radios, only: %i[ create update ]
  resources :answer_checks, only: %i[ create update ]

  get "up" => "rails/health#show", as: :rails_health_check

  root "questions#index"
end

質問のコントローラーを実装します。

# app/controllers/questions_controller.rb
class QuestionsController < ApplicationController
  def index
    @questions = Question.all
  end

  def show
    current_user = User.second # TODO: ログインユーザーを取得する。
    question = Question.find(params[:id])
    @answer = Answer.find_or_initialize_by(user: current_user, question: question)
    @answer.answerable = question.questionable.answer_class.new if @answer.new_record?
  end
end

質問の一覧ページを実装します。

# app/views/questions/index.html.erb
<% @questions.each do |question| %>
  <div>
    <%= link_to question.content, question.becomes(Question) %>
  </div>
<% end %>

質問の詳細ページを実装します。

# app/views/questions/show.html.erb
<p style="color: green"><%= notice %></p>

<%= @answer.question.content %>

<%= form_with(model: @answer.answerable) do |form| %>
  <% if @answer.errors.any? %>
    <div style="color: red">
    <h2><%= pluralize(@answer.errors.count, "error") %> prohibited this answer from being saved:</h2>

    <ul>
      <% @answer.errors.each do |error| %>
        <li><%= error.full_message %></li>
      <% end %>
    </ul>
  </div>
  <% end %>

  <%= form.hidden_field :user_id, value: @answer.user_id %>
  <%= form.hidden_field :question_id, value: @answer.question_id %>
  <%= form.hidden_field :answerable_type, value: @answer.answerable_type %>

  <%= form.fields_for :answerable_attributes do |f| %>
    <%= render "questions/answerables/#{@answer.answerable_name}", form: f, answer: @answer %>
  <% end %>

  <div>
    <%= form.submit %>
  </div>
<% end %>

<br>

<%= link_to "一覧", questions_url %>

回答の部分テンプレートを作ります。

mkdir app/views/questions/answerables
touch app/views/questions/answerables/_answer_radio.html.erb
touch app/views/questions/answerables/_answer_check.html.erb

択一回答のコントローラーを作ります。

# app/controllers/answer_radios_controller.rb
class AnswerRadiosController < ApplicationController
  def create
    @answer = Answer.new(answer_radio_params)
    if @answer.save
      redirect_to @answer.question, notice: "Answer was successfully created."
    else
      render "questions/show", status: :unprocessable_entity
    end
  end

  def update
    answer_radio = AnswerRadio.find(params[:id])
    @answer = answer_radio.answer
    if answer_radio.update(radio_id: answer_radio_params[:answerable_attributes][:radio_id])
      redirect_to @answer.question, notice: "Answer was successfully updated."
    else
      render "questions/show", status: :unprocessable_entity
    end
  end

  private
    def answer_radio_params
      params.require(:answer_radio).permit(:user_id, :question_id, :answerable_type, answerable_attributes: [:radio_id])
    end
end
択一回答の部分テンプレートを作ります。
# app/views/questions/answerables/_answer_radio.html.erb
<%# locals: (form:, answer:) -%>
<div>
  <%= form.collection_radio_buttons :radio_id, answer.question.questionable.radios, :id, :content, checked: answer.answerable.radio_id %>
</div>

複数回答のコントローラーを作ります。

# app/controllers/answer_checks_controller.rb
class AnswerChecksController < ApplicationController
  def create
    @answer = Answer.new(answer_check_params)
    if @answer.save
      redirect_to @answer.question, notice: "Answer was successfully created."
    else
      render "questions/show", status: :unprocessable_entity
    end
  end

  def update
    answer_check = AnswerCheck.find(params[:id])
    @answer = answer_check.answer
    if answer_check.update(check_ids: answer_check_params[:answerable_attributes][:check_ids])
      redirect_to @answer.question, notice: "Answer was successfully updated."
    else
      render "questions/show", status: :unprocessable_entity
    end
  end

  private
    def answer_check_params
      params.require(:answer_check).permit(:user_id, :question_id, :answerable_type, answerable_attributes: [check_ids: []])
    end
end

複数回答の部分テンプレートを作ります。

# app/views/questions/answerables/_answer_check.html.erb
<%# locals: (form:, answer:) -%>
<div>
  <%= form.collection_check_boxes :check_ids, answer.question.questionable.checks, :id, :content, checked: answer.answerable.check_ids %>
</div>

前回と同様にレイアウトや文言など見栄えを洗練する必要がありますが、機能としては完成しました。



▼この記事がいいね!と思ったらブックマークお願いします
このエントリーをはてなブックマークに追加