Railsを学ぶ② ~画像投稿&ユーザー機能追加~

はじめに

Rubyを学習していて、Railsを学びながら掴んでいこうと思ったので、Railsの学習をこちらで記録していきます。

※前回の記録

Railsを学ぶ ~環境構築とCRUD実装~

※Rubyに関しては以下で記録

Rubyを学ぶ

✅参考

Progate:Rails学習コース

書籍:現場Rails

ユーザー機能追加

ProgateⅥの内容

①テーブルとモデルの作成

rails g model User name:string email:string
rails db:migrate

②データ追加

irb > user = User.new(name: "sato", email: "sato@example.com")
irb > user.save
⇒true

③バリデーション追加

models/user.rbの編集

class User < ApplicationRecord
  validates :email, {uniqueness: true}
end

「uniqueness: true」で重複をチェックしている。

④ユーザー一覧ページ作成

rails g controller users index

http://localhost:3000/users/indexへアクセスするとビューが作成されているのがわかる。

config/routes.rbを編集

※indexアクションはhttp://localhost:3000/usersでアクセスできるようにします。

# get 'users/index'
get 'users' => 'users#index'

users_controllerの編集

class UsersController < ApplicationController
  def index
    @users = User.all
  end
end

users/index.html.slimの編集

.ly
  h2 ユーザー一覧
  - @users.each do | user |
    = user.name

application.html.slimの編集

= link_to("Users", "/users")

確認

⑤ユーザー詳細ページ作成

routes.rb

get 'users/:id' => 'users#show'

post_controller.rbへshowアクションを追加

def show
  @users = User.find_by(id: params[:id])
end

users/show.html.slimの作成

.ly
  p = @user.name
  p = @user.email

users/index.html.slimに詳細ページへのリンクを作成

.ly
  h2 ユーザー一覧
  - @users.each do | user |
    p = link_to(user.name, "/users/#{user.id}")

確認

⑥登録ページの作成

登録ページ作成→保存

routes.rbにsignupを追記

get 'signup' => 'users#new'
post 'users/create' => 'users#create'

users_controller.rbにnewアクションを追記

def new
  @user = User.new
end
def create
  @user = User.new(name: params[:name], email: params[:email])
  @user.save
  redirect_to("/users/#{@user.id}")
end

new.html.slimの作成

.ly
  = form_tag("/post/create") do
    p name
    input name="name" type="text" value=""
    p email
    input name="email" type="text" value=""
    input type="submit" value="投稿"

※name属性の値がハッシュのキーになる。

保存してみる

確認

irb > users = User.all
User Load (0.4ms) SELECT "users".* FROM "users" LIMIT ? [["LIMIT", 11]]
=> #<ActiveRecord::Relation 
[#<User id: 1, name: "sato", email: "sato@example.com", created_at: "2020-07-02 08:39:39", updated_at: "2020-07-02 08:39:39">,
 #<User id: 2, name: "test", email: "test@example.com", created_at: "2020-07-03 03:41:06", updated_at: "2020-07-03 03:41:06">]>

⑦エラー・サクセスメッセージの表示

new.html.slimの編集

.ly
  - @user.errors.full_messages.each do | message | 
    = message
  = form_tag("/users/create") do
    p name
    input name="name" type="text" value="#{@user.name}"
    p email
    input name="email" type="text" value="#{@user.email}"
    input type="submit" value="新規登録"

🙄value属性に変数使う際は#{}で記載するんですね~!!

users_controller.rbの編集

※createアクションで条件分岐をする

def create
  @user = User.new(name: params[:name], email: params[:email])
  if @user.save
    flash[:notice] = "ユーザー登録が完了しました"
    redirect_to("/users/#{@user.id}")
  else
    render("users/new")
  end
end

メールが重複した際のエラーメッセージ確認

登録成功時の確認

※この後新規登録ページに飛べず地味にハマりました。。。

原因はnewアクションに@userを定義していなかったこと。注意ですね。。。

⑧編集ページ作成&メッセージの表示

routes.rbにeditを追記

get 'users/:id/edit' => 'users#edit'
post 'users/:id/update' => 'users#update'

users_controller.rbにeditアクションを追加

def edit
  @user = User.find_by(id: params[:id])
end
def update
  @user = User.find_by(id: params[:id])
  @user.name = params[:name]
  @user.email = params[:email]
  if @user.save
    flash[:notice] = "ユーザー情報を編集しました"
    redirect_to("/users/#{@user.id}")
  else
    render("users/edit")
  end
end

edit.html.slimを作成

.ly
  - @user.errors.full_messages.each do |message|
    = message
  = form_tag("/users/#{@user.id}/update") do
    p ユーザー名
    input name="name" value="#{@user.name}"
    p メールアドレス
    input name="email" value="#{@user.email}"
    input type="submit" value="保存"

show.html.slimの詳細ページに編集リンクを作成

= link_to("編集", "/users/#{@user.id}/edit")

※form_tagで地味にハマりました。

原因はform_tagメソッドのURLにスラッシュを忘れていたから。

×  = form_tag("users/#{@user.id}/update") do
○ = form_tag("/users/#{@user.id}/update") do

🙄絶対パスと相対パスの参照部分注意したいですね。。。

プロフィール写真機能実装

ProgateⅦの内容です。

①ファイル名を保存するカラムを追加

マイグレーションファイルを作成

※rails g modelだとモデルまで作成されてしまうので別のコマンドを使用する。

rails g migration add_image_name_to_users

編集

class AddImageNameToUsers < ActiveRecord::Migration[5.2]
  def change
    add_column :users, :image_name, :string
  end
end

カラムを追加する場合の記法は「add_column :テーブル名, :カラム名, :データ型

実行

rails db:migrate

②ユーザー登録時に初期画像を保存し表示する

初期画像としてpublic/user_images/default_user.jpgを用意。

newメソッドの引数にimage_nameを追加。

def create
  @user = User.new(
    name: params[:name],
    email: params[:email],
    image_name: "default_user.jpg"
  )

show.html.slim

.ly
  img src="/user_images/#{@user.image_name}"
  p = @user.name
  p = @user.email
  = link_to("編集", "/users/#{@user.id}/edit")

👇erbで記載する際の注意

○  <img src="<%= "/user_images/#{@user.image_name}" %>">
×  <img src="/user_images/#{@user.image_name}">

表示されてますね!!

③画像編集機能の実装

edit.html.slimの編集

= form_tag("/users/#{@user.id}/update",{multipart: true}) do
  p プロフィール画像
  input name="image" type="file"

※画像を送信したいときには、form_tagに{multipart: true}をつける。つけないとファイル名しか受信できない。

users_controller.rbのupdateアクションでファイル名を保存するよう編集

if params[:image]
  @user.image_name = "#{@user.id}.jpg"
  image = params[:image]
  File.binwrite("public/user_images/#{@user.image_name}",image.read)
end

画像データにはFile.writeではなくFile.binwriteを用いる。また、readメソッドを用いることで画像データを取得できる。

確認

🙄user.idで保存するとファイル名が一意になるので更新する度に画像が更新されますね!なるほどと思いました。

※実際にはActiveRecordやcarrierWaveなどを用いるのが一般的

ログイン機能実装

ProgateⅧの内容

①ログインページ作成

※application.html.slimのリンク部分は割愛

routes.rbの編集

get 'login' => 'users#login_form'

app/views/users/login_form.html.erbの作成

.ly
  h2 ログイン
  p メールアドレス
  input type="text"
  p パスワード
  input type="password"
  input type="submit" value="ログイン"

🙄inputのtypeがpasswordですね!!

users_controller.rb

def login_form
end

確認

②パスワードカラムの作成

マイグレーションファイル作成

rails g migration add_password_to_users

編集

class AddPasswordToUsers < ActiveRecord::Migration[5.2]
  def change
    add_column :users, :password, :string
  end
end

実行

rails db:migrate

バリデーションにパスワードカラム追加

class User < ApplicationRecord
  validates :email, {uniqueness: true}
  validates :password, {presence: true}
end

パスワードをコンソールで登録

user = User.find_by(id: 1)
user.password = "testtest"
user.save

確認

③フォーム送信処理(form_tagメソッド)

routes.rb

get 'login' => 'users#login_form'
post 'login' => 'users#login'

users_controller.rb

def login_form
end
def login
end

login_form.html.slim

.ly
  = form_tag("login") do
    h2 ログイン
    p メールアドレス
    input type="text"

④ユーザー特定

users_controller.rb

def login_form
end
def login
  @user = User.find_by(
    email: params[:email],
    password: params[:password]
  )
  if @user
    flash[:notice] = "ログインしました"
    redirect_to("/post")
  else
    render("users/login_form")
  end
end

※ログイン失敗時の処理がまだですね。

⑤ログイン処理でユーザーが見つからない場合の処理

loginアクションに追記

if @user
  flash[:notice] = "ログインしました"
  redirect_to("/post")
else
  @error_message = "メールアドレスまたはパスワードが間違っています"
  @email = params[:email]
  @password = params[:password]
  render("users/login_form")
end

login_form.html.slimでinputタグにvalue属性を追加

p メールアドレス
input name="email" type="text" value="#{@email}"
p パスワード
input name="password" type="password" value="#{@password}"

確認

⑥ログイン処理(状態保持)実装

ユーザー情報を保持し続けるにはsession変数を用いる。

ログインユーザーの情報保持のためにloginアクションにsession変数を追記。

if @user
  session[:user_id] = @user.id
  flash[:notice] = "ログインしました"
  redirect_to("/post")

application.html.slimを編集

h1 RailsApp
- if session[:user_id]
  .ly
    p ログインユーザー #{session[:user_id]}

確認

⑦ログアウト機能実装

ログアウトはsession変数にnilを代入することで実装する。

ルーティング

post 'logout' => 'users#logout'

データベースを変更する場合とsessionの値を変更する場合はpost送信を用いる

コントローラでlogoutアクションを作成

def logout
  session[:user_id] = nil
  flash[:notice] = "ログアウトしました"
  redirect_to("/login")
end

application.html.slimの編集

- if session[:user_id]
  = link_to("ログアウト", "/logout", {method: :post})
  span  ログインユーザー #{session[:user_id]}
- else
  = link_to("ログイン", "/login")

🙄link_toの第3引数でpost送信を明示するのがポイントですね!

⑧ユーザー登録時のログイン処理

新規作成画面でパスワードの項目を追加

p password
input name="password" type="text" value="#{@user.password}"

createアクションでnewメソッドの引数にpasswordを追加、session変数も定義

def create
  @user = User.new(
    name: params[:name],
    email: params[:email],
    image_name: "default_user.jpg"
    password: params[:password]
  )
  if @user.save
    session[:user_id] = @user.id

新規ユーザー登録時に自動ログインされることを確認。

⑨ログイン中のユーザー名の表示

current_user変数を定義して実装していく。

application.html.slim

○ - if current_user
○   = link_to(current_user.name, "/users/#{current_user.id}")
○   = link_to("ログアウト", "/logout", {method: :post})
× - if session[:user_id]
×   span  ログインユーザー #{session[:user_id]}

確認

⑩before_actionの実装

通常はコントローラ側で変数を定義するが、applicationビューの場合はコントローラ側のすべてのアクションで変数を定義しなくてはいけなくなるため⑨ではビュー側で定義していた。

この全アクション共通の処理があるような場合はbefore_actionをapplicationコントローラに定義するとよい。

application_controller.rb編集。before_actionにset_current_userアクション(※メソッド)を指定

class ApplicationController < ActionController::Base
  before_action :set_current_user
  def set_current_user
    @current_user = User.find_by(id: session[:user_id])
  end
end

ビュー側書き換え

= link_to(@current_user.name, "/users/#{@current_user.id}")

🙄session変数に入れているログインユーザーのidを活用している感じですね!!

⑪アクセス制限

現状ログインしていなくてもユーザー詳細ページなどにアクセスできてしまうのでその処理。

before_actionを用いて実装。

application_controller.rbにauthenticate_userアクションメソッドを定義

def authenticate_user
  if @current_user == nil
    flash[:notice] = "ログインが必要です"
    redirect_to("/login")
  end
end

※@変数で定義した変数は同じクラスの異なるメソッド間で共有して使用できる。

post_controller.rbを編集

before_action :authenticate_user

users_controller.rbを編集

before_action :authenticate_user, {only: [:index, :show, :edit, :update]}

onlyを用いて各コントローラで指定したアクションでのみメソッドを実行できる。

🙄各コントローラはapplicationコントローラを継承しているのがポイントですね!!

⑫ログインユーザーがアクセスできない処理

applicationコントローラ

def forbid_login_user
  if @current_user
    flash[:notice] = "すでにログインしています"
    redirect_to("/post")
  end
end

homeコントローラ

before_action :forbid_login_user, {only: [:top]}

usersコントローラ

before_action :forbid_login_user, {only: [:new, :create, :login_form, :login]}

⑬自身の情報のみ編集できるようにする(認可)

ビュー側で非表示にする

users/show.html.slimの編集

- if @user.id == @current_user.id
  = link_to("編集", "/users/#{@user.id}/edit")

userコントローラ側でアクセス権をなくす

before_action :ensure_correct_user, {only: [:edit, :update]}
~~~~
def ensure_correct_user
  if @current_user.id != params[:id].to_i
    flash[:notice] = "権限がありません"
    redirect_to("/posts")
  end
end

to_iメソッドで文字列を数値に変換。

深掘りメモ

✅session変数などRails特有の変数

@が不要

erbで記載する際の注意

○  <img src="<%= "/user_images/#{@user.image_name}" %>">
×  <img src="/user_images/#{@user.image_name}">

○  <input name="email" value="<%= @email %>"
×  <input name="email" value="<%= @email %>"

✅session変数の使い方

session[:user_id] = @user.id
# session[:データ名] = 値

👆セッション変数には何が格納されるのか??

セッション変数はあくまで値を保持するだけ。ブラウザを閉じるまでは値を保持する特殊な変数のため、認証周りで用いられる。
※自動ログインを用いたい際は通常のクッキーに情報を保存する。

参考:改訂4版 基礎Ruby on Rails 基礎シリーズ ※会社の本棚にありました

おわりに

ここまででログイン機能と画像投稿の処理を実装できた。

今までLaravelの学習をしていたが、ドキュメントの充実具合にびっくり。

初学者は学びやすさという点ではLaravelよりRailsのほうが上かなとすでに感じる。

✅その他

slim使ってて思ったけどインデントのルールが若干あいまいなのがなんとも。pugだとコンパイル時にしっかりエラー出してくれるので。erbも慣れておいたほうがよいかもな~

またJSと違ってコンソールでデバッグができないので、テストはマストなんだなとやっと実感を持てるようになってきた。

備忘録

✅その他参考

paramsとhtmlの理解(初心者の方向け)

✅Rubyでファイルを扱う

Fileクラスを用いて作成

※File.write(ファイルの場所, ファイルの中身)

irb > File.write("public/sample.txt", "Hello World")

publicフォルダに作成されているのを確認できる。

✅今後確認したいこと

・CSSのスコープ

参考:Rails の CSS にスコープを持たせてファイル分割する

✅その他試したこと

 

コメントを残す