Try   HackMD

Re:Rails 2022 QAサービス作成詳細

tags: rails

Documents

Work

メイン教材

補助ドキュメント

module SampleApp
  class Application < Rails::Application
    config.generators do |g|
      g.stylesheets false   #styleシート
      g.javascripts false   #javascript
      g.helper false        #ヘルパー
      g.test_framework false #テストファイル 
    end
  end
end

備考:

  • タイプミス/間違えた時
    • モデルの削除 rails destroy model answer

質問回答アプリの基盤作成

コントローラーを作成

要件

  • questionsという名前で以下コントローラー作成
    • 閲覧
    • 詳細
    • 編集
    • 新規作成

Issue

  • なぜやるのか
    • RailsはMVCモデルなので、静的な表示をするにも、何をするにもコントローラーが必要だから
  • このタスクのゴール
    • index show edit newのコントローラー基盤が作成されていること

実装

rails g controller questions index show edit new

Commit 83530c6

モデル作成

要件

  • questionというモデル名で以下作成
    • name
    • title
    • content

各カラムの型の設計

カラム
name string
title string
content text

レコードのイメージ

ID name title content
1 山田 題名A 投稿内容A
2 吉岡さん 題名B 投稿内容BBB

Issue

  • なぜやるのか
    • データベースを入れるための最初の基盤を作成する
  • このタスクのゴール
    • モデルquestionを作成する
    • 投稿のための名前、題名、内容を保存するためのカラムを作成する
    • DBが反映された事をコマンドにて確認する

実装

 rails g model question name:string title:string content:text
  • migrateして反映
    • rails db:migrate

備考:

  • モデル名先頭は大文字Questionsで作成した方が良い
    • モデル名は先頭が小文字、モデルのクラスは先頭が大文字」と区別されるので、モデル作成時に先頭を大文字/小文字のどちらでで入力しても、自動で修正される

cmmit 15e9b11

DB確認

rails dbconsole SQLite version 3.32.3 2020-06-18 14:00:33 Enter ".help" for usage hints. sqlite> .schem CREATE TABLE IF NOT EXISTS "schema_migrations" ("version" varchar NOT NULL PRIMARY KEY); CREATE TABLE IF NOT EXISTS "ar_internal_metadata" ("key" varchar NOT NULL PRIMARY KEY, "value" varchar, "created_at" datetime NOT NULL, "updated_at" datetime NOT NULL); CREATE TABLE IF NOT EXISTS "questions" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar, "title" varchar, "content" text, "created_at" datetime NOT NULL, "updated_at" datetime NOT NULL); CREATE TABLE sqlite_sequence(name,seq); sqlite> .q

ルーティングの設定

Issue

  • なぜやるのか
    • Railsの規約によるURLパス=各ページ表示の設定を行う
  • このタスクのゴール
    • ルーティングの設定
    • URLでの表示確認
    • コマンドでのURLパス設定確認

rails routes

questions_index GET  /questions/index(.:format)                                                               questions#index
questions_show GET  /questions/show(.:format)                                                                questions#show
questions_edit GET  /questions/edit(.:format)                                                                questions#edit
questions_new GET  /questions/new(.:format)

routes変更

一括で基本的なルーティングを設定してくれる

resources :questions

rails routes

        questions GET    /questions(.:format)                                                                     questions#index
                  POST   /questions(.:format)                                                                     questions#create
     new_question GET    /questions/new(.:format)                                                                 questions#new
    edit_question GET    /questions/:id/edit(.:format)                                                            questions#edit
         question GET    /questions/:id(.:format)                                                                 questions#show
                  PATCH  /questions/:id(.:format)                                                                 questions#update
                  PUT    /questions/:id(.:format)                                                                 questions#update
                  DELETE /questions/:id(.:format)

ルート設定

root to: 'questions#index'

commit 0de96f9

Bootstrap4の導入

Issue

  • なぜやるのか
    • デザインの半自動化、視認性の向上
  • このタスクのゴール
    • Bootstrapの設定
    • ブラウザのソース表示での反映の確認
    • CSSクラス付与の反映テスト

Gemfile

### Add Bootstrap
gem 'bootstrap', '~> 4.1.1'
gem 'jquery-rails', '~> 4.3.1'

bundle

  • CSS to SCSS
    • application.css -> application.scss
      • @import "bootstrap"
  • application.js
//= require activestorage //= require turbolinks ↓今回追加したBootstrapの設定3ファイル //= require jquery3 //= require popper //= require bootstrap-sprockets //= require_tree .

app/views/layouts/application.html.erb

  • Bootstrapのラッピングスタイル、最大幅の調整
<div class="container">
    <%= yield %>
</div>

commit 1cc1e21


indexリストの表示UI + Seedでダミーデータ流し込み

コントローラーアクション

  • app/controllers/questions_controller.rb
  • @questions=インスタンス変数に、Questionモデル内全てのータを取得し、格納する
def index
    @questions = Question.all
end

確認方法

データを入れたタイミングでターミナルでrails c > Question.allにて確認出来る

データのView化 リスト表示基盤作成

<table class="table">
    <thead>
        <tr>
            <th scope="col">#id</th>
            <th scope="col">title</th>
            <th scope="col">Menu</th>
        </tr>
    </thead>
    <tbody>
        <% @questions.each do |qs| %>
            <tr>
                <td><%= qs.id %></td>
                <td><%= qs.title %></td>
                <td>[edit] [delete]</td>
            </tr>
        <% end %>
    </tbody>
</table>

ポイント

@questions.each do |qs|

で作成したインスタンス変数でfor文を作成し、ここで一時的に作成した変数qsにモデルのカラム名を当てていく

ダミーデータの作成

db/seeds.rb

Question.create(id: 1, name: "山下 妃里", title: '手前ルー・ルー攻め派', content: "狸はゆうべゴーシュとみんなにならて行っまし")
Question.create(id: 2, name: "川西 樹里", title: '左ルー・ルー攻め派', content: "ほっとどうもゴーシュから夜中に結んたた")
Question.create(id: 3, name: "村上 仁", title: '左ルー・せき止め派', content: "何そうにあとをすっばゴーシュへすわり込んましな")

シードの反映

rails db:seed

確認

>> rails c
>> irb(main):001:0> Question.all
Question Load (1.0ms)  SELECT  "questions".* FROM "questions" LIMIT ?  [["LIMIT", 11]]

=> #<ActiveRecord::Relation [
#<Question id: 1, name: "Test name 1", title: "Test question 1", content: "content 1", created_at: "2022-01-11 13:46:45", updated_at: "2022-01-11 13:46:45">, 
#<Question id: 2, name: "Test name 2", title: "Test question 2", content: "content 2", created_at: "2022-01-11 13:46:45", updated_at: "2022-01-11 13:46:45">, 
#<Question id: 3, name: "Test name 3", title: "Test question 3", content: "content 3", created_at: "2022-01-11 13:46:45", updated_at: "2022-01-11 13:46:45">]>

>> irb(main):002:0> exit

Point

DBまわりではまったら、DBを作り直す(モデル自体はそのまま)

rails db:drop
rails db:create
rails db:migrate
rails db:seed

commit c57fcb8 e4a3111

現UI

新規投稿画面の作成

  • issue
    • 現在のままだとユーザーが新規にデータを入れられないので、入力出来るインターフェースが必要
  • ゴール
    • 入力UIを通して、ユーザーがデータを追加出来るようにする
  • How
    • new.htmnl.erbに入力可能なUIを設置する
      • Rails固有のフォーム形式で設置する
    • コントローラーのnewにデータ投入のコードを設定
    • ストロングパラメータでセキュアにする

HTML

  • eachでループをまわす。
  • form_with
    • Railsのフォームヘルパー
  • model: @question
    • モデル:(モデルは) コントローラーで設定した、@モデルオブジェクトを設置
  • local: true
    • 非同期処理の防止。使う時はまた別の設定を行う
  • 取得したプロパティに対してdo |f|eachをまわす、変数をfにする。 jsのmapと同じような働きをする
  • f.text_field Railsのフォーム機能。
    • :nameそれにプロパティを当てます

参考: 【Rails】form_forの基本の基 - Qiita

<div class="row">
    <div class="col-md-8 offset-md-2">
        <h2>新規質問投稿</h2>
        <%= form_with model: @question, local: true do |f| %>
            <div class="form-group">
                <label for="">name</label>
                <%= f.text_field :name, class:"form-control" %>
            </div>
            <div class="form-group">
                <label for="">title</label>
                <%= f.text_field :title, class:"form-control" %>
            </div>
            <div class="form-group">
                <label for="">content</label>
                <%= f.text_area :content, class:"form-control" %>
            </div>
            <div class="">
                <%= f.submit "Submit" , class:"btn btn-primary" %>
            </div>
        <% end %>
    </div>
</div>

コントローラ

new

  • @question = Question.new新規作成メソッドにてインスタンスでに格納し、@オブジェクトを作成する

create

  • @question = Question.new(questions_params)
    • プロテクトされたアクションを値に入れる
  • if @question.save 保存すれば
    • redirect_to root_path リダイレクト
    • notice: "Succusess!" 成功表示
  • else 失敗時
    • flash[:alert] = "Save Error" フラッシュ表示
    • render :new レンダーでnew 入力状態に戻す
  • params.require(:question).permit(:name, :title, :content)
    • プロテクトのパラメーター
      • resuireモデルとパーミットする値の指定
def new
    @question = Question.new
  end

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

  private
    def questions_params
      params.require(:question).permit(:name, :title, :content)
    end

byebugでデバッグ

フォーム送信直前に設置

  private
    def questions_params
      byebug

ターミナルでデバッグ

(byebug) params
<ActionController::Parameters {
"utf8"=>"✓", "authenticity_token"=>"0rX/TswqOSCCqUC+9L6PiVyB9xQuudJ+GDDmFyrXmKmntihgiLI2wQq68nmKu+MtS//M2I79RhIIAViM9mTXSQ==",
"question"=><ActionController::Parameters 
{"name"=>"sds", "title"=>"sだdふぁdふぁdふぁ", "content"=>"ldjlsjfljlskdf\r\n"} permitted: false>, "commit"=>"Submit", "controller"=>"questions", "action"=>"create"} permitted: false>

> quit

再起動後、newのフォームHTMLのトークンが一致しているか確認

commit f632402

バリデート

未入力だと送信出来ないようにする

app/models/question.rb

class Question < ApplicationRecord
  validates :name, presence: true
  validates :title, presence: true
  validates :content, presence: true
end

共通HTML

<div class="container">
    <% if flash[:notice] %>
        <p class="text-success"><%= flash[:notice] %></p>
    <% end %>
    <% if flash[:alert] %>
        <p class="text-danger"><%= flash[:alert] %></p>
    <% end %>
    <%= yield %>
</div>

該当コントローラー

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

commit 1a08adf

リンクの作成

パスの確認

base) camp-rails $ (dev) rails routes
                   Prefix Verb   URI Pattern                                                                              Controller#Action
                     root GET    /                                                                                        questions#index
                questions GET    /questions(.:format)                                                                     questions#index
                          POST   /questions(.:format)                                                                     questions#create
             new_question GET    /questions/new(.:format)                                                                 questions#new
            edit_question GET    /questions/:id/edit(.:format)                                                            questions#edit

HTMLに設置

  • フォームヘルパーを使ったリンクの作成
    • xxx_pathでRailsルールにのっとり、リンクの自動作成
  • editは_path(パラメーター)で自動的に書くレコードのオブジェクトを取得し、idも紐付いてリンクが生成される
<div class="row">
    <%= link_to 'question' , root_path %>
</div>

<div class="row">
    <%= link_to 'New question' , new_question_path %>
</div>

<%= link_to "編集" , edit_question_path(qs) %>] [削除] %>

commit 1697191

編集画面の作成

  • 新規作成とほぼ同じUIなので、newから一旦コピペ
  • コントローラーでedit/createを作成する
  • 基本的にはnewと同じ仕組み
  • updateが反映されてDBが上書きされる
  def edit
    @question = Question.find(params[:id])
  end

  def update
    @question = Question.find(params[:id])
    if @question.update(questions_params)
      # byebug
      redirect_to root_path, notice: "Success!"
    else
      flash[:alert] = "Save Error"
      render :edit
    end
  end

メモ

  • form_withで作られたフォームに入力された情報は、create アクションかupdate アクションかの どちらかに送信される
  • 今回は作成済のモデルを扱っているため、saveボタンによってupdateアクションが呼ばれている。
    • もし新規に作成する場合はsaveをトリガーにcreateアクションが呼ばれる

commit e67819c

HTML共通化

Sassやwordpressのように複数回使われるHTMLはパーシャルを作成する

_form.html.erb

<div class="row">
<%= form_with model: @question, local: true do |f| %>
    <div class="form-group">
        <label for="">name</label>
        <%= f.text_field :name, class:"form-control" %>
    </div>
    <div class="form-group">
        <label for="">title</label>
        <%= f.text_field :title, class:"form-control" %>
    </div>
    <div class="form-group">
        <label for="">content</label>
        <%= f.text_area :content, class:"form-control" %>
    </div>
    <div class="">
        <%= f.submit "Submit" , class:"btn btn-primary" %>
    </div>
<% end %>
</div>

<div class="row">
    <%= link_to 'TOP' , root_path %>
</div>

各HTML

<%= render "form" %>

commit 108bf0e

削除機能

コントローラー

def destroy
    @question = Question.find(params[:id])
    @question.destroy
    redirect_to root_path, notice: "Success!"
end

commit dbf5021

HTML

[<%= link_to "編集" , edit_question_path(qs) %>] 
[<%= link_to "削除" , question_path(qs), method: :delete, data:{confirm: "削除して良いですか?" } %>]

詳細画面

Controller

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

HTML

...
<tbody>
    <tr>
        <td>
            <%= @question.id %>
        </td>
        <td>
            <%= @question.title %>
        </td>
        <td>
            <%= @question.content %>
        </td>
        <td>
            [<%= link_to "編集" , edit_question_path(@question) %>] [<%= link_to "削除" ,
                question_path(@question), method: :delete, data:{confirm: "削除して良いですか?" } %>]
        </td>
    </tr>
</tbody>
...

commit 7b1576e


コメント機能の作成

Answers

コントローラーの作成

rails g controller answers edit

create  app/controllers/answers_controller.rb
       route  get 'answers/edit'
      invoke  erb
      create    app/views/answers
      create    app/views/answers/edit.html.erb

モデルの作成

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

生成ファイル

app/models/answer.rb

class Answer < ApplicationRecord
  belongs_to :question
end

belongs_to = 所属元、紐付け元

db/migrate/20220115071500_create_answers.rb

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

app/models/question.rb

  • 1対多の設定
    • 1つのQ(name,title,コンテンツ)に複数のA(投稿者名とコンテンツの1セット)
  • 紐付け元のDB削除に関連answerも削除する。親の道連れ
has_many :answers, dependent: :destroy

反映 rails db:migrate

ルーティング

resources :questions do
    resources :answers
end

結果/ question同様にanswersにも反映

(base) camp-rails $ (dev) rails routes
                   Prefix Verb   URI Pattern                                                                              Controller#Action
             answers_edit GET    /answers/edit(.:format)                                                                  answers#edit
                     root GET    /                                                                                        questions#index
         question_answers GET    /questions/:question_id/answers(.:format)                                                answers#index
                          POST   /questions/:question_id/answers(.:format)                                                answers#create
      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
                questions GET    /questions(.:format)                                                                     questions#index

Answerコントローラーの設定

コントローラー設定 question

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

HTML

  • 配列で2つのインスタンスを渡す
    • <%= form_with model: [@question, @answer]...
  • hiddenで回答のFKをトリガーにしたFKのValueを渡す = 見えないがHTMLには出てくる値
    • <%= f.hidden_field :question_id, {value: @question.id } %>``
    <div class="row">
    <%= form_with model: [@question, @answer], local: true do |f| %>
        <%= f.hidden_field :question_id, {value: @question.id } %>
        <div class="form-group">
            <label for="">name</label>
            <%= f.text_field :name, class:"form-control" %>
        </div>
        <div class="form-group">
            <label for="">content</label>
            <%= f.text_area :content, class:"form-control" %>
        </div>
        <div class="">
            <%= f.submit "Submit" , class:"btn btn-primary" %>
        </div>
    <% end %>
    </div>

commit f5f92b7

新規回答投稿のコントローラー answer

  • questionのインスタンス内容は、FKquestion_id
  • Answeの新規@answer = Answer.new
  • 保存処理
    • if @answer.update(answer_params)
  • リダイレクト 保存処理してその場で再表示その場 redirect_to question_path(@question), notice: "Success!"
  def create
    @question = Question.find(params[:question_id])
    @answer = Answer.new
    if @answer.update(answer_params)
      redirect_to question_path(@question), notice: "Success!"
    else
      redirect_to question_path(@question), alert: "Invalid!"
      # flash[:alert] = "Save Error"
    end
  end

  def edit
  end

  private
    def answer_params
      # byebug
      params.require(:answer).permit(:name, :content, :question_id)
    end

バリデート

未回答保存の防止

class Answer < ApplicationRecord
  belongs_to :question
  validates :name, presence: true
  validates :content, presence: true
end

commit ab60417

回答一覧表示

  • how many で設定していたanswersをトリガーにeachする@question.answers.each do |answer| %>
    <div class="container">
        <div class="row">
        <h3 class="mt-4">回答一覧</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']['Detete']</td>
                    </tr>
                    <% end %>
                </tbody>

            <% else %>
                <p>No answer yet</p>
            <% end%>
        </table>
        </div>
    </div>

commit 98c49a8

編集

  • link on HTML
    • 第1引数
      • 質問インスタンス
    • 第2引数
      • それに対応した回答オブジェクト/id
<td>[<%= link_to 'Edit', edit_question_answer_path(@question, answer)%>]
  • コントローラ
    • 質問インスタンスに回答のFKにから探して投入
    • 回答インスタンスに質問インスタンスの中の回答オブジェクトのIDを探して入れる
    • 回答インスタンスをアップデート
  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] = "Save Error"
      render :edit
    end
  end

show html = newと同じ

<h2>回答の編集</h2>
<%= form_with model: [@question, @answer], local: true do |f| %>
    <%= f.hidden_field :question_id, {value: @question.id } %>
    <div class="form-group">
        <label for="">name</label>
        <%= f.text_field :name, class:"form-control" %>
    </div>
    <div class="form-group">
        <label for="">content</label>
        <%= f.text_area :content, class:"form-control" %>
    </div>
    <div class="">
        <%= f.submit "Submit" , class:"btn btn-primary" %>
    </div>
<% end %>
</div>

commit 8dae309

削除

HTML

link_to "削除" , question_answer_path(@question, answer), method: :delete,
data:{confirm: "削除して良いですか?" } %>]

controller

  def destroy
    @question = Question.find(params[:question_id])
    @answer = @question.answers.find(params[:id])
    @answer.destroy
    redirect_to question_path(@question), notice: "Success!"
  end

commit 044f623

next

リファクタリング https://www.udemy.com/course/the-ultimate-ruby-on-rails-bootcamp/learn/lecture/12235586#questions/6616350