Railsでブログアプリを作成（MVP）

# はじめに
Webエンジニアを目指して、RubyやRailsをいじってます。
今回は、Rails でブログアプリを作っていきたいと思います。ご指摘がありましたらよろしくお願いします。

# ブログアプリの雛形の作成
```terminal:terminal
rails new blog_app
cd blog_app

git init
// GitHubでリポジトリを作成（README.mdは、rails newで生成されるのでチェック不要）
git remote add origin URL.git
git add .
git commit -m"initial commit"
git push origin main
```

# 事前準備とヘッダーの作成
## トップページの作成
```terminal:terminal
rails g controller welcome index
```
ルートパスを設定
```ruby:config/routes.rb
  root "welcome#index"
```

## viewの編集
変更点を以下にまとめます。
・`<%= javascript_importmap_tags %>`は、見た目に影響しないのでbodyタグに移動
・headタグに`<meta charset="uft-8">`と`<meta name="description" content="sample text" >`(contentに書いた内容が検索結果の説明欄に表示される)を追加
・titleタグを`<title><%= full_title(yield(:title)) %>`に変更して、カスタムヘルパーを作成
```ruby:app/helpers/application_helper.rb
    def full_title(page_title = "")
        base_title = "memo_app"
        if page_title.empty?
            base_title
        else
            page_title + " | " + base_title
        end
    end 
```
・[sanitize.cssの適用](https://youtu.be/Hbxulm8iXSA?t=442)
・[web fontsの適用](https://youtu.be/Hbxulm8iXSA?t=531)
・bodタグにidを当ててそれぞれのページごとにレイアウトを調整できるようにする

```erb:app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title><%= full_title(yield(:title)) %></title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <meta name="description" content="sample text">

    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;700&display=swap" rel="stylesheet">

    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
    <%= stylesheet_link_tag "sanitize.css" %>
  </head>

  <body id="<%= controller.controller_name %>-<%= controller.action_name %>">
    <%= yield %>
    <%= javascript_importmap_tags %>
  </body>
</html>
````
## Sassの導入
・Gemfileの`gem "sassc-rails"`のコメントを外して`bundle install`
・application.cssをapplication.scssに変更する
※ 例えば、posts.scssを作成してそこからレイアウトを変更したいときはapplication.scssでpost.scssを読み込む必要があります。
```scss:application.scss
@import "posts";
```

## Headerの実装
bodyタグに以下を追加
```erb:app/views/layouts/application.html.erb
    <header class="header">
      <%= link_to "ロゴ", root_path %>
      <ul class="header-navlist">
        <%# この時点では、まだ設定していないのでアクセスしてもエラーになります %>
        <li><%= link_to "投稿する", post_new_path, class: "header-navlink" %></li>
      </ul>
    </header>
```

# ブログの基本機能の実装
## Postモデルの作成
```terminal:terminal
rails g model post content:text
```
NOT NULL制約を追加して、`rails db:migrate`
```ruby:db/migrate/文字列_create_post.rb
t.text :content, null: false
```

## Postsコントローラの作成
```terminal:terminal
rails g controller posts
```
Postsコントローラにアクションを追加
```ruby:app/controllers/posts_controller.rb
class PostsController < ApplicationController
    before_action :find_post, only: [:show, :edit, :update, :destroy]

    def show
    end

    def new
        @post = Post.new
    end

    def edit
    end

    def create
        @post = Post.new(post_params)
        if @post.save
            redirect_to root_path, notice: "設定しました"
        else
            render :new
        end
    end

    def update
        if @post.update(post_params)
            redirect_to root_path, notice: "更新しました"
        else
            render :edit
        end
    end

    def destroy
        if @post.destroy
            redirect_to root_path, notice: "削除しました"
        else
            redirect_to root_path, alert: "削除できませんでした"
        end
    end

    private

    def post_params
        params.require(:post).permit(:content)
    end

    def find_post
        @post = Post.find(params[:id])
    end
end
```
ルーティングを設定
```ruby:config/routes.rb
resources :posts, only: [:show, :new, :edit, :create, :update, :destroy]
```

フラッシュメッセージが表示されるようbodyタグに以下を追加
```erb:app/views/layouts/application.erb
    <% if flash[:notice].present? %>
      <p class="notice"><%= notice %></p>
    <% end %>
    <% if flash[:alert].present? %>
      <p class="alert"><%= alert %></p>
    <% end %>
````

## それぞれのアクションに対応するviewの実装
```erb:app/views/welcome/index.html.erb
<%= provide(:title, "一覧") %>
<% @posts.each do|post| %>
    <div class="content-block">
        <%= link_to "#{post.content}", post, class: "content" %>
    </div>
<% end %>
```
ここで、画像のようなエラーを吐かれました。
![スクリーンショット 2023-03-30 3.46.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3303899/1643daae-2ec9-ca46-ab3b-24077e93ad5c.png)
@ postsがnillになっているみたいです。
→ Welcomeコントローラのindexアクションに@ postsを定義してなかったことが原因でした。
```app/controllers/welcome_controller.rb
class WelcomeController < ApplicationController
  def index
    @posts = Post.all.order(created_at: :desc)
  end
end
```

パーシャルを使って部分テンプレート化（すごい...！）
```erb:app/views/posts/_form.html.erb
<%= form_with model: @post, local: true do|f|%>
    // フラッシュメッセージを表示
    <% if @post.errors.any? %>
        <ul>
        <% @post.errors.full_messages.each do |message| %>
            <li><%= message %></li>
        <% end %>
        </ul>
    <% end %>

    <table>
        <tr>
            <th>
                <%= f.label "内容", for: "content" %>
            </th>
            <td>
                <%= f.text_area :content, id: "content" %>
            </td>
        </tr>
    </table>
    <%= f.file_field :images, multiple: true %>
    <%= f.submit class: "button" %>
<% end %>
```

```erb:app/views/posts/new.html.erb
<%= provide(:title, "作成") %>
<%= render "form" %>
```

```erb:app/views/posts/edit.html.erb
<%= provide(:title, "編集") %>
<%= render "form" %>
```
ボタンの文字を日本語化します。
・`gem "rails-i18n"`を追加して`bundle install`
・config/application.rbに`config.i18n.default_locale = :ja`を追加
・config/localesにja.ymlを作成し以下を追加して、railsを再起動
```config/ja.yml
ja:
    activerecord:
        attributes:
            post:
                content: 内容
```

```erb:app/views/posts/show.html.erb
<%= provide(:title, "詳細") %>
<div class="wrapper">
    <div class="detail-block">
        <h4 class="headline-content">内容</h4>
        <p class="detail-content"><%= @post.content %></p>
    </div>
    <ul>
        <li><%= button_to "ブログを編集する", edit_post_path, method: :get, class: "change-button" %></li>
        // link_toだとできない
        <li><%= button_to "ブログを削除する", post_path, method: :delete, class: "change-button" %></li>
    </ul>
</div>
```

トップページの表示をブログの内容が長かった場合は、末尾を3点リーダーにするようにしました。
```diff_scss:welcome.scss
#welcome-index{
    .content-block{
+       overflow: hidden;
+       text-overflow: ellipsis;
+       white-space: nowrap;
    }
}
```
・ページネーションの実装

Gemfileに`gem "kaminari"`を追加して`bundle install`
```diff_ruby:app/controllers/welcome_controller.rb
- @posts = Post.all.order(created_at: :desc)
# １０ページ単位でページネーション
+ @posts = Post.all.page(params[:page]).per(10).order(created_at: :desc)
```
```diff_erb:app/views/welcome/index.html.erb
<%# 一番下 %>
+ <%= paginate @posts %>
```
```diff_erb:app/views/layouts/application.html.erb
<head>
<%# 中略 %>

<%# bootstarap の適用 %>
+   <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
    <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
    <%= stylesheet_link_tag "sanitize.css" %>
</head>
```
```terminal:terminal
// ページネーションにbootstrapを適用
rails g kaminari:views bootstrap4
```
config/localesにkaminari.ja.ymlを作成して以下を追加
```yml:config/locales/kaminari.ja.yml
ja:
    views:
        pagination:
            first: "&laquo; 最初"
            last: "最後 &raquo;"
            previous: "&lsaquo; 前"
            next: "次 &rsaquo;"
            truncate: "&hellip;" 
```
railsを再起動して、適当に10個より多い数登録してみてください。ページ下部にページネーションが表示されればOKです。

# 画像投稿できるようにする
## Active Storageのインストール
```terminal:terminal
rails active_storage:install
rails db:migrate
```
## 複数の添付ファイル用の設定
```diff_ruby:app/models/post.rb
class Post < ApplicationRecord
+     has_many_attached :images
end
```
```diff_ruby:app/controllers/posts_contoroller.rb
def post_params
-    params.require(:post).permit(:content)
+    params.require(:post).permit(:content, images: [])
end
```
```diff_erb:app/views/posts/_form.html.erb
<%= form_with model: @post, local: true do|f|%>
    <% if @post.errors.any? %>
        <ul>
        <% @post.errors.full_messages.each do |message| %>
            <li><%= message %></li>
        <% end %>
        </ul>
    <% end %>
    <table>
        <tr>
            <th>
                <label for="content">
                    内容
                </label>
            </th>
            <td>
                <%= f.text_area :content, id: "content" %>
            </td>
        </tr>
    </table>
+   <%= f.file_field :images, multiple: true %><br>
    <%= f.submit class: "button" %>
<% end %>
```
```diff_erb:app/views/welcome/index.html.erb
<%= provide(:title, "一覧") %>
<% @posts.each do|post| %>
    <div class="content-block">
        <%= link_to "#{post.content}", post, class: "content" %>
+       <div>
+           <% if post.images.attached? %>
+              <% post.images.each do |image| %>
+                  <%= image_tag image, class: "image"%><br>
+              <% end %>
+           <% end %>
+       </div>
    </div>
<% end %>
```
```diff_erb:app/views/posts/show.html.erb
<%= provide(:title, "詳細") %>
<div class="wrapper">
    <div class="detail-block">
        <h4 class="headline-content">内容</h4>
        <p class="detail-content"><%= @post.content %></p>
+       <div>
+           <% if @post.images.attached? %>
+               <% @post.images.each do |image| %>
+                   <%= image_tag image, class: "image"%>
+               <% end %>
+           <% end %>
+       </div>
    </div>
    <ul>
        <li><%= button_to "ブログを編集する", edit_post_path, method: :get, class: "change-button" %></li>
        <li><%= button_to "ブログを削除する", post_path, method: :delete, class: "change-button" %></li>
    </ul>
</div>
```
# ログイン機能の実装
※ メール機能の設定は割愛します。
```terminal:terminal
// Gemfileにgem "devise"を追加して、以下のコマンドを順に実行
bundle install
rails g devise:install
rails g devise User
rails db:migrate
rails g devise:views
```
railsを再起動して、http://localhost:3000/users/sign_up にアクセス -> 登録画面が出ればOK

headerのliタグを変更
```erb:app/views/layouts/application.html.erb
<ul class="header-navlist">
    <% if user_signed_in? %>
        <li><%= link_to "投稿する", new_post_path, class: "header-navlink" %></li>
        <li><%= link_to "ログアウト", destroy_user_session_path, method: :delete, class: "header-navlink" %></li>
    <% else %>
        <li><%= link_to "新規登録", new_user_registration_path, class: "header-navlink" %></li>
        <li><%= link_to "ログイン", new_user_session_path, class: "header-navlink" %></li>
    <% end %>
</ul
```
config/localesにdevise.ja.ymlを作成し、以下を追加
```yml:config/locales/devise.ja.yml
ja:
  activerecord:
    attributes:
      user:
        confirmation_sent_at: パスワード確認送信時刻
        confirmation_token: パスワード確認用トークン
        confirmed_at: パスワード確認時刻
        created_at: 作成日
        current_password: 現在のパスワード
        current_sign_in_at: 現在のログイン時刻
        current_sign_in_ip: 現在のログインIPアドレス
        email: Eメール
        encrypted_password: 暗号化パスワード
        failed_attempts: 失敗したログイン試行回数
        last_sign_in_at: 最終ログイン時刻
        last_sign_in_ip: 最終ログインIPアドレス
        locked_at: ロック時刻
        password: パスワード
        password_confirmation: パスワード（確認用）
        remember_created_at: ログイン記憶時刻
        remember_me: ログインを記憶する
        reset_password_sent_at: パスワードリセット送信時刻
        reset_password_token: パスワードリセット用トークン
        sign_in_count: ログイン回数
        unconfirmed_email: 未確認Eメール
        unlock_token: ロック解除用トークン
        updated_at: 更新日
    models:
      user: ユーザー
  devise:
    confirmations:
      confirmed: メールアドレスが確認できました。
      new:
        resend_confirmation_instructions: アカウント確認メール再送
      send_instructions: アカウントの有効化について数分以内にメールでご連絡します。
      send_paranoid_instructions: メールアドレスが登録済みの場合、本人確認用のメールが数分以内に送信されます。
    failure:
      already_authenticated: すでにログインしています。
      inactive: アカウントが有効化されていません。メールに記載された手順にしたがって、アカウントを有効化してください。
      invalid: "%{authentication_keys}またはパスワードが違います。"
      last_attempt: もう一回誤るとアカウントがロックされます。
      locked: アカウントはロックされています。
      not_found_in_database: "%{authentication_keys}またはパスワードが違います。"
      timeout: セッションがタイムアウトしました。もう一度ログインしてください。
      unauthenticated: ログインもしくはアカウント登録してください。
      unconfirmed: メールアドレスの本人確認が必要です。
    mailer:
      confirmation_instructions:
        action: メールアドレスの確認
        greeting: "%{recipient}様"
        instruction: 以下のリンクをクリックし、メールアドレスの確認手続を完了させてください。
        subject: メールアドレス確認メール
      email_changed:
        greeting: こんにちは、%{recipient}様。
        message: メールアドレスの（%{email}）変更が完了したため、メールを送信しています。
        message_unconfirmed: メールアドレスが（%{email}）変更されたため、メールを送信しています。
        subject: メール変更完了
      password_change:
        greeting: "%{recipient}様"
        message: パスワードが再設定されました。
        subject: パスワードの変更について
      reset_password_instructions:
        action: パスワード変更
        greeting: "%{recipient}様"
        instruction: パスワード再設定の依頼を受けたため、メールを送信しています。下のリンクからパスワードの再設定ができます。
        instruction_2: パスワード再設定の依頼をしていない場合、このメールを無視してください。
        instruction_3: パスワードの再設定は、上のリンクから新しいパスワードを登録するまで完了しません。
        subject: パスワードの再設定について
      unlock_instructions:
        action: アカウントのロック解除
        greeting: "%{recipient}様"
        instruction: アカウントのロックを解除するには下のリンクをクリックしてください。
        message: ログイン失敗が繰り返されたため、アカウントはロックされています。
        subject: アカウントのロック解除について
    omniauth_callbacks:
      failure: "%{kind} アカウントによる認証に失敗しました。理由：（%{reason}）"
      success: "%{kind} アカウントによる認証に成功しました。"
    passwords:
      edit:
        change_my_password: パスワードを変更する
        change_your_password: パスワードを変更
        confirm_new_password: 確認用新しいパスワード
        new_password: 新しいパスワード
      new:
        forgot_your_password: パスワードを忘れましたか？
        send_me_reset_password_instructions: パスワードの再設定方法を送信する
      no_token: このページにはアクセスできません。パスワード再設定メールのリンクからアクセスされた場合には、URL をご確認ください。
      send_instructions: パスワードの再設定について数分以内にメールでご連絡いたします。
      send_paranoid_instructions: メールアドレスが登録済みの場合、パスワード再設定用のメールが数分以内に送信されます。
      updated: パスワードが正しく変更されました。
      updated_not_active: パスワードが正しく変更されました。
    registrations:
      destroyed: アカウントを削除しました。またのご利用をお待ちしております。
      edit:
        are_you_sure: 本当によろしいですか？
        cancel_my_account: アカウント削除
        currently_waiting_confirmation_for_email: "%{email} の確認待ち"
        leave_blank_if_you_don_t_want_to_change_it: 空欄のままなら変更しません
        title: "%{resource}編集"
        unhappy: 気に入りません
        update: 更新
        we_need_your_current_password_to_confirm_your_changes: 変更を反映するには現在のパスワードを入力してください
      new:
        sign_up: アカウント登録
      signed_up: アカウント登録が完了しました。
      signed_up_but_inactive: ログインするためには、アカウントを有効化してください。
      signed_up_but_locked: アカウントがロックされているためログインできません。
      signed_up_but_unconfirmed: 本人確認用のメールを送信しました。メール内のリンクからアカウントを有効化させてください。
      update_needs_confirmation: アカウント情報を変更しました。変更されたメールアドレスの本人確認のため、本人確認用メールより確認処理をおこなってください。
      updated: アカウント情報を変更しました。
      updated_but_not_signed_in: あなたのアカウントは正常に更新されましたが、パスワードが変更されたため、再度ログインしてください。
    sessions:
      already_signed_out: 既にログアウト済みです。
      new:
        sign_in: ログイン
      signed_in: ログインしました。
      signed_out: ログアウトしました。
    shared:
      links:
        back: 戻る
        didn_t_receive_confirmation_instructions: アカウント確認のメールを受け取っていませんか？
        didn_t_receive_unlock_instructions: アカウントのロック解除方法のメールを受け取っていませんか？
        forgot_your_password: パスワードを忘れましたか？
        sign_in: ログイン
        sign_in_with_provider: "%{provider}でログイン"
        sign_up: アカウント登録
      minimum_password_length: "（%{count}字以上）"
    unlocks:
      new:
        resend_unlock_instructions: アカウントのロック解除方法を再送する
      send_instructions: アカウントのロック解除方法を数分以内にメールでご連絡します。
      send_paranoid_instructions: アカウントが見つかった場合、アカウントのロック解除方法を数分以内にメールでご連絡します。
      unlocked: アカウントをロック解除しました。
  errors:
    messages:
      already_confirmed: は既に登録済みです。ログインしてください。
      confirmation_period_expired: の期限が切れました。%{period} までに確認する必要があります。 新しくリクエストしてください。
      expired: の有効期限が切れました。新しくリクエストしてください。
      not_found: は見つかりませんでした。
      not_locked: はロックされていません。
      not_saved:
        one: エラーが発生したため %{resource} は保存されませんでした。
        other: "%{count} 件のエラーが発生したため %{resource} は保存されませんでした。"
```
ログアウト時にエラーが出ないように以下を修正
```diff_ruby:config/initializers/devise.rb
- config.sign_out_via = :delete
+ config.sign_out_via = :get
```
```erb:appviews/devise/resistrations/new.html.erb
<h3 class="headline">新規登録</h3>

<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
  <%= render "devise/shared/error_messages", resource: resource %>

  <div class="field">
    <%= f.label :email %><br />
    <%= f.email_field :email, autofocus: true, autocomplete: "email" %>
  </div>

  <div class="field">
    <%= f.label :password %>
    <% if @minimum_password_length %>
    <em>(<%= @minimum_password_length %> 文字以上)</em>
    <% end %><br />
    <%= f.password_field :password, autocomplete: "new-password" %>
  </div>

  <div class="field">
    <%= f.label :password_confirmation %><br />
    <%= f.password_field :password_confirmation, autocomplete: "new-password" %>
  </div>
  <%= f.submit "新規登録", class: "button" %>
<% end %>
```
```erb:app/views/devise/sessions/new.html.erb
<h3 class="headline">ログイン</h3>

<%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %>
  <div class="field">
    <%= f.label :email %><br />
    <%= f.email_field :email, autofocus: true, autocomplete: "email" %>
  </div>

  <div class="field">
    <%= f.label :password %><br />
    <%= f.password_field :password, autocomplete: "current-password" %>
  </div>

  <% if devise_mapping.rememberable? %>
    <div class="field">
      <%= f.check_box :remember_me %>
      <%= f.label :remember_me %>
    </div>
  <% end %>
  <%= f.submit "ログイン", class: "button" %>
<% end %>
```

# プロフィール登録機能の実装
## Profileモデルの作成
```terminal:terminal
rails g model profile user:references name purpose
// NOT NULL制約を追加する
rails db:migrate
```
```diff_ruby:app/models/user.rb
class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

+ has_one :profile, dependent: :destroy
end
```
```diff_ruby:app/models/profile.rb
class Profile < ApplicationRecord
+ has_one_attached :image
  belongs_to :user
end
```
## Profilesコントローラの作成
```terminal:terminal
rails g controller profiles
```
Profilesコントローラにアクションを追加
```diff_ruby:app/controllers/profiles_contoroller.rb
class ProfilesController < ApplicationController
-   before_action :find_fofile, only[:show, :edit, :update]
    # 追記（正しいのは以下のコード）
+   before_action :find_fofile, only: [:show, :edit, :update]

    def show
    end

    def new
        @profile = Profile.new
    end

    def edit
    end

    def create
        @profile = Profile.new(profile_params)
        if @profile.save
            redirect_to root_path, notice: "プロフィールの登録が完了しました"
        else
            render :new
        end
    end

    def update
        if @profile.update(profile_params)
            redirect_to root_path, notice: "プロフィールの更新が完了しました"
        else
            render :edit
        end
    end

    private

    def profile_params
        params.required(:profile).permit(:name, :purpose, :image)
    end

    def find_fofile
        @profile = Post.find(params[:id])
    end
end
```
ルーティングを設定
```diff_ruby:config/routes.rb
Rails.application.routes.draw do
  devise_for :users
  resources :posts
+ resources :profiles, only: [:show, :new , :edit, :create, :update]
  root "welcome#index"
end
```
## それぞれのアクションに対応するviewの実装
ここで、サイトにアクセスすると画像のようなエラーを吐かれました。
![スクリーンショット 2023-03-31 15.58.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3303899/4a270753-b81b-04ad-27fc-5a9f0ba4c259.png)
エラーメッセージでonlyという変数またはメソッドはないとあるように、onlyの記述が間違えていました。
```diff_ruby:app/controllers/profiles_contoroller.rb
- before_action :find_fofile, only[:show, :edit, :update]
+ before_action :find_fofile, only: [:show, :edit, :update]
```
```ruby:app/views/profiles/_form.html.erb
<%= form_with model: @profile, local: true do|f|%>
    <% if @profile.errors.any? %>
        <ul>
        <% @profile.errors.full_messages.each do |message| %>
            <li><%= message %></li>
        <% end %>
        </ul>
    <% end %>
    <table>
        <tr>
            <th>
                <%= f.label "ユーザー名", for: "name" %>
            </th>
            <td>
                <%= f.text_field :name, id: "name" %>
            </td>
        <tr>
        <tr>
            <th>
                <%= f.label "目的", for: "purpose" %>
            </th>
            <td>
                <%= f.text_field :purpose, id: "purpose" %>
            </td>
        <tr>
    </table>
    <%= f.file_field :image %>
    <%= f.submit class: "button" %>
<% end %>
```
```ruby:app/views/profiles/new.html.erb
<%= provide(:title, "プロフィールの作成") %>
<%= render "form" %>
```
```ruby:app/views/profiles/edit.html.erb
<%= provide(:title, "プロフィールの更新") %>
<%= render "form" %>
```
プロフィールのユーザーとカレントユーザーを紐付ける
```diff_ruby:app/controllers/profiles_contoroller
def create
    @profile = Profile.new(profile_params)
+   @profile.user = current_user
    if @profile.save
        redirect_to root_path, notice: "プロフィールの登録が完了しました"
    else
        render :new
    end
end
```
delegateメソッドを使うことで、USer側でもProfileにおけるnameメソッドなどを使えるようになります。例えば、`user.name`みたいに呼び出すことができます。
```diff_ruby:app/models/user.rb
class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

  has_one :profile, dependent: :destroy

+ delegate :name, :purpose, :image, to: :profile
end
```
```ruby:app/views/profiles/show.html.erb
<%= provide(:title, "プロフィールの詳細") %>
<div class="wrapper">
    <div class="detail-block">
        <h4 class="headline-content">プロフィール内容</h4>
        <div>
            <% if @post.user.image.attached? %>
                <%= image_tag @post.user.image%><br>
            <% end %>
        </div>
        <p class="detail-content">ユーザー名：<%= @post.user.name %></p>
        <p class="detail-content">自己紹介：<%= @post.user.purpose %></p>
    </div>
    <ul>
        <li><%= button_to "プロフィールを更新する", edit_profile_path, method: :get, class: "change-button" %></li>
    </ul>
</div>
```
```diff_ruby:app/views/welcome/index.html.erb
<%= provide(:title, "一覧") %>
<% @posts.each do|post| %>
    <div class="content-block">
+       <div class="profile-wrapper">
+           <% if post.user.image.attached? %>
+               <%= image_tag post.user.image, class: "profile-img" %>
+           <% end %>
+           <div class="profile-block">
+               ユーザー名：<%= link_to post.user.name, post.user.profile, class: "detail-content" %>
+               <p class="detail-content">自己紹介：<%= post.user.purpose %></p>
+           </div>
+       </div>
        <%= link_to "#{post.content}", post, class: "content" %>
        <div>
            <% if post.images.attached? %>
                <% post.images.each do |image| %>
                    <%= image_tag image%><br>
                <% end %>
            <% end %>
        </div>
    </div>
<% end %>
<%= paginate @posts %>
```
この段階では、まだPostとUserを紐づけていないためエラーが吐かれます。
```diff_ruby:app/views/layouts/application.html.erb
# 中略

<ul class="header-navlist">
<% if user_signed_in? %>
    <li><%= link_to "投稿する", new_post_path, class: "header-navlink" %></li>
+   <li><%= link_to "プロフィール", new_profile_path, class: "header-navlink" %></li>
    <li><%= link_to "ログアウト", destroy_user_session_path, method: :delete, class: "header-navlink" %></li>
<% else %>
    <li><%= link_to "新規登録", new_user_registration_path, class: "header-navlink" %></li>
    <li><%= link_to "ログイン", new_user_session_path, class: "header-navlink" %></li>
<% end %>
</ul>

# 以下、略
```

 # PostとUserの紐付け
```terminal:terminal
rails g migration AddUserIdToPosts 
```
```diff_ruby:文字列_add_user_id_to_posts.rb
class AddUserIdToPosts < ActiveRecord::Migration[7.0]
  def change
+   add_reference :posts, :user, foreign_key: true
  end
end
```
`rails db:migrate`を実行
```diff_ruby:app/models/post.rb
class Post < ApplicationRecord
    has_many_attached :images
+   belongs_to :user
end
```
 ```diff_ruby:app/models/user.rb
class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

  has_one :profile, dependent: :destroy
+ has_many :posts, dependent: :destroy

  delegate :name, :purpose, :image, to: :profile
end
```
```diff_ruby:app/controllers/posts_controller.rb
# 中略
def create
    @post = Post.new(post_params)
+   @post.user = current_user
    if @post.save
        redirect_to root_path, notice: "設定しました"
    else
        render :new
    end
end
# 以下、略
```
同様にして、ProfileとUserを紐づけます。

# アクセス制限とバリデーション
１）もし、プロフィールがあれば、プロフィール作成画面にアクセスしたとき編集画面へリダイレクトさせる。
```diff_ruby:app/controllers/profiles_contoroller.rb
 def new
+    redirect_to edit_profile_path(current_user.profile) if current_user.profile.present?
     @profile = Profile.new
 end
```
２）ログインしないとプロフィールの作成や更新、投稿ができないようにする。
```diff_ruby:app/controllers/profiles_contoroller.rb
+ before_action :authenticate_user!
  before_action :find_fofile, only: [:show, :edit, :update]

# 以下、略
```
```diff_ruby:app/controllers/posts_contoroller.rb
+ before_action :authenticate_user!
  before_action :find_post, only: [:show, :edit, :update, :destroy]

# 以下、略
```
3）ブログの編集・削除ボタンとプロフィールの更新ボタンは作成者にだけ表示されるようにする。
```diff_erb:app/views/posts/show.html.erb
<ul>
+   <% if @post.created_by?(current_user) %>
        <li><%= button_to "ブログを編集する", edit_post_path, method: :get, class: "change-button" %></li>
        <li><%= button_to "ブログを削除する", post_path, method: :delete, class: "change-button" %></li>
+  <% end %>
</ul>
```
```diff_erb:app/views/profiles/show.html.erb
// 中略
<ul>
+   <% if @profile.created_by?(current_user) %>
        <li><%= button_to "プロフィールを更新する", edit_profile_path, method: :get, class: "change-button" %></li>
+   <% end %>
</ul>
```
```diff_ruby:app/models/post.rb
class Post < ApplicationRecord
  has_many_attached :images
  belongs_to :user

+ def created_by?(current_user)
+   return false unless current_user
+   user == current_user
+ end
end
```
```diff_ruby:app/models/profile.rb
class Profile < ApplicationRecord
  has_one_attached :image
  belongs_to :user

+ def created_by?(current_user)
+   return false unless current_user
+   user == current_user
+ end
end
```

4）作成者でない人が投稿またはプロフィールの編集画面にアクセスしたら、トップページに強制リダイレクトさせる。
```diff_ruby:app/controller/posts_controller.rb
class PostsController < ApplicationController
    before_action :authenticate_user!
# 追記：このままだとエラーが出ます。追加する位置をfind_postの下にしてください。
+   before_action :force_redirect_unless_my_post, only: [:edit, :update, :destroy]
    before_action :find_post, only: [:show, :edit, :update, :destroy]

# 中略

    private

    def post_params
        params.require(:post).permit(:content, images: [])
    end

+   def force_redirect_unless_my_post
+       redirect_to root_path, alert:"自分の投稿ではありません" if @post.user != current_user
+   end

    def find_post
        @post = Post.find(params[:id])
    end
end
```
ここで、別のアカウントでログインして自分の投稿ではない編集画面に移動したところ画像のようなエラーを吐かれました。
![スクリーンショット 2023-04-01 21.33.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3303899/80b9cec4-b032-9346-ea5d-1f686423db53.png)
すでに紐づけたはずのuserとpostが紐づけられていないと認識されています。
→ 原因は、before_actionを書く位置が不適切で、find_postで情報を取得する前にuser情報を取得しようとしていたためでした。
```diff_ruby:app/controller/posts_controller.rb
# 修正後
class PostsController < ApplicationController
    before_action :authenticate_user!
    before_action :find_post, only: [:show, :edit, :update, :destroy]
+   before_action :force_redirect_unless_my_post, only: [:edit, :update, :destroy]

# 中略

    private

    def post_params
        params.require(:post).permit(:content, images: [])
    end

    def find_post
        @post = Post.find(params[:id])
    end

+   def force_redirect_unless_my_post
+       redirect_to root_path, alert:"自分のではプロフィールではありません" if @post.user != current_user
+   end
end
```
これでアクセスし直すと、うまくいけました。プロフィールについても同様に制限をかけます。
```diff_ruby:app/controller/profiles_controller.rb
class ProfilesController < ApplicationController
    before_action :authenticate_user!
    before_action :find_fofile, only: [:show, :edit, :update]
    before_action :force_redirect_unless_my_profile, only: [:edit, :update]

# 中略

    private

    def profile_params
        params.required(:profile).permit(:name, :purpose, :image)
    end

    def find_fofile
        @profile = Post.find(params[:id])
    end

    def force_redirect_unless_my_profile
        redirect_to root_path, alert:"自分のプロフィールではありません" if @profile.user != current_user
    end
end
```
５）プロフィールを登録せずに投稿しようとするとdelegateメソッドやattached?メソッドでエラーになります。
→ プロフィールを登録せずに投稿しようとしたら、プロフィール登録画面に強制リダイレクトさせる
```diff_ruby:app/controllers/posts_controller.rb
# 中略
def new
+   redirect_to new_profile_path, alert: "プロフィールを登録してください" unless current_user.profile.present?
    @post = Post.new
end

# 以下、略
```
6）バリデーション
入力を必須にし、文字数制限もかけています。
```diff_ruby:app/models/post.rb
class Post < ApplicationRecord
    has_many_attached :images
    belongs_to :user

+   validates :content, presence: true, length: { maximum: 500, too_long: "は500文字以内にしてください"}

    def created_by?(current_user)
        return false unless current_user
        user == current_user
    end
end
```
```diff_ruby:app/models/profile.rb
class Profile < ApplicationRecord
  belongs_to :user
  has_one_attached :image

+ validates :name, presence: true, length: { maximum: 20, too_long: "は20文字以内にしてください"}
+ validates :purpose, presence: true, length: { maximum: 50, too_long: "は50文字以内にしてください"}

  def created_by?(current_user)
    return false unless current_user
    user == current_user
  end
end
```

# ロゴとファビコン
## ロゴ
ロゴメーカーなどで用意したロゴを`app/assets/images`に置き、ヘッダーを修正します。
```diff_erb:app/layouts/applicaion.html.erb
# 中略

    <header class="header">
+     <%= link_to image_tag("logo.png") ,root_path %>
      <ul class="header-navlist">
        <% if user_signed_in? %>
            <li><%= link_to "投稿する", new_post_path, class: "header-navlink" %></li>
            <li><%= link_to "ログアウト", destroy_user_session_path, method: :delete, class: "header-navlink" %></li>
        <% else %>
            <li><%= link_to "新規登録", new_user_registration_path, class: "header-navlink" %></li>
            <li><%= link_to "ログイン", new_user_session_path, class: "header-navlink" %></li>
        <% end %>
      </ul>
    </header>

# 以下、略
```

## ファビコン
[Favicon Generator](https://realfavicongenerator.net/)でファビコンをダウンロードします（ダウンロード手順は[セイト先生のYouTube](https://youtu.be/Hbxulm8iXSA?t=8280)を参考にしてください）。zipを展開して生成したfaviconフォルダをpublicフォルダに移動してください。そして、生成したHTMLコードをheadタグに追加して、サイトにアクセスし直せばファビコンが反映されているはずです。
```diff_erb:app/layouts/applicaion.html.erb
  <head>
    # 中略

    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
    <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
    <%= stylesheet_link_tag "sanitize.css" %>

+   <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
+   <link rel="icon" type="image/png" sizes="32x32" href="/facicon/favicon-32x32.png">
+   <link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png">
+   <meta name="msapplication-TileColor" content="#da532c">
+   <meta name="theme-color" content="#ffffff">
  </head>

# 以下、略
```
なお、追加したコードの2, 3行目はfaviconフォルダのパスを追記しています。

# 終わりに
うるぞーさんの動画を参考にSassとパーシャルを使ってみましたが、コードがわかりやすくなるメリットは大きいと感じました。また、２つ目のアプリということで前回に比べてエラーへの対応をスムーズに行うことができました。

実際の開発では、minitestやRspecを使ってテストを行ないながら開発を進めていくことが一般的なようです。テストの必要性やメリット、TDD（テスト駆動開発）、Guardによるテストの自動化など理解できていない部分が多いのでテストに関することを今後記事にまとめていけたらと思います。

これからもRailsアプリの作成を通して、スキルを身につけていきたいと思います。最後まで読んでくださりありがとうございました！

## ソースコード
https://github.com/shoya15/apps_public

## 質問
「アクセス制限とバリデーション」の３つ目の項目でcreated_by?メソッドをまったく同じ内容なのにPostモデルProfileモデルの両方に書いています。この部分は、リファクタリングできますか？コメントで教えていただけると幸いです。

# 参考
・[Ruby on Railsチュートリアル](https://railstutorial.jp/chapters/rails_flavored_ruby?version=5.1#sec-built_in_helpers)
・[セイト先生のYouTube](https://www.youtube.com/watch?v=Hbxulm8iXSA&t=3613shttps://www.youtube.com/watch?v=Hbxulm8iXSA&t=3613s)
・[うるぞーさんのYouTube](https://www.youtube.com/watch?v=bH3UJie0els&t=762s)
・[【Rails】 日本語化のやり方](https://qiita.com/mmaumtjgj/items/93ab3ef8cbcf9591fc30)
・[【複数行にも対応】長過ぎる文字列を省略して末尾を三点リーダー（…）にする方法いろいろ](https://blog.recruit.co.jp/rmp/front-end/tips-ellipsis/)
・[【Rails 5.2】 Active Storageの使い方](https://qiita.com/hmmrjn/items/7cc5e5348755c517458a)
・[Rails 後からNot null 制約を付与する](https://qiita.com/pank24ever/items/b411fe0a238aadc03fb0)
・[【Rails】deviseの導入手順](https://qiita.com/taki_21/items/cc8bc157d5742294ec98)
・[【Rails】メソッドを委譲することができるdelegateメソッドについて初心者向けに解説](https://techtechmedia.com/how-to-use-delegate-rails/)
・[【Rails】後からカラムを追加して外部キーを張る際に、add_referenceを使う場合の注意点。](https://qiita.com/kurawo___D/items/e3694f7a870a1cc4738e)