はじめに
Webエンジニアを目指して、RubyやRailsをいじってます。
今回は、Rails でブログアプリを作っていきたいと思います。ご指摘がありましたらよろしくお願いします。
ブログアプリの雛形の作成
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
事前準備とヘッダーの作成
トップページの作成
rails g controller welcome index
ルートパスを設定
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)) %>
に変更して、カスタムヘルパーを作成
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を当ててそれぞれのページごとにレイアウトを調整できるようにする
<!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を読み込む必要があります。
@import "posts";
Headerの実装
bodyタグに以下を追加
<header class="header">
<%= link_to "ロゴ", root_path %>
<ul class="header-navlist">
<%# この時点では、まだ設定していないのでアクセスしてもエラーになります %>
<li><%= link_to "投稿する", post_new_path, class: "header-navlink" %></li>
</ul>
</header>
ブログの基本機能の実装
Postモデルの作成
rails g model post content:text
NOT NULL制約を追加して、rails db:migrate
t.text :content, null: false
Postsコントローラの作成
rails g controller posts
Postsコントローラにアクションを追加
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
ルーティングを設定
resources :posts, only: [:show, :new, :edit, :create, :update, :destroy]
フラッシュメッセージが表示されるようbodyタグに以下を追加
<% if flash[:notice].present? %>
<p class="notice"><%= notice %></p>
<% end %>
<% if flash[:alert].present? %>
<p class="alert"><%= alert %></p>
<% end %>
それぞれのアクションに対応するviewの実装
<%= provide(:title, "一覧") %>
<% @posts.each do|post| %>
<div class="content-block">
<%= link_to "#{post.content}", post, class: "content" %>
</div>
<% end %>
ここで、画像のようなエラーを吐かれました。
@ postsがnillになっているみたいです。
→ Welcomeコントローラのindexアクションに@ postsを定義してなかったことが原因でした。
class WelcomeController < ApplicationController
def index
@posts = Post.all.order(created_at: :desc)
end
end
パーシャルを使って部分テンプレート化(すごい...!)
<%= 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 %>
<%= provide(:title, "作成") %>
<%= render "form" %>
<%= provide(:title, "編集") %>
<%= render "form" %>
ボタンの文字を日本語化します。
・gem "rails-i18n"
を追加してbundle install
・config/application.rbにconfig.i18n.default_locale = :ja
を追加
・config/localesにja.ymlを作成し以下を追加して、railsを再起動
ja:
activerecord:
attributes:
post:
content: 内容
<%= 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-index{
.content-block{
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
}
・ページネーションの実装
Gemfileにgem "kaminari"
を追加してbundle install
- @posts = Post.all.order(created_at: :desc)
# 10ページ単位でページネーション
+ @posts = Post.all.page(params[:page]).per(10).order(created_at: :desc)
<%# 一番下 %>
+ <%= paginate @posts %>
<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>
// ページネーションにbootstrapを適用
rails g kaminari:views bootstrap4
config/localesにkaminari.ja.ymlを作成して以下を追加
ja:
views:
pagination:
first: "« 最初"
last: "最後 »"
previous: "‹ 前"
next: "次 ›"
truncate: "…"
railsを再起動して、適当に10個より多い数登録してみてください。ページ下部にページネーションが表示されればOKです。
画像投稿できるようにする
Active Storageのインストール
rails active_storage:install
rails db:migrate
複数の添付ファイル用の設定
class Post < ApplicationRecord
+ has_many_attached :images
end
def post_params
- params.require(:post).permit(:content)
+ params.require(:post).permit(:content, images: [])
end
<%= 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 %>
<%= 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 %>
<%= 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>
ログイン機能の実装
※ メール機能の設定は割愛します。
// 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タグを変更
<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を作成し、以下を追加
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.sign_out_via = :delete
+ config.sign_out_via = :get
<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 %>
<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モデルの作成
rails g model profile user:references name purpose
// NOT NULL制約を追加する
rails db:migrate
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
+ has_one :profile, dependent: :destroy
end
class Profile < ApplicationRecord
+ has_one_attached :image
belongs_to :user
end
Profilesコントローラの作成
rails g controller profiles
Profilesコントローラにアクションを追加
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
ルーティングを設定
Rails.application.routes.draw do
devise_for :users
resources :posts
+ resources :profiles, only: [:show, :new , :edit, :create, :update]
root "welcome#index"
end
それぞれのアクションに対応するviewの実装
ここで、サイトにアクセスすると画像のようなエラーを吐かれました。
エラーメッセージでonlyという変数またはメソッドはないとあるように、onlyの記述が間違えていました。
- before_action :find_fofile, only[:show, :edit, :update]
+ before_action :find_fofile, only: [:show, :edit, :update]
<%= 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 %>
<%= provide(:title, "プロフィールの作成") %>
<%= render "form" %>
<%= provide(:title, "プロフィールの更新") %>
<%= render "form" %>
プロフィールのユーザーとカレントユーザーを紐付ける
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
みたいに呼び出すことができます。
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
has_one :profile, dependent: :destroy
+ delegate :name, :purpose, :image, to: :profile
end
<%= 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>
<%= 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を紐づけていないためエラーが吐かれます。
# 中略
<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の紐付け
rails g migration AddUserIdToPosts
class AddUserIdToPosts < ActiveRecord::Migration[7.0]
def change
+ add_reference :posts, :user, foreign_key: true
end
end
rails db:migrate
を実行
class Post < ApplicationRecord
has_many_attached :images
+ belongs_to :user
end
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
# 中略
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)もし、プロフィールがあれば、プロフィール作成画面にアクセスしたとき編集画面へリダイレクトさせる。
def new
+ redirect_to edit_profile_path(current_user.profile) if current_user.profile.present?
@profile = Profile.new
end
2)ログインしないとプロフィールの作成や更新、投稿ができないようにする。
+ before_action :authenticate_user!
before_action :find_fofile, only: [:show, :edit, :update]
# 以下、略
+ before_action :authenticate_user!
before_action :find_post, only: [:show, :edit, :update, :destroy]
# 以下、略
3)ブログの編集・削除ボタンとプロフィールの更新ボタンは作成者にだけ表示されるようにする。
<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>
// 中略
<ul>
+ <% if @profile.created_by?(current_user) %>
<li><%= button_to "プロフィールを更新する", edit_profile_path, method: :get, class: "change-button" %></li>
+ <% end %>
</ul>
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
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)作成者でない人が投稿またはプロフィールの編集画面にアクセスしたら、トップページに強制リダイレクトさせる。
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
ここで、別のアカウントでログインして自分の投稿ではない編集画面に移動したところ画像のようなエラーを吐かれました。
すでに紐づけたはずのuserとpostが紐づけられていないと認識されています。
→ 原因は、before_actionを書く位置が不適切で、find_postで情報を取得する前にuser情報を取得しようとしていたためでした。
# 修正後
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
これでアクセスし直すと、うまくいけました。プロフィールについても同様に制限をかけます。
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?メソッドでエラーになります。
→ プロフィールを登録せずに投稿しようとしたら、プロフィール登録画面に強制リダイレクトさせる
# 中略
def new
+ redirect_to new_profile_path, alert: "プロフィールを登録してください" unless current_user.profile.present?
@post = Post.new
end
# 以下、略
6)バリデーション
入力を必須にし、文字数制限もかけています。
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
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
に置き、ヘッダーを修正します。
# 中略
<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タグに追加して、サイトにアクセスし直せばファビコンが反映されているはずです。
<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を使う場合の注意点。