1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Railsでブログアプリを作成(MVP)

Last updated at Posted at 2023-04-02

はじめに

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

ブログアプリの雛形の作成

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
rails g controller welcome index

ルートパスを設定

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)) %>に変更して、カスタムヘルパーを作成

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の適用
web fontsの適用
・bodタグにidを当ててそれぞれのページごとにレイアウトを調整できるようにする

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を読み込む必要があります。

application.scss
@import "posts";

Headerの実装

bodyタグに以下を追加

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
rails g model post content:text

NOT NULL制約を追加して、rails db:migrate

db/migrate/文字列_create_post.rb
t.text :content, null: false

Postsコントローラの作成

terminal
rails g controller posts

Postsコントローラにアクションを追加

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

ルーティングを設定

config/routes.rb
resources :posts, only: [:show, :new, :edit, :create, :update, :destroy]

フラッシュメッセージが表示されるようbodyタグに以下を追加

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の実装

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
@ postsがnillになっているみたいです。
→ Welcomeコントローラのindexアクションに@ postsを定義してなかったことが原因でした。

app/controllers/welcome_controller.rb
class WelcomeController < ApplicationController
  def index
    @posts = Post.all.order(created_at: :desc)
  end
end

パーシャルを使って部分テンプレート化(すごい...!)

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 %>
app/views/posts/new.html.erb
<%= provide(:title, "作成") %>
<%= render "form" %>
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: 内容
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点リーダーにするようにしました。

welcome.scss
#welcome-index{
    .content-block{
+       overflow: hidden;
+       text-overflow: ellipsis;
+       white-space: nowrap;
    }
}

・ページネーションの実装

Gemfileにgem "kaminari"を追加してbundle install

app/controllers/welcome_controller.rb
- @posts = Post.all.order(created_at: :desc)
# 10ページ単位でページネーション
+ @posts = Post.all.page(params[:page]).per(10).order(created_at: :desc)
app/views/welcome/index.html.erb
<%# 一番下 %>
+ <%= paginate @posts %>
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
// ページネーションにbootstrapを適用
rails g kaminari:views bootstrap4

config/localesにkaminari.ja.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
rails active_storage:install
rails db:migrate

複数の添付ファイル用の設定

app/models/post.rb
class Post < ApplicationRecord
+     has_many_attached :images
end
app/controllers/posts_contoroller.rb
def post_params
-    params.require(:post).permit(:content)
+    params.require(:post).permit(:content, images: [])
end
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 %>
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 %>
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
// 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タグを変更

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を作成し、以下を追加

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} は保存されませんでした。"

ログアウト時にエラーが出ないように以下を修正

config/initializers/devise.rb
- config.sign_out_via = :delete
+ config.sign_out_via = :get
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 %>
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
rails g model profile user:references name purpose
// NOT NULL制約を追加する
rails db:migrate
app/models/user.rb
class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

+ has_one :profile, dependent: :destroy
end
app/models/profile.rb
class Profile < ApplicationRecord
+ has_one_attached :image
  belongs_to :user
end

Profilesコントローラの作成

terminal
rails g controller profiles

Profilesコントローラにアクションを追加

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

ルーティングを設定

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
エラーメッセージでonlyという変数またはメソッドはないとあるように、onlyの記述が間違えていました。

app/controllers/profiles_contoroller.rb
- before_action :find_fofile, only[:show, :edit, :update]
+ before_action :find_fofile, only: [:show, :edit, :update]
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 %>
app/views/profiles/new.html.erb
<%= provide(:title, "プロフィールの作成") %>
<%= render "form" %>
app/views/profiles/edit.html.erb
<%= provide(:title, "プロフィールの更新") %>
<%= render "form" %>

プロフィールのユーザーとカレントユーザーを紐付ける

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みたいに呼び出すことができます。

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
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>
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を紐づけていないためエラーが吐かれます。

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
rails g migration AddUserIdToPosts 
文字列_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を実行

app/models/post.rb
class Post < ApplicationRecord
    has_many_attached :images
+   belongs_to :user
end
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
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を紐づけます。

アクセス制限とバリデーション

1)もし、プロフィールがあれば、プロフィール作成画面にアクセスしたとき編集画面へリダイレクトさせる。

app/controllers/profiles_contoroller.rb
 def new
+    redirect_to edit_profile_path(current_user.profile) if current_user.profile.present?
     @profile = Profile.new
 end

2)ログインしないとプロフィールの作成や更新、投稿ができないようにする。

app/controllers/profiles_contoroller.rb
+ before_action :authenticate_user!
  before_action :find_fofile, only: [:show, :edit, :update]

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

# 以下、略

3)ブログの編集・削除ボタンとプロフィールの更新ボタンは作成者にだけ表示されるようにする。

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>
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>
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
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)作成者でない人が投稿またはプロフィールの編集画面にアクセスしたら、トップページに強制リダイレクトさせる。

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
すでに紐づけたはずのuserとpostが紐づけられていないと認識されています。
→ 原因は、before_actionを書く位置が不適切で、find_postで情報を取得する前にuser情報を取得しようとしていたためでした。

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

これでアクセスし直すと、うまくいけました。プロフィールについても同様に制限をかけます。

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

5)プロフィールを登録せずに投稿しようとするとdelegateメソッドやattached?メソッドでエラーになります。
→ プロフィールを登録せずに投稿しようとしたら、プロフィール登録画面に強制リダイレクトさせる

app/controllers/posts_controller.rb
# 中略
def new
+   redirect_to new_profile_path, alert: "プロフィールを登録してください" unless current_user.profile.present?
    @post = Post.new
end

# 以下、略

6)バリデーション
入力を必須にし、文字数制限もかけています。

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
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に置き、ヘッダーを修正します。

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でファビコンをダウンロードします(ダウンロード手順はセイト先生のYouTubeを参考にしてください)。zipを展開して生成したfaviconフォルダをpublicフォルダに移動してください。そして、生成したHTMLコードをheadタグに追加して、サイトにアクセスし直せばファビコンが反映されているはずです。

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とパーシャルを使ってみましたが、コードがわかりやすくなるメリットは大きいと感じました。また、2つ目のアプリということで前回に比べてエラーへの対応をスムーズに行うことができました。

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

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

ソースコード

質問

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

参考

Ruby on Railsチュートリアル
セイト先生のYouTube
うるぞーさんのYouTube
【Rails】 日本語化のやり方
【複数行にも対応】長過ぎる文字列を省略して末尾を三点リーダー(…)にする方法いろいろ
【Rails 5.2】 Active Storageの使い方
Rails 後からNot null 制約を付与する
【Rails】deviseの導入手順
【Rails】メソッドを委譲することができるdelegateメソッドについて初心者向けに解説
【Rails】後からカラムを追加して外部キーを張る際に、add_referenceを使う場合の注意点。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?