noggy’s blog

自分用の備忘録です。。。

質問投稿サイトの作成(3/3)

ここでは,ユーザーが質問に対して,回答を投稿できるようにする

130.回答投稿機能の概要

  • ユーザーが質問に対して,回答を投稿できる
  • ユーザーが回答一覧を閲覧できる
  • ユーザーが回答を編集できる
  • ユーザーが回答を削除できる

131.質問詳細画面の実装

コントローラ

 def show
   @question = Question.find(params[:id])  # 追加
 end

viewの編集

<div class="row">
  <div class="col-md-12">
    <h2><%= @question.title %></h2>
      <div>
        Content: <%= @question.content %>
      </div>
      <div>
        Name: <%= @question.name %>
      </div>
    <hr>
    <div>
      <%= link_to '> Home', root_path %>
    </div>
  </div>
</div>

f:id:nogicchi:20210918145840p:plain
3番目の質問のタイトルをクリック,その詳細画面は↑こんな感じ

132.Answers コントローラの作成

質問に対する回答用のコントローラを作成する.

rails g controller answers edit

メソッドは,editだけを作成.
回答の一覧はquestionsコントローラのshowメソッドを利用

133.Answerモデルの作成

テーブルの構造

QuestionsAnswers
カラム 説明 1対多 カラム 説明
id id id id
name 質問投稿者名 question_id question_id
title 質問タイトル name 質問回答者名
content 質問本文 content 回答本文
すでにquestionsテーブルがあり,これからanswersテーブルを作成する.
question_idは回答にひもづくid.
1つの質問(question)に対して,複数の回答(answers)が投稿されるので「1対多」の関係.

rails g model answer question:references name:string content:text

上記で,answerは単数形であることに注意.
answerから見ると、1つのanswerが1つのquestionにひもづく.

Running via Spring preloader in process 6161
      invoke  active_record
      create  db/migrate/XX_create_answers.rb
      create  app/models/answer.rb
      invoke  test_unit
      create  test/models/answer_test.rb
      create  test/fixtures/answers.yml

answerモデルは↓こんな感じ.

 class Answer < ApplicationRecord
   belongs_to :question
 end

このとき,answerがquestionにbelongしている.
つまり,ひもづくという意味answerから見てquestionは1つあるという意味.

class Question < ApplicationRecord
  has_many :answers, dependent: :destroy #追加 
  validates :name, presence: true
  validates :title, presence: true
  validates :content, presence: true  
end

Questionモデルは上記のようになってる.
has_many :answers:1つのquestionは複数のanswerを持っているという意味
dependent: :destroy:親であるquestionが削除されたら,ひもづくanswerが全て削除されるという設定

class CreateAnswers < ActiveRecord::Migration[5.2]
  def change
    create_table :answers do |t|
      t.references :question, foreign_key: true
      t.string :name
      t.text :content

      t.timestamps
    end
  end
end

DBに反映

rails db:migrate

== XX CreateAnswers: migrating ====================================
-- create_table(:answers)
   -> 0.0052s
== XX CreateAnswers: migrated (0.0064s) ===========================

answersテーブルの確認

rails dbconsole

sqlite> .schema answers
CREATE TABLE "answers" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "question_id" integer, "name" varchar, "content" text, "created_at" datetime NOT NULL, "updated_at" datetime NOT NULL, CONSTRAINT "fk_rails_3d5ed4418f"
FOREIGN KEY ("question_id")
  REFERENCES "questions" ("id")
);
CREATE INDEX "index_answers_on_question_id" ON "answers" ("question_id");

134.回答機能関連のルーティング設定

get 'answers/edit'

root 'questions#index'
resources :questions do  #追加
  resources :answers     #追加
end

rails routesでanswers関連のルーティングが用意できたことが分かる.

new_question_answer  GET    /questions/:question_id/answers/new(.:format)        answers#new
edit_question_answer GET    /questions/:question_id/answers/:id/edit(.:format)   answers#edit
question_answer      GET    /questions/:question_id/answers/:id(.:format)        answers#show
                     PATCH  /questions/:question_id/answers/:id(.:format)        answers#update
                     PUT    /questions/:question_id/answers/:id(.:format)        answers#update
                     DELETE /questions/:question_id/answers/:id(.:format)        answers#destroy

使いながら理解していく

135.回答の投稿機能(Questionsコントローラ)

質問詳細画面(app/views/questions/show.html.erb)で,回答の投稿をできるようにするため,
空のインスタンス@answer = Answer.newを追加

def show
  @question = Question.find(params[:id])
  @answer = Answer.new     #追加
end

136.回答の投稿機能View

Questionsコントローラのshowアクションに@answer = Answer.newを追加したので、
質問詳細画面(app/views/questions/show.html.erb)に,回答投稿フォームを追加

<div class="row">
  <div class="col-md-12">
    <h2><%= @question.title %></h2>
    <div>
      Content: <%= @question.content %>
    </div>
    <div>
      Name: <%= @question.name %>
    </div>
    <hr>
    <!-- 追加ココから -->
    <h3>Post new answer.</h3>
      <%= form_with model: [@question, @answer], local: true do |f| %>
        <%= f.hidden_field :question_id, { value: @question.id } %>
        <div class="form-group">
          <label>Name</label>
          <%= f.text_field :name, class: 'form-control' %>
        </div>
	<div class="form-group">
	  <label>Content</label>
          <%= f.text_area :content, class: 'form-control' %>
        </div>
	<div class="text-center">
          <%= f.submit "Post", class: 'btn btn-primary' %>
        </div>
      <% end %>
      <!-- ココまで -->
      <div>
        <%= link_to '> Home', root_path %>
      </div>
    </div>
  </div>
form_with model: [@question, @answer], local: true do |f|

questionモデルにひもづくanswerモデルをフォームに送信したいときは,このように配列でかく.
@question@answerはコントローラーから渡している.
ここも質問の入力画面と同様に,@answerが空(つまり新規)ならanswersコントローラのcreateメソッドを実行してくれる?

f.hidden_field :question_id, { value: @question.id }

ユーザーに見せる必要はないシステム内の処理をするのに必要な画面情報(ID値や商品名など)を画面に保持しておき,次の処理のときに渡すためのパラメータなどを格納しておくといった役割.第一引数にシンボル(パラメータ名),第二引数にvalueとして受け渡したい値を設定する

渡す値
:question_id,  :name,  :content

f:id:nogicchi:20210918162650p:plain
詳細画面に回答投稿画面を追加

137.質問が投稿された時の保存処理

answersコントローラ

#追加ココから
def create
  @question = Question.find(params[:question_id])  #質問は入力フォームで渡されたquestion_idで検索
  @answer = Answer.new                             #回答は空のインスタンスを用意し、
  if @answer.update(answer_params)                 #①、ここでupdateできれば
    redirect_to question_path(@question), notice: 'Success!'
  else                                             #updateできなければ
    redirect_to question_path(@question), alert: 'Invalid!'
  end
end
#ココまで
def edit
end

#追加ココから
private
   def answer_params
    params.require(:answer).permit(:content, :name, :question_id)
  end
#ココまで

①:if @answer.update(answer_params) ⇒ .saveだと上手くいかない
redirect_to question_path(@question) ⇒ 質問詳細ページへ,投稿した回答の表示は後で実装

138.Answerモデルのバリデーション

class Answer < …
  belongs_to :question
  # 追加ココから
  validates :name, presence: true
  validates :content, presence: true
  # ココまで 
end

139.回答一覧表示

質問詳細ページ(app/views/questions/show.html.erb)に,回答が1件以上あれば回答一覧を,0件の場合は「No Answer」と表示

<div class="row">
  <div class="col-md-12">
    <h2><%= @question.title %></h2>
    <div>
      Content: <%= @question.content %>
    </div>
    <div>
      Name: <%= @question.name %>
    </div>
    <hr>
    <!-- 追加ココから -->
    <div>
      <h3>Answers</h3>
      <table class="table table-striped">
        <% if @question.answers.any? %>
          <thead class="thead-light">
	    <tr>
	      <td>Answer</td>
	      <td>Name</td>
	      <td>Menu</td>
	    </tr>
	  </thead>
	  <tbody>
            <% @question.answers.each do |answer| %>
              <tr>
	        <td>
                  <%= answer.content %>
                </td>
		<td>
                  <%= answer.name %>
                </td>
		<td>
                  [Edit][Delete]
                </td>
	      </tr>
            <% end %>
          </tbody>
        <% else %>
          <p>No answer yet.</p> 
        <% end %>
      </table><!-- ココまで -->
    <h3>Post new answer.</h3>
    <%= form_with model: [@question, @answer], local: true do |f| %>
      <%= f.hidden_field :question_id, { value: @question.id } %>
      <div class="form_group">
        <label>Name</label>
        <%= f.text_filed :name, class: 'form-control' %>
      </div>
      <div class="form_group">
        <label>Content</label>
        <%= f.text_area :content, class: 'form-control' %>
      </div>
      <div class="text-center">
        <%= f.submit "Post", class='btn btn-primary' %>
      </div>
    <% end %>
    <div>
      <%= link_to '> Home', root_path %>
    </div>
  </div>
</div>

if @question.answers.any?

questionにひもづく回答が1件以上ある場合.
questionは単数で,answersは複数形であることに注意.ここは間違えやすいな.

answersと複数形なのは,questionモデルの

class Question < ApplicationRecord

has_many :answers, dependent: :destroy

から来ている
f:id:nogicchi:20210918175234p:plain
回答を投稿してみる

f:id:nogicchi:20210918175300p:plain
無事,回答ができた

140.回答の編集①

rails routesでルーティングの確認

edit_question_answer GET /questions/:question_id/answers/:id/edit(.:format) answers#edit

URLの中に、questionのid(question_id,回答の入力フォームでわたされたもの)とanswersのidが含まれていることに注意.よって,次のように変更

<div class="row">
  <div class="col-md-12">
    <h2><%= @question.title %></h2>
    <div>
      Content: <%= @question.content %>
    </div>
    <div>
      Name: <%= @question.name %>
    </div>
    <hr>
    <div>
      <h3>Answers</h3>
      <table class="table table-striped">
        <% if @question.answers.any? %>
          <thead class="thead-light">
            <tr>
              <td>Answer</td>
              <td>Name</td>
              <td>Menu</td>
            </tr>
          </thead>
          <tbody>
            <% @question.answers.each do |answer| %>
              <tr>
                <td>
                  <%= answer.content %>
                </td>
                <td>
                  <%= answer.name %>
                </td>
                <td>
                  [<%= link_to 'Edit', edit_question_answer_path(@question, answer) %>] 
                  [Delete]<!-- 変更 -->
                </td>
              </tr>
            <% end %>
          </tbody>
        <% else %>
          No answer yet. 
        <% end %>
      </table>
    <h3>Post new answer.</h3>
    <%= form_with model: [@question, @answer], local: true do |f| %>
      <%= f.hidden_field :question_id, { value: @question.id } %>
        <div class="form_group">
          <label>Name</label>
          <%= f.text_filed :name, class: 'form-control' %>
        </div> 
        <div class="form_group">
          <label>Content</label>
          <%= f.text_area :content, class: 'form-control' %>
        </div> 
        <div class="text-center">
          <%= f.submit "Post", class='btn btn-primary' %>
        </div>
    <% end %>
    <div>
      <%= link_to '> Home', root_path %>
    </div>
  </div>
</div>

answersコントローラのeditアクション

def edit
  @question = Question.find(params[:question_id])  # 追加,question_idで検索 
  @answer = @question.answers.find(params[:id])    # 追加,answersと複数形に注意! 
end
view

回答者の名前と内容を編集できるようにする.
質問詳細画面(app/views/questions/show.html.erb)内に作成した回答投稿フォーム(Post new answer.)とほぼ同じ.

<div>
  <h2>Update answer</h2>
  <%= form_with model: [@question, @answer], local: true do |f| %>
    <div class="form-group">
      <label>Name</label>
      <%= f.text_field :name, class:"form-control" %>
    </div>
    <div class="form-group">
      <label>Content</label>
      <%= f.text_area :content, class:"form-control" %>
    </div>
    <div class="text-center">
      <%= f.submit "Update", class: "btn btn-primary" %>
    </div>
  <% end %>
</div>

f:id:nogicchi:20210918182054p:plain
1番目の回答を編集してみる

f:id:nogicchi:20210918182127p:plain
回答の編集画面

updateボタンを押したときの保存処理は,次で行う.

141.回答の編集②

ここもanswersコントローラのcreateアクションとほぼ同じ.

def edit
  @question = Question.find(params[:question_id])
  @answer = @question.answers.find(params[:id])
end
# 追加ココから  
def update
  @question = Question.find(params[:question_id])
  @answer = @question.answers.find(params[:id])
  if @answer.update(answer_params)
    redirect_to question_path(@question), notice: "Success!"
  else
    flash[:alert]='Invalid!'
    render :edit
  end
end
# ココまで

f:id:nogicchi:20210918183045p:plain
回答を編集してみる

f:id:nogicchi:20210918183113p:plain
無事に回答を編集できた

142.回答の削除

回答にある仮のボタン[Delete]を削除できるようにする.
まずはrails routesでURIを確認

Prefix  Verb     URI Pattern                                    Controller#Action
        DELETE   /questions/:question_id/answers/:id(.:format)  answers#destroy
View

<div class="row">
  <div class="col-md-12">
    <h2><%= @question.title %></h2>
  <div>
    Content: <%= @question.content %>
  </div>
  <div>
    Name: <%= @question.name %>
  </div>
  <hr>
  <div>
  <h3>Answers</h3>
  <table class="table table-striped">
    <% if @question.answers.any? %>
      <thead class="thead-light">
        <tr>
	  <td>Answer</td>
	  <td>Name</td>
	  <td>Menu</td>
        </tr>
      </thead>
      <tbody>
        <% @question.answers.each do |answer| %>
          <tr>
            <td>
              <%= answer.content %>
            </td>
	    <td>
              <%= answer.name %>
            </td>
	    <td>
              [<%= link_to 'Edit', edit_question_answer_path(@question, answer) ] 
              [<%= link_to 'Delete', question_answer_path(@question, answer), 
                method: :delete, data: { confirm: 'R U sure?'} %>] 
                         <!-- ↑変更 -->
            </td>
          </tr>
        <% end %>
      </tbody>
    <% else %>
      No answer yet. 
    <% end %>
  </table>
  <h3>Post new answer.</h3>
    <%= form_with model: [@question, @answer], local: true do |f| %>
      <%= f.hidden_field :question_id, { value: @question.id } %>
        <div class="form_group">
	  <label>Name</label>
          <%= f.text_filed :name, class: 'form-control' %>
        </div>
	<div class="form_group">
	  <label>Content</label>
          <%= f.text_area :content, class: 'form-control' %>
        </div>
	<div class="text-center">
          <%= f.submit "Post", class='btn btn-primary' %>
        </div>
      <% end %>
    <div>
      <%= link_to '> Home', root_path %>
    </div>
  </div>
</div>

answersコントローラ

def update
  @question = Question.find(params[:question_id])
  @answer = @question.answers.find(params[:id])
  if @answer.update(answer_params)
    redirect_to question_path(@question), notice: "Success!"
  else
    flash[:alert]='Invalid!'
    render :edit
  end
end
# 追加ココから 
 def destroy
  @question = Question.find(params[:question_id])
  @answer = @question.answers.find(params[:id])
  @answer.destroy
  redirect_to question_path(@question), notice: 'Deleted!'
end
# ココまで

143.Questionsコントローラのリファクタリング

@question = Question.find(params[:question_id])が4か所あるのでまとめる.

class QuestionsController < ApplicationController
  before_action :set_question, only: [:show, :edit, :update, :destroy] # 追加
  def index
    @questions = Question.all
  end

  def show 
    # @question = Question.find(params[:id]) # 変更
    @answer = Answer.new
  end

  def new
    @question = Question.new
  end
  
  def create
    @question = Question.new(question_params)
    if @question.save
      redirect_to root_path, notice: "Success!"
    else
      flash[:alert] ="Save error!"
      render :new
    end
  end

  def edit
    # @question = Question.find(params[:id]) # 変更 
  end
  
  def update
    # @question = Question.find(params[:id]) # 変更 
    if @question.update(question_params)
      redirect_to root_path, notice: 'Success!'
    else
      flash[:alert]='Save error!'
      render :edit
    end
  end
  
  def destroy
    # @question = Question.find(params[:id]) # 変更 
    @question.destroy
    redirect_to root_path, notice: 'Success!'
  end
  
  private 
   # 追加ココから 
    def set_question
      @question = Question.find(params[:id])
    end
 # ココまで 
    def question_params
      params.require(:question).permit(:name, :title, :content)
    end
end