今まで色々な記事を見て学習してきた知識を一旦整理するため、
つぶやきアプリを作る流れを記事に残していきます。
(※長文です)
以下の機能をもつアプリを作ります。
・新規登録・ログイン機能
・投稿・保存・編集・更新・削除機能
・詳細一覧ページ
・コメント機能
・いいね機能
・検索機能
ER図作成
ディレクトリ作成
cdコマンドでProjectディレクトリまで移動した上で下記実行。
$ mkdir 作成したいディレクトリ名
Gemfileの生成
cdコマンドで作成したディレクトリまで移動した上で、下記実行。
$ bundle init
Writing new Gemfile to /path/to/ディレクトリ名/Gemfile
と返ってきたら、Gemfile生成成功。
Gemfile編集
$ vim Gemfileで編集(railsのコメントを外すだけです)
# frozen_string_literal: true
source "https://rubygems.org"
git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
gem "rails"
Railsのインストール
$ bundle install --path vendor/bundle
この時に--path vendor/bundleを忘れずにつけること。
一度オプションをつけてbundle installしたら、次回以降はオプションを付けなくてもvendor/bundle以下に格納されるようになります。
Railsアプリを生成
これで下準備が整ったので、ここからはアプリを作っていきます。
$ bundle exec rails new . -B -d mysql --skip-turbolinks --skip-test
Railsのインストール実行時にGemfileを上書きしていいか聞かれるので、yesにして続行します。
githubで新しいレポジトリを作成
git init → git add → git commit → git remote add origin → git pushの順に実行
$ git init
$ git add .
$ git commit -m "first commit"
$ git remote add origin https://github.com/githubアカウント名/アプリ名.git
$ git push -u origin master
これでWebアプリの土台が出来上がりました。
次はデータベースの作成です。
データベースの作成
まずはmysql@5.7を立ち上げる。その上で下記実行。
$ bundle exec rails db:create
$ bundle exec rails db:migrate
このとき、mysql2に関するエラーが出ましたが下記記事に沿って
$ bundle config --local build.mysql2 "--with-cppflags=-I/usr/local/opt/openssl@1.1/include"
$ bundle config --local build.mysql2 "--with-ldflags=-L/usr/local/opt/openssl@1.1/lib"
$ bundle install
を実行したところ解決しました。
Sequel Proを使って、ちゃんとデータベースが作成されたか確認。
サーバーを立ち上げる
$ bundle exec rails s
Webpackerがまだインストールされていないですといった内容のエラーが出てくるので、インストールする。
$ bundle exec rails webpacker:install
その上で再度サーバーを立ち上げる
$ bundle exec rails s
成功!
下記の順で実行
$ git add .
$ git commit -m "create_database"
$ git push origin master
Railsで使うライブラリを導入
gem 'pry-rails'
$ bundle install
Gemファイルを追加、修正した場合はサーバーを再起動する。
その時に設定したデータが読み込まれるため。
$ bundle exec rails s
はじめにつぶやき機能を作っていきます。
まずはモデル作成
$ bundle exec rails g model post
マイグレーションファイルを編集
class CreatePost < ActiveRecord::Migration[6.0]
def change
create_table :posts do |t|
t.string :name
t.string :text
t.timestamps
end
end
end
マイグレーションを実行
$ bundle exec rails db:migrate
ルーティング設定
7つのアクションのルーティングは、resourcesメソッドを使用し一度に設定する。
resourcesとは、7つのアクションへのルーティングを自動生成するメソッドのこと。
resourcesの引数に、:posts というシンボルを指定すると対応するルーティングが生成される。
resourcesにオプションとしてonlyを加えると、指定したアクションのみのルーティングを設定する。
Rails.application.routes.draw do
resources :posts, only: :index
end
コントローラーとビューの作成
$ bundle exec rails g controller posts index #postsコントローラーindexアクションとそれに対応するビューを作成
情報を全て取得できるよう、allメソッドを記述
def index
@posts = Post.all
end
ビューファイルに下記を記述
<div class="contents row">
<% @posts.each do |post| %>
<div class="content_post" >
<p><%= post.text %></p>
<span class="name">
<%= post.name %>
</span>
</div>
<% end %>
</div>
レイアウトテンプレート
ヘッダーやフッターなど、共通部分をまとめた部分をレイアウトテンプレートという。
Railsではapplication.html.erbファイル。
app/views/layoutsディレクトリ配下にある。
yieldメソッド
レイアウトテンプレートに、各テンプレートファイルを展開するためのメソッド。
yieldを記述することで、コントローラーに対応するビューがyield記述部分へ展開されるようになる。
# 途中省略
<body>
<header class="header">
<div class="header__bar row">
<h1 class="grid-6"><a href="/">Tsubuyaki</a></h1>
<div class="user_nav grid-6">
<a class="post" href="/posts/new">投稿する</a>
</div>
</div>
</header>
<%= yield %>
<footer>
<p>
Copyright Tsubuyaki 2021.
</p>
</footer>
</body>
</html>
CSSファイルの入力
app/assets/stylesheets/の中にstyle.cssというファイルを作成&以下を記入
.btn,
header.header div.header__bar.row div.grid-6 a.post {
padding: 8px 20px;
font-size: 14px;
border: 2px solid #57C3E9;
color: #57C3E9;
font-weight: bold;
text-align: center;
border-radius: 3px;
display: inline-block;
}
.btn:hover,
header.header div.header__bar.row div.grid-6 a.post:hover {
border-color: #9bdbf2;
color: #9bdbf2;
}
footer {
margin: 30px auto;
padding: 10px;
color: #D8D8D8;
text-align: center;
}
header {
margin: 30px auto;
padding: 10px;
color: #D8D8D8;
text-align: center;
}
.content_post {
text-align: center;
}
コンソール上でデータを入力するため、bundle exec rails cを実行
[1] pry(main)> Post.create(name: "asami", text: "こんにちは。")
ルートパスの設定
Rails.application.routes.draw do
root to: 'posts#index'
get 'posts/index'
resources :posts
end
一度サーバーを立ち上げて
http://localhost:3000 にアクセスし、表示を確認。
次に投稿ページを作っていく。
まずは投稿画面に遷移するページをnewアクションを使って作る。
ルーティング設定
Rails.application.routes.draw do
root to: 'posts#index'
resources :posts, only: [:index, :new, :create] #のちにクリエイトアクションも使うので記入
end
コントローラー設定
class PostsController < ApplicationController
def index
@posts = Post.all
end
def new
@post = Post.new
end
end
app/views/postsにnew.html.erbというビューファイルを作成
<div class="contents_form">
<div class="container_box">
<h3>投稿する</h3>
<%= form_with(model: @post, local: true) do |form| %>
<%= form.text_field :name, placeholder: "ニックネーム", class: 'container'%>
<%= form.text_area :text, placeholder: "text", rows: "10", class: 'container'%>
<%= form.submit "つぶやく", class: 'container'%>
<% end %>
</div>
</div>
createアクションでデータを保存
フォームで送られてきたデータを元に、レコードを保存。
プライベートメソッド
クラス外から呼び出すことのできないメソッドのことで
privateと記述した以下のコードがプライベートメソッドになります。
class PostsController < ApplicationController
def index
@posts = Post.all
end
def new
@post = Post.new
end
def create
Post.create(post_params)
end
private
def post_params
params.require(:post).permit(:name, :text)
end
end
投稿完了画面作成のため
app/views/postsにcreate.html.erbというビューファイルを作成
<div class="contents">
<div class="success">
<h3>投稿が完了しました。</h3>
<a class="btn" href="/">投稿一覧へ戻る</a>
</div>
</div>
空のツイート投稿ができないよう、バリデーションの設定をする。
バリデーション
データを登録する際に、一定の制約をかけること。
空のデーターを登録できなくしたり、文字数制限をかけたりすることができる。
validates :カラム名, バリデーションの種類
class Post < ApplicationRecord
validates :text, presence: true
end
これにより空のつぶやきができなくなる。
cssファイルを整える。
.btn,
.post {
padding: 8px 20px;
font-size: 14px;
border: 2px solid #57C3E9;
color: #57C3E9;
font-weight: bold;
text-align: center;
border-radius: 3px;
display: inline-block;
}
.btn:hover,
.post:hover {
border-color: #9bdbf2;
color: #9bdbf2;
}
footer {
margin: 30px auto;
padding: 10px;
color: #D8D8D8;
text-align: center;
}
header {
margin: 30px auto;
padding: 10px;
color: #D8D8D8;
text-align: center;
}
.content_post {
text-align: center;
}
.container_box {
text-align: center;
}
.container {
margin: auto;
display: block;
}
.contents {
text-align: center;
}
新規投稿ができて、一覧表示に遷移・表示される状態になっていればOK。
次は画像投稿機能の実装に入る。
画像のアップロードには、画像の保存・表示・サイズの調整が必要。
そこでRailsのActive Storageという機能を使っていく。
そのためにいくつかの機能をインストールしていく。
画像加工のために必要なImageMagickという画像変換ツールと、
それをRailsから使うためのGemをインストールする。
ImageMagickはGemではなく、ソフトウェアであるためHomebrewからインストールする。
GemではないImageMagickをRubyやRailsで扱うには、MiniMagickというGemが必要。
MiniMagickによって、ImageMagickの機能がRailsで使用できるようになるが、
画像サイズの変更にはもう1つImageProcessingというGemを追加する必要もある。
$ brew install imagemagick #imagemagickをインストール
Gemをインストール
gem 'mini_magick'
gem 'image_processing', '~> 1.2'
bundle install
続いてActive Storageのインストール
$ bundle exec rails active_storage:install
マイグレート。
$ bundle exec rails db:migrate
これでactive_storageのテーブルが追加された。
一応Sequel Proでテーブル名が追加されているか、確認してみよう。
次に、Active Storageテーブルに画像を保存するための実装を行う。
【手順】
①Active StorageテーブルとPostsテーブルのアソシエーションを定義
②posts_controller.rbにて、imageカラムの保存を許可
has_one_attachedメソッド
各レコードとファイルを1対1の関係で紐づけるメソッド。
has_one_attachedメソッドを記述したモデルの各レコードは、それぞれ1つのファイルを添付できる。
class モデル < ApplicationRecord
has_one_attached :ファイル名
end
では、Postモデルに記述していく。
class Post < ApplicationRecord
has_one_attached :image
end
ストロングパラメーターにimageを保存できるようにするため、追記。
private
def post_params
params.require(:post).permit(:name,:text,:image)
end
次は保存した画像を表示していく。
image_tagメソッド
ヘルパーメソッドで複雑なパスを指定しなくても、
モデルから画像ファイルを呼び出して引数に記述するだけで、画像を表示するimg要素を生成できる。
attached?メソッド
レコードにファイルが添付されているかどうかで、trueかfalseを返すメソッド。
モデル.ファイル名.attached?
variantメソッド
Active Storageを導入している場合に使用可能なメソッド。
ファイルの表示サイズを指定できる。
モデル.ファイル名.variant(resize: '幅x高さ')
ビューファイルを変更①
<div class="contents row">
<% @posts.each do |post| %>
<div class="content_post" >
<p><%= post.text %></p>
<p><%= image_tag post.image.variant(resize: '500x500'), class: 'post-image' if post.image.attached?%></p>
<span class="name">
<%= post.name %>
</span>
</div>
<% end %>
</div>
ビューファイルを変更②
<div class="contents_form">
<div class="container_box">
<h3>投稿する</h3>
<%= form_with(model: @post, local: true) do |form| %>
<%= form.text_field :name, placeholder: "ニックネーム", class: 'container'%>
<%= form.text_area :text, placeholder: "text", rows: "10", class: 'container'%>
<%= form.file_field :image %>
<%= form.submit "つぶやく", class: 'container'%>
<% end %>
</div>
</div>
モデルファイルを変更
class Post < ApplicationRecord
has_one_attached :image
validates :text, presence: true, unless: :was_attached?
def was_attached?
self.image.attached?
end
end
validatesのunlessオプションにメソッド名を指定することで、
「メソッドの返り値がfalseならばバリデーションによる検証を行う」という条件を作っている。
指定されたwas_attached?メソッドは、self.image.attached?という記述により、
画像があればtrue、なければfalseを返す。
これにより、画像が存在しなければテキストが必要となり、画像があればテキストは不要になった。
※画像は拡張子が「.png」または「.jpeg」の画像を投稿すること。
挙動を確認して、画像投稿がうまくできればOK。
次に削除機能を作っていく。
つぶやき削除を行うにはdestroyアクションを使う。
ルーティング設定
Rails.application.routes.draw do
root to: 'posts#index'
resources :posts, only: [:index, :new, :create, :destroy]
end
ビューにリンクを追加
次にlink_toメソッドを使用して、削除ボタンを追加する。
/posts/《postのid》というパスにすることで、削除するツイートを区別でき、パラメーターとして受け取れるようになる。
HTTPメソッドは、DELETEを指定する。
<%= link_to '削除', post_path(post.id), method: :delete %> #追加する
コントローラーにdestroyアクション追加
class PostsController < ApplicationController
def index
@posts = Post.all
end
def new
@post = Post.new
end
def create
Post.create(post_params)
end
def destroy #destroyアクション追加
post = Post.find(params[:id])
post.destroy
end
private
def post_params
params.require(:post).permit(:name,:text,:image)
end
end
パラメータとして受け取ったparams[:id]をもとに、
ツイートをfindメソッドで取得しdestroyメソッドで削除する、という流れ。
destroyビューの作成
<div class="contents row">
<div class="success">
<h3>削除が完了しました。</h3>
<a class="btn" href="/">投稿一覧へ戻る</a>
</div>
</div>
記事の削除ができるかどうか挙動を確認し、できたらOK。
次に、ツイートを編集していく。
まずはeditアクションを使って、ツイート編集ページへ遷移する処理を実装していく。
ルーティング設定
Rails.application.routes.draw do
https://guides.rubyonrails.org/routing.html
root to: 'posts#index'
resources :posts, only: [:index, :new, :create, :destroy, :edit]
end
editアクションへのルーティングができた。
ビューに編集ボタンと編集ページへのリンクを設定する。
<div class="contents row">
<% @posts.each do |post| %>
<div class="content_post" >
<p><%= post.text %></p>
<p><%= image_tag post.image.variant(resize: '500x500'), class: 'post-image' if post.image.attached?%></p>
<span class="name">
<%= post.name %>
</span>
<%= link_to '編集', edit_post_path(post.id), method: :get %>
<%= link_to '削除', "/posts/#{post.id}", method: :delete %>
</div>
<% end %>
</div>
コントローラーにeditアクション追加
新規投稿時と異なる点は、すでに存在しているレコードを選択して中身を上書きする点。
編集したいレコードを@postに代入し、ビューに受け渡すことで編集画面で利用できるようにする。
class PostsController < ApplicationController
def index
@posts = Post.all
end
def new
@post = Post.new
end
def create
Post.create(post_params)
end
def destroy
post = Post.find(params[:id])
post.destroy
end
def edit
@post = Post.find(params[:id]) #編集したいつぶやきを取得して@postへ代入
end
private
def post_params
params.require(:post).permit(:name,:text,:image)
end
end
editビューの作成
app/views/postsディレクトリの中にedit.html.erbというビューファイルを作成する。
<div class="contents_form">
<div class="container_box">
<h3>編集する</h3>
<%= form_with(model: @post, local: true) do |form| %>
<%= form.text_field :name, placeholder: "ニックネーム", class: 'container'%>
<%= form.text_area :text, placeholder: "text", rows: "10", class: 'container'%>
<%= form.file_field :image %>
<%= form.submit "つぶやく", class: 'container'%>
<% end %>
</div>
</div>
つぶやき更新機能の実装
ルーティング設定
つぶやきを更新する際には、/posts/《編集するツイートのid》にPATCHメソッドでアクセスする。
postsコントローラーのupdateアクションが実行されるようにする。
Rails.application.routes.draw do
root to: 'posts#index'
resources :posts, only: [:index, :new, :create, :destroy,:edit,:update]
end
コントローラーにupdateアクション追加
class PostsController < ApplicationController
def index
@posts = Post.all
end
def new
@post = Post.new
end
def create
Post.create(post_params)
end
def destroy
post = Post.find(params[:id])
post.destroy
end
def edit
@post = Post.find(params[:id])
end
def update
post = Post.find(params[:id])
post.update(post_params)
end
private
def post_params
params.require(:post).permit(:name,:text,:image)
end
end
app/views/postsディレクトリの中にupdate.html.erbというビューファイルを作成。
<div class="contents row">
<div class="success">
<h3>更新が完了しました。</h3>
<a class="btn" href="/">投稿一覧へ戻る</a>
</div>
</div>
挙動を確認し、記事の編集&更新ができればOK。
次はつぶやき詳細ページを作っていく。
showアクションのルーティング設定
Rails.application.routes.draw do
root to: 'posts#index'
resources :posts, only: [:index, :new, :create, :destroy, :edit, :update, :show]
end
7つのアクションが揃ったため、onlyオプションは必要なくなる。↓
Rails.application.routes.draw do
root to: 'posts#index'
resources :posts
end
ルーティング設定完了。
次はトップページのビューを編集していく。
<div class="contents row">
<% @posts.each do |post| %>
<div class="content_post" >
<p><%= post.text %></p>
<p><%= image_tag post.image.variant(resize: '500x500'), class: 'post-image' if post.image.attached?%></p>
<span class="name">
<%= post.name %>
</span>
<%= link_to '詳細', post_path(post.id), method: :get %>
<%= link_to '編集', edit_post_path(post.id), method: :get %>
<%= link_to '削除', "/posts/#{post.id}", method: :delete %>
</div>
<% end %>
</div>
コントローラーにshowアクション追加
class PostsController < ApplicationController
def index
@posts = Post.all
end
def new
@post = Post.new
end
def create
Post.create(post_params)
end
def destroy
post = Post.find(params[:id])
post.destroy
end
def edit
@post = Post.find(params[:id])
end
def update
post = Post.find(params[:id])
post.update(post_params)
end
def show
@post = Post.find(params[:id])
end
private
def post_params
params.require(:post).permit(:name,:text,:image)
end
end
show(詳細ページ)ビューの作成
<div class="contents row">
<div class="content_post" >
<p><%= @post.text %></p>
<p><%= image_tag @post.image.variant(resize: '500x500'), class: 'post-image' if @post.image.attached?%></p>
<span class="name">
<%= @post.name %>
</span>
<%= link_to '編集', edit_post_path(@post.id), method: :get %>
<%= link_to '削除', "/posts/#{@post.id}", method: :delete %>
</div>
</div>
詳細ボタンを押して詳細ページへ遷移できれば、機能としては完成。
しかしコントローラーの記述を見てみると、同じ記述が繰り返し使われている。
同じ処理を一まとめにするため、リファクタリングをしていこう。
before_action
コントローラで定義されたアクションが実行される前に、共通の処理を行うことができる。
class コントローラ名 < ApplicationController
before_action :処理させたいメソッド名
editアクションとshowアクションの記述が同じであるため、
set_postというアクションにまとめて、before_actionを設定。
class PostsController < ApplicationController
before_action :set_post, only: [:edit, :show]
def index
@posts = Post.all
end
def new
@post = Post.new
end
def create
Post.create(post_params)
end
def destroy
post = Post.find(params[:id])
post.destroy
end
def edit
end
def update
post = Post.find(params[:id])
post.update(post_params)
end
def show
end
private
def post_params
params.require(:post).permit(:name, :image, :text)
end
def set_post
@post = Post.find(params[:id])
end
end
次に新規登録、ログイン、ログアウトなどを実装するため、ユーザー管理機能を追加していく。
deviceの利用
gem 'devise'
$ bundle install
Gemを導入した際にはサーバーの再起動が必要。
$ bundle exec rails s
追加したdeviseの「設定関連に使用するファイル」を自動で生成するコマンド
$ bundle exec rails g devise:install
rails g deviseコマンド
モデルとマイグレーションの生成やルーティングの設定などをまとめて処理してくれる。
ルーティングにはdeviseに関連するパスが追加される。
# deviseコマンドでUserモデルを作成
$ bundle exec rails g devise user
ルーティングを確認
Rails.application.routes.draw do
devise_for :users
root to: 'posts#index'
resources :posts
end
マイグレーションファイルを確認
class DeviseCreateUser < ActiveRecord::Migration[6.0]
def change
create_table :users do |t|
## Database authenticatable
t.string :email, null: false, default: ""
t.string :encrypted_password, null: false, default: ""
## Recoverable
t.string :reset_password_token
t.datetime :reset_password_sent_at
## Rememberable
t.datetime :remember_created_at
# 省略
t.timestamps null: false
end
add_index :users, :email, unique: true
add_index :users, :reset_password_token, unique: true
# add_index :users, :confirmation_token, unique: true
# add_index :users, :unlock_token, unique: true
end
end
Eメールやパスワードなどの設定が自動でされているのが確認できる。
マイグレーション実行
# マイグレーションを実行
$ bundle exec rails db:migrate
テーブル・カラム情報を変更したため、ローカルサーバーを再起動
$ bundle exec rails s
deviseのビューファイルを作成
ビューファイルは自動生成されないため、コマンドを実行する必要がある。
$ bundle exec rails g devise:views
このコマンドでビューファイルを生成できる。
●サインアップ画面のビュー app/views/devise/registrations/new.html.erb
●ログイン画面のビュー
app/views/devise/sessions/new.html.erb
装飾したい場合はこれらのビューを変更すればいい。
<h2 class="contents" >新規登録</h2>
<div class="center">
<%= 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 %> characters minimum)</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>
<div class="actions">
<%= f.submit "新規登録する" %>
</div>
<% end %>
</div>
<h2 class="contents" >ログイン</h2>
<div class="center">
<%= 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>
<div class="actions">
<%= f.submit "ログインする" %>
</div>
<% end %>
</div>
cssも修正。
.btn,
.post {
padding: 8px 20px;
font-size: 14px;
border: 2px solid #57C3E9;
color: #57C3E9;
font-weight: bold;
text-align: center;
border-radius: 3px;
display: inline-block;
margin: 5px;
}
.btn:hover,
.post:hover {
border-color: #9bdbf2;
color: #9bdbf2;
}
footer {
margin: 30px auto;
padding: 10px;
color: #D8D8D8;
text-align: center;
}
header {
margin: 30px auto;
padding: 10px;
color: #D8D8D8;
text-align: center;
}
.content_post {
text-align: center;
}
.container_box {
text-align: center;
}
.container {
margin: auto;
display: block;
}
.contents {
text-align: center;
}
.top_contents {
display: flex;
justify-content: center;
}
.center {
width: 100vw;
display: flex;
justify-content: center;
}
以下にアクセスするとログイン画面が確認できる。
http://localhost:3000/users/sign_in
以下にアクセスするとサインアップ画面が確認できます。
http://localhost:3000/users/sign_up
サインアップ時に登録する情報はメールアドレスとパスワードの2つになっているが、
加えてニックネームを登録できるようにしていこう。
usersテーブルにnicknameカラムをstring型で追加
テーブルにカラムを追加するには、下記を実行しマイグレーションを生成する。
$ bundle exec rails g migration Addカラム名To追加先テーブル名 追加するカラム名:型
# usersテーブルにnicknameカラムをstring型で追加するマイグレーションファイルを作成
$ bundle exec rails g migration AddNicknameToUsers nickname:string
# 作成したマイグレーションを実行
$ bundle exec rails db:migrate
テーブル・カラム情報を変更したため、ローカルサーバーを再起動
$ bundle exec rails s
次はニックネーム情報をフォームから登録できるようにしていく。
そのためにはビューを編集する。
今回は、ニックネームを6文字以内で登録させるようにする。
# maxlengthオプションの一例
<div class="field">
<%= f.text_field :nickname, autofocus: true, maxlength: "6" %>
</div>
<h2 class="contents" >新規登録</h2>
<div class="center">
<%= 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 :nickname %> <em>(6 characters maximum)</em><br />
<%= f.text_field :nickname, autofocus: true, maxlength: "6" %>
</div>
<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 %> characters minimum)</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>
<div class="actions">
<%= f.submit "新規登録する" %>
</div>
<% end %>
</div>
ストロングパラメーターの設定
deviseに関しても、他と同様にストロングパラメーターをコントローラーに記述したい。
しかしdeviseの処理を行うコントローラーはGem内に記述されているため、編集することができない。
devise_parameter_sanitizerメソッド
deviseにおけるparamsのようなメソッドで、deviseのUserモデルに関わる「ログイン」「新規登録」などのリクエストからパラメーターを取得できる。
このメソッドとpermitメソッドを組み合わせることで、deviseに定義されているストロングパラメーターに対し、新しく追加したカラムも指定して含めることができる。
これまでのストロングパラメーターと同じく、新たに定義するプライベートメソッドの中で使用する。deviseの提供元では、新たに定義するメソッド名をconfigure_permitted_parametersと紹介していることから、慣習的にこのメソッド名で定義することが多いよう。
private
def configure_permitted_parameters # メソッド名は慣習
# deviseのUserモデルにパラメーターを許可
devise_parameter_sanitizer.permit(:deviseの処理名, keys: [:許可するキー])
end
devise_parameter_sanitizerに使用するpermitメソッドの引数の指定の仕方は異なる。
# paramsのpermitメソッド
params.require(:モデル名).permit(:許可するキー)
# devise_parameter_sanitizerのpermitメソッド
devise_parameter_sanitizer.permit(:deviseの処理名, keys: [:許可するキー])
deviseのpermitは、第一引数にdeviseの処理名、第二引数にkeysというキーに対し、配列でキーを指定することで、許可するパラメーターを追加していく。
第一引数の処理名には、deviseで設定されているsign_in, sign_up, account_updateが使用でき、それぞれの処理に対応している。
第一引数で指定した処理に対して、第二引数のkeysで指定された名前と同じキーを持つパラメーターの取得を許可する。ビューに記述した各フォーム部品のname属性値が、フォームから送信されるパラメーターのキーとなる。
deviseにストロングパラメーターを追加するコードは、deviseのコントローラーが編集できないため、application_controller.rbに記述していく。
application_controller.rbを編集
class ApplicationController < ActionController::Base
before_action :configure_permitted_parameters, if: :devise_controller?
private
def configure_permitted_parameters
devise_parameter_sanitizer.permit(:sign_up, keys: [:nickname])
end
end
:devise_controller?というdeviseのヘルパーメソッド名を指定して、deviseに関するコントローラーの処理のときだけメソッドを実行するように設定している。
ビューを整えていく。
●ログインしているとき:ログアウト、新規投稿ボタンを表示
●ログインしていないとき:ログイン、新規登録ボタンを表示
# ログインしているユーザーのとき
user_signed_in?
# => true
# ログインしていないユーザーのとき
user_signed_in?
# => false
<!DOCTYPE html>
<html>
<head>
<title>Tsubuyaki</title>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
</head>
<body>
<header class="header">
<div class="header__bar row">
<h1 class="grid-6"><a href="/">Tsubuyaki</a></h1>
<div class="top_contents">
<div class="user_nav grid-6">
<% if user_signed_in? %>
<div class="user_nav grid-6">
<%= link_to "ログアウト", destroy_user_session_path, method: :delete,class: "post" %>
<%= link_to "投稿する", new_post_path, class: "post" %>
</div>
<% else %>
<div class="grid-6">
<%= link_to "ログイン", new_user_session_path, class: "post" %>
<%= link_to "新規登録", new_user_registration_path, class: "post" %>
</div>
<% end %>
</div>
</div>
</header>
<%= yield %>
<footer>
<p>
Copyright Tsubuyaki 2021.
</p>
</footer>
</body>
</html>
しかしこのままだと問題あり。
ログインしていなくてもURLを直接入力してしまうと新規投稿ページに行けてしまうのだ。
そこで対策していく。
unless
ifと同様に、条件式の返り値で条件分岐して処理を実行するRubyの構文。
ifは返り値がtrueのときにelseまでの処理が実行されるが、
unlessはfalseのときにelseまでの処理が実行される。
redirect_toメソッド
Railsでリクエストと違うページに返すリダイレクト処理を行う際に使用するメソッド。
コントローラー等での処理が終わった後、アクションに対応するビューファイルを参照せずに、別ページへリダイレクトさせることができる。
redirect_to action: :リダイレクト先となるアクション名
exceptオプション
before_actionで使用できるオプション。
exceptは「除外する」という意味があるとおり、この中に記述したアクションには処理が適用されずに済む。
class PostsController < ApplicationController
before_action :set_post, only: [:edit, :show]
before_action :move_to_index, except: [:index, :show]
def index
@posts = Post.all
end
def new
@post = Post.new
end
def create
Post.create(post_params)
end
def destroy
post = Post.find(params[:id])
post.destroy
end
def edit
end
def update
post = Post.find(params[:id])
post.update(post_params)
end
def show
end
private
def post_params
params.require(:post).permit(:name, :image, :text)
end
def set_post
@post = Post.find(params[:id])
end
def move_to_index
unless user_signed_in?
redirect_to action: :index
end
end
end
挙動を確認してみて、新規登録、ログイン、ログアウトが正しくできればOK。
次に、showアクションを使用してマイページを実装していく。
誰がどのつぶやきをしたか判断するためにつぶやきにuser_idを付与する。
postsテーブルにuser_idカラムをinteger型で追加
$ bundle exec rails g migration AddUserIdToPosts user_id:integer
$ bundle exec rails db:migrate
$ bundle exec rails s #サーバー再起動
ツイートしたユーザー=現在ログインしているユーザーのこと。
そのためpostsテーブルのuser_idカラムには、current_userのidを保存する。
current_userメソッド
device導入後、ログイン中のユーザーの情報を取得できる。
ログイン中のユーザーIDとツイートを一緒に保存したいので、
そのために2つのハッシュを統合するときに使う「mergeメソッド」を利用する。
mergeメソッド
ハッシュを結合させるときに使用するメソッド
post = { name: "スズキ", text: "おはよう!", image: "sun.jpeg" }
id = { user_id: "1" }
post.merge(id)
=> {:name=>"スズキ", :text=>"おはよう!", :image=>"sun.jpeg", :user_id=>"1"}
class PostsController < ApplicationController
before_action :set_post, only: [:edit, :show]
before_action :move_to_index, except: [:index, :show]
def index
@posts = Post.all
end
def new
@post = Post.new
end
def create
Post.create(post_params)
end
def destroy
post = Post.find(params[:id])
post.destroy
end
def edit
end
def update
post = Post.find(params[:id])
post.update(post_params)
end
def show
end
private
def post_params
params.require(:post).permit(:name, :image, :text).merge(user_id: current_user.id) #current_user_idをマージ
end
def set_post
@post = Post.find(params[:id])
end
def move_to_index
unless user_signed_in?
redirect_to action: :index
end
end
end
has_manyメソッドでUserモデルを編集
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
has_many :posts
end
belongs_toメソッドでPostモデルを編集
class Post < ApplicationRecord
validates :text, presence: true
belongs_to :user
end
ユーザーマイページ用のルーティングを設定
Rails.application.routes.draw do
devise_for :users
# For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
root to: 'posts#index'
resources :posts
resources :users, only: :show
end
上記で、/users/:id のパスでリクエストした際にusers_controller.rbのshowアクションを実行するルーティングが設定できた。
トップページのビューにマイページへのリンクを追加
<!DOCTYPE html>
<html>
<head>
<title>Tsubuyaki</title>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
</head>
<body>
<header class="header">
<div class="header__bar row">
<h1 class="grid-6"><a href="/">Tsubuyaki</a></h1>
<div class="top_contents">
<div class="user_nav grid-6">
<% if user_signed_in? %>
<div class="user_nav grid-6">
<%= link_to "マイページ", "/users/#{current_user.id}", class: "post" %>
<%= link_to "ログアウト", destroy_user_session_path, method: :delete,class: "post" %>
<%= link_to "投稿する", new_post_path, class: "post" %>
</div>
<% else %>
<div class="grid-6">
<%= link_to "ログイン", new_user_session_path, class: "post" %>
<%= link_to "新規登録", new_user_registration_path, class: "post" %>
</div>
<% end %>
</div>
</div>
</header>
<%= yield %>
<footer>
<p>
Copyright Tsubuyaki 2021.
</p>
</footer>
</body>
</html>
usersコントローラー作成
$ bundle exec rails g controller users
showアクションで表示するページ、つまりマイページには
「ニックネーム」と「ログイン中ユーザーのツイート」が必要。
それぞれ@nickname、@postsというインスタンス変数に代入。
@nickname⇒current_userを利用し、
ログイン中ユーザーが持つnicknameカラムの値を取得する。
@posts⇒ログイン中ユーザーのツイートを取得し、インスタンス変数に代入する。
マイページのビューを作成
app/views/usersディレクトリの配下にshow.html.erbというファイルを作成。
<div class="contents row">
<p><%= @nickname %>さんの投稿一覧</p>
<% @posts.each do |post| %>
<div class="content_post" >
<p><%= post.text %></p>
<p><%= image_tag post.image.variant(resize: '500x500'), class: 'post-image' if post.image.attached?%></p>
<span class="name"><%= post.name %></span>
</div>
<% end %>
</div>
ここでトップページに行こうとするとエラーが起きる。
理由:先程current_userメソッド、mergeメソッドで
ツイートを投稿した際にuser_idも一緒に保存するようにしたが、
それ以前に投稿したツイートはuser_idがNULLのままになっているため。
⇒sequel proで確認して、以前ツイートした分を一旦削除してしまおう。
エラーが解決する。
他の投稿者のマイページにも行けるよう、投稿一覧ページのビューを編集していく。
<div class="contents row">
<% @posts.each do |post| %>
<div class="content_post" >
<p><%= post.text %></p>
<p><%= image_tag post.image.variant(resize: '500x500'), class: 'post-image' if post.image.attached?%></p>
<span class="name">
<a href="/users/<%= post.user.id %>">
<span>投稿者</span><%= post.user.nickname %>
</a>
</span>
<%= link_to '詳細', post_path(post.id), method: :get %>
<%= link_to '編集', edit_post_path(post.id), method: :get %>
<%= link_to '削除', "/posts/#{post.id}", method: :delete %>
</div>
<% end %>
</div>
しかしこのままだと、どの投稿の投稿者名をクリックしても、ログインユーザーの詳細ページに移動してしまう。
この問題は後ほど解決していく。
ツイート詳細画面の編集
ツイート詳細画面の投稿者の部分も@post.nameとnameを使う文になっているため、
アソシエーションを利用する形に変更する。
併せてマイページに飛ぶリンクも設置。
<div class="contents row">
<div class="content_post" >
<p><%= @post.text %></p>
<p><%= image_tag @post.image.variant(resize: '500x500'), class: 'post-image' if @post.image.attached?%></p>
<span class="name">
<a href="/users/<%= @post.user.id %>">
<span>投稿者</span><%= @post.user.nickname %>
</a>
</span>
<%= link_to '編集', edit_post_path(@post.id), method: :get %>
<%= link_to '削除', "/posts/#{@post.id}", method: :delete %>
</div>
</div>
N+1問題
ここで起こっているのが、N+1問題。
N+1問題とは、アソシエーションを利用した場合に限り、データベースへのアクセス回数が多くなってしまう問題のこと。
今回の場合postsに関連するusersの情報取得に、ツイート数と同じ回数のアクセスが必要となる。
これはアプリケーションのパフォーマンス低下につながる。
それを解決するのがincludesメソッドだ。
includesメソッド
includesメソッドは、引数に指定された関連モデルを1度のアクセスでまとめて取得できるようにする。
書き方は、includes(:紐付くモデル名)とする。
引数に関連モデルをシンボルで指定する。
モデル名.includes(:紐付くモデル名)
postsコントローラーを編集
class PostsController < ApplicationController
before_action :set_post, only: [:edit, :show]
before_action :move_to_index, except: [:index, :show]
def index
@posts = Post.includes(:user)
end
#中略
end
includesメソッドを使用するとすべてのレコードを取得するため、allメソッドが省略可能。
新規ツイート画面のビューを変更
投稿を表示する際、アソシエーションを利用することで投稿者のニックネームを表示できるようになったため、
nameカラムは必要なくなり、投稿時に「Nickname」の値を入力する必要もなくなった。
なので削除する。
<div class="contents_form">
<div class="container_box">
<h3>投稿する</h3>
<%= form_with(model: @post, local: true) do |form| %>
<%= form.text_field :name, placeholder: "ニックネーム", class: 'container'%> #この行を削除
<%= form.text_area :text, placeholder: "text", rows: "10", class: 'container'%>
<%= form.file_field :image %>
<%= form.submit "つぶやく", class: 'container'%>
<% end %>
</div>
</div>
編集画面のビューも同様に削除
<div class="contents_form">
<div class="container_box">
<h3>編集する</h3>
<%= form_with(model: @post, local: true) do |form| %>
<%= form.text_field :name, placeholder: "ニックネーム", class: 'container'%> #この行を削除
<%= form.text_area :text, placeholder: "text", rows: "10", class: 'container'%>
<%= form.file_field :image %>
<%= form.submit "つぶやく", class: 'container'%>
<% end %>
</div>
</div>
nameカラムはもう使用しないため、
ツイートの保存時にnameカラムへ情報を保存しないよう、ストロングパラメーターに変更を加える。
class PostsController < ApplicationController
before_action :set_post, only: [:edit, :show]
before_action :move_to_index, except: [:index, :show]
def index
@posts = Post.includes(:user)
end
def new
@post = Post.new
end
def create
Post.create(post_params)
end
def destroy
post = Post.find(params[:id])
post.destroy
end
def edit
end
def update
post = Post.find(params[:id])
post.update(post_params)
end
def show
end
private
def post_params
params.require(:post).permit(:image, :text).merge(user_id: current_user.id)
end
def set_post
@post = Post.find(params[:id])
end
def move_to_index
unless user_signed_in?
redirect_to action: :index
end
end
end
上記のとおり、:nameカラムを削除した。
テーブルからカラムを削除するためのマイグレーションを作成し、カラムの削除を実行。
$ bundle exec rails g migration Removeカラム名From削除元テーブル名 削除するカラム名:型
# マイグレーションの作成
$ bundle exec rails g migration RemoveNameFromPosts name:string
# マイグレーションの実行
$ bundle exec rails db:migrate
# 「control + C」でローカルサーバーを停止
# ローカルサーバーを起動
$ bundle exec rails s
マイページのビュー内で、削除されたnameカラムをpost.nameと記述し利用している文があるため、1行まるごと削除
<div class="contents row">
<p><%= @nickname %>さんの投稿一覧</p>
<% @posts.each do |post| %>
<div class="content_post" >
<p><%= post.text %></p>
<p><%= image_tag post.image.variant(resize: '500x500'), class: 'post-image' if post.image.attached?%></p>
<span class="name"><%= post.name %></span> #この行を削除
</div>
<% end %>
</div>
さて、ようやくここで以前置き去りにしていた問題を解決していく。
投稿者名をクリックするとログインユーザーのページに遷移してしまうという問題。
・コントローラー内のインスタンス変数を変更する。
・クリックされたユーザーのidから情報を取得し、ビューに受け渡す。
下記の通り。
class UsersController < ApplicationController
def show
user = User.find(params[:id])
@nickname = user.nickname
@posts = user.posts
end
end
ただ、このままだと全てのユーザーに編集ボタンと削除ボタンが表示されてしまっている。
そのためビューに条件を追加し、投稿した本人だけに表示されるようにしていく。
current_user.id == post.user_idと記述することで、
「ログイン中のユーザー」と「ツイートを投稿したユーザー」が同じか否かを判定することができる。
この記述にif文を組み合わせることで表現できる。
一覧ページのビューを編集
<div class="contents row">
<% @posts.each do |post| %>
<div class="content_post" >
<p><%= post.text %></p>
<p><%= image_tag post.image.variant(resize: '500x500'), class: 'post-image' if post.image.attached?%></p>
<span class="name">
<a href="/users/<%= post.user.id %>">
<span>投稿者</span><%= post.user.nickname %>
</a>
</span>
<%= link_to '詳細', post_path(post.id), method: :get %>
<% if user_signed_in? && current_user.id == post.user_id %>
<%= link_to '編集', edit_post_path(post.id), method: :get %>
<%= link_to '削除', "/posts/#{post.id}", method: :delete %>
<% end %>
</div>
<% end %>
</div>
マイページのビューを編集
<div class="contents row">
<p><%= @nickname %>さんの投稿一覧</p>
<% @posts.each do |post| %>
<div class="content_post" >
<p><%= post.text %></p>
<p><%= image_tag post.image.variant(resize: '500x500'), class: 'post-image' if post.image.attached?%></p>
</div>
<% end %>
</div>
以上。
部分テンプレート
次に、同じコードをまとめてテンプレートとして共通して使えるようにしていく。
テンプレート自体のファイル名は、命名規則として、アンダースコア_を先頭に記述する。
app/views/postsの配下に_post.html.erbというファイルを作成する。
index.html.erbの以下の部分を選択して切り取る。
<div class="contents row">
<% @posts.each do |post| %>
# ここから切り取る
<div class="content_post" >
<p><%= post.text %></p>
<p><%= image_tag post.image.variant(resize: '500x500'), class: 'post-image' if post.image.attached?%></p>
<span class="name">
<a href="/users/<%= post.user.id %>">
<span>投稿者</span><%= post.user.nickname %>
</a>
</span>
<%= link_to '詳細', post_path(post.id), method: :get %>
<% if user_signed_in? && current_user.id == post.user_id %>
<%= link_to '編集', edit_post_path(post.id), method: :get %>
<%= link_to '削除', "/posts/#{post.id}", method: :delete %>
<% end %>
</div>
# ここまで切り取る
<% end %>
</div>
今index.html.erbから切り取った部分を_post.html.erbに貼り付ける。
<div class="content_post" >
<p><%= post.text %></p>
<p><%= image_tag post.image.variant(resize: '500x500'), class: 'post-image' if post.image.attached?%></p>
<span class="name">
<a href="/users/<%= post.user.id %>">
<span>投稿者</span><%= post.user.nickname %>
</a>
</span>
<%= link_to '詳細', post_path(post.id), method: :get %>
<% if user_signed_in? && current_user.id == post.user_id %>
<%= link_to '編集', edit_post_path(post.id), method: :get %>
<%= link_to '削除', "/posts/#{post.id}", method: :delete %>
<% end %>
</div>
renderメソッド
部分テンプレートを呼び出す際に利用するメソッド。
呼び出す部分テンプレートは、partialで指定する。
partialオプション
renderメソッドで使用できるオプションであり、部分テンプレート名を指定することで表示できる。
【例】renderメソッドのpartialオプション
<% render partial: "sample" %>
また、呼び出す部分テンプレートに値を渡すために、localsを使用できる。
localsオプション
renderメソッドで使用できるオプションである。 localsというオプションを付けることで、部分テンプレート内でその変数を使えるようになる。
【例】renderメソッドのlocalsオプション
<% render partial: "sample", locals: { post: "good!" } %>
上の記述で、部分テンプレート内においてgood!という文字列の代入されたpostという変数が使えるようになる。
ではindex.html.erbを編集する。
先ほど切り取って残った箇所に、部分テンプレートを挿入する。
<div class="contents row">
<% @posts.each do |post| %>
<%= render partial: "post", locals: { post: post } %>
<% end %>
</div>
マイページも同じコードであるため、部分テンプレートを適用
使用するファイルを呼び出すが、同じディレクトリではないため、posts/postのようにどこのディレクトリにある部分テンプレートなのかを指定する必要がある。
<div class="contents row">
<p><%= @nickname %>さんの投稿一覧</p>
<% @posts.each do |post| %>
<%= render partial: "posts/post", locals: { post: post } %>
<% end %>
</div>
各formも部分テンプレートでまとめる
app/views/postsディレクトリに_form.html.erbファイルを作成。
該当部分を切り取り・・・
<div class="contents_form">
<div class="container_box">
<h3>投稿する</h3>
# ここから切り取る
<%= form_with(model: @post, local: true) do |form| %>
<%= form.text_area :text, placeholder: "text", rows: "10", class: 'container'%>
<%= form.file_field :image %>
<%= form.submit "つぶやく", class: 'container'%>
<% end %>
# ここまで切り取る
</div>
</div>
部分テンプレートに貼り付ける。
<%= form_with(model: post, local: true) do |form| %>
<%= form.text_area :text, placeholder: "text", rows: "10", class: 'container'%>
<%= form.file_field :image %>
<%= form.submit "つぶやく", class: 'container'%>
<% end %>
# 一行目の@postとしてあった部分だけpostに変えておこう。
先ほど切り取って残った箇所に、部分テンプレートを挿入する。new,edit両方同じように記述する。
<div class="contents_form">
<div class="container_box">
<h3>投稿する</h3>
<%= render partial: "form", locals: { post: @post } %>
</div>
</div>
問題なく新規投稿・編集ができるかどうか動作を確認してみて、
動作に問題がなければ部分テンプレートを用いた表示は成功。
コメント機能の実装
ツイート詳細画面からツイートに対して、コメントが書き込めるようなコメント機能を実装していく。
コメントは、ツイートが必ず所有する情報ではないため、ツイートと別のテーブルで管理する必要がある。
そのため、commentsテーブルを作る。
さらに、コメントはどのツイートに対してのコメントなのか、どのユーザーが投稿したコメントなのか判る必要もある。
そのため、UserモデルとPostモデルの2つにアソシエーションを組む。
まずはUserモデルとPostモデルに加えて、Commentモデルを作成する。
$ bundle exec rails g model comment
マイグレーションファイルを編集する。
class CreateComments < ActiveRecord::Migration[6.0]
def change
create_table :comments do |t|
t.integer :user_id
t.integer :post_id
t.text :text
t.timestamps
end
end
end
ここで注意する点は、カラムにuser_idとpost_id。
「誰が投稿したコメントなのか」が分かるようにするために
関連するユーザーのidを保存する必要がある。
そのidを保存するカラム名をuser_idとした。
加えて「どのツイートに対してのコメントなのか」が分かるようにするために
関連するツイートのidを保存する必要がある。
そのidを保存するカラム名をpost_idとした。
マイグレーションの実行
$ bundle exec rails db:migrate
# 「ctrl + C」でローカルサーバーを終了
# 再度、ローカルサーバーを起動
$ bundle exec rails s
Sequel Proを開き、テーブルが追加されているか確認する。
ここで、3つのテーブル同士の関係性を確認しておく。
テーブルとテーブルをつなぐ線は、1対多の関係を表している。
例としてpostsテーブルからcommentsテーブルに伸びる線は、一つのツイートが複数のコメントを持つ様を表している。
comment.rbを編集する。
コメントは、1人のユーザーと1つのツイートに所属するので、belongs_to :モデル単数形と記述することで、アソシエーションを定義する。
コメントモデルを編集
class Comment < ApplicationRecord
belongs_to :post # postsテーブルとのアソシエーション
belongs_to :user # usersテーブルとのアソシエーション
end
ツイートモデルを編集
class Post < ApplicationRecord
validates :text, presence: true
belongs_to :user
has_many :comments, dependent: :destroy # commentsテーブルとのアソシエーション
end
dependent: :destroyオプションを追加記述することで、
親モデルのPostモデルが削除された際に、子モデルのCommentモデル(インスタンス)も削除される。
ユーザーモデルを編集
class User < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
has_many :posts, dependent: :destroy
has_many :comments, dependent: :destroy # commentsテーブルとのアソシエーション
end
コメントを投稿するためのルーティングの設定
コメントを投稿する際に、どのツイートに対するコメントなのかをパスから判断できるようにしておく必要がある。
その一番の理由は、アソシエーション先のレコードのidをparamsに追加してコントローラーに送るため。
ルーティングのネストという方法を使っていく。
Rails.application.routes.draw do
resources :親となるコントローラー do
resources :子となるコントローラー
end
end
今回は「コメント情報を作る機能」を実装するのでcreateアクションのルーティングとする。
Rails.application.routes.draw do
devise_for :users
root to: 'posts#index'
resources :posts do
resources :comments, only: :create
end
resources :users, only: :show
end
ルーティングの確認
Prefix Verb URI Pattern Controller#Action
# 中略
post_comments POST /posts/:post_id/comments(.:format) comments#create
# 中略
コントローラーの作成
$ bundle exec rails g controller comments
class CommentsController < ApplicationController
def create
Comment.create(comment_params)
end
private
def comment_params
params.require(:comment).permit(:text).merge(user_id: current_user.id, post_id: params[:post_id])
end
end
user_idカラムには、ログインしているユーザーのidとなるcurrent_user.idを保存し、
post_idカラムは、paramsで渡されるようにするので、params[:post_id]として保存している。
続けて、コメントしたらツイートの詳細画面に戻る処理をcomments_controller.rbに記述していく。方法:redirect_toメソッド。
class CommentsController < ApplicationController
def create
comment = Comment.create(comment_params)
redirect_to "/posts/#{comment.post.id}" # コメントと結びつくツイートの詳細画面に遷移する
end
private
def comment_params
params.require(:comment).permit(:text).merge(user_id: current_user.id, post_id: params[:post_id])
end
end
postsコントローラーのshowアクションを実行するには、ツイートのidが必要。
そのため、ストロングパラメーターを用いた上で変数commentに代入する。
次に、コメント投稿フォームを作っていく。
コメントはツイートの詳細画面から投稿したいので、
views/posts/show.html.erbにフォームを記述する。
<div class="contents row">
<div class="content_post" >
<p><%= @post.text %></p>
<p><%= image_tag @post.image.variant(resize: '500x500'), class: 'post-image' if @post.image.attached?%></p>
<span class="name">
<a href="/users/<%= @post.user.id %>">
<span>投稿者</span><%= @post.user.nickname %>
</a>
<% if user_signed_in? && current_user.id == @post.user_id %>
</span>
<%= link_to '編集', edit_post_path(@post.id), method: :get %>
<%= link_to '削除', "/posts/#{@post.id}", method: :delete %>
<% end %>
</div>
<div class="container">
<% if user_signed_in? %>
<%= form_with(model: [@post, @comment], local: true) do |form| %>
<%= form.text_area :text, placeholder: "コメントする", rows: "2" %>
<%= form.submit "コメント" %>
<% end %>
<% else %>
<strong><p>※※※ コメントの投稿には新規登録/ログインが必要です ※※※</p></strong>
<% end %>
</div>
</div>
ツイートの詳細画面でツイートと結びつくコメントを表示するためには、ビューを呼び出す前のコントローラーが実行されている時点で、コメントのレコードをデータベースから取得する必要がある。
そのためposts_controller.rbのshowアクションを編集していく。
class PostsController < ApplicationController
before_action :set_post, only: [:edit, :show]
before_action :move_to_index, except: [:index, :show]
def index
@posts = Post.includes(:user).order("created_at DESC")
end
def new
@post = Post.new
end
def create
Post.create(post_params)
end
def destroy
post = Post.find(params[:id])
post.destroy
end
def edit
end
def update
post = Post.find(params[:id])
post.update(post_params)
end
def show
@comment = Comment.new
@comments = @post.comments.includes(:user)
end
private
def post_params
params.require(:post).permit(:image, :text).merge(user_id: current_user.id)
end
def set_post
@post = Post.find(params[:id])
end
def move_to_index
unless user_signed_in?
redirect_to action: :index
end
end
end
posts/show.html.erbでform_withを使用して、comments#createを実行するリクエストを飛ばしたいので、@comment = Comment.newというインスタンス変数を生成しておかないといけない。
postsテーブルとcommentsテーブルはアソシエーションが組まれているため、@post.commentsとすることで、@postへ投稿されたすべてのコメントを取得できる。
ビューでは誰のコメントか明らかにするため、
アソシエーションを使ってユーザーのレコードを取得する処理を繰り返し行う。
その際「N+1問題」が発生してしまうため、includesメソッドを使って解決している。
コメントを表示させるためにツイート詳細ページを編集する。
<div class="contents row">
<div class="content_post" >
<p><%= @post.text %></p>
<p><%= image_tag @post.image.variant(resize: '500x500'), class: 'post-image' if @post.image.attached?%></p>
<span class="name">
<a href="/users/<%= @post.user.id %>">
<span>投稿者</span><%= @post.user.nickname %>
</a>
<% if user_signed_in? && current_user.id == @post.user_id %>
</span>
<%= link_to '編集', edit_post_path(@post.id), method: :get %>
<%= link_to '削除', "/posts/#{@post.id}", method: :delete %>
<% end %>
</div>
<div class="container">
<% if user_signed_in? %>
<%= form_with(model: [@post, @comment], local: true) do |form| %>
<%= form.text_area :text, placeholder: "コメントする", rows: "2" %>
<%= form.submit "コメント" %>
<% end %>
<% else %>
<strong><p>※※※ コメントの投稿には新規登録/ログインが必要です ※※※</p></strong>
<% end %>
<div class="comments">
<h4><コメント一覧></h4>
<% @comments.each do |comment| %>
<p>
<strong><%= link_to comment.user.nickname, "/users/#{comment.user_id}" %>:</strong>
<%= comment.text %>
</p>
<% end %>
</div>
</div>
</div>
以上でコメント機能の実装が完了。
つぶやき検索機能の実装
投稿が増えてくると、見たい投稿を探すのが大変になってくる。
「検索したテキストを含む投稿」を表示させられると便利なため、検索機能を実装していく。
今回はsearchという命名で、7つの基本アクション以外のアクションを定義する。
ここでcollectionとmemberを学習しておこう。
collectionとmember
collectionとmemberは、ルーティング設定の際に使用する。
これを使用すると、生成されるルーティングのURLと実行されるコントローラーを任意にカスタムすることができる。
【両者の違い】
collection ルーティングに:idがつかない
member ルーティングに:idがつく
Rails.application.routes.draw do
resources :posts do
collection do
get 'search'
end
end
end
Prefix Verb URI Pattern
search_posts GET /posts/search(.:format) posts#search
上記を見ると、ルーティングに:idが付いていないことがわかる。
続いてmemberの場合を見てみよう。
Rails.application.routes.draw do
resources :posts do
member do
get 'search'
end
end
end
Prefix Verb URI Pattern
search_post GET /posts/:id/search(.:format) posts#search
URLの指定先が、collectionは:idなし、memberが:idありとなっていることが確認できる。
今回の検索機能の場合は、特に:idを指定して特定のページにいく必要はないため、
collectionを使用してルーティングを設定していく。
ルーティングの設定
Rails.application.routes.draw do
devise_for :users
root to: 'posts#index'
resources :posts do
resources :comments, only: :create
collection do
get 'search'
end
end
resources :users, only: :show
end
投稿一覧ページのビューを編集
<%= form_with(url: search_posts_path, local: true, method: :get, class: "top_contents") do |form| %>
<%= form.text_field :keyword, placeholder: "投稿を検索する", class: "post" %>
<%= form.submit "検索", class: "search-btn", class: "post"%>
<% end %>
<div class="contents row">
<% @posts.each do |post| %>
<%= render partial: "post", locals: { post: post } %>
<% end %>
次に、Postモデルに検索する処理を記述したメソッドを定義していく。
メソッド名は「searchメソッド」とする。
検索キーワードが含まれた投稿を取得するために、「whereメソッド」と「LIKE句」を利用する。
whereメソッド
モデルが使用できる、ActiveRecordメソッドの1つで
引数部分に条件を指定することで、テーブル内の「条件に一致したレコードのインスタンス」を配列の形で取得することができる。
引数の条件式は、「検索対象となるカラム」を含めて記述する。
モデル.where('検索対象となるカラムを含む条件式')
LIKE句
曖昧な文字列の検索をするときに使用するもので、whereメソッドと一緒に使う。
文字列 | 意味 |
---|---|
% | 任意の文字列(空白文字列含む) |
_ | 任意の1文字 |
実行例 | 詳細 |
---|---|
where('title LIKE(?)', "a%") | aから始まるタイトル |
where('title LIKE(?)', "%b") | bで終わるタイトル |
where('title LIKE(?)', "%c%") | cが含まれるタイトル |
where('title LIKE(?)', "d_") | dで始まる2文字のタイトル |
where('title LIKE(?)', "_e") | eで終わる2文字のタイトル |
whereメソッドとLIKE句を使用することで「検索」の処理を作っていく。
class Post < ApplicationRecord
has_one_attached :image
validates :text, presence: true, unless: :was_attached?
def was_attached?
self.image.attached?
end
validates :text, presence: true
belongs_to :user
has_many :comments
def self.search(search)
if search != ""
Post.where('text LIKE(?)', "%#{search}%")
else
Post.all
end
end
end
引数のsearchは、検索フォームから送信されたパラメーターが入るため、
if search != ""と記述し、検索フォームに何か値が入力されていた場合を条件としている。
検索フォームに何も入力をせずにボタンを押すと、引数で渡されるsearchの中身は空になり
elseに該当し、Post.allという記述はすべての投稿を取得して表示するためのもの。
ビジネスロジックは基本的にモデルに置く。
実際の開発現場でも、テーブルとのやりとりに関するメソッドはモデルに置くという意識が必要になるとのこと。
コントローラーはあくまでモデルの機能を利用し処理を呼ぶだけで、複雑な処理は組まない。
postsコントローラーの編集
class PostsController < ApplicationController
before_action :set_post, only: [:edit, :show]
before_action :move_to_index, except: [:index, :show, :search]
def index
@posts = Post.includes(:user).order("created_at DESC")
end
def new
@post = Post.new
end
def create
Post.create(post_params)
end
def destroy
post = Post.find(params[:id])
post.destroy
end
def edit
end
def update
post = Post.find(params[:id])
post.update(post_params)
end
def show
@comment = Comment.new
@comments = @post.comments.includes(:user)
end
def search
@posts = Post.search(params[:keyword])
end
private
def post_params
params.require(:post).permit(:image, :text).merge(user_id: current_user.id)
end
def set_post
@post = Post.find(params[:id])
end
def move_to_index
unless user_signed_in?
redirect_to action: :index
end
end
end
Postモデルに書いたsearchメソッドを呼び出している。
searchメソッドの引数にparams[:keyword]と記述することで、検索結果を渡している。
未ログイン時に検索するとトップページへリダイレクトされてしまうのを回避するために、
before_actionのexceptオプションに:searchを追加している。
それでは、検索結果を表示できるようにsearch.html.erbを作成しよう。
app/views/postsディレクトリの配下にsearch.html.erbを作成。
<%= form_with(url: search_posts_path, local: true, method: :get, class: "top_contents") do |form| %>
<%= form.text_field :keyword, placeholder: "投稿を検索する", class: "post" %>
<%= form.submit "検索", class: "post" %>
<% end %>
<div class="contents row">
<% @posts.each do |post| %>
<%= render partial: "post", locals: { post: post } %>
<% end %>
</div>
これで検索機能は完成!
最後に、いいね♡機能を追加しよう。
Likeモデルを作る。
「どのユーザー」が「どの投稿」をいいねしたかを記録するために、
データベースに「user_id」と「post_id」2つのカラムを持つlikesテーブルを作成する。
likeモデルを作成
$ bundle exec rails g model like user_id:integer post_id:integer
$ bundle exec rails db:migrate
これで、likesテーブルにinteger型のuser_idカラムとpost_idカラムを作成できた。
モデル間のアソシエーションを組む
class User < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
has_many :posts, dependent: :destroy
has_many :comments, dependent: :destroy # commentsテーブルとのアソシエーション
has_many :likes, dependent: :destroy
end
class Post < ApplicationRecord
has_one_attached :image
validates :text, presence: true, unless: :was_attached?
def was_attached?
self.image.attached?
end
validates :text, presence: true
belongs_to :user
has_many :comments, dependent: :destroy
has_many :likes, dependent: :destroy
def self.search(search)
if search != ""
Post.where('text LIKE(?)', "%#{search}%")
else
Post.all
end
end
end
class Like < ApplicationRecord
belongs_to :user
belongs_to :post
end
ルーティングの設定
Rails.application.routes.draw do
devise_for :users
root to: 'posts#index'
resources :posts do
resources :comments, only: :create
resources :likes, only: :create
collection do
get 'search'
end
end
resources :users, only: :show
end
コントローラーの作成
$ bundle exec rails g controller likes create #likesコントローラーとcreateアクションを一括生成
class LikesController < ApplicationController
def create
@like = Like.new(
user_id: current_user.id,
post_id: params[:post_id]
)
@like.save
redirect_to("/")
end
end
投稿詳細ページのビューの編集
<div class="contents row">
<div class="content_post" >
<p><%= @post.text %></p>
<p><%= image_tag @post.image.variant(resize: '500x500'), class: 'post-image' if @post.image.attached?%></p>
<span class="name">
<a href="/users/<%= @post.user.id %>">
<span>投稿者</span><%= @post.user.nickname %>
</a>
<% if user_signed_in? && current_user.id == @post.user_id %>
</span>
<%= link_to '編集', edit_post_path(@post.id), method: :get %>
<%= link_to '削除', "/posts/#{@post.id}", method: :delete %>
<% end %>
<% if Like.find_by(user_id: current_user.id,post_id: @post.id)%>
<%= link_to("いいね!済み","/posts/#{@post.id}/likes/#{@like.id}",{method: "delete"}) %>
<% else %>
<%= link_to("いいね!","/posts/#{@post.id}/likes",{method: "post"}) %>
<% end %>
</div>
<div class="container">
<% if user_signed_in? %>
<%= form_with(model: [@post, @comment], local: true) do |form| %>
<%= form.text_area :text, placeholder: "コメントする", rows: "2" %>
<%= form.submit "コメント" %>
<% end %>
<% else %>
<strong><p>※※※ コメントの投稿には新規登録/ログインが必要です ※※※</p></strong>
<% end %>
<div class="comments">
<h4><コメント一覧></h4>
<% @comments.each do |comment| %>
<p>
<strong><%= link_to comment.user.nickname, "/users/#{comment.user_id}" %>:</strong>
<%= comment.text %>
</p>
<% end %>
</div>
</div>
</div>
いいね取り消しボタンの作成
「いいね」を取り消す機能を作るためには、まずはlikesコントローラにdestroyアクションを作成する。
destroyアクション内では、受け取った@current_user.idとparams[:post_id]をもとに削除すべきLikeデータを取得し、destroyメソッドを用いて削除する。
destroyアクションへのルーティングの設定
Rails.application.routes.draw do
get 'likes/create'
devise_for :users
root to: 'posts#index'
resources :posts do
resources :comments, only: :create
resources :likes, only: [:create, :destroy]
collection do
get 'search'
end
end
resources :users, only: :show
end
コントローラーにdestroyアクションの追加
class LikesController < ApplicationController
def create
@like = Like.new(
user_id: current_user.id,
post_id: params[:post_id]
)
@like.save
redirect_to("/posts/#{params[:post_id]}")
end
def destroy
@like = Like.find_by(user_id: current_user.id,post_id: params[:post_id])
@like.destroy
redirect_to("/posts/#{params[:post_id]}")
end
end
次に、いいねボタンを「♡」のアイコンにしていく。
Font Awesome
「Font Awesome」は、様々なアイコンをフォントとして利用できるようにしたもの。
利用するには、タグ内で読み込みをする必要がある。
application.html.erbに読み込み用のタグを追加しよう。
「fa fa-heart」というクラス名をつけることでハートアイコンを表示できる。しかし、下図のようにlink_toメソッド内にHTML要素を記述すると正しく表示することができない。
HTML要素に対してlink_toメソッドを使うには、少し異なる書き方が必要。
下図のように<%= link_to(URL) do %>と<% end %>の間にHTML要素を書くことで、その部分をリンクにすることができる。
ダメなパターン
<%= link_to("表示する文字列","URL") %> #文字列と判断されうまく表示されない
良いパターン
<%= link_to("URL") do %>
<HTML要素の記述>
<% end %>
<!DOCTYPE html>
<html>
<head>
<title>Tsubuyaki</title>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
</head>
<body>
<header class="header">
<div class="header__bar row">
<h1 class="grid-6"><a href="/">Tsubuyaki</a></h1>
<div class="top_contents">
<div class="user_nav grid-6">
<% if user_signed_in? %>
<div class="user_nav grid-6">
<%= link_to "マイページ", "/users/#{current_user.id}", class: "post" %>
<%= link_to "ログアウト", destroy_user_session_path, method: :delete,class: "post" %>
<%= link_to "投稿する", new_post_path, class: "post" %>
</div>
<% else %>
<div class="grid-6">
<%= link_to "ログイン", new_user_session_path, class: "post" %>
<%= link_to "新規登録", new_user_registration_path, class: "post" %>
</div>
<% end %>
</div>
</div>
</header>
<%= yield %>
<footer>
<p>
Copyright Tsubuyaki 2021.
</p>
</footer>
</body>
</html>
いいねの数を数える
テーブルからデータの件数を取得するには、「countメソッド」を用いる。
配列の要素数を取得するメソッドだが、テーブルのデータ数を取得することもできる。
postsコントローラーにカウントメソッドの追加
class PostsController < ApplicationController
before_action :set_post, only: [:edit, :show]
before_action :move_to_index, except: [:index, :show, :search]
def index
@posts = Post.includes(:user).order("created_at DESC")
end
def new
@post = Post.new
end
def create
Post.create(post_params)
end
def destroy
post = Post.find(params[:id])
post.destroy
end
def edit
end
def update
post = Post.find(params[:id])
post.update(post_params)
end
def show
@comment = Comment.new
@comments = @post.comments.includes(:user)
@like = Like.find_by(user_id: current_user.id, post_id: @post.id)
@likes_count = Like.where(post_id: @post.id).count #カウントメソッド
end
def search
@posts = Post.search(params[:keyword])
end
private
def post_params
params.require(:post).permit(:image, :text).merge(user_id: current_user.id)
end
def set_post
@post = Post.find(params[:id])
end
def move_to_index
unless user_signed_in?
redirect_to action: :index
end
end
end
ツイート詳細ページのビューも編集して、「♡」と「回数」が表示されるようにする。
<div class="contents row">
<div class="content_post" >
<p><%= @post.text %></p>
<p><%= image_tag @post.image.variant(resize: '500x500'), class: 'post-image' if @post.image.attached?%></p>
<span class="name">
<a href="/users/<%= @post.user.id %>">
<span>投稿者</span><%= @post.user.nickname %>
</a>
<% if user_signed_in? && current_user.id == @post.user_id %>
</span>
<%= link_to '編集', edit_post_path(@post.id), method: :get %>
<%= link_to '削除', "/posts/#{@post.id}", method: :delete %>
<% end %>
<% if Like.find_by(user_id: current_user.id,post_id: @post.id)%>
<%= link_to("/posts/#{@post.id}/likes/#{@like.id}",{method: "delete"})do %>
<span class="fa fa-heart like-btn-unlike"></span>
<% end %>
<% else %>
<%= link_to("/posts/#{@post.id}/likes",{method: "post"})do %>
<span class="fa fa-heart like-btn"></span>
<% end %>
<% end %>
<%= @likes_count %>
</div>
<div class="container">
<% if user_signed_in? %>
<%= form_with(model: [@post, @comment], local: true) do |form| %>
<%= form.text_area :text, placeholder: "コメントする", rows: "2" %>
<%= form.submit "コメント" %>
<% end %>
<% else %>
<strong><p>※※※ コメントの投稿には新規登録/ログインが必要です ※※※</p></strong>
<% end %>
<div class="comments">
<h4><コメント一覧></h4>
<% @comments.each do |comment| %>
<p>
<strong><%= link_to comment.user.nickname, "/users/#{comment.user_id}" %>:</strong>
<%= comment.text %>
</p>
<% end %>
</div>
</div>
</div>
以上で、つぶやきアプリの完成です!
masakichi_eng様の記事を参考にさせていただきました。
大変わかりやすい記事をありがとうございましたmm
https://qiita.com/masakichi_eng