Rails初学者によるRailsチュートリアル学習記録⑮ 第13章

#目次
- [1. はじめに](#1-はじめに)
- [2. 第13章の概要](#2-第13章の概要)
- [3. 学習内容](#3-学習内容)
- [4. 終わりに](#4-終わりに)

#1. はじめに
- この記事は、Rails初学者の工業大学三年生がRailsチュートリアルの学習記録を<br>つけるための記事です。
- 筆者自体がRailsやWebについて知識が少ないので、内容の解釈などに<br>間違いがある可能性があります。(その時はコメントで指摘してくださると助かります！)
- Railsチュートリアル内ではRailsの内容以外にも、gitでのバージョン管理やHerokuを使ったデプロイも<br>学習しますが、gitに関しては既に私が学習済みのため学習記録には記述しません。
- 演習の記録も省略します。

#2. 第13章の概要
この章では、最大140文字のマイクロポストを投稿する機能を実装します。
マイクロポストには画像も追加できるようにします。

マイクロポストはプロフィールページとホームページに表示されます。
プロフィールページにはその人の投稿が表示され、ホームページには自分とフォローしているユーザーの
マイクロポストが表示されるようにします。
ただ、フォロー機能は14章で実装するのでこの章では、ホームページとプロフィールページの内容はほぼ同じになります。

1. Micropostモデル
    1. Micropostモデルの作成
    1. UserモデルとMicropostモデルの関連付け
    1. マイクロポストの改良
1. マイクロポストの操作
    1. マイクロポストの表示
    1. マイクロポストの作成・削除
1. 画像の投稿
    1. マイクロポストに画像を追加する
    1. 画像のバリデーション 

#3. 学習内容
###1. Micropostモデル



####1-1. Micropostモデルの作成
マイクロポストモデルには以下のような属性を持たせます。

| 1 | 2 |
|:-|:-|
|id   |integer   |
|content   |string   |
|user_id   |integer   |
|created_at   |datetime   |
|updated_at   |datetime   |
 この属性を持ったMicropostモデルを以下のコマンドで作成します。
`rails generate model Micropost content:text user:references`
上記のコマンドで引数に渡した、user:referencesによって、user_id属性が追加されてユーザーとの関連付けが行われます。

生成されたmicropostモデルのファイルには`belongs_to :user`と記述され、
これによりマイクロポストとユーザーの関係が1対1になります。
関連付けの詳細は次節で記述します。



####1-2. UserモデルとMicropostモデルの関連付け
Micropostモデルの作成時にUserモデルとの関連付けを行いました。
それにより、マイクロポストは1人のユーザーのidを持ちます。
そのidを参照してマイクロポストと一緒にユーザーのアイコンや名前を表示することができます。

この1対1の関係はマイクロポストから見たユーザーとの関係です。
一方のユーザーから見たマイクロポストとの関係は1対多の関係があります。

1対多の関係というのは、1人のユーザーは複数のマイクロポストを持つということです。
1人のユーザーはマイクロポストを何度も投稿できるのは、この関係によるものです。
1対1の関係がbelong_toによって表されたのに対して、1対多の関係はhas_manyによって表されます。
Micropostモデルはコマンドによってbelong_toが追加されましたが、
Userモデルは自動的に追加されないのでhas_manyを手動で追加します。

```ruby:app/models/micropost.rb
class User < ApplicationRecord
  has_many :microposts #Micropostモデルとの1対多の関係を追加
.
.
.
end
```

これらの関連付けを行うことで、マイクロポストに関する以下のメソッドがRailsで使用できるようになります。

|メソッド | 用途 |
|:-|:-|
|micropost.user   |Micropost に紐付いた User オブジェクトを返す   |
|user.microposts   |User のマイクロポストの集合を返す   |
|user.microposts.create(arg)    |user に紐付いたマイクロポストを作成する   |
|user.microposts.create!(arg)   |user に紐付いたマイクロポストを作成する（失敗時に例外を発生）   |
|user.microposts.build(arg)    |user に紐付いた新しい Micropost オブジェクトを返す   |
|user.microposts.find_by(id: 1)   | user に紐付いていて、id が 1 であるマイクロポストを検索する  |

上記のメソッドを使用することで、マイクロポストを作成します。


####1-3. マイクロポストの改良
マイクロポストを使用する上で必要な、UserとMicropostの関連付けの改良を行う。
改良の具体的な内容は以下の2つです。
・マイクロポストを読み込むときに順序付けをする
・ユーザーが削除された時にそのユーザーのマイクロポストも同時に削除されるようにする

1つ目の改良を行う理由は、`user.microposts`を使用してマイクロポストの集合を取り出しても、
順番について考慮されません。
アプリでは新しい順にマイクロポストを表示したいので、
マイクロポストを取り出すタイミングで順番を指定する必要があります。

順番を指定するにはdefault_scopeメソッドを使用します。
以下のコードがdefault_scopeメソッドを使用したMicropostモデルです。

```ruby:app/models/micropost.rb
class Micropost < ApplicationRecord
  belongs_to :user
  default_scope -> { order(created_at: :desc) } # マイクロポストを新しい順に取得する
  validates :user_id, presence: true
  validates :content, presence: true, length: { maximum: 140 }
end
```
descというのはSQLで降順を表す単語です。
created_action属性の値の降順で取得することになります。
これで1つ目の改良が完了しました。


ここから2つ目の改良を行います。
ユーザーが削除された時にマイクロポストも削除されるようにするには、
has_manyメソッドにオプションを1つ渡してあげるだけです。

```ruby:app/models/user.rb
class User < ApplicationRecord
  has_many :microposts, dependent: :destroy
.
.
.
end
```
これにより、ユーザーが削除された時にマイクロポストも同時に削除されます。
これでマイクロポストの2つの改良ができました。




###2.マイクロポストの操作
####2-1. マイクロポストの表示
ここからはマイクロポストをプロフィールページに表示させていきます。
処理の流れとしては、
・showアクション(プロフィールページに対応するアクション)でマイクロポストの集合を取得
・showページにマイクロポストを表示させるパーシャルを埋め込む
・パーシャルでマイクロポストを1つずつ表示
以上の流れでマイクロポストを表示させます。

まず、showアクションに以下のコードを追加して、マイクロポストをデータベースから取得します。
`@microposts = @user.microposts.paginate(page: params[:page])`

以下のコードが、プロフィールページにマイクロポストを表示させるコードです。

```html:app/views/users/show.html.erb
<% provide(:title, @user.name) %>
<div class="row">
  <aside class="col-md-4">
    <section class="user_info">
      <h1>
        <%= gravatar_for @user %>
        <%= @user.name %>
      </h1>
    </section>
  </aside>

  <div class="col-md-8">
    <% if @user.microposts.any? %>
      <h3>Microposts (<%= @user.microposts.count %>)</h3>
      <ol class="microposts">
        <%= render @microposts %>
      </ol>
      <%= will_paginate @microposts %>
    <% end %>
  </div>
</div>
```

`render @micoroposts`でマイクロポストを表示するパーシャルを埋め込んでいます。
ここでもユーザー一覧の時のように、ページネーションを設定しています。

パーシャルの中身が以下のコードです。

```html:app/views/microposts/_micropost.html.erb
<li id="micropost-<%= micropost.id %>">
  <%= link_to gravatar_for(micropost.user, size: 50), micropost.user %>
  <span class="user"><%= link_to micropost.user.name, micropost.user %></span>
  <span class="content"><%= micropost.content %></span>
  <span class="timestamp">
    Posted <%= time_ago_in_words(micropost.created_at) %> ago.
  </span>
</li>
```

ここではマイクロポストの投稿者の画像、名前、内容、投稿してからの時間を表示しています。


####2-2. マイクロポストの作成・削除
ここまでで、マイクロポストを表示する準備ができたので、
マイクロポストを作成するフォームの設置と、マイクロポストの削除機能の実装を行います。

マイクロポストを作成するフォームはホームページに追加します。

```html:app/views/static_pages/home.html.erb
<% if logged_in? %>
  <div class="row">
    <aside class="col-md-4">
      <section class="user_info">
        <%= render 'shared/user_info' %>
      </section>
      <section class="micropost_form">
          <%= render 'shared/micropost_form' %>
      </section>
    </aside>
  </div>
<% else %>
```
上のコードでは、`render user_info`と`micropost_form`の2つのパーシャルが埋め込まれています。
1つ目のuser_infoはサイドバーにユーザー情報を表示するためのパーシャルで、
2つ目のmicropost_formがフォームを表示するためのパーシャルです。

フォーム用のパーシャルが以下のコードです。

```html:app/views/shared/_micropost_form.html.erb
<%= form_with(model: @micropost, local: true) do |f| %>
  <%= render 'shared/error_messages', object: f.object %>
  <div class="field">
    <%= f.text_area :content, placeholder: "Compose new micropost..." %>
  </div>
  <%= f.submit "Post", class: "btn btn-primary" %>
<% end %>
```
このフォームが動作するためには、@micropostをアクションで定義する必要があります。
ホームページに対応するアクションは、StaticPageコントローラのhomeアクションなので、
そこで@micropost変数を定義します。

```ruby:app/controllers/static_pages_controller.rb
def home
  @micropost = current_user.microposts.build if logged_in?
end
```
後置ifを使用してログイン中の場合のみ、現在のユーザーに紐づけられたマイクロポストを作成できます。

フォームを設置したことで、ブラウザからマイクロポストを作成できるようになりました。
ここからは、ユーザー一覧でのユーザーの削除の時のように、
マイクロポストも削できるように変更を加えていきます。

削除機能の実装には、削除を行うためのリンクが必要です。
このリンクは自分のマイクロポストに対してのみ表示させるので、
current_user?メソッドを使用して、リンクでdeleteリンクを表示させます。

```html:app/views/microposts/_micropost.html.erb
<li id="micropost-<%= micropost.id %>">
  <%= link_to gravatar_for(micropost.user, size: 50), micropost.user %>
  <span class="user"><%= link_to micropost.user.name, micropost.user %></span>
  <span class="content"><%= micropost.content %></span>
  <span class="timestamp">
    Posted <%= time_ago_in_words(micropost.created_at) %> ago.

    <% if current_user?(micropost.user) %>  # deleteリンク
      <%= link_to "delete", micropost, method: :delete,
                                              data: { confirm: "You sure?" } %>
    <% end %>
  </span>
</li>
```
これでdeleteリンクが設置できたので、リンクに対応するdestroyアクションを作成します。
destroyアクションは、他のユーザーのマイクロポストに対して実行しようとしたとき、
自動的に失敗するようにフィルターをかけます。

```ruby:app/controllers/microposts_controller.rb
class MicropostsController < ApplicationController
  before_action :logged_in_user, only: [:create, :destroy]
  before_action :correct_user, only: :destroy
.
.
.
  def destroy
    @micropost.destroy
    flash[:success] = "Micropost deleted"
    redirect_to request.referrer || root_url
  end

  private
    def micropost_params
    params.require(:micropost).permit(:content)
  end

  def correct_user
    @micropost = current_user.microposts.find_by(id: params[:id])
    redirect_to root_url if @micropost.nil?
  end
end
```
destroyアクション内での処理は、マイクロポストを削除して、
1つ前のページ、つまりホームページにリダイレクトします。

correct_userフィルターでは関連付けを使用して、
現在のユーザーが持っているマイクロポストの中に、@micropostが入っているかを確認することで、
持ち主かどうかを判断しています。


###3. 画像の投稿
####3-1. マイクロポストに画像を追加する
ここまででマイクロポストに関する操作が実装できました。
この章では、それに加えマイクロポストに画像を追加する機能も追加します。

この機能の実装には、2つの要素の追加が必要です。
1つは画像を追加するときにクリックする「Upload image」ボタンと投稿された画像を表示する領域です。

Railsでの画像のアップロードにはActive Storageをという機能を使用します。
今回は画像のアップロードに使用しますが、Active StorageはPDFファイルや音声ファイルなどもアップロードできます。

Active Storageを使用するには以下のコマンドを使用します。
`rails active_storage:install`
このコマンドによって添付ファイル用のデータモデルが作成されるため、
コマンドの実行後にマイグレーションを行います。

Active Storageで重要なメソッドにhas_one_attachedメソッドがあります。
これは、指定したモデルとアップロードされたファイルを関連付けます。
この場合imageをMicropostモデルを関連付けます。
MicropostモデルとUserモデルの関連付けの時のように、1対1の関係付けを行います。

```ruby:app/models/micropost.rb
class Micropost < ApplicationRecord
  belongs_to :user
  has_one_attached :image # imageとの関連付け
  default_scope -> { order(created_at: :desc) }
  validates :user_id, presence: true
  validates :content, presence: true, length: { maximum: 140 }
end
```
今回は1対1の関連付けですが、複数枚の画像と関連付けるときは、has_many_attachedオプションを使用します。

関連付けができたらボタンを配置します。
ボタンはマイクロポストの投稿フォームに配置します。

```html:app/views/shared/_micropost_form.html.erb
<%= form_with(model: @micropost, local: true) do |f| %>
  <%= render 'shared/error_messages', object: f.object %>
  <div class="field">
    <%= f.text_area :content, placeholder: "Compose new micropost..." %>
  </div>
  <%= f.submit "Post", class: "btn btn-primary" %>
    <span class="image">
    <%= f.file_field :image %>
</span>
<% end %>
```
これでMicropostとimageの関連付け、ボタンの配置ができたので、アップロードの処理を実装します。
マイクロポストを作成するcreateに変更を加えます。

```ruby:app/controllers/microposts_controller.rb
def create
  @micropost = current_user.microposts.build(micropost_params)
  @micropost.image.attach(params[:micropost][:image])
  if @micropost.save
    flash[:success] = "Micropost created!"
    redirect_to root_url
  else
    @feed_items = current_user.feed.paginate(page: params[:page])
    render 'static_pages/home'
  end
end
```
ここではattachメソッドで@micropostオブジェクトにimageを追加できます。
追加したら、画像は、image_tagヘルパーを使用して、
`image_tag micropost.image`と書いて表示できます。

```ruby:app/views/microposts/_micropost.html.erb
<li id="micropost-<%= micropost.id %>">
  <%= link_to gravatar_for(micropost.user, size: 50), micropost.user %>
  <span class="user"><%= link_to micropost.user.name, micropost.user %></span>
  <span class="content">
    <%= micropost.content %>
    <%= image_tag micropost.image if micropost.image.attached? %>
  </span>
  <span class="timestamp">
    Posted <%= time_ago_in_words(micropost.created_at) %> ago.
    <% if current_user?(micropost.user) %>
      <%= link_to "delete", micropost, method: :delete,
                                       data: { confirm: "You sure?" } %>
    <% end %>
  </span>
</li>
```



####3-2. 画像のバリデーション
画像のアップロード機能は実装できたのですが、アップロードされる画像に制限を加えます。
制限の内容はファイル形式と、ファイルの容量です。
また、画像のサイズが大きいとマイクロポストを表示するときにレイアウトが崩れてしまうので、
画像サイズの調整も行います。

これらの実装を行うために、
`active_storage_validations`、`image_processing`、`mini_magick`の3つのgemをインストールします。
ファイル形式のバリデーションは、active_storage_validationsでcotent_typeを指定します。
今回はjpg、gif、pngのみを許可します。

同じくファイルの容量もこのジェムで指定します。
今回は容量の上限を5MBにして、それを超えていた時は「"should be less than 5MB」とメッセージを表示します。

```ruby:app/models/micropost.rb
class Micropost < ApplicationRecord
  belongs_to :user
  has_one_attached :image
  default_scope -> { order(created_at: :desc) }

  validates :user_id, presence: true
  validates :content, presence: true, length: { maximum: 140 }
  validates :image, content_type: { in: %w[image/jpeg image/gif image/png], #ファイル形式の指定
                                    message: "must be a valid image format" },
                    size: { less_than: 5.megabytes,                         #ファイル容量のバリデーション
                            message: "should be less than 5MB" }
end
```

次にレイアウトが崩れないように画像のリサイズを行います。
ここで使用するgemがimage_procesingとmini_magickです。
このgemを使用することで、画像に対して処理を加えた画像を出力できます。
今回は画像のリサイズを行うのでresize_to_limitオプションを渡します。
ここでは、画像を500×500ピクセル以内にリサイズするdisplay_imageメソッドを定義します。

```ruby:画像処理を行うdisplay_imageメソッド
def display_image
  image.variant(resize_to_limit: [500, 500])
end
```

これで画像を渡せば、リサイズされた画像が返ってくるようになったので、
マイクロポストを表示させるパーシャルで、リサイズされた画像を表示させます。

```html:app/views/microposts/_micropost.html.erb
<li id="micropost-<%= micropost.id %>">
  <%= link_to gravatar_for(micropost.user, size: 50), micropost.user %>
  <span class="user"><%= link_to micropost.user.name, micropost.user %></span>
  <span class="content">
    <%= micropost.content %>
　　#画像があるときのみリサイズされた画像を表示
    <%= image_tag micropost.display_image if micropost.image.attached? %> 
  </span>
  <span class="timestamp">
    Posted <%= time_ago_in_words(micropost.created_at) %> ago.
    <% if current_user?(micropost.user) %>
      <%= link_to "delete", micropost, method: :delete,
                                                data: { confirm: "You sure?" } %>
     <% end %>
  </span>
</li>
```
attached?メソッドはマイクロポストに画像があるかを確認するメソッドで、
これを使ってマイクロポストに画像があるときのみ画像を表示しています。
以上で、リサイズされた画像を表示できるようになりました。

#4. 終わりに
この章では、マイクロポストの投稿機能を実装しました。
やっとWebアプリケーションらしい機能が実装できて、この章は学習を進めるのが楽しかったです。

個人開発をする予定のアプリでは画像投稿機能を実装する予定なので、
この章は何回も読み直して理解を深めていきたいです。