# 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