# Re:Rails 2022 QAサービス作成詳細 ###### tags: `rails` ## Documents ### Work - 実装[BoxPistols/camp-rails: Rails5 Post SNS Service](https://github.com/BoxPistols/camp-rails) - コミットパス https://github.com/BoxPistols/camp-rails/commit/xxx - URL http://localhost:3000/questions/index - Bootstrap4.1 https://getbootstrap.com/docs/4.1/components/alerts/ ### メイン教材 - [はじめてのRuby on Rails入門-RubyとRailsを基礎から学びウェブアプリケーションをネットに公開しよう \| Udemy](https://www.udemy.com/course/the-ultimate-ruby-on-rails-bootcamp/learn/) ### 補助ドキュメント - My Rails Doc[\[Create 01\] Rails install · BoxPistols/tweetApp_Rails_TP Wiki](https://github.com/BoxPistols/tweetApp_Rails_TP/wiki/%5BCreate-01%5D-Rails-install) - テストファイルを作らない[【Ruby on Rails】Railsで余計なファイルを作らない - ENTRANCE](https://kattsundesu.hatenablog.com/entry/2019/02/11/000610) ```rb 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 ``` - ダミー参考 [テスト用に使うダミーデータが欲しい](https://kanto-t.jp/knowledge/entry_2463/#toc0) - [すぐ使えるダミーテキスト - 日本語 Lorem ipsum](https://lipsum.sugutsukaeru.jp/index.cgi) - [なんちゃって個人情報](http://kazina.com/dummy/) ### 備考: - タイプミス/間違えた時 - モデルの削除 `rails destroy model answer` # 質問回答アプリの基盤作成 ## コントローラーを作成 ### 要件 - questionsという名前で以下コントローラー作成 - 閲覧 - 詳細 - 編集 - 新規作成 ### Issue - なぜやるのか - RailsはMVCモデルなので、静的な表示をするにも、何をするにもコントローラーが必要だから - このタスクのゴール - index show edit newのコントローラー基盤が作成されていること ### 実装 ```rb 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が反映された事をコマンドにて確認する ### 実装 ```rb rails g model question name:string title:string content:text ``` - migrateして反映 - `rails db:migrate` 備考: - モデル名先頭は大文字`Questions`で作成した方が良い - モデル名は先頭が小文字、モデルのクラスは先頭が大文字」と区別されるので、モデル作成時に先頭を大文字/小文字のどちらでで入力しても、自動で修正される cmmit 15e9b11 #### DB確認 ```sql= 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` ```rb 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変更 一括で基本的なルーティングを設定してくれる ```rb resources :questions ``` rails routes ```rb 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) ``` #### ルート設定 ```rb root to: 'questions#index' ``` - http://localhost:3000/questions#index - ↓ 同等の表示 - http://localhost:3000/questions commit 0de96f9 ## Bootstrap4の導入 ### Issue - なぜやるのか - デザインの半自動化、視認性の向上 - このタスクのゴール - Bootstrapの設定 - ブラウザのソース表示での反映の確認 - CSSクラス付与の反映テスト Gemfile ```rb ### 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 ```json=//= require rails-ujs //= require activestorage //= require turbolinks ↓今回追加したBootstrapの設定3ファイル //= require jquery3 //= require popper //= require bootstrap-sprockets //= require_tree . ``` app/views/layouts/application.html.erb - Bootstrapのラッピングスタイル、最大幅の調整 ```erb <div class="container"> <%= yield %> </div> ``` commit 1cc1e21 --- ## indexリストの表示UI + Seedでダミーデータ流し込み ### コントローラーアクション - app/controllers/questions_controller.rb - `@questions`=インスタンス変数に、`Question`モデル内全てのータを取得し、格納する ```rb def index @questions = Question.all end ``` #### 確認方法 データを入れたタイミングでターミナルで`rails c` > `Question.all`にて確認出来る ### データのView化 リスト表示基盤作成 - index.html - 参考 https://getbootstrap.jp/docs/4.2/content/tables/ - テーブル基盤 ```erb <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> ``` #### ポイント ```rb @questions.each do |qs| ``` で作成したインスタンス変数でfor文を作成し、ここで一時的に作成した変数`qs`にモデルのカラム名を当てていく ### ダミーデータの作成 - DBが無い表示確認が出来ないため、一旦ダミーデータを入れておく - UIとしての静的なHTMLでも良いが、DBがの反映テストとしてもシードの活用が望ましい - ダミー参考 - [すぐ使えるダミーテキスト - 日本語 Lorem ipsum](https://lipsum.sugutsukaeru.jp/index.cgi) - [なんちゃって個人情報](http://kazina.com/dummy/) db/seeds.rb ```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` 確認 ```rb >> 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を作り直す(モデル自体はそのまま) ```rb rails db:drop rails db:create rails db:migrate rails db:seed ``` commit c57fcb8 e4a3111 #### 現UI ![](https://i.imgur.com/1ML8urK.png) ## 新規投稿画面の作成 - 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](https://qiita.com/manbolila/items/b8336ab115f3aebacbb9) ```erb <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モデルとパーミットする値の指定 ```rb 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でデバッグ フォーム送信直前に設置 ```rb private def questions_params byebug ``` ターミナルでデバッグ ```rb (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のトークンが一致しているか確認 ![](https://i.imgur.com/AZix6dx.png) commit f632402 ## バリデート ### 未入力だと送信出来ないようにする app/models/question.rb ```rb class Question < ApplicationRecord validates :name, presence: true validates :title, presence: true validates :content, presence: true end ``` #### 共通HTML ```erb <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> ``` #### 該当コントローラー ```rb 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 ## リンクの作成 #### パスの確認 ```rb 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も紐付いてリンクが生成される ```erb <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が上書きされる ```rb 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 ```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 ```erb <%= render "form" %> ``` commit 108bf0e ## 削除機能 #### コントローラー ```rb def destroy @question = Question.find(params[:id]) @question.destroy redirect_to root_path, notice: "Success!" end ``` commit dbf5021 #### HTML ```erb [<%= link_to "編集" , edit_question_path(qs) %>] [<%= link_to "削除" , question_path(qs), method: :delete, data:{confirm: "削除して良いですか?" } %>] ``` ## 詳細画面 Controller ```rb def show @question = Question.find(params[:id]) end ``` HTML ```rb ... <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` ```rb create app/controllers/answers_controller.rb route get 'answers/edit' invoke erb create app/views/answers create app/views/answers/edit.html.erb ``` ## モデルの作成 ```rb rails g model answer question:references name:string content:text ``` ### 生成ファイル app/models/answer.rb ```rb class Answer < ApplicationRecord belongs_to :question end ``` > belongs_to = 所属元、紐付け元 db/migrate/20220115071500_create_answers.rb ```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も削除する。親の道連れ ```rb has_many :answers, dependent: :destroy ``` 反映 `rails db:migrate` ## ルーティング ```rb resources :questions do resources :answers end ``` 結果/ question同様にanswersにも反映 ```rb (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 ```rb 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 } %>`` ` ```erb <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のインスタンス内容は、FK`question_id` - Answeの新規`@answer = Answer.new` - 保存処理 - `if @answer.update(answer_params)` - リダイレクト 保存処理してその場で再表示その場 `redirect_to question_path(@question), notice: "Success!"` ```rb 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 ``` ### バリデート 未回答保存の防止 ```rb 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| %>` ```erb <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 ```erb <td>[<%= link_to 'Edit', edit_question_answer_path(@question, answer)%>] ``` - コントローラ - 質問インスタンスに回答のFKにから探して投入 - 回答インスタンスに質問インスタンスの中の回答オブジェクトのIDを探して入れる - 回答インスタンスをアップデート ```rb 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と同じ ```erb <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 ```erb link_to "削除" , question_answer_path(@question, answer), method: :delete, data:{confirm: "削除して良いですか?" } %>] ``` controller ```rb 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