20200409のRailsに関する記事は27件です。

-これからポートフォリオを作成する方へ- 設計図の作成方法:前半

ポートフォリオを作成する際に、何から始めれば良いかわからない・・・という方に向けて。
第1歩目として設計図を作成する事を強くオススメします。理由は以下の通りです。

・自分が今どの部分を実装しているか把握が容易になるため、迷子にならないスクリーンショット 2020-04-09 23.08.08.png

・少しずつ目標に近く感覚を可視化でき、モチベーションの維持に繋がる

そのためにどのようなツールを使用し、どのような物を作成すれば良いかまとめました。

●使用ツール

こちらはCacooというサービスをオススメします。
https://cacoo.com/

会員登録から一定期間は無料ですので、無料期間中にさくっと作ってしまいましょう。
またPDF出力が可能なので、作成した設計図をPDFとして保存してしまえば無料期間中だけでも事足ります!

●作成すべき物① -ER図-

ER図は下記のような物をいいます。
スクリーンショット 2020-04-09 23.08.46.png

詳しい作成方法はこちらの投稿が非常にわかりやすかったのでオススメです。
「やさしい図解で学ぶ ER図 表記法一覧」
https://qiita.com/ramuneru/items/32fbf3032b625f71b69d

あくまでポートフォリオ作成時の準備なので、ここに学習コストを割く必要はないかと思います。
ポートフォリオ作成レベルであれば、上記の記事にかかれている内容を把握すれば十分記載できるかと思います。

●作成すべき物② -テーブル設計図-

テーブル設計は以下のようにまとめるとわかりやすいかと思います。
スクリーンショット 2020-04-09 23.03.56.png

ポートフォリオは就職活動用に作成する物なので、期限も限られていますし他に労力を割く必要もあるかと思います。
ですので、自分が作成する最低限のサービスをまず作成し、その後に状況にあわせ機能追加していく形で計画すると良いと思います。

カラムの型の種類や、カラムの追加・削除・編集方法を簡単にまとまっているのがこちらです。
「カラムの型」
https://qiita.com/mokku/items/39ced66550e9b623b5a8

railsを勉強してる人にとってDB周りは苦手意識のある方が多いと思うので(というか自分ですが)、まずは基本的なことから。

残すはサイトマップとワイヤーフレームです。そちらに関しては今後またまとめたいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】 deviseで新規登録後またはログイン後にroot_path以外のページに飛ばす

deviseで新規登録したけどroot_path以外のページに飛ばしたい...

今までdevise使ってきたけど新規登録やログインをするとroot_pathのページに勝手に戻ってログインしている状態になってたんですが(いやー便利)、サイト作っている途中にroot_path以外のページに飛ばしたことになりまして調べてみました。

コントローラーに記述して飛ばす

root_path以外のページ遷移は多分コントローラに書かないと出来ないかも
調べてたらapplication_controller.rbにまとめて書く方やそれぞれの動作に合わせてregistrations_controller.rbやsessions_controller.rbに書く方がいましたが今回はapplication_controller.rbにまとめて書きました。

その前にrails routesで飛ばしたいpathを確認

3e319ab23a667d951a0ea7f6b885d3ee.png
今回は下から2番目のuser_pathに飛ばします。

application_controller.rbへ記述

application_controller.rb
class ApplicationController < ActionController::Base
  def after_sign_in_path_for(resource)
    user_path(resource)
  end
end

この'after_sign_in_path_for(resource)'でログイン後にどこのページに飛ぶか指定できるんで中にpathを書けばそこに飛んじゃいます。今回はuser_pathですね。

ちなみにpathの横に(resource)と書いているのはURI Patternが/users/:idなのでこの:idを渡すために(resource)が必要になります。これないとエラー出ます。

補足

今回は新規登録後やログイン後に指定したページに飛ばすやり方だけどログアウトした後にも同様のやり方で指定したページに飛ばせるので紹介

application_controller.rb
class ApplicationController < ActionController::Base
  def after_sign_out_path_for(resource)
    index_path #今回は仮にindex_pathを置いていますがここにログアウト後のpathを書く
  end
end

まあ何もしなければroot_pathに飛ぶのであんま使うことなさそう

ではでは

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails メール自動配信機能をActionMailerとwheneverを使用して実装する

Railsで毎日メールを送るためのバッチ処理を、ActionMailerとgemのwheneverを使用して実装しました。

手順

  1. gem 'whenever'導入
  2. schedule.rbにバッチ処理を記述
  3. schedule.rbの内容を登録
  4. メールを送信するメソッドを定義
  5. viewに送信するメール本文を作成
  6. logファイルを確認して実行されているか確認

環境

ruby '2.5.7'
gem 'rails', '~> 5.2.4', '>= 5.2.4.1'

前提

  1. Userテーブルにemailカラムがあること
  2. ActionMailerで送信するサーバーの設定が出来ていること
  3. メール送信するために必要なApplicationMailerを継承するクラスを作成していること

※ (UserMailerという名前で作成済)

1. gem 'whenever'導入

'whenever'って?
自動的にメール送信するなど、バッチ処理を行うためにはcronというものを使ってcrontabにそのための記述をします。
でもその小難しいものを簡単に書けるようにするgemが'whenever'です。

それではgem 'whenever'をインストールします。
gemファイルに入力。

Gemfile
gem 'whenever', require: false

ターミナルでGemをインストールします。

$ bundle install

2. schedule.rbにバッチ処理を記述する

まずはschedule.rb ファイルを作成します。
以下をターミナルに入力する

$ wheneverize .
 [add]  writing './config/schedule.rb'
 [done] wheneverized!

今作ったschedule.rbの中にバッチ処理を記述します

config/schedule.rb
# Use this file to easily define all of your cron jobs.
#
# It's helpful, but not entirely necessary to understand cron before proceeding.
# http://en.wikipedia.org/wiki/Cron

# Example:
#
# 絶対パスから相対パス指定
env :PATH, ENV['PATH']
# ログファイルの出力先
set :output, 'log/cron.log'
# ジョブの実行環境の指定
set :environment, :development
#
# every 12.hours do
#   command "/usr/bin/some_great_command"
#   runner "MyModel.some_method"
#   rake "some:great:rake:task"
# end
#
every 1.days, at: '9:00 am' do

# Rails内のメソッド実行
  runner "UserMailer.notify_user"
end

# Learn more: http://github.com/javan/whenever

絶対パスから相対パス指定

env :PATH, ENV['PATH']
ENV['PATH']を特に何も定義してないけど書いたら上手くいった。
これがないとエラーになるので注意。

ログファイルの出力先

set :output, 'log/cron.log'
log/cron.logが新規作成され、バッチ処理の記録が入る。

ジョブの実行環境の指定

set :environment, :development
environmentが環境という意味。

:development
rails内の開発環境。今回はこれを指定してます。
:production
デプロイ後の本番環境
:test
テスト実行環境(Rspecなど)

バッチ処理の記述

every 1.days, at: '9:00 am' do
英文そのまま。毎日朝の9時に行います。

runner "UserMailer.notify_user"
UserMailer内のnotify_userメソッドを実行します。
ApplicationMailerを継承するクラスの中のメソッドを指定しています。

runner
Railsに定義されたメソッドを呼び出すために使用します。
今回の場合はUserMailernotify_userメソッドを呼び出すのでこれに当てはまる。
UserMailerは以前に自分で作成したものです)
※ユーザーに直接関係のある機能の場合に使用する。コントローラやモデルを呼び出せる。

rake
実行させたいrakeファイルを指定する場合に使用します。
今回は使ってません。使用する場合は別途rakeファイルを作成する。
※システムの管理上必要な機能の場合に使用する。定時になると自動的にデータを削除するなど。

3. schedule.rbの内容を登録する

現状のcrontabに何が記述されているのかを確認します。

$ crontab -e

中はいろいろ書いてるけど、先ほど記述したUserMailer.notify_userの一文があればOK

※nanoエディタが起動されます。
終了する時は焦らずにCtrl + x

問題がなければ以下のコマンドを実行して登録します。

$ bundle exec whenever --update-crontab

上記のコマンドで、作成したジョブがcrontabに登録されました。
反映されているか確認します。

$ crontab  -l

ちゃんとUserMailer.notify_userの記述があれば登録されてます

4. メールを送信するメソッドを定義

メールを送信するnotify_userメソッドを定義します。
user全員にメールを送信するようにしてます。

mailers/user_mailer
class UserMailer < ApplicationMailer
    def notify_user
        default to: -> { User.pluck(:email) }
        mail(subject: "everyday Bookers!yay!")
    end
end

default to: -> { User.pluck(:email) }
宛先を指定します。pluckでUserのemailを全て取得する。

mailers/application_mailer
class ApplicationMailer < ActionMailer::Base
  default from: 'from@example.com'
  layout 'mailer'
end

application_mailerには全部のメールに共通する設定を記述します。
default from: 'from@example.com'
サーバーに登録してあるメールアドレスが勝手に入ります。

5. viewに送信するメール本文を作成する

user_mailer内にnotify_user.text.erbと新規作成する。
※textで作成しましたがhtmlでも良い。user.nameなど変数が呼び出せます。

user_mailer/notify_user.text.erb
内容を書きます

6. logファイルで実行されているか確認する

実行されているか確認したい場合は、1分毎にメールが送られる設定に書き換えてみましょう。logを確認するためです、仕方ない。
every 1.days, at: '9:00 am' do
をコメントアウトして、以下の一文に書き換えます。
every 1.minutes do

config/schedule.rb
# Use this file to easily define all of your cron jobs.
#
# It's helpful, but not entirely necessary to understand cron before proceeding.
# http://en.wikipedia.org/wiki/Cron

# Example:
#
# 絶対パスから相対パス指定
env :PATH, ENV['PATH']
# ログファイルの出力先
set :output, 'log/cron.log'
# ジョブの実行環境の指定
set :environment, :development
#
# every 12.hours do
#   command "/usr/bin/some_great_command"
#   runner "MyModel.some_method"
#   rake "some:great:rake:task"
# end
#

every 1.minutes do ##ここを追加
# every 1.days, at: '9:00 am' do ##ここをコメントアウト

# Rails内のメソッド実行
  runner "UserMailer.notify_user"
end

# Learn more: http://github.com/javan/whenever

忘れずにターミナルで設定を登録します。

$ bundle exec whenever --update-crontab

logファイルを見ると1分毎にメールが送られているのが確認できます。
下記の通りに1分毎に一行増えて表示されていたらOK
Userにめっちゃメールが送られているのが分かる。

log/cron.log
Running via Spring preloader in process 8692
Running via Spring preloader in process 8714
Running via Spring preloader in process 8731
Running via Spring preloader in process 8748

おまけ

そんな頻繁にメールが送られてきたらいやだからcrontabの中身を消しましょう。
以下のコマンドで消せます。

$ whenever --clear-crontab

バッチ処理を再開したい時は以下のコマンドを入力。
またメールが送られるようになります。

$ bundle exec whenever --update-crontab

参考にさせて頂きました

https://freecamp.life/rails-whenever/
https://qiita.com/Esfahan/items/e7a924f7078faf3294f2
https://www.school.ctc-g.co.jp/columns/masuidrive/masuidrive22.html

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rrailsにcsvファイルを簡単に読み込めるgemを作る

背景

とあるサービスをrailsで作っていたが、初期データが多くてseedにすべて書くのは大変&エンジニア以外にも手伝ってもらいたかったので、csvファイルでseedデータを作ることにしました。
csvファイルをseed.rbで読み込む方法はすでに存在しますが、csvファイル1つずつ読み込んだり、一括で読み込むものもvalidationを無視するものが多いです。かといって、validationを守りつつcsvファイルを読み込むためには、読み込む順番を制御しなければなりません。
コードを書いてると60行ぐらいになり、rubyのメタプログラミング的手法(カッコイイ)も使ったので、練習も兼ねてgem化することにしました。

成果物

https://github.com/aitaro/csv_seeder
(よかったら使ってね)

他のcsv読み込み系qiita

無限にあったので検索結果から探してください。

準備

RubyGemsの作り方を参考に初期設定をしていきます。
ちなみにgem名はcsv_seederにしました。

設計

まず、想定するcsvファイルは下のようなものです。(csvにidまで含めているのは、relationで指定することができるからです。)

posts.csv
id,name,status
1,プログラミングの書き方,release
2,rails基礎,draft
comments.csv
id,post_id,body,rate
1,1,よく分かった,5
2,1,少し分かった,4
3,2,わからなかった,3

基本的な設計方針として、csvファイルを順番に読み込みつつ、依存関係(belongs_to等)によりvalidationに引っかかるcsvファイルを後回しにします。

例えば以下のようなモデルのとき、

class Post
  has_many :comments
end

class Comment
  belongs_to :post
end

先にCommentを読み込むと、指定したPostがないよと怒られます。なのでPostを先に読み込んでからComnmentを読まないいけません。

このように"validationに引っかかるもの"のほとんどはbelongs_toによるものです。

実装

基本ループの実装です。

def initialize(folder_path, orders)
  @folder_path = folder_path
  @orders = orders
  @dirs = Dir.glob(folder_path + "/*").shuffle 
end

def dirs_loop!
  while !@dirs.empty?
    if invalid_order?
      postpone!
      next
    end
    if has_relations?
      postpone!
      next
    end
    /db\/seeds\/(\w*)\.csv/ =~ @dirs[0]
    model = $1.classify
    CSV.foreach(@dirs[0], headers: true) do |row|
      Object.const_get(model).create!(**row.to_hash)
    rescue => e
      p "Error! During #{model}"
      p e
    end
    p "#{model} saved!"
    @dirs.shift
  end    
end

@dirsにcsvのpath一覧が入っています。これからinvalid_order?has_relations?等の条件を適合したものだけをcreateしています。
CSV.foreachによりcsvファイルの読み込み、Object.const_get(model)により動的にクラスを呼び出しています。

has_relations?の実装です。親クラスがまだ読み込まれていない場合は後回しにします。

def has_relations?
  CSV.open(@dirs[0], &:readline).any? do |header| 
    id_list = @dirs.map do |dir|
      dir_to_plural(dir).singularize + '_id'
    end
    id_list.include? header
  end
end

invalid_order?の実装です。@ordersはhas_relations?で補足できない依存関係を、ユーザーからのインプットパラメータとして受け取ったものです。@ordersに禁止するものを後回しにしています。

def invalid_order?
  @orders.each do |order|
    index = order.index(dir_to_plural(@dirs[0]).to_sym)
    next if !index || index == 0
    return true if @dirs.any?{|dir| order[0..(index-1)].map(&:to_s).include? dir_to_plural(dir)}
  end
  false
end

所管

簡単なgemですが、公開するとやりきった感があってたのしいです。なにか提案等あればこの記事のコメントかissueかに書いてください。(反応できるかわからないですが、、)

TODO

個人的メモです。ここまで書いて力尽きました。以下のことはまた今度やります。

  • gem使い方の説明を書く
  • 依存関係をはっきりさせる。
  • gemのtestコードを書く
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsのscopeを読む

Railsのscopeのソースコードを読んでいきます。

u = User.new(name: 'sample name')

scopeを読んでいくために、上記のようなオブジェクトをサンプルとして作成します。

    1: class User < ApplicationRecord
    2: 
    3: binding.pry
 => 4:   scope :recent, -> { order(id: :desc).limit(5) }
    5: end

中を読んでいきます。

    165:         # We are able to call the methods like this:
    166:         #
    167:         #   Article.published.featured.latest_article
    168:         #   Article.featured.titles
    169:         def scope(name, body, &block)
 => 170:           unless body.respond_to?(:call)
    171:             raise ArgumentError, "The scope body needs to be callable."
    172:           end
    173: 
    174:           if dangerous_class_method?(name)
    175:             raise ArgumentError, "You tried to define a scope named \"#{name}\" " \

bodyがcallを呼べなかったら、例外を返す。
今はbodyがcallを呼べるので次へ。

    169:         def scope(name, body, &block)
    170:           unless body.respond_to?(:call)
    171:             raise ArgumentError, "The scope body needs to be callable."
    172:           end
    173: 
 => 174:           if dangerous_class_method?(name)
    175:             raise ArgumentError, "You tried to define a scope named \"#{name}\" " \
    176:               "on the model \"#{self.name}\", but Active Record already defined " \
    177:               "a class method with the same name."
    178:           end
    179: 

nameがクラスメソッドとして定義されていないか判定。
今は定義されていないので次へ。

    175:             raise ArgumentError, "You tried to define a scope named \"#{name}\" " \
    176:               "on the model \"#{self.name}\", but Active Record already defined " \
    177:               "a class method with the same name."
    178:           end
    179: 
 => 180:           if method_defined_within?(name, Relation)
    181:             raise ArgumentError, "You tried to define a scope named \"#{name}\" " \
    182:               "on the model \"#{self.name}\", but ActiveRecord::Relation already defined " \
    183:               "an instance method with the same name."
    184:           end
    185: 

nameがインスタンスメソッドとして定義されていないか判定。
今は定義されていないので次へ。

    181:             raise ArgumentError, "You tried to define a scope named \"#{name}\" " \
    182:               "on the model \"#{self.name}\", but ActiveRecord::Relation already defined " \
    183:               "an instance method with the same name."
    184:           end
    185: 
 => 186:           valid_scope_name?(name)
    187:           extension = Module.new(&block) if block
    188: 
    189:           if body.respond_to?(:to_proc)
    190:             singleton_class.define_method(name) do |*args|
    191:               scope = all._exec_scope(name, *args, &body)

scopeの名前として有効かどうか判定する。nameがメソッドとして定義されていないので、次へ。

    182:               "on the model \"#{self.name}\", but ActiveRecord::Relation already defined " \
    183:               "an instance method with the same name."
    184:           end
    185: 
    186:           valid_scope_name?(name)
 => 187:           extension = Module.new(&block) if block
    188: 
    189:           if body.respond_to?(:to_proc)
    190:             singleton_class.define_method(name) do |*args|
    191:               scope = all._exec_scope(name, *args, &body)
    192:               scope = scope.extending(extension) if extension

blockが存在しているなら、blockを引数としたてModuleを新しく作る。今回はblockが存在しないので、次へ。

    184:           end
    185: 
    186:           valid_scope_name?(name)
    187:           extension = Module.new(&block) if block
    188: 
 => 189:           if body.respond_to?(:to_proc)
    190:             singleton_class.define_method(name) do |*args|
    191:               scope = all._exec_scope(name, *args, &body)
    192:               scope = scope.extending(extension) if extension
    193:               scope
    194:             end

bodyがto_procメソッドをを呼べるか判定。呼べるので、if分の中へ。

    185: 
    186:           valid_scope_name?(name)
    187:           extension = Module.new(&block) if block
    188: 
    189:           if body.respond_to?(:to_proc)
 => 190:             singleton_class.define_method(name) do |*args|
    191:               scope = all._exec_scope(name, *args, &body)
    192:               scope = scope.extending(extension) if extension
    193:               scope
    194:             end
    195:           else

bodyで与えられた処理を実装する、nameというクラスメソッドを定義する。

    198:               scope = scope.extending(extension) if extension
    199:               scope
    200:             end
    201:           end
    202: 
 => 203:           generate_relation_method(name)
    204:         end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

可読性 のある書き方

モデル

人の年齢を表すカラムに「歳」を加えたり、重さを表すカラムに「キログラム」を加えるメソッドは、ビューでのみ使用される見た目を整えるロジックです。このような見た目に関わるロジックは、ヘルパーかデコレーターに記述します。

# ☓:ビューでしか使用しないメソッドをモデルに定義している

# 例) Userモデルにcreated_atを年, 月, 日の形に変換するメソッドを定義している
class User < ApplicationRecord
  def adjust_created_at
    created_at.to_date.strftime("%Y年%m月%d日")
  end
end

 # ◯:ヘルパーファイルにビューで使用するロジックを定義している
module ApplicationHelper
  def set_created_at(created_at)
    created_at.to_date.strftime("%Y年%m月%d日")
  end
end

# ◎:ビューで使用するロジックのうち、表示フォーマットを整えるものを、DraperもしくはActiveDecoratorを導入して実装している

# 例) ActiveDecoratorを使用してcreated_atを年, 月, 日の形に変換するメソッドを定義している
module UserDecorator
  def set_created_at
    created_at.to_date.strftime("%Y年%m月%d日")
  end
end

1つのメソッドで多くのことをやりすぎていないかの確認
例として、Userモデルに定義されたクラスメソッドchange_attributesが、tweet, commentの変更まで行なってしまっています。
1つのメソッドで行う処理はなるべく簡潔にまとめます。

# X:Userモデルに定義された、change_attributesメソッドが、Tweet, Commentまで変更してしまっている
class User < ApplicationRecord
  def self.change_attributes(user, tweet, comment)
    user.attributes = { family_name: "Hoge", first_name: "Fuga" }
    tweet.attributes = { body: Foo }
    comment.attributes = { body: Bar}
  end
end

ロジックをスコープとしてモデルに定義します。

#  ◯:コントローラでよく使用するロジックをスコープとしてモデルに定義している

class User < ApplicationRecord
# 〜省略〜
 scope :team_red, -> { where(team: 0) }
 scope :captain, ->{ where(captain: true) }
end

# 上記のスコープにより、User.team_red.captainのように直感的にロジックを記述できる

コントローラ

不必要な自作アクションの確認
基本的にはrailsの7つのアクションで実装します。
アクションの内部の記述が複雑になってしまう場合は、モデルやモジュールを活用して、別のファイルに処理を移します。

# X:makeアクションはcreateとやっていることが変わらない

def create
  @user.create(create_params)
end

def make
  @user.create(create_params)
end

# ◯:なるべく7つのアクションの範囲でルーティングを定義する

def create
  @user.create(create_params)
end

N+1問題は起こっていないかを確認します。
has_many, belongs_toなどアソシエーションを設定しているモデルを扱う際には、includeなどの対策を忘れずに行います。

# X:1対Nの関係で、includeをしていない
# 1つのCompanyが複数のEmployeeをもっている例
# includeをしないとEmployeeの数だけCompanyをSELECTするSQLが発行されるため、レコードが増えるほど重くなる

def index
  @employees = Employee.all 
end

# ◯:変数を定義する際にincludeしている
# includes(:employee)と記述することで、SQLの発行回数を抑えて動作を軽くできる

def index
  @employees = Employee.all .includes(:employee)
end

ビュー

部分テンプレートの活用
特定のビューを繰り返したい時は、eachより動作の軽い部分テンプレートを活用します。

# X:レコードの数だけ同じビューを繰り返す部分で、eachを使用している
h2
- @users.each do |user|
  .user-container
    = user.name
    = user.profile
    = user.image
end

# ◯:繰り返しを部分テンプレートで記述している
# 部分テンプレートを使用する理由は動作が軽いから
h2
  = render partial: 'user-contaier', collection @users

ルーティング

不要なルーティングを定義していないかの確認
不要なルーティングを定義してしまうと、万が一該当するパスにユーザーが遷移した時に500エラーが表示されてしまいます。また、意図せずupdate, destroyなどのメソッドが動いてしまう危険性もあるため、必要なルーティングのみを定義するようにします。

# X : indexしか必要ないのに、resourcesで丸ごと定義している
resources :items

# ◯:only, exceptを使用して必要なルーティングのみ定義している
resources: items, only: :index

可読性

スペースの数、ネストの深さの確認

# X:スペースの数がバラバラになっている
# fugafugaもhogehogeも同じクラスのインスタンスメソッドなのに、スペースの数が多い

def hogehoge
  puts "こんにちは"
end

  def fugafuga
    puts "Hello"
  end

# ◯:スペースの数が揃っている

def hogehoge
  puts "こんにちは"
end

def fugafuga
  puts "Hello"
end

命名規則

変数の名前は適切か
変数の名前は、一目見れば変数の中身が分かるような命にします。

// X:アルファベット1文字
// ※Javascriptのローカル変数ではたまに1文字だけの変数名を定義するが、rubyではあまり使わない

p = Person.first
s = Ship.all

// X:変数の中身が分からない名前
// 「一体どのモデルのeverythingなのか?」となってしまい、変数の目的が伝わらない

@everything = Tweet.all

// X:略語
// 略語は元の単語を想像しづらく、略し方も人それぞれ異なってしまうので使うべきでない
// 変数の中身が単数なのか複数なのかも分かりづらい

@au = User.admin

// ◯:
first_person = Person.first
ships = Ship.all

@tweets = Tweet.all

@admin_users  = User.admin

キャメルケースとスネークケースを使い分けているか

# キャメルケースは先頭が小文字で、単語の区切りを大文字で表す
# アッパーキャメルケースは先頭から大文字を使用する

キャメルケース: adminUserCommentCreator
アッパーキャメルケース: AdminUserCommentCreator


# スネークケースは単語の区切りをアンダースコアで表す
スネークケース: admin_user_comment_creator


Railsの慣習的な命名規則として、下記のように使い分ける。

クラス名:アッパーキャメルケース
メソッド名:スネークケース
変数名:スネークケース

また、Javascriptでは一般的にキャメルケースを用いて記述する。

ハイフンとアンダースコアを使い分けとBEMでクラス名を定義できているかの確認
ハイフンとアンダースコアを明確に使い分けて命名を行うと、可読性が大きく向上します。CSSも書きやすくなる、BEM記法を用いてクラス名を定義します。

// X:ハイフンとアンダーバーを使い分けていない

.review_box
  %ul.reviews-list
   %li.review_1
   %li.review_2
   %li.review_3_red
   %li.review_4


// ◯:ハイフンとアンダーバーを使い分け、BEMでクラス名を定義している
// 親要素のクラス名からアンダースコアを2つ並べて、クラス名を定義している
// 同じ要素のうち、差分を定義するクラス名をハイフンを使って定義している(-red)

.reviews
  %ul.reviews__list
    %li.reviews__list__row
    %li.reviews__list__row    
    %li.reviews__list__row.reviews__list__row-red
    %li.reviews__list__row

設計

単一責任原則
単一責任原則とは、あるクラスを変更するべき理由は2つ以上あるべきでないというオブジェクト志向の原則の1つです。当該原則を満たしていないコードは、バグの原因となりやすく、将来の変更にも弱くなってしまいます。
参考サイト

# ☓:Gearクラスが車輪に関係するリム、タイヤの情報を持っている

# 自転車のギアを表すGearクラスが、「車輪のリムの直径、タイヤの厚み」まで責任を持ってしまっている
# Wheelクラスを作り、rim, tireの定義を移すべき

class Gear
  attr_reader :chainring, :cog, :rim, :tire
  def initialize(chainring, cog, rim, tire)
    @chainring = chainring
    @cog       = cog
    @rim       = rim
    @tire      = tire
  end

  def ratio
    chainring / cog.to_f
  end

  def gear_inches
    ratio * (rim + (tire * 2))
  end
end

# 下記のコードはギアに関する情報しか渡していないにも関わらずエラーになる
puts Gear.new(52, 11).ratio
# ArgumentError: wrong number of arguments (2 for 4)


# ◯:Wheelクラスを作り、車輪に関する定義を移す

class Gear
  attr_reader :chainring, :cog, :wheel
  def initialize(chainring, cog, wheel=nil)
    @chainring = chainring
    @cog       = cog
    @wheel     = wheel
  end

  def ratio
    chainring / cog.to_f
  end

  def gear_inches
    ratio * wheel.diameter
  end
end

class Wheel
  attr_reader :rim, :tire

  def initialize(rim, tire)
    @rim       = rim
    @tire      = tire
  end

  def diameter
    rim + (tire * 2)
  end

end


@wheel = Wheel.new(26, 1.5)
puts @wheel.circumference
# -> 91.106186954104

puts Gear.new(52, 11, @wheel).gear_inches
# -> 137.090909090909

単一責任原則に違反していないかチェックする方法
(1)クラスの持つメソッドを質問に置き換える

「Gearさん、あなたのgear_inchesを教えてください」
# Gearにギアに関する質問をしているのでセーフ

「Gearさん、タイヤの厚みとリムの直径を教えてください」
# Gearと無関係なタイヤの質問をしているのでアウト
# 該当する定義、メソッドを別クラスに切り出すべき

(2)1つの文章でクラスを説明してみる

「Gearクラスはチェーンリングとコグ、その比率を持っています」
# ギアの役割を短い文章で表現できているのでセーフ

「Gearクラスはチェーンリングとコグ、その比率を持っており、更に関連した車輪のタイヤの厚みとリムの直径を持っていて、ギアインチを計算することがで計算することができます」

# 文章が長くなりすぎている
# 上手く文章にできない場合は、1つのクラスで多くのことをやりすぎている可能性が高い

マジックナンバーを使用していないかの確認
変数を定義する際、メソッドで数値をやり取りする際などに、生のまま扱われている数字をマジックナンバーといいます。
マジックナンバーを含むコードは、数字を見ただけでは「その数字が何を意味するのか」が分からないため、コメントアウトで数字の説明をする、定数を使う、といった工夫をして、なるべく数字に意味を持たせます。

# X:インスタンスを作成する際にマジックナンバーを用いて値を代入している
# コードを見ただけは「type: 1」がどういった商品タイプを示しているのか分からない

Product.create {name: 'hogehoge', type: 1}

# ◯:コメントアウトで数字の説明をしている

# type: 1は日本産の商品
Product.create {name: 'hogehoge', type: 1}


# ◯:定数を定義して値を代入している
# コードを読むだけで「type: 1」が日本に関連した商品であることがわかる

JAPANESE_PRODUCT_TYPE = 1.freeze
Product.create {name: 'hogehoge', type: JAPANESE_PRODUCT_TYPE}

変数にnilや0が入っても大丈夫な構造か
バリデーションをかけていない、null制約を定義していないなどの理由で、変数にnil、0が入ってしまうことがあります。このままnilの変数に対してメソッドの呼び出しを行うと、Undefined method <メソッド名> for for nil:NilClassというエラーが出てしまいます。nilの場合は呼び出しが行われないようにするなどの対策を取ります。

# X:非ログイン時に表示されるビューで、current_user.idを指定している
# current_userはログイン時のみ使用できるdeviseのヘルパーなので、非ログイン時に使おうとするとエラーになる

= link_to "プロフィール", "users/profile/#{current_user.id}"


# ◯:該当箇所がログイン時のみ読み込まれるように条件を設ける

- if user_signed_in?
  = link_to "プロフィール", "users/profile/#{current_user.id}"
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Windows10でのRailsの導入がうまくいかず、WSLを用いた話

はじめに

Windows10でRailsの環境構築をしたところ、rails serverコマンドを実行したときにページをうまく表示できなかったので、WSLを用いることにしたときの備忘録です。

環境

  • Windows10(64bit)
  • Surface Pro 4
  • Ruby 2.6.5

Windows10での環境構築(失敗)

結局解決せずに諦めたので、興味がない方は読み飛ばしてください。

まず gem 経由で Rails をインストールします。

$ gem install rails -v 5.0.7.2

Rails プロジェクト用のディレクトリを作成し、そのディレクトリに移動してプロジェクトnew-appを作成します。

$ mkdir workspace
$ cd workspace
$ rails new new-app 

new-appディレクトリに移動してrails serverを実行したところ、次のエラーが発生しました。

cannot load such file -- sqlite3/sqlite3_native (LoadError)

そこで、色々調べた結果、以下の方法を実行しました。

1. sqlite3.dllとsqlite3.exeをPathがとおったディレクトリに配置

SQLite Download Page
の「Precompiled Binaries for Windows」欄にある「sqlite-dll-win64-x64-3310100.zip」をダウンロード・解凍し、入っている「sqlite3.dll」をC:\Ruby26-x64\binに配置します。

また、同じ欄にある「sqlite-tools-win32-x86-3310100.zip」をダウンロード・解凍し、入っている「sqlite3.exe」をC:\Ruby26-x64\binに配置します(圧縮ファイル名の数字が変わっているときもあるので、適時読み替えてください)。

2. sqlite3_native.soファイルを生成して配置

同様にSQLite Download Pageの「Source Code」欄にある「sqlite-amalgamation-3310100.zip」をダウンロード・解凍してC:下に配置し、以下のコマンドを実行してsqlite3_native.soファイルを生成します(ファイル名の数字やRubyのバージョンは適宜読み替えてください)。

gem install sqlite3 --platform=ruby -- --with-sqlite3-include=C:/sqlite-amalgamation-3310100 --with-sqlite3-lib=C:\Ruby26-x64\bin

これでC:\Ruby26-x64\lib\ruby\gems\2.6.0\gems\sqlite3-1.3.14\lib\sqlite3\sqlite3_native.soにファイルが生成されます。

次に、C:\Ruby26-x64\lib\ruby\gems\2.6.0\gems\sqlite3-1.3.13-x64-mingw32\lib\sqlite3に新しくディレクトリ2.6を作成します。このディレクトリの名前はRubyのバージョンに対応させてください。そしてこの中に先ほど生成したsqlite3_native.soをコピーします。

上記の作業の後にもう一度rails serverコマンドを実行します。すると今度は以下のように表示され、ローカルサーバーが立ち上がりました。

=> Booting Puma
=> Rails 5.0.7.2 application starting in development on http://localhost:3000
=> Run `rails server -h` for more startup options
*** SIGUSR2 not implemented, signal based restart unavailable!
*** SIGUSR1 not implemented, signal based restart unavailable!
*** SIGHUP not implemented, signal based logs reopening unavailable!
Puma starting in single mode...
* Version 3.12.4 (ruby 2.6.5-p114), codename: Llamas in Pajamas
* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://localhost:3000
Use Ctrl-C to stop

ところが、http://localhost:3000/ をブラウザで開くと、以下のエラーページが表示されました。
sqlite3-error.PNG

そこで、Gemfile中の

gem 'sqlite3'

の部分を

gem 'sqlite3', '1.3.13'

に書き換え、bundle installを実行した後に再度試したところ、以下のエラーページに変わりました。
wrong-number.PNG

これを解決しようとしたのですが、 Windows10 で結局 Rails の環境構築に失敗したという記事が多数あり、解決できない可能性が高いと思ったのでWSL( Windows Subsystem for Linux ) を用いてみることにしました。

WSLを用いたRailsの環境構築

WSLの導入

まずWSLをインストールします。
Windowsの検索ボックスから「Windowsの機能の有効化または無効化」のウィンドウを出し、「Windows Subsystem for Linux」のチェックボックスにチェックを入れます。インストールが始まるので、それが終わったら再起動します。再起動後、Microsoft StoreでUbuntuを検索し、「入手」ボタンを押してインストールします。その後「起動」ボタンを押すと、そこそこ時間がかかったあとにユーザー名とパスワードの設定を求められます。これでWSLの導入が終わりました。次にいよいよ Rails の環境構築を行います。

Railsの環境構築

1. Rubyのインストール

Rubyのインストールに必要な各種ライブラリを入れておきます。

$ sudo apt-get update
$ sudo apt-get install make
$ sudo apt-get install gcc
$ sudo apt-get install -y libssl-dev libreadline-dev zlib1g-dev

Rubyのバージョン管理を楽にしてくれる rbenv を導入してPATHをとおし、初期設定を行います。

$ git clone https://github.com/sstephenson/rbenv.git ~/.rbenv
$ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc
$ ~/.rbenv/bin/rbenv init
# Load rbenv automatically by appending
# the following to ~/.bashrc:

eval "$(rbenv init -)"
$ echo 'eval "$(rbenv init -)"' >> ~/.bashrc   

Node.jsも導入しておきます。ここでもバージョン管理用ツールである n を導入します。

$ sudo apt-get install npm
$ sudo npm install -g n
$ sudo n stable

Ruby を導入します。

$ git clone https://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build
$ rbenv install 2.6.5
$ rbenv rehash
$ rbenv global 2.6.5

Rails を導入します。

$ gem install rails -v 5.0.3

Railsを用いるのに必要なSQlite3とbundlerを導入します。

$ sudo apt-get install libsqlite3-dev
$ gem install sqlite3 -v 1.3.13
$ gem install bundler

以上で準備が整いました。ここからは、失敗したときと同じようにプロジェクトを作成し、サーバーを立てることを試みます。

$ mkdir workspace  
$ cd workspace
$ rails new new-app 
$ cd new-app
$ bundle install
$ rails server

すると先ほどと同じエラーページ
sqlite3-error.PNG
が表示されるので、同様にGemfile中の

gem 'sqlite3'

の部分を

gem 'sqlite3', '1.3.13'

に書き換え、bundle installを実行した後に再度試したところ、無事に以下のページが表示されました!
rails-server.PNG

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

素人がWebサービスを作る備忘録(実装編)①

はじめに

今回は前準備編設計編の続きです。そちらに筆者のスキルレベルやこの記事の目的などが書いてありますので、先にお読みください。

実装にあたって

ここから実装に入っていくわけですが、稚拙な設計となっている為、かなりの回数の手戻り作業が発生することが見込まれます。その辺りもうまくいかなかった理由と改善策をまとめつつまとめていきたいと思います。
そして、実装にあたり個人的にまとめておきたいと思った昨日の追加などは別途記事にしてまとめていきたいと思います。(その方があとで見返すのとかも楽だし、、、)

そして差し当たり、
 ・ユーザー管理機能
 ・投稿機能
 ・投稿一覧、投稿詳細機能
 ・画像ファイルアップロード機能
 ・ページネーション機能or無限スクロール機能
 ・DBテーブルのリレーション管理
 ・単体、統合テスト

この辺のrailstutorialで行った内容を実装し、Webサービスの全体像を作りたいと思います。

基本的に開発環境やデプロイはrailstutorialをなぞる感じで行っていきます。

Railsをインストール

まずはじめに
gem install rails -v 5.1.6というコマンドで使用する。railsのバージョンを指定してインストールします。

アプリケーションを立ち上げよう

さて、railsのインストールが終わったらやることはアプリの骨組みを作ることです。
ここではrails newコマンドを使用して骨組みを作成します。
しかし、ここで
No value provided for required arguments 'app_path'
というエラーメッセージが出てきました。これは要は「rails newしかしていません」という意味です。
rails newコマンドは出力先を指定してやらないと実行することはできません。なのでここでは
rails new appなどとしてやるのが正しいやり方です。
(このミスをしたということで筆者のレベル感を分かって頂きたい。。。)

Bundler

そしたらここでbundlerを実行して必要なgemをインストールしていきましょう。
「???」
初めて見たときは私はこうなりました。分解して見ていきましょう。
gemというのは誰かが作ってくれた、コンピューターの機能(例えばEnter押すだけで勝手にテストしてくれたり)です。
そして、これをインストールしたりアップデートしたりするgemがbundlerという機能(以下ライブラリと言います。)です。
面白いのはこのbundlerはgemfileとかgemfile.lockなどのgemを使用し他のgemを管理してくれる。ライブラリなのですが自身もgemというところです。

はい!ではgemfileをインストールしていきましょう。gemfileにインストールしたいgemとバージョンを書いて(今回はrails tutorialと同じgemで基本行っていきます。)bundle installを実行します。
ここで「bundle updateをしてください」というエラーメッセージが出た場合はbundle updateした後にもう一度bundle installしましょう。

Gitで管理

rails tutorialに倣いGitを使用してバージョン管理を行なっていきたいと思います。
Gitって何や?ってことは記事は書こうと思っています。
とりあえず
ルートディレクトリに戻ってgit initをします。これでローカルリポジトリが作成されました。
そしてリポジトリに追加したいファイルをgit add -Aでインデックスに追加します。(追加されたか確かめたいときはgit statusで確認できます。)
そして、git commitでインデックスに追加しておいたファイルをリモートリポジトリに追加します。この時にgit commit -m "コメント"とすることでlogにコメントを残しておくことができます。

Github

rails tutorialではソースコードの完全なバックアップと他の開発者との共同作業のためにはbitbucketを使用していましたが、新しい取り組みとしてGithubを使用して行なっていきたいと思います。

まず、Githubで登録します。そしたら公開鍵と秘密鍵を作成します。

ターミナル
$ cd     #ホームディレクトリに移動
$ cd .ssh  #sshを作るディレクトリに移動
$ ssh-keygen #鍵を作成
Generating public/private rsa key pair.
Enter file in which to save the key (/home/ec2-user/.ssh/id_rsa): ここにid_rsaと入力
Enter passphrase (empty for no passphrase): パスフレーズを入力
Enter same passphrase again: 確認の為もう一度入力
Your identification has been saved in id_rsa.
Your public key has been saved in id_rsa.pub.
.
.
.
#変な絵が出る
.
.
.
/.ssh $ ls  #ここで秘密鍵と公開鍵が生成されていればおっけい

作った後はGithubに公開鍵をコピーしましょう。

ターミナル
$ cd ~/.ssh
ec2-user:~/.ssh $ ls
authorized_keys id_rsa  id_rsa.pub
$ cat id_rsa.pub
#ここに出てくる奴をコピーしてGithubに登録

これで公開鍵と秘密鍵の作成とGithubへの登録ができました。

次に新規リポジトリをを作成しました。
そしたら

ターミナル
$ git add -A    
$ git commit -m "new my_app"
$ git remote add origin リモートリポジトリのアドレス
$ git push origin master

これでおっけい

コマンド確認

・git remote add origin ~
リモートリポジトリに反映させる前にリモートリポジトリの情報を追加しておいて、どのリモートリポジトリに反映させるのか明らかにさせておく。
・git remote rm origin ~
上記のコマンドの逆でリモートリポジトリの情報を削除する場合に使う。
・git push origin master
このコマンドでローカルリポジトリに貯めておいた変更履歴をリモートリポジトリにpushする。

Heroku

デプロイはHerokuで行います。頻繁に本番環境にデプロイすることによって早い段階でサービスの問題点が発見できます。

Gemfile
group :development, :test do
  gem 'sqlite3', '1.3.13'  
.
.

end
.
.
.
group :production do
  gem 'pg', '0.20.0'
end

HerokuはSQLiteをサポートしていないためgemを上記のようにいじっておいてローカルではSQLite、本番環境ではpostgreSQLを使用できるようにしておく。

ターミナル
$bundle install --without production

このようにすることにより開発環境でのpg gem が作動しないようにしておく。

さてHerokuをインストールするにあたり以下のコマンドを実行しておく

ターミナル
$ source <(curl -sL https://cdn.learnenough.com/heroku_install)

意味は理解できませんがこれでインストールできます。

ターミナル
$ heroku login   # Herokuにログイン
$ heroku create  # Herokuに実行環境を構築
$ git push heroku master  # Herokuにデプロイ

これで作成したURLでブラウザを立ち上げて公開できていたら成功です。

参考

今さら聞けない!GitHubの使い方【超初心者向け】
git remoteを使ってリモートリポジトリの追加と削除を行う方法【初心者向け】
Ruby on Rails チュートリアル 第1章 MVCモデルからGitやHeroku 鍵生成までの流れを解説

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsでデフォルトのアプリケーションサーバ、Pumaについて

はじめに

今まではあまり気にしてこなかったのですが、そろそろ $ rails server したときに表示されるpumaについて知っていた方がいいなと思い調べたのでまとめておきます。

筆者は初学者なので間違いがあれば優しく教えていただけると幸いです。

Pumaとは何か

Pumaとは、webサーバとしての機能もあるアプリケーションサーバです。

と言われて、自分は何言ってるのか分からなかったので1から説明します。

・webサーバとは

ユーザーからのリクエストを必要に応じてアプリケーションサーバに渡すプログラムです。
有名なものではNginx(エンジンエックス)などがあります。

・アプリケーションサーバとは

webサーバからのリクエストをrailsアプリケーションに渡す橋渡し的な存在です。
例えばユーザーがなんらかのリクエストをwebページに送ったとします。このリクエストは

webサーバ → アプリケーションサーバ(Pumaなど) → Rack → Railsのrouter → controller

という風に受け渡されて、アプリケーションで処理が行われます。

Rackについてはアプリケーションサーバとアプリケーションを繋ぐ魔法と思ってください。

Pumaの特徴

マルチスレッドで大量のアクセスを効率的に処理することができるそうです。
複数のことを同時に(並列処理で)できるので、速いのだと思います。

終わりに

自分用にまとめてみた感じなので、読みにくいかもしれません。
参考にした記事たちを紹介しておきます。僕みたいな初学者におすすめの記事たちです。

https://qiita.com/jnchito/items/3884f9a2ccc057f8f3a3
https://kitsune.blog/rails-summary

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails入門5: deviseのユーザー情報を利用しよう

Rails入門5: deviseのユーザー情報を利用しよう


#01:掲示板でユーザー情報を使おう
このレッスンでは、Scaffoldで作成した1行掲示板に、deviseを使ってユーザー認証機能を追加します。まずは、どのような機能を作るのか整理しましょう。

このレッスンで作る掲示板
- deviseで作成したユーザー認証に、1行掲示板を組み合わせる
- ログインしている時だけ投稿できる
- 投稿者名をdeviseのユーザー名にする
- 自分の投稿だけ、編集・削除できる

作成手順
- ログイン時だけ投稿できる掲示板を作る
- 1行掲示板の記事に、Emailを表示する
- Userモデルに、nameカラムを追加する
- Userモデルに、ユーザー名を保存する
- 投稿時にログインユーザー名を保存
- 自分の記事だけを編集・削除

参考になるWebページ
- [ASCII.jp:ユーザー認証でなにができるのですか?|セキュリティの素朴な疑問を解く]
http://ascii.jp/elem/000/000/436/436614/


#02:ログイン時だけ投稿できる掲示板を作ろう
ここでは、前回のレッスンで作成したユーザー認証機能を利用して、1行掲示板へのアクセスを制御します。誰でも記事を表示できて、登録したユーザーだけが新しい記事を投稿・編集できるようにしましょう。

1行掲示板を作成する

$ cd bbs_users
$ rails g scaffold article user_id:integer content:string
$ rails db:migrate

掲示板の初期データを投入する
db/seeds.rb

Article.create(user_id: 1, content: 'hello world')
Article.create(user_id: 1, content: 'hello paiza')
Article.create(user_id: 2, content: '世界の皆さん、こんにちは')

データベースに反映するには、次のコマンドを実行する

$ rails db:seed

ログイン時に、特定のアクションだけ実行できるようにする
articles_controller.rb

before_action :authenticate_user!, only: [:new, :create, :edit, :update, :destroy]
before_action :set_article, only: [:show, :edit, :update, :destroy]


演習課題「ユーザーログインに初期ユーザーを登録する・作成する」
右の環境には、「myblog」プロジェクトに「talk」という1行掲示板が作成されています(user_idとcontentというカラムを持っています)。

ここに、以下の初期投稿を登録してください。この時、seed.rbファイルで一括登録します。

  • user_id: 1, content: '吾輩は猫である'
  • user_id: 1, content: '名前はまだない'
  • user_id: 2, content: 'どこで生まれたか'
  • user_id: 3, content: 'とんと見当がつかぬ'
  • user_id: 1, content: 'なんでも'

採点して、すべてのジャッジに正解すれば、演習課題クリアです!


模範解答1
/home/ubuntu/myblog/db/seed.rbファイルに登録するデータを記述する。

Talk.create(user_id: 1, content: '吾輩は猫である')
Talk.create(user_id: 1, content: '名前はまだない')
Talk.create(user_id: 2, content: 'どこで生まれたか')
Talk.create(user_id: 3, content: 'とんと見当がつかぬ')
Talk.create(user_id: 1, content: 'なんでも')


演習課題「ログインしているときだけ投稿・編集・削除」
右の環境には、「myblog」プロジェクトに「talk」という1行掲示板が作成されています(user_idとcontentというカラムを持っています)。また、ユーザー認証構築用にdeviseを導入して、「User」というモデルを作成してあります。

この環境で、ログインしているユーザーだけが編集・削除できるように、talk_controllers.rbファイルを設定してください。編集・削除に対応するのは、以下のアクションです。

  • edit
  • update
  • destroy

採点して、すべてのジャッジに正解すれば、演習課題クリアです!

模範解答1
/home/ubuntu/myblog/app/controllers/talks_controller.rbに次の内容を記述する

class TalksController < ApplicationController
  before_action :authenticate_user!, only: [:edit, :update, :destroy]
  before_action :set_talk, only: [:show, :edit, :update, :destroy]

  # GET /talks
  # GET /talks.json
  def index
    @talks = Talk.all
  end

  # GET /talks/1
  # GET /talks/1.json
  def show
  end

  # GET /talks/new
  def new
    @talk = Talk.new
  end

  # GET /talks/1/edit
  def edit
  end

  # POST /talks
  # POST /talks.json
  def create
    @talk = Talk.new(talk_params)

    respond_to do |format|
      if @talk.save
        format.html { redirect_to @talk, notice: 'Talk was successfully created.' }
        format.json { render :show, status: :created, location: @talk }
      else
        format.html { render :new }
        format.json { render json: @talk.errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /talks/1
  # PATCH/PUT /talks/1.json
  def update
    respond_to do |format|
      if @talk.update(talk_params)
        format.html { redirect_to @talk, notice: 'Talk was successfully updated.' }
        format.json { render :show, status: :ok, location: @talk }
      else
        format.html { render :edit }
        format.json { render json: @talk.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /talks/1
  # DELETE /talks/1.json
  def destroy
    @talk.destroy
    respond_to do |format|
      format.html { redirect_to talks_url, notice: 'Talk was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_talk
      @talk = Talk.find(params[:id])
    end

    # Never trust parameters from the scary internet, only allow the white list through.
    def talk_params
      params.require(:talk).permit(:user_id, :content)
    end
end

#03:1行掲示板にEmailアドレスを表示しよう
ここでは、ユーザーの情報を掲示板に利用します。ここでは、投稿一覧にユーザーのメールアドレスを表示しましょう。

ArticlesモデルとUserモデルを関連付ける
model/article.rb

class Article < ApplicationRecord
belongs_to :user
end

投稿者のメールアドレスを表示する
iews/articles/index.html.erb

User Content

Welcomeページから、掲示板にリンクする
index.html.erb

Welcome#index

Find me in app/views/welcome/index.html.erb

<%= link_to "articles", articles_path %>

ナビゲーションを共通で表示する
app/views/layouts/application.html.erb


<% if user_signed_in? %>
Logged in as <%= current_user.email %>.
<%= link_to "Settings", edit_user_registration_path %> |
<%= link_to "Logout", destroy_user_session_path, method: :delete %>
<% else %>
<%= link_to "Sign up", new_user_registration_path, :class => 'navbar-link' %> |
<%= link_to "Login", new_user_session_path, :class => 'navbar-link' %>
<% end %>

<%= notice %>


<%= alert %>


<%= yield %>

演習課題「掲示板とユーザーを関連付ける」
右の環境には、「myblog」プロジェクトに「talk」という1行掲示板が作成されています(user_idとcontentというカラムを持っています)。また、ユーザー認証構築用にdeviseを導入して、「User」というモデルを作成してあります。

掲示板の投稿一覧では、Userモデルのメールアドレスを表示するようになっていますが、エラーになってしまいます。データベースの関連付けを設定して、メールアドレスが表示されるようにしてください。


模範解答1
/home/ubuntu/myblog/app/models/talk.rbに次の内容を記述する

class Talk < ApplicationRecord
belongs_to :user
end


演習課題「投稿一覧にメールアドレスを表示する」
右の環境には、「myblog」プロジェクトに「talk」という1行掲示板が作成されています(user_idとcontentというカラムを持っています)。また、ユーザー認証構築用にdeviseを導入して、「User」というモデルを作成し、talkと関連付けてあります。

この掲示板の投稿一覧に、Userモデルのメールアドレスを表示するように設定してください。

模範解答1
/home/ubuntu/myblog/app/views/talks/index.html.erbに次の内容を記述する

Talks

User Content


<%= link_to 'New Talk', new_talk_path %>


#04:Userモデルにnameカラムを追加しよう
ここでは、記事を投稿したユーザーの名前を表示するため、deviseのUserモデルに名前のカラムを追加します。それに合わせて、ユーザーの登録フォームを変更しましょう。

Userモデルにカラムを追加

$ rails g migration AddNameToUser name:string
$ rails db:migrate

コンソールで確認

rails console
User.all

サインアップ画面に「name」カラムを追加
app/views/devise/registrations/new.html.erb


ユーザー情報の変更画面に「name」カラムを追加
app/views/devise/registrations/edit.html.erb


参考になるWebページ
- [devise にusername カラムを追加し、usernameを登録できるようにする。 - Qiita]
https://qiita.com/yasuno0327/items/ff17ddb6a4167fc6b471

  • [初めてのdevise ② -- カラムを追加してみる -- ~ やってみようカスマイズ! ~ - Qiita]
    https://qiita.com/uloruson/items/40154b4be19d1ac900f3

  • [Railsのログイン認証gemのDeviseのカスタマイズ方法 - Rails Webook]

    http://ruby-rails.hatenadiary.com/entry/20140804/1407168000

    演習課題「Userモデルにnameカラムを追加する」
    右の環境には、「myblog」プロジェクトに「talk」という1行掲示板が作成されています(user_idとcontentというカラムを持っています)。また、ユーザー認証構築用にdeviseを導入して、「User」というモデルを作成して、talkと関連付けてあります。

このUserモデルに、nameカラムを追加してください。


模範解答1
次のコマンドを順にターミナルで実行する
cd myblog
rails g migration AddNameToUser name:string
rails db:migrate


演習課題「サインアップ画面と変更画面に、nameカラムを追加する」
右の環境には、「myblog」プロジェクトに「talk」という1行掲示板が作成されています(user_idとcontentというカラムを持っています)。また、ユーザー認証構築用にdeviseを導入して、「User」というモデルを作成し、talkと関連付けてあります。

このUserモデルにnameカラムを追加したので、次のフォームに、ラベルとテキストフィールドを追加してください。

  • edit.html.erb
  • new.html.erb

採点して、すべてのジャッジに正解すれば、演習課題クリアです!


模範解答1
/home/ubuntu/myblog/app/views/devise/registrations/edit.html.erbに次の内容を記述する

<h2>Edit <%= resource_name.to_s.humanize %></h2>

<%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %>
  <%= devise_error_messages! %>

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

  <div class="field">
    <%= f.label :name %><br />
    <%= f.text_field :name %>
  </div>

  <% if devise_mapping.confirmable? && resource.pending_reconfirmation? %>
    <div>Currently waiting confirmation for: <%= resource.unconfirmed_email %></div>
  <% end %>

  <div class="field">
    <%= f.label :password %> <i>(leave blank if you don't want to change it)</i><br />
    <%= f.password_field :password, autocomplete: "off" %>
    <% if @minimum_password_length %>
      <br />
      <em><%= @minimum_password_length %> characters minimum</em>
    <% end %>
  </div>

  <div class="field">
    <%= f.label :password_confirmation %><br />
    <%= f.password_field :password_confirmation, autocomplete: "off" %>
  </div>

  <div class="field">
    <%= f.label :current_password %> <i>(we need your current password to confirm your changes)</i><br />
    <%= f.password_field :current_password, autocomplete: "off" %>
  </div>

  <div class="actions">
    <%= f.submit "Update" %>
  </div>
<% end %>

<h3>Cancel my account</h3>

<p>Unhappy? <%= button_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?" }, method: :delete %></p>

<%= link_to "Back", :back %>


#05:Userモデルのユーザー名を保存しよう
ここでは、Userモデルにある名前のカラムをデータベースに保存できるようにします。

コントローラで、nameカラムを保存する
app/controllers/application_controller.rb

before_action :configure_permitted_parameters, if: :devise_controller?

protected

def configure_permitted_parameters
devise_parameter_sanitizer.permit(:sign_up, keys: [:name])
devise_parameter_sanitizer.permit(:account_update, keys: [:name])
end


演習課題「nameカラムを保存できるようにする」
右の環境には、「myblog」プロジェクトに「talk」という1行掲示板が作成されています(user_idとcontentというカラムを持っています)。また、ユーザー認証構築用にdeviseを導入して、「User」というモデルを作成し、talkと関連付けてあります。

このUserモデルに、nameカラムを追加したので、保存できるようにコードを修正してください。

採点して、すべてのジャッジに正解すれば、演習課題クリアです!


模範解答1
/home/ubuntu/myblog/app/controllers/application_controller.rbに次の内容を記述する

class ApplicationController < ActionController::Base
protect_from_forgery with: :exception

before_action :configure_permitted_parameters, if: :devise_controller?

protected

def configure_permitted_parameters
devise_parameter_sanitizer.permit(:sign_up, keys: [:name])
devise_parameter_sanitizer.permit(:account_update, keys: [:name])
end
end


#06:掲示板にユーザー名を表示しよう
ここでは、1行掲示板のナビゲーションと投稿一覧に、Userモデルのnameカラムを表示しましょう。

ナビゲーションのログイン情報に、ユーザー名を表示
app/views/layouts/application.html.erb

<% if user_signed_in? %>
Logged in as <%= current_user.name %>.
<%= link_to "Settings", edit_user_registration_path, :class => "navbar-link" %> |
<%= link_to "Logout", destroy_user_session_path, method: :delete, :class => "navbar-link" %>
<% else %>
<%= link_to "Sign up", new_user_registration_path, :class => 'navbar-link' %> |
<%= link_to "Login", new_user_session_path, :class => 'navbar-link' %>
<% end %>

投稿一覧に、nameカラムを表示する
views/articles/index.erb.html

Name Content

投稿の詳細画面に、nameカラムを表示する
views/articles/show.erb.html

User:


演習課題「投稿一覧と詳細画面に、Userモデルのnameカラムを表示する」
右の環境には、「myblog」プロジェクトに「talk」という1行掲示板が作成されています(user_idとcontentというカラムを持っています)。また、ユーザー認証構築用にdeviseを導入して、「User」というモデルを作成し、talkと関連付けてあります。

このUserモデルに、nameカラムを追加したので、投稿一覧と詳細画面で、user_idの代わりにUserモデルのnameカラムを表示してください。

採点して、すべてのジャッジに正解すれば、演習課題クリアです!


模範解答1
/home/ubuntu/myblog/app/views/talks/index.html.erbに次の内容を記述する

Talks

User Content


<%= link_to 'New Talk', new_talk_path %>

模範解答2
/home/ubuntu/myblog/app/views/talks/show.html.erbに次の内容を記述する

User:

Content:

<%= link_to 'Edit', edit_talk_path(@talk) %> |

<%= link_to 'Back', talks_path %>

#07:ログインユーザー名で投稿を保存しよう
ここでは、1行掲示板の投稿を、ログインしたユーザーの名前で保存できるようにします。すでに、ログインした時だけ投稿できるようになっているので、現在のログインユーザーで投稿できるようにしましょう。

新規投稿フォームを修正して、user_idを削除する
app/views/articles/_form.html.erb

<%= form_with(model: article, local: true) do |form| %>
<% if article.errors.any? %>


<%= pluralize(article.errors.count, "error") %> prohibited this article from being saved:

  <ul>
  <% article.errors.full_messages.each do |message| %>
    <li><%= message %></li>
  <% end %>
  </ul>
</div>

<% end %>


<%= form.label :content %>
<%= form.text_field :content, id: :article_content %>


<%= form.submit %>

<% end %>

createメソッドを修正する
app/controllers/articles_controller.rb

POST /articles

POST /articles.json

def create
@article = Article.new(article_params)
@article.user_id = current_user.id


演習課題「投稿一覧と詳細画面に、Userモデルのnameカラムを表示する」
右の環境には、「myblog」プロジェクトに「talk」という1行掲示板が作成されています(user_idとcontentというカラムを持っています)。また、ユーザー認証構築用にdeviseを導入して、「User」というモデルを作成し、talkと関連付けてあります。

talks_controllerを修正し、ユーザーがログインしている時だけ、
新規投稿のユーザー名をログインユーザー名にしてください。なお、ログインしているかどうかは「user_signed_in?」で判別できます。

採点して、すべてのジャッジに正解すれば、演習課題クリアです!


模範解答1
/home/ubuntu/myblog/app/controllers/talks_controller.rbに次の内容を記述する

class TalksController < ApplicationController
  before_action :authenticate_user!, only: [:edit, :update, :destroy]
  before_action :set_talk, only: [:show, :edit, :update, :destroy]

  # GET /talks
  # GET /talks.json
  def index
    @talks = Talk.all
  end

  # GET /talks/1
  # GET /talks/1.json
  def show
  end

  # GET /talks/new
  def new
    @talk = Talk.new
  end

  # GET /talks/1/edit
  def edit
  end

  # POST /talks
  # POST /talks.json
  def create
    @talk = Talk.new(talk_params)

    if user_signed_in?
        @talk.user_id = current_user.id
    end

    respond_to do |format|
      if @talk.save
        format.html { redirect_to @talk, notice: 'Talk was successfully created.' }
        format.json { render :show, status: :created, location: @talk }
      else
        format.html { render :new }
        format.json { render json: @talk.errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /talks/1
  # PATCH/PUT /talks/1.json
  def update
    respond_to do |format|
      if @talk.update(talk_params)
        format.html { redirect_to @talk, notice: 'Talk was successfully updated.' }
        format.json { render :show, status: :ok, location: @talk }
      else
        format.html { render :edit }
        format.json { render json: @talk.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /talks/1
  # DELETE /talks/1.json
  def destroy
    @talk.destroy
    respond_to do |format|
      format.html { redirect_to talks_url, notice: 'Talk was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_talk
      @talk = Talk.find(params[:id])
    end

    # Never trust parameters from the scary internet, only allow the white list through.
    def talk_params
      params.require(:talk).permit(:user_id, :content)
    end
end

#08:自分の記事だけ編集・削除 その1
ここでは、投稿したユーザーだけが自分の記事を編集・削除できるようにしましょう。そのために、contollerで、user_idが一致した時だけ、アクションを実行させます。

updateアクションを修正する
```app/controllers/articles_controller.rb

def update
if @article.user_id == current_user.id
respond_to do |format|
if @article.update(article_params)
format.html { redirect_to @article, notice: 'Article was successfully updated.' }
format.json { render :show, status: :ok, location: @article }
else
format.html { render :edit }
format.json { render json: @article.errors, status: :unprocessable_entity }
end
end
else
redirect_to @article, notice: "You don't have permission."
end
end
```

destroyアクションを修正する
```app/controllers/articles_controller.rb

def destroy
if @article.user_id == current_user.id
@article.destroy
msg = "Article was successfully destroyed."
else
msg = "You don't have permission."
end
respond_to do |format|
format.html { redirect_to articles_url, notice: msg }
format.json { head :no_content }
end
end
```


#09:自分の記事だけ編集・削除 その2
ここでは先ほどの続きとして、使わないアクションを呼び出すリンクを非表示にします。

updateアクションを修正する
```app/controllers/articles_controller.rb

def update
if @article.user_id == current_user.id
respond_to do |format|
if @article.update(article_params)
format.html { redirect_to @article, notice: 'Article was successfully updated.' }
format.json { render :show, status: :ok, location: @article }
else
format.html { render :edit }
format.json { render json: @article.errors, status: :unprocessable_entity }
end
end
else
redirect_to @article, notice: "You don't have permission."
end
end
```

destroyアクションを修正する
```app/controllers/articles_controller.rb

def destroy
if @article.user_id == current_user.id
@article.destroy
msg = "Article was successfully destroyed."
else
msg = "You don't have permission."
end
respond_to do |format|
format.html { redirect_to articles_url, notice: msg }
format.json { head :no_content }
end
end
```

投稿一覧を修正する
```app/views/articles/index.html.erb

<% if user_signed_in? && article.user_id == current_user.id %>

<%= link_to 'Edit', edit_article_path(article) %>
<%= link_to 'Destroy', article, method: :delete, data: { confirm: 'Are you sure?' } %>
<% end %>
```

詳細画面を修正する

app/views/articles/show.html.erb
<% if user_signed_in? && @article.user_id == current_user.id %>
    <%= link_to 'Edit', edit_article_path(@article) %> |
<% end %>
<%= link_to 'Back', articles_path %>

演習課題「ユーザーがログインしている時だけ、編集・削除を可能にする」
右の環境には、「myblog」プロジェクトに「talk」という1行掲示板が作成されています(user_idとcontentというカラムを持っています)。また、ユーザー認証構築用にdeviseを導入して、「User」というモデルを作成し、talkと関連付けてあります。

この掲示板で、ユーザーがログインしている時だけ、投稿一覧と詳細画面に、編集と削除のリンクを表示してください。なお、ログインしているかどうかは「user_signed_in?」で判別できます。

採点して、すべてのジャッジに正解すれば、演習課題クリアです!


模範解答1
/home/ubuntu/myblog/app/views/talks/index.html.erbに次の内容を記述する

<p id="notice"><%= notice %></p>

<h1>Talks</h1>

<table>
  <thead>
    <tr>
      <th>User</th>
      <th>Content</th>
      <th colspan="3"></th>
    </tr>
  </thead>

  <tbody>
    <% @talks.each do |talk| %>
      <tr>
        <td><%= talk.user.name %></td>
        <td><%= talk.content %></td>
        <td><%= link_to 'Show', talk %></td>
        <% if user_signed_in? && talk.user_id == current_user.id %>
          <td><%= link_to 'Edit', edit_talk_path(talk) %></td>
          <td><%= link_to 'Destroy', talk, method: :delete, data: { confirm: 'Are you sure?' } %></td>
        <% end %>
        </tr>
    <% end %>
  </tbody>
</table>

<br>

<%= link_to 'New Talk', new_talk_path %>

模範解答2
/home/ubuntu/myblog/app/views/talks/show.html.erbに次の内容を記述する

<p id="notice"><%= notice %></p>

<p>
  <strong>User:</strong>
  <%= @talk.user.name %>
</p>

<p>
  <strong>Content:</strong>
  <%= @talk.content %>
</p>

<% if user_signed_in? && @talk.user_id == current_user.id %>
  <%= link_to 'Edit', edit_talk_path(@talk) %>
<% end %>
<%= link_to 'Back', talks_path %>

お疲れ様でした!!!!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails入門4: deviseでユーザー認証してみよう

Rails入門4: deviseでユーザー認証してみよう

#01:ユーザー認証機能を理解しよう
このレッスンでは、Railsのユーザー認証用ライブラリdeviseを使って、Webアプリケーションで必要になるユーザー認証の基本を学習します。
まず初めに、ユーザー認証の概要と、deviseの役割を理解しましょう。

ユーザー認証とは
相手を確認してアクセスを許可する仕組みを「ユーザー認証」と呼びます。

ユーザー認証
- 認証:Authentication 本人かどうか確認する
- 許可:Authorization  認証の結果をあたえ、利用許可

deviseの主な機能
deviseは、ユーザー認証の仕組みをRailsに提供するライブラリです。

  • サインアップ:ユーザー情報と暗号化したパスワードをデータベースに保存
  • メールによるユーザー確認
  • ログイン:メールとパスワードによる認証
  • クッキーによるセッション管理
  • ユーザー追跡:ログイン回数、日時、IPアドレスなど
  • パスワードリセット
  • ユーザーのロック
  • OmniAuth対応:TwitterやFacebookなどによるSNS認証

参考になるWebページ
- [ASCII.jp:ユーザー認証でなにができるのですか?|セキュリティの素朴な疑問を解く]
http://ascii.jp/elem/000/000/436/436614/


#02:ログインフォームの動作確認
ここでは、deviseで作ったログインフォームを実際に使ってみましょう。deviseでは、ユーザー認証のために多くの機能を提供します。

ログイン情報
Email: kirishima@paiza.jp
パスワード: password

deviseの主な機能
deviseは、ユーザー認証の仕組みをRailsに提供するライブラリです。

  • サインアップ:ユーザー情報と暗号化したパスワードをデータベースに保存
  • メールによるユーザー確認
  • ログイン:メールとパスワードによる認証
  • クッキーによるセッション管理
  • ユーザー追跡:ログイン回数、日時、IPアドレスなど
  • パスワードリセット
  • ユーザーのロック
  • OmniAuth対応:TwitterやFacebookなどによるSNS認証

#03:deviseを導入する
ここでは、Ruby on Railsに、deviseを導入します。そして、いくつかの基本設定で、deviseを使えるようにします。

プロジェクトと静的ページの作成

$ rails new bbs_users
$ cd bbs_users
$ rails g controller welcome index

動画0:50でエラーが出る場合
rails g controller welcome index でエラーが出る場合、bbs_users/db/Gemfileの12行目でバージョンを指定する

gem 'sqlite3', '~> 1.3.6'

Railsを始めてsqlite3まわりのエラーで躓いている人たちへ
https://qiita.com/Kta-M/items/254a1ba141827a989cb7

deviseライブラリを追加してインストールする
Gemfile

gem 'devise'

$ bundle install

$ rails g devise:install

手動設定1.デフォルトURLを追加する
config/environments/development.rb

config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
config.action_mailer.raise_delivery_errors = false
config.action_mailer.perform_caching = false

手動設定2.root_urlを指定する
config/routes.rb

root 'welcome#index'

手動設定3.フラッシュメッセージの表示場所を作る
app/views/layouts/application.html.erb


<%= notice %>


<%= alert %>


<%= yield %>

手動設定4.ユーザー認証用のviewを生成する

$ rails g devise:views

deviseのViewの対応
- ログイン: app/views/devise/sessions/new.html.erb
- サインアップ: app/views/devise/registrations/new.html.erb
- ユーザ情報変更: app/views/devise/registrations/edit.html.erb
- パスワード変更: app/views/devise/passwords/edit.html.erb
- メール認証: app/views/devise/confirmations/new.html.erb
- パスワードリセット: app/views/devise/passwords/new.html.erb

- アカウントアンロック: app/views/devise/unlocks/new.html.erb

演習課題「deviseをインストールする」
右の環境には、「myblog」プロジェクトに「Welcome#index」という静的ページが作成してあります。
すでに、deviseのgemが導入してあります。

ここで、deviseのインストールを実行してください。なお、手動設定は全て不要です。

採点して、すべてのジャッジに正解すれば、演習課題クリアです!


模範解答1
次のコマンドをターミナルで実行する

rails g devise:install

#04:ユーザー認証用のUserモデルを作成
ここでは、ユーザー認証のためにUserモデルを作成します。そして、できあがったログイン画面を呼び出してみましょう。

Userモデルを作成する

$ rails g devise User
$ rails db:migrate

Deviseに初期ユーザを一括登録
db/seeds.rb

User.create(email: 'admin@paiza.jp', password: 'password')
User.create(email: 'kirishima@paiza.jp', password: 'password')
User.create(email: 'neko@paiza.jp', password: 'password')

$ cd bbs_users
$ rails db:seed

ユーザー認証
サインアップ
https://xxxx.paiza-app:3000/users/sign_up

ログイン
https://xxxx.paiza-app:3000/users/sign_in


演習課題「管理者ログインを作成する」
右の環境には、「myblog」プロジェクトに「Welcome#index」という静的ページが作成してあります。また、ユーザー認証構築用にdeviseが導入済みです。

ここに、deviseを使って「Admin」というモデルを作成してください。
また、マイグレーションも実行してください。

採点して、すべてのジャッジに正解すれば、演習課題クリアです!


模範解答1
次のコマンドを順にターミナルで実行する
rails g devise Admin
rails db:migrate


#05:アクセスを制限する
ここでは、deviseで作成したユーザー認証機能にログアウト機能を追加します。また、ログインしている時だけ、Welcomeページを表示できるようにアクセス制御します。

ログインナビゲーションを追加
app/views/welcome/index.html.erb

<% if user_signed_in? %>
Logged in as <%= current_user.email %>.
<%= link_to "Settings", edit_user_registration_path %> |
<%= link_to "Logout", destroy_user_session_path, method: :delete %>
<% end %>

この時、ヘルパーメソッドの「user」の部分は、モデル名の「User」に合わせて記述しています。

ログインページに強制移動
app/controllers/welcome_controller.rb

class WelcomeController < ApplicationController
before_action :authenticate_user!
def index
end
end

参考になるWebページ
- [ログイン認証も簡単!Railsでのdeviseの使い方 | TechAcademyマガジン]
https://techacademy.jp/magazine/7336
- [STEP21:Rails5にdeviseでログイン機能を実装しよう! #Rails #Ruby | TickleCode]
http://ticklecode.com/devise/
- [Devise に初期ユーザを追加 - Ruby and Rails]
http://rubyandrails.hatenablog.com/entry/devise-user-create
- [RailsのDBの初期データ(rake db:seed用)をyamlで美しく管理する方法 - Qiita]
https://qiita.com/yukimura1227/items/ff04eb6a771ffe1ab0b8
- [【Rails入門】seedの使い方まとめ | 侍エンジニア塾ブログ | プログラミング入門者向け学習情報サイト]
https://www.sejuku.net/blog/28395


演習課題「管理者ログインに初期ユーザーを登録する作成する」
右の環境には、「myblog」プロジェクトに「Welcome#index」という静的ページが作成してあります。
また、ユーザー認証構築用にdeviseを導入して、「Admin」というモデルを作成してあります。

ここに、以下の初期ユーザーを登録してください。この時、seed.rbファイルで一括登録します。

- email: 'admin@paiza.jp', password: 'password'

模範解答1
/home/ubuntu/myblog/db/seed.rbファイルに登録するデータを記述する。
Admin.create(email: 'admin@paiza.jp', password: 'password')

模範解答2
次のコマンドを順にターミナルで実行する
rails db:seed


演習課題「間違い探し:管理者ログインのナビゲーションを作成する」
右の環境には、「myblog」プロジェクトに「Welcome#index」という静的ページが作成してあります。
また、ユーザー認証構築用にdeviseを導入して、「Admin」というモデルを作成してあります。

ここに、全てのページにナビゲーションを表示するようコードを記述してありますが、エラーになってしまいます。

間違いを修正して、正常に表示されるようにしてください。


模範解答1
/home/ubuntu/myblog/app/views/layouts/application.html.erbに記述する。
ヘルパーメソッドは、すべてモデル名のAdminに合わせる。

<!DOCTYPE html>


Myblog
<%= csrf_meta_tags %>
<%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>

<% if admin_signed_in? %>

演習課題「ログインしていないと強制移動」
右の環境には、「myblog」プロジェクトに「Welcome#index」という静的ページが作成してあります。また、ユーザー認証構築用にdeviseを導入して、「Admin」というモデルを作成してあります。

この時、ログインしていないとログインページに強制移動するよう、Welcome#indexページを設定してください。

採点して、すべてのジャッジに正解すれば、演習課題クリアです!

模範解答1
/home/ubuntu/myblog/app/controllers/welcome_controller.rbに、
before_actionを記述する。
ヘルパーメソッドは、モデル名の「Admin」に合わせて記述する。

class WelcomeController < ApplicationController
before_action :authenticate_admin!
def index
end
end


#06:セッションとパスワードを理解する
ここでは、ログイン状態を保持するセッションとユーザーを認証するパスワードについて学習します。deviseを使うと、セッション管理機能も簡単に組み込むことができます。

セッションとは
ログインしてから、ログアウトするまでの一連のアクセスを「セッション」と呼びます。

Webサイトへの基本的なアクセスでは、アプリケーション側でそれぞれのアクセスは独立しています。そのために、同じ人が同じサイトに複数回アクセスしても、それぞれのアクセスを区別できません。これでは、アクセスするたびにログインし直す必要があります。

そこで、セッションという仕組みが使われています。セッションは、ログインすると開始して、そのアクセスを区別する「セッション情報」を記録します。そして、ログアウトしたり、ブラウザを閉じて一定時間が経ったりすると終了します。この「セッション情報」おかげで、セッションが有効な間、Webアプリケーションに同じ人がアクセスしていると判断できるようになります。

セッションを確認する手順
Google Chromeの場合

1.「設定」メニューを呼び出す
2.「詳細設定」-「コンテンツの設定」-「Coookies」-「すべてのクッキーとサイトデータ」
3.Webアプリケーションのドメイン名を検索する

暗号化されたパスワードの確認手順

$ rails console
user = User.find(2)
user.email
user.encrypted_password

参考になるWebページ
- [セッション (session)とは|「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典]
http://wa3.i-3-i.info/word1791.html


演習課題「セッションidを削除する」
右の環境には、「myblog」プロジェクトに「Welcome#index」という静的ページが作成してあります。また、ユーザー認証構築用にdeviseを導入して、「Admin」というモデルを作成してあります。そして、1個のブラウザが立ち上がって、ログイン状態になっています。

このアプリのセッションidを削除して、ブラウザをログアウト状態にしてください。


模範解答1
ブラウザの設定メニューからクッキーを削除する
ブラウザの設定ページで「_myblog_session」のクッキーを削除する。


  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails入門3: Railsサービスを機能アップしてみよう

Rails入門3: Railsサービスを機能アップしてみよう

#01:RailsにBootstrapを導入しよう
このレッスンでは、お勧めのお店を投稿できる「ランチマップ」アプリのデザインを整えて、スマートフォン対応と日本語表示を実現します。

まず初めに、HTMLテンプレートのBootstrapを導入します。

RailsのWebページにアクセスするには
- paizaの場合: New Broweserタブに表示されたアドレスに「:3000」を追加する
https://xxx.paiza-app.cloud:3000
- 作成した掲示板にアクセスする:https://xxx.paiza-app.cloud:3000/categories

※ xxxには、コンテナ名が入ります

RailsのWebサーバーを停止するには
「CTRL」キー(コントロールキー)を押しながら「C」のキーを押します

RailsにBootstrapを導入する
Gemfileを修正

gem 'bootstrap-sass', '~> 3.3.6'
gem 'sass-rails', '~> 5.0'

コマンドを実行
$ bundle install

scssを修正
app/assets/stylesheets/にある「application.css」のファイル名をapp/assets/stylesheets/application.css.scssに変更する

app/assets/stylesheets/application.css.scssを修正

@import 'bootstrap-sprockets';
@import 'bootstrap';

/* universal */

body {
padding: 60px 15px 0;
}

jsファイルの修正
app/assets/javascripts/application.js

//= require bootstrap-sprockets

コンテナを割り当て
app/views/layouts/application.html.erbを修正



<%= yield %>


演習課題「コンテナを追加する」
右の環境には、Railsで、おすすめのネコ情報を投稿する「catmap」というプロジェクトに、「cat」という掲示板と「feed」という掲示板が作られており、Bootstrapが導入してあります。
エディタを使って、app/views/layouts/application.html.erbを修正して、Bodyタグにcontainerクラスを追加してください。

採点して、すべてのジャッジに正解すれば、演習課題クリアです!


模範解答1
class='container'を要素にしたdivタグを配置します。



<%= yield %>


#02:Bootstrapでレスポンシブデザインにしよう
ここでは、BootstrapでRailsアプリをレスポンシブデザインにします。
そして、テーブルやボタン・フォームにBootstrapのスタイルを適用します。

お店リストのテーブルを修正
app/views/shops/index.html.erb

 ・・・

リンクにボタンを追加

app/views/shops/index.html.erb

<%= link_to 'New Shop', new_shop_path, class: 'btn btn-primary' %> | <%= link_to 'Show category list', categories_path %>

フォームを修正1
app/views/shops/_form.html.erb

<%= form_for(shop, html: {class: 'form-horizontal', role: 'form'}) do |f| %>

フォームを修正2
フォームの部品
app/views/shops/_form.html.erb

フォームを修正3
登録ボタン
app/views/shops/_form.html.erb

グリッドの横幅の比率
Bootstrapでは、HTML要素の"class"属性に"col-sm-X"を適用すると、ページの横幅を12としたときに、その要素が占める横幅をXとすることができます。
例えば、"col-sm-3"が適用された要素と"col-sm-9"が適用された要素を並べたならば、これらの横幅の比率は、3:9になります。

このようなレイアウト方法をグリッドレイアウトと呼びます。

演習課題「テーブルへのクラスの適用」
右の環境には、Railsで、おすすめのネコ情報を投稿する「catmap」というプロジェクトに、「cat」という掲示板と「feed」という掲示板が作ってあり、Bootstrapが導入してあります。

エディタを使って、app/views/cats/index.html.erbを修正して、猫リストのテーブルにBootstrapのデザインを割り当てて、1行おきに色を付け、マウスポインタを重ねたら色が変わるようにしてください。
割り当てるのは、次のクラスです。

  • table
  • table-striped
  • table-hover

採点して、すべてのジャッジに正解すれば、演習課題クリアです!


模範解答1
divタグに、class='table table-striped table-hover'を記述します。


...

演習課題「ボタンにクラスを適用する」
右の環境には、Railsで、おすすめのネコ情報を投稿する「catmap」というプロジェクトに、「cat」という掲示板と「breed」という掲示板が作られており、Bootstrapが導入されています。

エディタを使って、app/views/cats/index.html.erbを修正して、
「New Cat」ボタンに、'btn btn-primary'スタイルを割り当ててください。


模範解答1
class='btn btn-primary'を要素にしたdivタグを配置します。
<%= link_to 'New Cat', new_cat_path, class: 'btn btn-primary' %>


演習課題「グリッドの幅を設定する」
右の環境には、Railsで、おすすめのネコ情報を投稿する「catmap」というプロジェクトに、「cat」という掲示板と「feed」という掲示板が作られてあり、Bootstrapが導入してあります。

エディタを使って、app/views/cats/_form.html.erbを修正して、グリッドの割合を1:11にしてください。

採点して、すべてのジャッジに正解すれば、演習課題クリアです!


模範解答1
最初は、2:10になっています。

<%= form_for(cat, html: {class: 'form-horizontal', role: 'form'}) do |f| %>
<% if cat.errors.any? %>


<%= pluralize(cat.errors.count, "error") %> prohibited this cat from being saved:

  <ul>
  <% cat.errors.full_messages.each do |message| %>
    <li><%= message %></li>
  <% end %>
  </ul>
</div>

<% end %>

#03:スマートフォン向けの画面にしよう vol.1
ここでは、Railsアプリをスマートフォン向けの画面に設定します。そのために、ナビゲーションバーを設置し、お店リストの操作を整理します。

ナビゲーションバーを追加する
app/views/layouts/application.html.erb

Toggle navigation Lunch Map

ナビゲーションバーにリンクを追加する
app/views/layouts/application.html.erb

<%= link_to 'Lunch Map', root_path, class: 'navbar-brand' %>

app/views/layouts/application.html.erb


  • <%= link_to 'Shop', shops_path %>

  • <%= link_to 'Category', categories_path %>

リストの行クリックで、詳細ページを表示
app/views/shops/index.html.erb

<%= link_to shop.category.name, shop, class: 'widelink' %>
<%= link_to shop.name, shop, class: 'widelink' %>
<%= link_to shop.address, shop, class: 'widelink' %>

app/assets/stylesheets/application.css.scss
/* table row for link */
a.widelink {
display: block;
width: 100%;
height: 100%;
text-decoration: none;
}

一覧から各リストのボタンを削除

app/views/shops/index.html.erb







演習課題「ナビゲーションバーのリンクの追加」
右の環境には、Railsで「catmap」というプロジェクトに、「cat」という掲示板と「feed」という掲示板が作られており、Bootstrapのナビゲーションバーが導入されています。

エディタを使って、app/views/layouts/application.html.erbを修正して、ナビゲーションバーの「Cat」と「Feed」という項目に、各掲示板へのリンクを貼ってください。

採点して、すべてのジャッジに正解すれば、演習課題クリアです!


模範解答1
link_to でリンクする


#04:スマートフォン向けの画面にしよう vol.2
このチャプターでは、引き続き、Railsアプリをスマートフォン向けの画面に変えたいと思います。今度は、詳細ページと登録ページのレイアウトを整理しましょう。

地図を画面に収める
app/views/shops/show.html.erb

<%= content_tag(:iframe,
'map',
src:'https://www.google.com/maps/embed/v1/place?key=AIzaSyCJBgcuCowQa5-V8owXaUCHhUNBN8bfMfU&q=' + @shop.address,
width: '100%',
height: 320,
frameborder: 0) %>

詳細ページにボタンを設置
app/views/shops/show.html.erb

<%= link_to 'Delete', @shop,
method: :delete, data: { confirm: 'Are you sure?' }, class: 'btn btn-danger' %>
<%= link_to 'Edit', edit_shop_path(@shop), class: 'btn btn-primary' %>
<%= link_to 'Back', shops_path, class: 'btn btn-default' %>

登録ページに戻るボタンを追加
app/views/shops/_form.html.erb

新規と更新ページのボタンを削除
app/views/shops/edit.html.erb

Editing Shop

<%= render 'form', shop: @shop %>

app/views/shops/new.html.erb

New Shop

<%= render 'form', shop: @shop %>


演習課題「Googleマップの横幅の調整」
右の環境には、Railsで「catmap」というプロジェクトに、「cat」という掲示板と「feed」という掲示板が作ってあり、Googleマップが導入されています。

エディタを使って、app/views/cats/show.html.erbを修正して、ウィンドウの幅に合わせて、Googleマップの横幅が変わるようにください。

採点して、すべてのジャッジに正解すれば、演習課題クリアです!


模範解答1
width: '100%'を指定する

<%= content_tag(:iframe,
'map',
src:'https://www.google.com/maps/embed/v1/place?key=AIzaSyCJBgcuCowQa5-V8owXaUCHhUNBN8bfMfU&q=' + @cat.address,
width: '100%',
height: 320,
frameborder: 0) %>


#05:Railsの日本語化を試そう
ここでは、Railsアプリのボタンやメッセージを日本語で表示させます。Ruby on Railsは、このような多言語対応のため国際化機能を装備しています。

URLのオプションに合わせて切り換えるようコントローラーを修正
app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
before_action :set_locale
protect_from_forgery with: :exception

def set_locale
I18n.locale = params[:locale] || I18n.default_locale
end

def default_url_options(options = {})
{ locale: I18n.locale }.merge options
end
end

en.ymlを確認
/config/locale/en.yml

en:
hello: "Hello world"

ja.yml をコピーして修正

$ cd ~/lunchmap
$ cp -a config/locales/en.yml config/locales/ja.yml

/config/locale/ja.yml
ja:
hello: "世界の皆さん、こんにちは"

ウェルカムページに翻訳用キーワードを埋め込む
app/views/welcome/index.html.erb

Lunch Map

Tasty meal on your place!!


演習課題「「ハロー パイザ」と表示」
右の環境には、Railsで「catmap」というプロジェクトに、start/index.html.erbというページが設置されています。また、国際化機能として、URLに日本語ロケール(?locale=ja)を指定すると、日本語表記に切り替わるようになっています。ja.ymlには「paiza: "ハロー パイザ"」と登録してあります。

エディタを使って、app/views/start/index.html.erbを修正して、pタグの位置に「ハロー パイザ」と表示してください。

採点して、すべてのジャッジに正解すれば、演習課題クリアです!


模範解答1
pタグに、<%= t('paiza') %>を記述する


#06:Railsアプリを日本語表記に切り替えよう
ここでは、引き続き、Railsアプリのボタンやメッセージを日本語で表示させます。今回は、テーブルとフォームの表記を日本語にします。特に、Railsが自動的に割り当てた表記の変更方法を取り上げます。

en.ymlに英単語を登録する
/config/locale/en.yml

en:
hello: 'Hello world'
shops: 'Shops'
category: 'Category'
name: 'Name'
address: 'Address'

ja.ymlに日本語を登録する
/config/locale/ja.yaml

ja:
hello: '世界の皆さん、こんにちは'
shops: 'お店リスト'
category: 'カテゴリ'
name: '店名'
address: '住所'

お店リストに翻訳用キーワードを埋め込む
app/views/shops/index.html.erb



フォーム用のため、ja.ymlにデータ名を追記する
/config/locale/ja.yaml

ja:
hello: '世界の皆さん、こんにちは'
shops: 'お店リスト'
category: 'カテゴリ'
name: '店名'
address: '住所'

activerecord:
models:
shop: お店

attributes:
  shop:
    category_id: カテゴリ
    name: 店名
    address: 住所

参考になるWebページ
- Railsで日本語化対応にする方法 - Qiita
http://qiita.com/kusu_tweet/items/b534c808ac1ee0382f05


Category Name Address
<%= t('category') %> <%= t('name') %> <%= t('address') %>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails覚書2: 実用的なRailsサービスを作ろう

Rails入門2: 実用的なRailsサービスを作ろう

#01:プロジェクトとウェルカムページを作ろう
このレッスンでは、お勧めのお店を投稿できる「ランチマップ」アプリをRuby on Railsで作ってみましょう。まず初めに、プロジェクトとウェルカムページを作ります。

今回使ったLinuxコマンド
現在のディレクトリ(フォルダ)の位置を確認する。
$ pwd

ファイルやディレクトリの情報を表示する。
$ ls

lunchmapディレクトリに移動する。
$ cd lunchmap

今回使ったRailsコマンド

lunchmapプロジェクトを作成する。
$ rails new lunchmap

Rails用のWebサーバーを起動する
$ rails s -b 0.0.0.0

ウェルカムページを生成する
rails generate controller welcome index

ウェルカムページを修正する
app/views/welcome/index.html.erb

Lunch Map

Tasty meal on your place!

config/routes.rbに追加
config/routes.rb

Rails.application.routes.draw do
get 'welcome/index'

root 'welcome#index'
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
end

RailsのWebページにアクセスするには
- paizaの場合: New Broweserタブに表示されたアドレスに「:3000」を追加する
https://xxx.paiza-app.cloud:3000
- 作成した掲示板にアクセスする:https://xxx.paiza-app.cloud:3000/categories

※ xxxには、コンテナ名が入ります

RailsのWebサーバーを停止するには
「CTRL」キー(コントロールキー)を押しながら「C」のキーを押します。


演習課題「Railsプロジェクトを作成する」
右のターミナルで、現在のディレクトリに、おすすめのネコ情報を投稿する「catmap」というRailsプロジェクトを作成してください。

$ rails new catmap


演習課題「Railsサーバーを起動する」
右のターミナルでは、「catmap」というRailsプロジェクトが作成されています。

catmapディレクトリに移動して、RailsのWebサーバーを起動してください。

$ cd ~/catmap
$ rails server -b 0.0.0.0


演習課題「ページを追加する」
右の環境には、Railsで「catmap」というプロジェクトが作られています。
ターミナルを使って、このプロジェクトに「https://xxx.paiza-app.cloud:3000/start/index」というWebページを作ってください。

rails serverを起動した上で、採点して、すべてのジャッジに正解すれば、演習課題クリアです!

$ cd ~/catmap
$ rails generate controller start index


#02:カテゴリリストとお店リストを作ろう
ここでは、ランチマップのカテゴリリストとお店リストを作成します。そして、このお店リストに、カテゴリ名を表示するよう設定します。

今回使ったLinuxコマンド
ファイルやディレクトリの情報を表示する。
$ ls

lunchmapディレクトリに移動する。
$ cd lunchmap

今回使ったRailsコマンド
カテゴリリストを自動生成する

$ rails generate scaffold category name:string
$ rails db:migrate

お店リストを自動生成する
$ rails generate scaffold shop category_id:integer name:string address:string
$ rails db:migrate

RailsのWebページにアクセスするには
- paizaの場合: New Broweserタブに表示されたアドレスに「:3000」を追加する
https://xxx.paiza-app.cloud:3000
- 作成したカテゴリリストにアクセスする:https://xxx.paiza-app.cloud:3000/categories
- 作成したお店リストにアクセスする:https://xxx.paiza-app.cloud:3000/shops

※ xxxには、コンテナ名が入ります

RailsのWebサーバーを停止するには
「CTRL」キー(コントロールキー)を押しながら「C」のキーを押します。


演習課題1「掲示板を作成しよう」
右の環境には、Railsで「catmap」というプロジェクトが作られています。
ターミナルを使って、このプロジェクトに「feed」という掲示板を作ってください。
この掲示板には、nameという文字列カラムを1つだけ用意します。

rails serverを起動した上で、採点して、すべてのジャッジに正解すれば、演習課題クリアです!

$ cd ~/catmap
$ rails generate scaffold feed name:string

$ rails db:migrate

演習課題2「掲示板をさらに追加しよう」
右の環境には、Railsで「catmap」というプロジェクトが作られています。
ターミナルを使って、このプロジェクトに「cat」という掲示板を作ってください。
この掲示板には、以下の3つのカラムを用意します。

  • feed_id : integer
  • name : string
  • address : string

rails serverを起動した上で、採点して、すべてのジャッジに正解すれば、演習課題クリアです!

$ cd ~/catmap
$ rails generate scaffold cat feed_id:integer name:string address:string

$ rails db:migrate

#03:カテゴリリストとお店リストを関連付けよう
ここでは、ランチマップのカテゴリリストとお店リストを関連付けて、このお店リストに、カテゴリ名を表示するよう設定します。

相互にリンクする
自動生成した掲示板の間で相互にリンクを貼るには、次のように記述します。

app/views/shops/index.html.erb
<%= link_to 'New Shop', new_shop_path %> <%= link_to 'Show Categories', categories_path %>
app/views/categories/index.html.erb
<%= link_to 'New Category', new_category_path %> <%= link_to 'Show Shops', shops_path %>

登録時に、カテゴリを選択できるようにする
登録・修正フォームで、カテゴリを選択できるようにするには、次のように記述します。

app/views/shops/_form.html.erb

カテゴリを表示
お店リストの一覧表示と詳細表示にカテゴリ名を表示するには、次のように記述します。

app/view/shops/index.html.erb
<td><%= shop.category.name %></td>
app/view/shops/show.html.erb
<%= @shop.category.name %>

演習課題「ネコの種別を選択できるようにしよう」
右の環境には、Railsで「catmap」というプロジェクトに、「cat」という掲示板と「feed」という掲示板が作られています。また、この2つの掲示板は、すでに関連付けてあります。

エディタを使って、app/views/cats/_form.html.erbを修正して、catの登録/修正フォームで、catのfeedを選択できるようにしてください。

rails serverを起動した上で、採点して、すべてのジャッジに正解すれば、演習課題クリアです!

模範解答1
ラベルとセレクトボックスを記述する
```


<%= f.label :feed_id %>
<%= f.select :feed_id, Feed.all.map{|o| [o.name, o.id]} %>

```

#04:検索機能を追加しよう
ここでは、ランチマップのためにお店の名前の検索機能を作成します。そのために、検索フォームを追加します。

Viewに検索フォームを追加
```/app/views/shops/index.html.erb

<%= form_tag('/shops', method: 'get') do %>
<%= label_tag(:name_key, 'Search name:') %>
<%= text_field_tag(:name_key) %>
<%= submit_tag('Search') %> <%= link_to 'Clear', shops_path %>
<% end %>



```

Controllerにindexメソッドを修正
/app/controllers/shops_controller.rb

def index
if params[:name_key]
@shops = Shop.where('name LIKE ?', "%#{params[:name_key]}%")
else
@shops = Shop.all
end
end

ウェルカムページからリンクする
app/views/welcome/index.html.erb

Lunch Map

Tasty meal on your place!!


演習課題「検索機能を作成しよう」
右の環境には、Railsで「catmap」というプロジェクトに、「cat」という掲示板と「feed」という掲示板が作られています。また、この2つの掲示板は、すでに関連付けてあります。

エディタを使って、app/views/cats/index.html.erbを修正して、ネコ一覧(cats)で猫の名前を検索できるようにしてください。

検索するためのコードは、すでにapp/controllers/cats_controller.rbに記述してあります。

rails serverを起動した上で、採点して、すべてのジャッジに正解すれば、演習課題クリアです!


模範解答1
ラベルとテキストフィールド、登録ボタンを記述する

<%= form_tag('/cats', method: 'get') do %>
<%= label_tag(:name_key, 'Search name:') %>
<%= text_field_tag(:name_key) %>
<%= submit_tag('Search') %> <%= link_to 'Clear', cats_path %>
<% end %>



#05:Googleマップを組み込む
ここでは、ランチマップに地図を追加します。そのために、このRailsアプリケーションにGoogleマップを組み込みます。

APIとは
APIとは、Application Programming Interfaceの略で、プログラムから別のプログラムの機能を呼び出すために用意された命令や関数のこと。

Google MAP API
- Google Maps API | Google Developers
https://developers.google.com/maps/

APIキーの取得手順
今回は以下のpaizaラーニング用のAPIキーを使ってください。

 AIzaSyCJBgcuCowQa5-V8owXaUCHhUNBN8bfMfU

自分でウェブサービスを公開する場合には、以下の手順を参考に取得してください。

  1. Google Developers Consoleにアクセスする

Google Developers Console
https://console.developers.google.com/

  1. プロジェクトを作成を選択
  2. Google APIが表示されたら、Google Maps APIから「Google Maps Embed API」を選択
  3. 「有効にする」をクリック
  4. 「認証情報を作成」をクリックして、「必要な認証情報」ボタンをクリック
  5. 表示されたAPIキーを記録する

※特定のWebサービスだけから利用できるよう、「API利用制限」を設定することをお勧めします。
※この手順や利用範囲はGoogle側で変更される場合があります。

ビューに、マップエリアを追加
app/views/shops/show.html.erb

<%= content_tag(:iframe, 'map', src:'https://www.google.com/maps/embed/v1/place?key=AIzaSyCJBgcuCowQa5-V8owXaUCHhUNBN8bfMfU&q=' + @shop.address, width: 800, height: 400, frameborder: 0) %>



演習課題「地図を表示しよう」
右の環境には、Railsで「catmap」というプロジェクトに、「cat」という掲示板と「feed」という掲示板が作られています。また、この2つの掲示板は、すでに関連付けてあります。

エディタを使って、app/views/cats/show.html.erbを修正して、ネコの居場所(address)をGoogleマップで表示できるようにしてください。

なお、Googleマップに必要なライブラリや設定は、すでに記述してあります。

rails serverを起動した上で、採点して、すべてのジャッジに正解すれば、演習課題クリアです!

模範解答1
content_tagで、Googleマップを表示するエリアを記述する
<%= content_tag(:iframe, 'map', src:'https://www.google.com/maps/embed/v1/place?key=AIzaSyCJBgcuCowQa5-V8owXaUCHhUNBN8bfMfU&q=' + @cat.address, width: 800, height: 400, frameborder: 0) %>


  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails6.0 + Nuxt.js] response.headersにcorsのexpose(access-tokenなど)が入らない

この記事を書くにいたるまで

現在Rails6.0とNuxt.jsを使って適当にアプリを作っています。(Nuxtの練習です
RailsはAPIモード、認証にはdevise_token_authを使用しており、
Nuxtからaxios経由でpostしてtoken認証を行うアプリになっています。

今回loginをaxiosで行ったのですが、そのresponse(response.headers)の中に
access-tokenclientなどが格納されていないというトラブルが発生しました。
解決すると「こんなことで…」と思いますが、ほぼ1日ハマってしまったので共有です。

ぶつかった壁

まずnuxt側ですが、以下のようにloginアクションを実装しています。

login.vue
  async login(vuexContext, data) {
    await this.$axios.post("/api/v1/auth/sign_in", data)
      .then(response => {
        console.log(response.headers)
      })
      .catch(e => {
        console.log(e)
      })
  }

次にrails側ですが、CORS対策としてはrack-corsを導入しています。
以下corsの設定です。

config/application.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins 'localhost:3001'

    resource '*',
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head],
      expose: ['access-token', 'expiry', 'client']
  end
end

exposeに'access-token', 'expiry', 'client'を記述しています。
通常だとこれでresponse.headers内に上記3つの結果が表示されるはずです。

しかし以下のような結果しか表示されませんでした。

console.log(response.headers)
headers:
  cache-control: "max-age=0, private, must-revalidate"
  content-type: "application/json; charset=utf-8"

chrome-devtools => [Network] => Response Headersを見たところ、
こちらにはaccess-tokenの情報が来ているのですが、
responseというオブジェクトから取り出すことができません。

解決策

調べたところ、rack-corsのissuesにたどり着きました。
するとどうやらRack::Corsという設定が重複しているようです。

念のためgit grep Rackを実行したところ、なんとconfig/initializers/cors.rbが見つかり
そこに同じ設定がありました…(それまでconfig/application.rbに書いていました)

コメントアウトされていたので重複はしていないはずですが、
そもそもの設定を書く場所が間違っていたことになります。

設定をconfig/initializers/cors.rbに移行し再ログインしたところ、
うまくheadersに反映されました。

config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins 'localhost:3001'

    resource '*',
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head],
      expose: ['access-token', 'expiry', 'client']
  end
end

結果

console.log(response.headers)
headers:
  access-token: "VPPrzn2DnJuqJi_uDEI4hg"
  cache-control: "max-age=0, private, must-revalidate"
  client: "TVmLwY1GM23koi9r9HY17g"
  content-type: "application/json; charset=utf-8"
  expiry: "1587634926"

おわり

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】 コントローラー、モデルの削除の仕方

コントローラーとモデルの削除ってどうするの?

Railsでコントローラーを作ったのはいいけど消す時は?ってなったのでこの機会にいつもお世話になっているQiitaの先人様たちと同様に投稿していこうと思う。初投稿だからダメな所あっても優しくしてクレメンスb

コントローラー、モデルの作成

まぁコントローラー、モデルを作るのはみんな知っていると思うけど知らない人も見るかもしれないので簡単に

rails g controller コントローラー名
rails g model モデル名

では消す時は?

コントローラー、モデルの削除

ここからが本題であまり消すことのなかったコントローラーを初めて消したいと思って調べました。
で、一番上に出てきたサイトを参考にやってみました。
参考サイト→リンク

rails destroy controller コントローラー名

はい、destroyです。

ただこのサイトでも書いているのですがdに省略出来ないの?ってなって自分で試してみたら出来ました。2015年の記事では出来なかったらしいですけどRailsもよくなっているわけです。

rails d controller コントローラー名

作って消すだけなら10秒あれば出来るので知らなかった方は試してみてください。

というわけで今回はここらへんで

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails]テンプレートエンジン(haml.slim)の導入方法と書き方

パフォーマンス比較グラフ

20170910222126.png

erb(比較用)

~.html.erb
<h1>Users</h1>

<table>
  <thead>
    <tr>
      <th>Name</th>
      <th>Email</th>
      <th colspan="3"></th>
    </tr>
  </thead>

  <tbody>
    <% @users.each do |user| %>
      <tr>
        <td><%= user.name %></td>
        <td><%= user.email %></td>
        <td><%= link_to 'Show', user %></td>
        <td><%= link_to 'Edit', edit_user_path(user) %></td>
        <td><%= link_to 'Destroy', user, method: :delete, data: { confirm: 'Are you sure?' } %></td>
      </tr>
    <% end %>
  </tbody>
</table>

<br>

<%= link_to 'New User', new_user_path %>

haml

1.導入方法

Gemfile
gem 'haml-rails' #hamlファイルを使えるようにする
ターミナル
$ bundle
ターミナル
$ rails haml:erb2haml
ターミナル
<!-- 下記の様に問われるので、既存のerbファイルを削除してもいいなら「y + Enter」、駄目なら 「n + Enter」 -->
Would you like to delete the original .erb files? (This is not recommended unless you are under version control.) (y/n)

既存のerbファイルをhamlファイルに変換

2.書き方

~.html.haml
%h1 Users

%table
  %thead
    %tr
      %th Name
      %th Email
      %th

  %tbody
    - @users.each do |user|
      %tr
        %td = user.name
        %td = user.email
        %td = link_to 'Show', user
        %td = link_to 'Edit', edit_user_path(user)
        %td = link_to 'Destroy', user, method: :delete, data: { confirm: 'Are you sure?' }

%br

= link_to 'New User', new_user_path

slim

1.導入方法

Gemfile
gem 'slim-rails' #slimファイルを使えるようにする
gem 'html2slim' #既存のerbファイルをslimファイルに変換出来る様にする
ターミナル
$ bundle
ターミナル
$ bundle exec erb2slim app/views app/views -d

既存のerbファイルをslimファイルに変換

2.書き方

~.html.slim
h1 Users

table
  thead
    tr
      th Name
      th Email
      th

  tbody
    - @users.each do |user|
      tr
        td = user.name
        td = user.email
        td = link_to 'Show', user
        td = link_to 'Edit', edit_user_path(user)
        td = link_to 'Destroy', user, method: :delete, data: { confirm: 'Are you sure?' }

br

= link_to 'New User', new_user_path
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Docker-composeを使ってRailsの環境構築をする

今回はDocker-composeを使ってRailsの環境構築をする方法をまとめました!
Dockerは既に導入しているということを前提とします。
今回は特に説明等を書かずに環境構築する方法だけをまとめています。

1.ファイル構成

・Dockerfile
・docker-compose.yml
・Gemfile
・Gemfile.lock

また各ファイルの中身は

Dockerfile.
FROM ruby:2.5.1
RUN curl -sL https://deb.nodesource.com/setup_10.x | bash - && apt-get update && apt-get install -y nodejs --no-install-recommends && rm -rf /var/lib/apt/lists/*
RUN mkdir /app_name

ENV APP_ROOT /app_name
WORKDIR $APP_ROOT

ADD ./Gemfile $APP_ROOT/Gemfile
ADD ./Gemfile.lock $APP_ROOT/Gemfile.lock

RUN bundle install
ADD . $APP_ROOT
docker-compose.yml
version: '3'
services:
  db:
    image: mysql:5.7
    environment:
      MYSQL_ROOT_PASSWORD: password
      MYSQL_DATABASE: root
    ports:
      - "3306:3306"

  web:
    environment:
      - SPROCKETS_CACHE=/cache
    build: .
    command: bundle exec rails s -p 3000 -b '0.0.0.0'
    volumes:
      - .:/app_name
    ports:
      - "3000:3000"
    links:
      - db

Gemfile.
source 'https://rubygems.org'
gem 'rails', '5.2.1'

Gemfile.lockは空で大丈夫です

2.Railsのプロジェクトを作成する

docker-compose run web rails new . --force --database=mysql --skip-bundle

docker-composeコマンドを使用し、rails newを実行します。

3.DBの設定をする

作成したらconfigというファイルがあるのでその中のdatabese.ymlを変更していきます

config/database.yml
default: &default
  adapter: mysql2
  encoding: utf8
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password: password
  host: db

4.Docker-composeを起動します

# 1.コンテナをビルドする
$ docker-compose build

# 2.コンテナの作成&起動
$ docker-compose up 
# 2.コンテナの作成&起動(バックグラウンドでの起動)
$ docker-compose up -d

#3.DBの作成をする
$ docker-compose run web rails db:create

#4.DBをmigrateする
$ docker-compose run web rails db:migrate

5.localhostに接続

localhost:3000に接続して
image.png
この画面がでれば成功!!
お疲れ様です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails覚書1: Ruby on Railsを理解しよう

Rails覚書1: Ruby on Railsを理解しよう

Railsの覚書、詳しくはコチラを参照:https://paiza.jp/works/rails/primer/

Webアプリケーションフレームワークである、Ruby on Railsについて学習してみたいと思います。
Railsを使って、基本的な機能を持ったWeb掲示板とWebページを作ることで、本格的なWebアプリケーション開発の第1歩を体験しましょう。


#01:Ruby on Railsとは

Ruby on Railsとは
プログラミング言語Ruby上で動作するWebアプリケーションフレームワーク。
Webアプリケーションフレームワークとは、Webアプリを開発するために便利な部品やツールをひとまとめにしたものです。
実践的なWeb開発では、このようなフレームワークを使うことで、効率よくWebアプリケーションを作っていくことができます。

Rails使用に必要な最低限の知識

  • Ruby
  • HTML/CSS
  • PHP+MySQL

参考チュートリアル


02:Railsを使う準備をしよう

今回使ったrailsコマンド

Railsのバージョンを確認する。

rails -v

bbsプロジェクトを作成する。

rails new bbs

Rails用のWebサーバーを起動する

rails server -b 0.0.0.0

RailsのWebページにアクセスするには

RailsのWebサーバーを停止するには
「CTRL」キー(コントロールキー)を押しながら「C」のキーを押します。


03:3分で掲示板を作ってみよう

今回使ったrailsコマンド

Rails用のWebサーバーを起動する

rails server -b 0.0.0.0

掲示板を自動生成する

rails generate scaffold article content:string
rails db:migrate


04:Railsで、Webページを追加しよう

Welcomeページを作成する

$ rails generate controller welcome index

サーバーを起動する(ショートカット)

$ rails s -b 0.0.0.0

トップページに設定する

config/routes.rbに記述する

Rails.application.routes.draw do
  get 'welcome/index'

  resources :articles
  root 'welcome#index'
  # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
end

05:RailsのWebページを修正してみよう

ここでは、Railsコマンドで自動生成したWebページを修正します。

Webページの修正方法

RailsのWebページの表示は、「app/views」のerbファイルに記述します。

ウェルカムページのindex.html
app/views/welcome/index.html.erb

<h1>Hello BBS</h1>

Rubyのコードを埋め込む

app/views/welcome/index.html.erb
<h1>Hello BBS</h1>
<p><%= Date.today %></p>

リンクを貼る

app/views/welcome/index.html.erb
<h1>Hello BBS</h1>
<p><%= Date.today %></p>
<%= link_to 'Show list', articles_path %>

<%= ... %>

テンプレート中でRubyコードを実行します。対応するコントローラのインスタンスコンテキストで実行されるので、グローバル変数のほか、コントローラのインスタンス変数、インスタンス関数なども利用できます。


演習課題1:「h1タグの見出しの変更」

右の環境には、Railsで「myblog」というプロジェクトに「start#index」というWebページが作られています。
このWebページのh1タグの見出しを、「Welcome to my diary」に変更してください。

rails serverを起動した上で、採点して、すべてのジャッジに正解すれば、演習課題クリアです!

模範解答1

~/myblog/app/views/start/index.html.erbを次のように編集する

Welcome to my diary

Find me in app/views/start/index.html.erb


演習課題2「現在の日付を表示」

右の環境には、Railsで「myblog」というプロジェクトに「start#index」というWebページが作られています。
このWebページに、現在の日付を追加してください。

rails serverを起動した上で、採点して、すべてのジャッジに正解すれば、演習課題クリアです!

模範解答2
~/myblog/app/views/start/index.html.erbを次のように編集する

Welcome to my diary


演習課題3「リンクの追加」

右の環境では、Railsで「myblog」というプロジェクトに、「diary」掲示板と「start/index」というWebページが作られています。
この「start/index」に、「diary」掲示板へのリンクを追加してください。

「diary」掲示板へのリンクのパスは、diaries_pathで取得できます。

rails serverを起動した上で、プログラムを実行して、正しく出力されれば演習課題クリアです!


模範解答3

~/myblog/app/views/start/index.html.erbを次のように編集する

Welcome to my diary

<%= link_to 'Show list', diaries_path %>

06:Rails掲示板の構成を調べよう

ここでは、Ruby on Rails(ルビー オン レイルズ)で作成した掲示板の構成を調べます。

MVCアーキテクチャ
- Model:アプリで扱うデータを保持し、操作する。
- View:受け取ったデータを表示する。
- Controller:ユーザーからのリクエストを処理し、モデル・ビューを呼び出して結果を返す。

参考:
- RailsにおけるMVC(モデル/ビュー/コントローラ) - Ruby on Rails入門
http://www.rubylife.jp/rails/ini/index7.html


07:Rails掲示板を改良しよう

ここでは、Ruby on Railsで作成した掲示板に投稿者名を保存する機能をを追加してみたいと思います。

カラムの追加方法
データベースのarticlesテーブルに、nameカラムを追加する

$ rails generate migration AddNameToArticle name:string
$ rails db:migrate

viewファイルを変更する

index.html.erb
<table>
  <thead>
    <tr>
      <th>Content</th>
      <th>Name</th>
      <th colspan="3"></th>
    </tr>
  </thead>

  <tbody>
    <% @articles.each do |article| %>
      <tr>
        <td><%= article.content %></td>
        <td><%= article.name %></td>
        <td><%= link_to 'Show', article %></td>
        <td><%= link_to 'Edit', edit_article_path(article) %></td>
        <td><%= link_to 'Destroy', article, method: :delete, data: { confirm: 'Are you sure?' } %></td>
      </tr>
    <% end %>
  </tbody>
</table>
show.html.erb
<p>
  <strong>Name:</strong>
  <%= @article.name %>
</p>
_form.html.erb
<div class="field">
  <%= f.label :name %>
  <%= f.text_field :name %>
</div>

コントローラーを修正する

article_controller.rb
def article_params
    params.require(:article).permit(:content, :name)
end

演習課題1「nameカラムの追加」
右の環境には、Railsで「myblog」というプロジェクトに、「diary」掲示板が作られています。
ターミナルを使って、ここに「name」カラムを追加してください。

採点して、すべてのジャッジに正解すれば、演習課題クリアです!

範解答1
次のコマンドを順にターミナルで実行する

$ cd ~/myblog
$ rails generate migration AddNameToDiary name:string
$ rails db:migrate


Rails入門1-2: RailsのDBの動作を理解しよう


01:Railsの設計方針を知ろう

このチャプターでは、Rails の設計方針について学習すると共に、データベースとの連携の概要を理解します。

MVCアーキテクチャ
- Model:アプリで扱うデータを保持し、操作する。
- View:受け取ったデータを表示する。
- Controller:ユーザーからのリクエストを処理して、モデル・ビューを呼び出し、結果を返す。


02:Rails コンソールで動作を確認しよう

ここでは、Rails の動作確認に欠かせない Rails コンソールの基本的な使い方を学習します。
Rails コンソールを使うと、Rails アプリの環境を有効にした状態で、Ruby コードをひとつずつ実行することができます。

Railsコンソールの起動

cd bbs
rails console

次のように、短縮形も利用できる。

rails c

railsコンソール終了
exit

データ数を表示する

Article.count

特定のデータを表示する

Article.find(1)

特定のデータを変数に代入して、カラムを取り出す

article = Article.find(1)
article.content

カラムを更新する

article = Article.find(1)
article.content = "hello world"
article.save


演習課題「Railsコンソールで、データベースの値を変更する」
右の環境には、「myblog」プロジェクトに「Diary」という1行掲示板が作成されています。
Railsコンソールを使って、掲示板の最初の記事(id = 1)のcontentカラムの値を次の文字列に変更してください。

sunny day

採点して、すべてのジャッジに正解すれば、演習課題クリアです!


模範解答1
次のコマンドをターミナルで実行する

$ cd myblog
$ rails c
$ diary = Diary.find(1)
$ diary.content = 'sunny day'
$ diary.save


#03:ActiveRecord を理解しよう
ここでは、Rails のデータベースを操作する ActiveRecord について学習します。ActiveRecord は、データベースのテーブルを Ruby のオブジェクトに割り当てる機能です。

ActiveRecordとは
ActiveRecordは、データベースのテーブルをRubyのオブジェクトに割り当てる機能です。ActiveRecordを使うことで、SQLを書かなくても、Rubyの作法でデータベースを操作することができるんですよ。

すべてのデータを表示する

articles = Article.all
articles.each {|article| p article}


#04:データベースのマイグレーションを理解しよう
ここでは Rails のマイグレーションについて学習します。
マイグレーションは Rails で自動生成した設定内容をデータベースに反映させます。

マイグレーションとは
マイグレーションとは、簡単に言うと、データベースのテーブルを作成・変更するための仕組みです。

Railsは、マイグレーションファイルという設定ファイルを自動作成して、それをmigrationコマンドで、データベースに反映させます。

カラムを追加するマイグレーションファイルを生成する

rails generate migration AddCategoryToArticle category:string

生成されたマイグレーションファイルは、db/migrateにあります。

データベース設定を反映する

rails db:migrate

データベース設定を取り消す

rails db:rollback


演習課題1「マイグレーションでカラムを追加」
右の環境では、「myblog」プロジェクトに「Diary」という1行掲示板が作成されています。
「diaries」テーブルに「degree」という文字列のカラムを追加してください。

手順は次の通りです。

  1. カラムを追加するマイグレーションファイルを生成し、
  2. 生成したファイルの設定をデータベースに反映させてください。

採点して、すべてのジャッジに正解すれば、演習課題クリアです!

模範解答1
次のコマンドをターミナルで実行する
$ cd myblog
$ rails generate migration AddDegreeToDiary degree:string

$ rails db:migrate

演習課題2「ロールバックでカラムを削除」
右の環境では、「myblog」プロジェクトに「Diary」という1行掲示板が作成されています。
「diaries」テーブルの「degree」というカラムを削除してください。

ただし、「degree」というカラムを追加するマイグレーションファイル「作成日時_add_degree_to_diary.rb」がすでに存在します。
マイグレーションのロールバックによって、このファイルの設定を取り消すことで、「degree」カラムを削除してください。

採点して、すべてのジャッジに正解すれば、演習課題クリアです!

模範解答2
次のコマンドをターミナルで実行する
$ cd myblog

$ rails db:rollback

演習課題3「マイグレーションファイルのカラム名を変更する」

右の環境では、「myblog」プロジェクトに「Diary」という1行掲示板が作成されています。
ここに、「degree」というカラムを追加するマイグレーションファイルが作られています。
カラム名を変更するため、このマイグレーションファイルでカラム名を下記のように変更してから、
マイグレーションを実行してください。

degree -> temperature

採点して、すべてのジャッジに正解すれば、演習課題クリアです!


模範解答3
myblog/db/migrate/XXXX_add_degree_to_diary ファイルを修正する

class AddDegreeToDiary < ActiveRecord::Migration[5.0]
def change
add_column :diaries, :temperature, :string
end
end


#05:ビューにカラムを追加しよう
ここでは、モデルのデータをビューに表示する時、ActiveRecord のメソッドをどのように記述するか学習します。
そのために、モデルに追加したカラムを ビューに表示させてみましょう。

Railsコンソールで、新しい記事を投稿する

article = Article.new
article.feeling = "(^o^)"
article.save

erbとは
erbは、embedded Rubyの略。erbを使うと、htmlファイルを表示するときに、埋め込んでおいたRubyのコードが実行される。

erbの書き方
- <% %> : Rubyのコードを実行する
- <%= %> : Rubyのコードを実行し、その結果をhtmlの該当位置に出力する

  <% @articles.each do |article| %>
    <tr>
      <td><%= article.content %></td>
      <td><%= article.name %></td>
      <td><%= article.feeling %></td>
      <td><%= link_to 'Show', article %></td>
      <td><%= link_to 'Edit', edit_article_path(article) %></td>
      <td><%= link_to 'Destroy', article, method: :delete, data: { confirm: 'Are you sure?' } %></td>
    </tr>
  <% end %>

演習課題「Railsコンソールで、記事を投稿する」
右の環境には、「myblog」プロジェクトに「Diary」という1行掲示板が作成されています。
Railsコンソールを使って、この掲示板に、次の内容を持つ新しい記事を作成してください。

It was fine weather today

採点して、すべてのジャッジに正解すれば、演習課題クリアです!


模範解答1
次のコマンドをターミナルで実行する
$ cd myblog
$ rails c
$ diary = Diary.new
$ diary.content = 'It was fine weather today'
$ diary.save


演習課題「Viewにカラムを追加する」
右の環境には、「myblog」プロジェクトに「Diary」という1行掲示板が作成されています。
この一覧ページ(index.html.erb)に、次の順序で「temperature」カラムを追加してください。
「temperature」カラムは、すでにデータベースに追加してあります。

content, temperature

採点して、すべてのジャッジに正解すれば、演習課題クリアです!

模範解答1
index.html.erb に temperature を表示するよう修正する

<p id="notice"><%= notice %></p>

<h1>Diaries</h1>

<table>
  <thead>
    <tr>
      <th>Content</th>
      <th colspan="3"></th>
    </tr>
  </thead>

Rails入門1-3: Railsのデータの流れを理解しよう


#01:Webアプリのデータの流れを理解しよう
このレッスンでは、Ruby on Rails のアプリが動作するとき、どのようにデータを処理していくか学習します。先ほどのレッスンから引き続き、簡単な 1 行掲示板を題材にして、Rails の動作をさらに理解しましょう。

Webフォームのデータ送信方式
- GETメソッド
URLに含める

http://example.net?id=3&content=hello

  • POSTメソッド リクエストメッセージに含める

article[name]=paiza
article[content]=hello+world

Railsサーバーのログを上下にスクロールするには
ターミナルタブでは、マウスのホイールで、表示内容を上下にスクロールされることができます。
Railsサーバーのログなどが、画面の上部に消えてしまった場合に役立ちます。

なお、ホイールでは操作できない場合、下記の操作が有効です。

Mac:CTRLキー + Altキー + 上下矢印キー

Windows: CTRLキー + 上下矢印キー

#02:Railsのルーター機能を理解しよう
ここでは、Rails アプリへのアクセスを振り分けるルーター機能について学習します。Rails アプリでは、機能ごとに URL が割り当てられており、ルーターは、そのアクセスをどのコントローラーのどのメソッドに振り分けるか制御します。

Routersの設定内容を確認する

rails routes

ルーターの振り分け先を設定する
routes.rbで設定します。

config/routes.rb

Rails.application.routes.draw do
get 'welcome/index'

resources :articles
root 'welcome#index'
end

参考になるWebサイト
- Rails のルーティング | Rails ガイド
https://railsguides.jp/routing.html


演習課題「インデックスページのルーター設定」
右の環境には、「myblog」プロジェクトに「Diary」という1行掲示板が作成されています。
このroutes.rbファイルを修正して、アドレスに何も付けずにアクセスした時、
1行掲示板の一覧ページを表示するようにしてください。

採点して、すべてのジャッジに正解すれば、演習課題クリアです!


模範解答1
myblog/config/routes.rb ファイルを修正する
Rails.application.routes.draw do
resources :diaries
get 'start/index'
root 'diaries#index'

# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
end


#03:ビューのテンプレートをさらに理解しよう
ここでは、Rails アプリの表示内容を制御するビューの書き方について、さらに理解を深めます。そのために、詳細ページを例にとり、コントローラーとビューの連携について学習します。また、ビューを作る時に便利なヘルパーメソッドについても取り上げます。

ヘルパーメソッドとは
ヘルパーメソッドは、ビューを作るときに利用できる専用コマンドです。
ヘルパーメソッドは、このように不等号とパーセント記号の中に記述します。

<%= link_to 'Edit', edit_article_path(@article) %> |
<%= link_to 'Back', articles_path %>

link_toヘルパーメソッドについて
htmlのタグに相当するヘルパーメソッドで、Railsアプリのページ間のリンクなどをシンプルに記述できる。

<%= link_to 'text', path %>

上記のlink_toヘルパーメソッドは、次のhtmlに変換される。

text

link_toのパスには、rails routesコマンドで最初に表示される名前付きパスが利用できる。


演習課題「ビューにカラムを追加する」
右の環境には、「myblog」プロジェクトに「Diary」という1行掲示板が作成されています。
この詳細ページ(show.html.erb)に、次の順序で「weather」カラムを追加してください。
「weather」カラムは、すでにデータベースに追加してあります。

content, weather

採点して、すべてのジャッジに正解すれば、演習課題クリアです!

模範解答1
myblog/views/diaries/show.html.erb ファイルを修正する。

<%= notice %>

Content:

Weather:

<%= link_to 'Edit', edit_diary_path(@diary) %> |
<%= link_to 'Back', diaries_path %>


演習課題「リンク先を変更する」
右の環境には、「myblog」プロジェクトに「Diary」という1行掲示板が作成されています。
この詳細ページ(show.html.erb)で、記事一覧に戻る「Back」リンクを、Welcomeページへのリンクに変更してください。
Welcomeページは、すでにRailsアプリに追加してあります。

採点して、すべてのジャッジに正解すれば、演習課題クリアです!

模範解答1
myblog/views/diaries/show.html.erb ファイルを修正し、link_to の 'Back' を welcome_index_path にする。

<%= notice %>

Content:

Weather:

<%= link_to 'Edit', edit_diary_path(@diary) %> |

<%= link_to 'Back', welcome_index_path %>

#04:投稿フォームの動作を理解しよう
ここでは、Rails アプリの Web フォームの動作について、さらに理解を深めます。そのために、投稿フォームにカラムを追加すると共に、form_for メソッドの使い方について学習します。

部分テンプレートとは
複数のビューの共通要素を記述するテンプレート。
呼び出すには、次のようにrenderメソッドを利用する。

<%= render 'form', article: @article %>

この場合、「_form.html.erb」が部分テンプレートのファイル名になる。
また、article変数で、@articleのオブジェクトを利用できる。

フォームを作成するヘルパーメソッド
- form_for:投稿フォームのように、Modelの新規作成・更新に使用する
- form_tag:検索フォームのように、Modelを更新しない場合に使用する

form_forメソッド
対象となるモデルのカラムをf変数で取り出して、フォームの構成要素を指定する。

<%= form_for(@article) do |f| %>


<%= f.label :name %>
<%= f.text_field :name %>


<%= f.label :content %>
<%= f.text_field :content %>


<%= f.submit %>

<% end %>

演習課題「フォームにカラムを追加する」
右の環境には、「myblog」プロジェクトに「Diary」という1行掲示板が作成されています。
この投稿フォームページに、次の順序で「weather」カラムを追加してください。
「weather」カラムは、すでにデータベースに追加してあります。

content, weather

採点して、すべてのジャッジに正解すれば、演習課題クリアです!


模範解答1
myblog/app/views/diaries/_form.html.erb ファイルを修正し、weather カラムの内容を表示させる。

<%= form_for(diary) do |f| %>
<% if diary.errors.any? %>


<%= pluralize(diary.errors.count, "error") %> prohibited this diary from being saved:

  <ul>
  <% diary.errors.full_messages.each do |message| %>
    <li><%= message %></li>
  <% end %>
  </ul>
</div>

<% end %>


#05:controllerがデータを書き込む流れを理解しよう
ここでは、投稿フォームのデータを Controller でデータベースに書き込む流れについて学習します。そのために、Web フォームから受信した Parameters の処理について理解すると共に、データベースに書き込む際の安全性を高める strong parameter について説明します。

ストロングパラメータとは
データベースに安全にアクセスするために、データベースに書き込みできるカラムをリストアップしておく。
controllerのarticle_paramsメソッドに記述する。

articles_controller.rb(一部)

Never trust parameters from the scary internet, only allow the white list through.

def article_params
params.require(:article).permit(:content, :name, :feeling)
end


演習課題「コントローラにカラムを追加する」
右の環境には、「myblog」プロジェクトに「Diary」という1行掲示板が作成されています。
この掲示板に「weather」カラムを追加しましたが、フォームを投稿ボタンをクリックしても、データベースに書き込まれません。
コントローラーを修正して、「weather」カラムのデータが反映されるようにしてください。

採点して、すべてのジャッジに正解すれば、演習課題クリアです!


模範解答1
myblog/app/controllers/diaries_controller.rb を修正し、ストロングパラメータ部分に weather を追加する。

Never trust parameters from the scary internet, only allow the white list through.

def diary_params
params.require(:diary).permit(:content, :weather)

end

#06:検索フォームを追加してみよう
ここでは、1 行掲示板に検索フォームを追加します。そのために、検索フォームから送信した検索キーワードを、サーバー側で受け取って、一覧ページを絞り込み表示してみましょう。

ビューに検索フォームを追加する
index.html.erb(一部)

<%= form_tag('/articles', method: 'get') do %>
<%= label_tag(:name_key, 'Search name:') %>
<%= text_field_tag(:name_key) %>
<%= submit_tag('Search') %> <%= link_to 'Clear', articles_path %>
<% end %>


コントローラーにindexメソッドに検索コードを追加する
articles_controller.rb(一部)

GET /articles

GET /articles.json

def index
if params[:name_key]
@articles = Article.where('name LIKE ?', "%#{params[:name_key]}%")
else
@articles = Article.all
end
end


演習課題「間違い探し」
右の環境には、「myblog」プロジェクトに「Diary」という1行掲示板が作成されています。
この掲示板の一覧ページに「weather」カラムの検索フォームを追加しましたが、「search」ボタンをクリックしても、検索ができません。どうやら、index.html.erbに間違いがあるようです。

viewを修正して、「weather」カラムを検索できるようにしてください。

採点して、すべてのジャッジに正解すれば、演習課題クリアです!

模範解答1
myblog/app/views/diaries/index.html.erb ファイルを修正し、form_tag メソッドを使用して検索フォームを作成する。
<%= form_tag('/diaries', method: 'get') do %>
<%= label_tag(:weather_key, 'Search weather:') %>
<%= text_field_tag(:weather_key) %>
<%= submit_tag('Search') %> <%= link_to 'Clear', diaries_path %>

<% end %>

演習課題「間違い探し」
右の環境には、「myblog」プロジェクトに「Diary」という1行掲示板が作成されています。
この掲示板の一覧ページに「weather」カラムの検索フォームを追加しましたが、部分一致検索ができません。どうやら、diaries_controller.rbに間違いがあるようです。

コントローラーを修正して、「weather」カラムを検索できるようにしてください。

採点して、すべてのジャッジに正解すれば、演習課題クリアです!

模範解答1
myblog/app/controllers/diaries_controller.rb を修正し、部分一致検索を可能にする。

GET /diaries

GET /diaries.json

def index
if params[:weather_key]
@diaries = Diary.where('weather LIKE ?', "%#{params[:weather_key]}%")
else
@diaries = Diary.all
end

end

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Docker] コンテナ内で日本語を入力可能にするためのDockerfile記述方法

はじめに

Dockerコンテナ内でrails cからデータベースにレコードを追加しようとした時に、日本語を入力できなかったので、Dockerコンテナを立ち上げた時に、日本語対応がなされるようにDockerifleを書き換えた。

以下の記事を参考にした。
【Docker】MySQLの日本語化

内容

Dockerfile
FROM ruby:2.5.3
RUN apt-get update -qq && apt-get install -y vim nodejs default-mysql-client \
    locales locales-all && \ #追加
    echo "ja_JP.UTF-8 UTF-8" > /etc/locale.gen && \ #追加
    locale-gen && \ # 追加
    update-locale LANG=ja_JP.UTF-8 #追加
COPY . /fishingshares
ENV LANG="ja_JP.UTF-8" \ #追加
    TZ="Asia/Tokyo" \ #追加
    APP_HOME="/fishingshares"
WORKDIR $APP_HOME
RUN bundle install
ADD . $APP_HOME

コメントを書いているところが追記した箇所である。

以上のDockerfileを基に、コンテナを立ち上げて起動すると、コンテナ内で日本語が入力できるようになる。

以上

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

三項演算子の使い方

三項演算子

簡単にいうとif文をシンプルに書けます。
普通のif文

if 条件
 式1
else
 式2
end

三項演算子

条件 ? 式1 : 式2

となります
【例】if文

def add_button_name
    if controller.action_name == 'show'
      "コメントを投稿する"
    else
      "コメントを更新する"
    end
  end

【例】三項演算子

def add_button_name
  controller.action_name == 'show' ? "コメントを投稿する" : "コメントを更新する"
end

になります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】Ajaxを用いた非同期フォロー機能の実装

目標

ezgif.com-video-to-gif.gif

開発環境

・Ruby: 2.5.7
・Rails: 5.2.4
・Vagrant: 2.2.7
・VirtualBox: 6.1
・OS: macOS Catalina

前提

ログイン機能を実装済み。

ログイン機能 ➡︎ https://qiita.com/matsubishi5/items/5bd8fdd45af955cf137d

フォロー機能を実装

1.モデル

ターミナル
$ rails g model Relationship follower_id:integer followed_id:integer
ターミナル
$ rails db:migrate
schema.rb
ActiveRecord::Schema.define(version: 2020_04_05_115005) do
    create_table "relationships", force: :cascade do |t|
        t.integer "follower_id"
        t.integer "followed_id"
        t.datetime "created_at", null: false
        t.datetime "updated_at", null: false
    end  

    create_table "users", force: :cascade do |t|
        t.string "email", default: "", null: false
        t.string "encrypted_password", default: "", null: false
        t.string "reset_password_token"
        t.datetime "reset_password_sent_at"
        t.datetime "remember_created_at"
        t.integer "sign_in_count", default: 0, null: false
        t.datetime "current_sign_in_at"
        t.datetime "last_sign_in_at"
        t.string "current_sign_in_ip"
        t.string "last_sign_in_ip"
        t.string "name"
        t.datetime "created_at", null: false
        t.datetime "updated_at", null: false

        t.index ["email"], name: "index_users_on_email", unique: true
        t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
    end
end
user.rb
class User < ApplicationRecord
    devise :database_authenticatable, :registerable,
           :recoverable, :rememberable, :trackable,:validatable

    has_many :books, dependent: :destroy
    has_many :follower, class_name: "Relationship", foreign_key: "follower_id", dependent: :destroy
    has_many :followed, class_name: "Relationship", foreign_key: "followed_id", dependent: :destroy
    has_many :following_user, through: :follower, source: :followed
    has_many :follower_user, through: :followed, source: :follower

#ユーザーをフォローする
    def follow(user_id)
        follower.create(followed_id: user_id)
    end

#ユーザーをアンフォローする
    def unfollow(user_id)
        follower.find_by(followed_id: user_id).destroy
    end

#フォローしているかを確認する
    def following?(user)
        following_user.include?(user)
    end
end

:one: has_many :follower, class_name: "Relationship", foreign_key: "follower_id", dependent: :destroy
➡︎ Relationshipモデルのfollower_idにuser_idを格納

:two: has_many :followed, class_name: "Relationship", foreign_key: "followed_id", dependent: :destroy
➡︎ Relationshipモデルのfollower_idにuser_idを格納

:three: has_many :following_user, through: :follower, source: :followed
➡︎ 自分がフォローしているユーザー

:four: has_many :follower_user, through: :followed, source: :follower
➡︎ 自分をフォローしているユーザー

relationship.rb
class Relationship < ApplicationRecord
    belongs_to :follower, class_name: "User"
    belongs_to :followed, class_name: "User"
end

2.コントローラー

ターミナル
$ rails g controller relationships
relationships_controller.rb
class RelationshipsController < ApplicationController
    def follow
        current_user.follow(params[:id])
        redirect_to root_path
    end

    def unfollow
        current_user.unfollow(params[:id])
        redirect_to root_path
    end
end
users_controller.rb
class UsersController < ApplicationController
    def index
        @users = User.all
    end

#自分がフォローしているユーザー一覧
    def following
        @user = User.find(params[:user_id])
    end

#自分をフォローしているユーザー一覧
    def follower
        @user = User.find(params[:user_id])
    end
end

3.ルーティング

routes.rb
Rails.application.routes.draw do
    root 'homes#top'
    devise_for :users
    resources :users
    post 'follow/:id' => 'relationships#follow', as: 'follow'
    post 'unfollow/:id' => 'relationships#unfollow', as: 'unfollow'
    get 'users/following/:user_id' => 'users#following', as:'users_following'
    get 'users/follower/:user_id' => 'users#follower', as:'users_follower'
end

4.ビュー

users/index.html.erb
<table class="table">
    <thead>
        <tr>
            <th>Image</th>
            <th>Name</th>
            <th></th>
            <th></th>
            <th></th>
            <th></th>
        </tr>
    </thead>

    <tbody>
        <% @users.each do |user| %>
            <tr>
                <td><%= attachment_image_tag(user, :profile_image, :fill, 50, 50, fallback: "no-image-mini.jpg") %></td>
                <td><%= user.name%></td>
                <td><%= link_to "Show", user %></td>
                <td><%= link_to "フォロー数:#{user.follower.count}", users_following_path(user) %></td>
                <td><%= link_to "フォロワー数:#{user.followed.count}", users_follower_path(user) %></td>
                <td>
                    <% unless user == current_user %>
                        <% if current_user.following?(user) %>
                            <%= link_to 'フォロー解除', unfollow_path(user.id), method: :POST,class:"btn btn-info", remote: true %>
                        <% else %>
                            <%= link_to 'フォローする', follow_path(user.id), method: :POST,class:"btn btn-default" %>
                        <% end %>
                    <% end %>
                </td>
            </tr>
        <% end %>
    </tbody>
</table>
users/following.html.erb
<h3>フォロー中</h3>
<table class="table">
    <thead>
        <tr>
            <th>Image</th>
            <th>Name</th>
            <th>Introduction</th>
            <th></th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        <% @user.following_user.each do |user| %>
            <tr>
                <td><%= attachment_image_tag user, :profile_image, :size =>'30x30', class: "img-circle pull-left profile-thumb", fallback: "no_image.jpg" %></td>
                <td><%= user.name %></td>
                <td><%= user.introduction %></td>
                <td><%= link_to "Show", user %></td>
                <td>
                    <% if current_user.following?(user) %>
                        <% unless user == current_user %>
                            <%= link_to 'フォロー解除', unfollow_path(user.id), method: :POST,class:"btn btn-info", remote: true %>
                        <% else %>
                            <%= link_to 'フォローする', follow_path(user.id), method: :POST,class:"btn btn-default" %>
                        <% end %>
                    <% end %>
                </td>
            </tr>
        <% end %>
    </tbody>
</table>
users/follower.html.erb
<table class="table">
    <thead>
        <tr>
            <th>Image</th>
            <th>Name</th>
            <th>Introduction</th>
            <th></th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        <% @user.follower_user.each do |user| %>
            <tr>
                <td><%= attachment_image_tag user, :profile_image, :size =>'30x30', class: "img-circle pull-left profile-thumb", fallback: "no_image.jpg" %></td>
                <td><%= user.name %></td>
                <td><%= user.introduction %></td>
                <td><%= link_to "Show", user %></td>
                <td>
                    <% unless user == current_user %>
                        <% if current_user.following?(user) %>
                            <%= link_to 'フォロー解除', unfollow_path(user.id), method: :POST,class:"btn btn-info", remote: true %>
                        <% else %>
                            <%= link_to 'フォローする', follow_path(user.id), method: :POST,class:"btn btn-default" %>
                        <% end %>
                    <% end %>
                </td>
            </tr>
        <% end %>
    </tbody>
</table>

非同期機能を実装

1.jQueryを導入

Gemfile
    gem 'jquery-rails'
ターミナル
    $ bundle
application.js
    //= require rails-ujs
    //= require activestorage
    //= require turbolinks
    //= require jquery
    //= require_tree .

2.各ビューのフォローボタンを編集

users/~.html.erb
<td class="followbutton_<%= user.id %>"> <!-- 各ボタンにidをつける -->
    <% unless user == current_user %>
        <%= render "followbutton", user: user %> <!-- ボタン部分をパーシャル化 -->
    <% end %>
</td>
users/_followbutton.html.erb
<!-- link_toの引数に「remote: true」を追記 -->
<% if current_user.following?(user) %>
        <%= link_to 'フォロー解除', unfollow_path(user.id), method: :POST,class:"btn btn-info", remote: true %>
<% else %>
        <%= link_to 'フォローする', follow_path(user.id), method: :POST,class:"btn btn-default", remote: true %>
<% end %>

2.JavaScriptファイルを作成

users/follow.js.erb
$("#followbutton_<%= @user.id %>").html("<%= j(render 'users/followbutton', user: @user) %>");
users/unfollow.js.erb
$("#followbutton_<%= @user.id %>").html("<%= j(render 'users/followbutton', user: @user) %>");

$("#followbutton_<%= @user.id %>")
➡︎ 「2」で付けたクラスを指定

.html("<%= j(render 'users/followbutton', user: @user) %>");
➡︎ フォローボタンのパーシャルをrenderしている

参考サイト

https://freecamp.life/rails-follow-follower/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RailsでjQueryを使う

学習中に作っていたrailsアプリでjQueryを使いたかったのですが、
設定方法があいまいだったのでざっと調べました。
参考にしていただけると幸いです。

jQueryを使いたい。

作業内容

1.  'jquery-rails'の導入
2.  turbolinksの停止。
3.  読み込みの確認。

1.  'jquery-rails'を導入する

最下行にjquery-railsを記載します。

Gemfile
gem 'jquery-rails'

2.  turbolinksを停止させる

Gemfileからturbolinksをコメントアウトする。

Gemfile
 ~変更前~
# Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks
gem 'turbolinks', '~> 5' 

 ~変更後~
# Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks
# gem 'turbolinks', '~> 5' # この行をコメントアウトする

( turbolinksはRails 4以降デフォルトでGemfileに導入されています。)

!ここでGemfileを変更したので、bundle installをします。


次に、application.html.haml から turbolinks の部分を削除します。
(application.html.erbから、hamlに変換しています。)

application.html.haml
 ~変更前~
!!!
%html
  %head
    %meta{:content => "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/
    %title giga-app
    = csrf_meta_tags
    = stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload'  このオプションを消す
    = javascript_include_tag 'application', 'data-turbolinks-track': 'reload'  このオプションを消す
  %body
    = render "layouts/notifications"
    = yield

 ~変更後~
 省略
    = stylesheet_link_tag    'application', media: 'all'
    = javascript_include_tag 'application'
 省略

最後にapplication.js から turbolinks の部分を削除します。

application.js
// This is a manifest file that'll be compiled into application.js, which will include all the files
// listed below.
//
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
//
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// compiled file. JavaScript code in this file should be added after the last require_* statement.
//
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
// about supported directives.
//
//= require jquery      
//= require jquery_ujs
//= require turbolinks  ⬅️ ここを削除する
//= require_tree .


3.  Jsファイルの読み込みを確認。

実際の処理を記述するjsファイル( 今回は item.jsとします )に、
以下の記述をし、ブラウザで読み込みを確認します。

item.js
$(function(){
});

ここで、コンソールに何もエラーが出なければ問題なく読み込めています。

うまく読み込めていないと、何かしらエラーが出ています。
Image from Gyazo


ここまで問題なく進むと、RailsにてjQuery を使う準備が完了です。
イベントの発火などの記事も書きたいと思います...

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

第13章〜マイクロポストを作る〜

ユーザーのマイクロポスト

今回ユーザーが短いメッセージを投稿できるような機能を実装する。

マイクロソフトモデルの作成

このMicropostモデルはデータ検証とUserモデルの関連付けを含んでいる
マイクロポストの内容を保存するcontent属性と特定のユーザーとマイクロポストを関連付けするuser_id属性の2つだけの属性だけをもつ。
Micropostはstring型ではなくtext型を使用する。
140文字制限をつけるので表示できる文字数などは差がないがtext型のフォームのほうがより自然な投稿フォームを実現できる。
Userモデルとの最大の違いはreferences型を利用している点である。
これを利用すると自動的にインデックスと外部キー参照付きのuser_idカラムが追加され、UserとMicropostを関連付けする下準備をしてくれる。

add_index:micropost,[use_id,:create_at]

このコードはあるユーザーを検索したいときにUsersテーブルのnameカラムにインデックスを張ってないと、プログラムは、Userテーブルのnameカラムを上から順にみて、そのユーザーのデータを取得します。もし、これが1万人もしくはそれ以上の大量のデータを含むカラムだった場合に時間がとてもかかってしまう。
Usersテーブルのnameカラムにindexを張ることで、アルファベット順にnameを並べ替え検索しやすいようにしてくれます。また、user_idとcreated_atの両方を1つの配列に含めている点にも注目です。こうすることでActive Recordは、両方のキーを同時に扱う複合キーインデックス (Multiple Key Index) を作成します。

バリデーションの作成

まずuser_idに対するバリデーションを作成。
次に、マイクロポストのcontent属性に対するバリデーションを追加。

 マイクロポストのuser_idに対する検証 green
app/models/micropost.rb
class Micropost < ActiveRecord::Base
  belongs_to :user
  validates :user_id, presence: true
 validates :content, presence: true, length: { maximum: 140 }
end

UserとMicropostの関連付け

Micropostモデルにコード化
belongs_to :user

MicropostとそのUserはbelongs_to(一対一)の関係性がある

Userモデルにコード化
has_many:micropost

UserとMicropostはhas_many(一対多)に関係性がある

@user.microposts.build

userに紐付いた新しいMicropostオブジェクトを返す

Micropostを改良する

UserとMicropostの関連付けを改良する。
ユーザーのマイクロポストを特定の順序で取得できるようにする
マイクロポストをユーザーに依存させて、ユーザーが削除されたらマイクロポストも自動的に削除されるようにしていく

デフォルトスコープ

user.micropostsメソッドはデフォルトでは読み出しの順序に対して何も保証しないが、 ブログやTwitterの慣習に従って、作成時間の逆順、つまり最も新しいマイクロポストを最初に表示するようにしてみましょう4。これを実装するためには、default scopeというテクニックを使う。

default_scopeでマイクロポストを順序付ける green
app/models/micropost.rb
class Micropost < ApplicationRecord
  belongs_to :user
  default_scope -> { order(created_at: :desc) }
  validates :user_id, presence: true
  validates :content, presence: true, length: { maximum: 140 }
end

Dependent: destroy

今度はマイクロポストに第二の要素を追加する。サイト管理者はユーザーを破棄する権限を持つ。ユーザーが破棄された場合、ユーザーのマイクロポストも同様に破棄されるべきです。

class User < ApplicationRecord
  has_many :microposts, dependent: :destroy
end

Micropostの操作

Micropostsリソースへのインターフェイスは、主にプロフィールページとHomeページのコントローラを経由して実行されるので、Micropostsコントローラにはnewやeditのようなアクションは不要ということになる。

Rails.application.routes.draw do

resources :microposts,          only: [:create, :destroy]
end

マイクロポストのアクセス制御

logged_in_userメソッドを使って、ログインを要求する。
Usersコントローラ内にこのメソッドがあったので、beforeフィルターで指定していたが、このメソッドはMicropostsコントローラでも必要です。そこで、各コントローラが継承するApplicationコントローラにこのメソッドを移し、まとめる。

logged_in_userメソッドをApplicationコントローラに移す red
app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  include SessionsHelper

  private

    # ユーザーのログインを確認する
    def logged_in_user
      unless logged_in?
        store_location
        flash[:danger] = "Please log in."
        redirect_to login_url
      end
    end
end

このコードによって、Micropostsコントローラからもlogged_in_userメソッドを呼び出せるようになる。

Micropostを作成

まずはcreateアクションを作成

 Micropostsコントローラのcreateアクション
app/controllers/microposts_controller.rb
class MicropostsController < ApplicationController
  before_action :logged_in_user, only: [:create, :destroy]

  def create
    @micropost = current_user.microposts.build(micropost_params)
    if @micropost.save
      flash[:success] = "Micropost created!"
      redirect_to root_url
    else
      render 'static_pages/home'
    end
  end

  def destroy
  end

  private

    def micropost_params
      params.require(:micropost).permit(:content)
    end
end

Strong Parametersを使用
ストロングパラメータは、Web上から受けつけたパラメータが、本当に安全なデータかどうかを検証した上で、取得するための仕組み。Rails4から実装されている。

具体的にどうやって防ぐか
メソッドにあらかじめ登録・更新を許可するカラム名を指定(ホワイトリスト形式)する。そうすると、万が一、未許可のカラムデータが送られてきても、データの登録前に未許可であることを検出し、登録対象として無視することができる。

フィードの原型

マイクロポスト投稿フォームが動くようになったが、今の段階では投稿した内容をすぐに見ることができない。Homeページにまだマイクロポストを表示する部分が実装されていないからである。
すべてのユーザーがフィードを持つので、feedメソッドはUserモデルで作るのが自然。フィードの原型では、まずは現在ログインしているユーザーのマイクロポストをすべて取得する。

マイクロポストの削除

マイクロポストリソースにポストを削除する機能を追加します。これはユーザー削除と同様に"delete" リンクで実現します。ユーザーの削除は管理者ユーザーのみが行えるように制限されていたのに対し、今回は自分が投稿したマイクロポストに対してのみ削除リンクが動作するように設定する。

Micropostsコントローラのdestroyアクション

class MicropostsController < ApplicationController
  before_action :logged_in_user, only: [:create, :destroy]
  before_action :correct_user,   only: :destroy
  .
  .
  .
  def destroy
    @micropost.destroy
    flash[:success] = "Micropost deleted"
    redirect_to request.referrer || root_url
  end

  private

    def micropost_params
      params.require(:micropost).permit(:content)
    end

    def correct_user
      @micropost = current_user.microposts.find_by(id: params[:id])
      redirect_to root_url if @micropost.nil?
    end
end

request.referrer || root_url
ここではrequest.referrerというメソッドを使っています12。このメソッドはフレンドリーフォワーディングのrequest.urlと似ていて、一つ前のURLを返す (今回の場合、Homeページになります)このため、マイクロポストがHomeページから削除された場合でもプロフィールページから削除された場合でも、request.referrerを使うことでDELETEリクエストが発行されたページに戻すことができるので、非常に便利です。ちなみに、元に戻すURLが見つからなかった場合でも (例えばテストではnilが返ってくることもあります)、||演算子でroot_urlをデフォルトに設定しているため心配ない。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】繰り返し処理でメニューを簡単に作る方法

はじめに

下の記事「【爆速実装】夢と魔法のeach文【Rails-haml】」の記事を見て爆速でメニューが作れることを知りました。
https://qiita.com/enjoy_omame/items/c9f14f9cbb9bd2408a18
すげぇ。なんじゃこりゃ!

で、この記事なんですが、あくまで"簡単に作る方法"を解説してくれています。遷移先などの実装には触れていません。
きっと「ここからは自分で考えて実用的にしましょうね」という意図があるのだろう。と、言っているんだと思います。僕には聞こえました。そんな副音声が!
なので、自分なりに考えて実用的な実装をしてみたので今回記事にしてみます。

実装した内容

こんな感じのメニューを繰り返し処理で実装します。
今回は、usersコントローラ内のshowアクション(ここで繰り返し処理の元になるネタをしこむ)を介して、show.html.haml(ここで仕込んだネタを繰り返し処理して)表示するといった感じに処理が走ってます。
スクリーンショット 2020-04-09 0.46.19.png
「【爆速実装】夢と魔法のeach文【Rails-haml】」の記事では、上記実装で言う、「プロフィール編集」「クレジットカード登録」「ログアウト」とかの文字列を繰り返し処理を使って実装する方法を紹介しています。

そのため、リンク先の設定とかについては触れていません。
なので、クリックした時の遷移先についても繰り返しで実装できるように挑戦しました!

実装結果

コントローラー

users_controller.rb(参考にした記事を自分なりに応用してみた)
  def show
    @user = User.find(params[:id])
    @contents = [{name:"プロフィール編集",path: "/profiles/edit",pattern: "GET"},{name: "クレジットカード登録", path:"#", pattern: "GET"},{name: "ログアウト", path: "/users/sign_out", pattern: "delete"}]
  end

※クレジットカードの導線はまだ決めてないので、遷移先を"#"で仮置きしてます。

ビュー

show.rb(部分テンプレートで切り出してます)
- @contents.each do |content|
  .MyPageSideMenuList
    .MyPageSideMenuList__Value
      = link_to content[:path],method: content[:pattern] do
        = content[:name]
    .MyPageSideMenuList__Disclosure
      = icon('fas','chevron-right')

解説(と、いうか実装に到るまでの脳内経緯)

「参考にした記事では、表示したい文字列をeach文を使って繰り返し処理で表示しているぞ。」

「ん?それなら、each文を使って繰り返し処理をする時にURLとかも一緒にインスタンス変数に入れられれば良い感じに遷移先も実装できるんじゃない?」

「そんなことできるのだろうか・・・。ん?まてよ、データベースから複数のレコードを取得している時って似たような事やってない?」

「あ、じゃあ配列の中身をハッシュにして、その中に文字列とURLをキーとして設定すればいいんじゃない?」

「ぉ?と言うことは、URLを設定するんだしHTTPメソッドも指定しないとルーティングエラー起こすぞこりゃ。methodを入れるキーも必要だ!」
(ごにょごにょ

えーと・・・端的に言いますと

配列の中に3つのキーを含んだハッシュを入れてみました。
ハッシュの内容はこんな感じ

キー(シンボル) 入れるバリュー
name: リストに表示したい文字列
path: 遷移させたいURL
pattern: HTTPメソッド
user_controller(代入内容の比較).rb
#before
@contents = ["プロフィール編集","クレジットカード登録","ログアウト"]
#After(ハッシュを配列に入れてる)
@contents = [{name: "プロフィール編集",path: "/profiles/edit",pattern: "GET"},{name: "クレジットカード登録", path: "#", pattern: "GET"},{name: "ログアウト", path: "/users/sign_out", pattern: "delete"}]

つまり、name: path: pattern:で1セットのハッシュにしてあげて、この塊をeach文で繰り返す。って感じです。

もうちょっと掘り下げて解説

show.html.haml
-#~省略(部分テンプレートで切り出してます)
- @contents.each do |content|
  .MyPageSideMenuList
    .MyPageSideMenuList__Value
      = link_to content[:path],method: content[:pattern] do
        = content[:name]
    .MyPageSideMenuList__Disclosure
      = icon('fas','chevron-right')

5行目の =link_to に注目です。

もし、繰り返しを使わずに、ログアウトの処理を記述する場合は、以下のような書き方ができます。
(※devise gem を利用している前提でのお話となります。)

sample.html.haml
-# URLをベタ書きした場合
= link_to "/users/sign_out", method: :delete do
 = "ログアウト"

今回は @contents をcontentに代入してeach文で繰り返し処理をしていますよね?
そのため、繰り返し処理の最中、contentさんは以下のように扱われます。

 #1週目のcontentの中身 → {name:"プロフィール編集",path: "#",pattern: "GET"}
 #2週目のcontentの中身 → {name: "クレジットカード登録", path:"#", pattern: "GET"}
 #3週目のcontentの中身 → {name: "ログアウト", path: "/users/sign_out", pattern: "delete"}

ではでは、この知識を前提として、繰り返し処理の3週目がどうなるかというと・・・。

show.html.haml
-#~省略(部分テンプレートで切り出してます)
- @contents.each do |content|
  .MyPageSideMenuList
    .MyPageSideMenuList__Value
      -#実際に記載されている下の2行が...
      = link_to content[:path],method: content[:pattern] do
        = content[:name]
      -#繰り返し処理の結果、下の2行の内容に書き換わる
      = link_to "/users/sign_out",method: :delete do
        = ログアウト
    .MyPageSideMenuList__Disclosure
      = icon('fas','chevron-right')

なんとなくイメージ掴めますかね?
最後は、上の結果を先ほどちょっと記述した
sample.html.haml(URLをベタ書きした時)と比較してみましょう!

sample.html.haml
-# URLをベタ書きした場合
= link_to "/users/sign_out", method: :delete do
 = "ログアウト"

どうでしょうか?同じ内容になってませんか?
これで、繰り返し処理で遷移先も実装が可能となりました!

まとめ

今回やったことをざっとまとめると・・・。

  • コントローラーの該当アクションで繰り返したいネタを配列で準備する
  • Hashの中身を{name: " ~~ ", path: " ~~ ", method: " ~~ "}を1セットとして配列に格納する
  • ヘルパーメソッド の=link_toの引数の箇所にhashのキーを置き換えてあげる

これだけを意識すれば、あら不思議!遷移できるメニューリストが作れちゃう!!
(補足)pathとmethodについては、ターミナルでrails routesをしてあげて、飛ばしたい遷移先がなんなのかを調べてあげれば問題なしです!

最後に

他にも実装方法を色々試しましたが、:pathにPrefixの値を記述した場合はどうもうまくいきませんでした。
理由としては、ダブルクォーテーションもしくはシングルクォーテーションで囲われた状態になってしまうからだと思います。

sample.html.haml
-#prefixの正しい指定方法
= link_to destroy_user_session_path, method: :delete do

-#@contentsの中に入れたハッシュの :path でprefixを指定した場合
= link_to "destroy_user_session_path", method: :delete do
-# →Prefixで指定しているのに""で囲われてしまうからprefixとして扱われない

解消方法やもっと良い実装方法などご存知の方いらっしゃいましたら、コメント頂けますと幸いです!

駄文にお付き合いくださりありがとうございました!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWS初心者の自分でも、RailsアプリをAWS EC2にデプロイ成功できた方法

はじめに

備忘録も兼ねて、RailsアプリをAWSにデプロイする手順をまとめました。筆者自身、ずっとデプロイをHerokuに頼り切っていたのもあり、そろそろHeroku以外の方法でデプロイをしてみたいなと思い、試みた次第です。
この記事では、非常に解説が丁寧な世界一丁寧なAWS解説シリーズベースにデプロイ作業を進めていき、途中で詰まった箇所をところどころ訂正していきます。概念などの詳しい説明は上記記事を見ていただくのが早いです。

なお、この記事では、コマンドは全てMacでの使用を想定しています。ご了承ください。
また、作業の過程で開発環境(local)でのコマンド操作と、EC2インスタンス内でのコマンド操作(remote)に分かれるので、ターミナルを2ウィンドウ用意しておくと捗るかと思います。

1.ざっくりとした流れ

この記事では、AWSのRDSを利用せず、EC2インスタンスにDBのシステムそのものをインストールする方式を採用しています。なので、AWSへのデプロイのざっくりとした順番は、

AWS上に自分のテリトリー(VPC)を作り、ネットワークやセキュリティの設定をする。

VPCの中に、仮想のパソコン(EC2インスタンス)を作成する

EC2インスタンスにSSHログインをし、rubyの環境構築をする。

EC2インスタンス内に、デプロイしたいRailsアプリをclone、更に必要な、Webサーバー(Nginx)や、Unicorn、DB(MySQL)などの環境構築をする。

RailsとWebサーバー、Unicorn、DBを接続する。

こんな感じです。頭のどこかにイメージしておくとよいと思います。

2.デプロイしたいRailsアプリへの事前準備

ローカルでの作業です。

ルーティングにトップページを設定します。これがないとエラーが起きます。
(下の例では、topsコントローラーのtopアクションを、トップページとしています。)

routes.rb
  root 'tops#top'

続いて、Gemfileに以下の追記をします。

Gemfile
group :production do
  gem 'mysql2', '>= 0.4.4'
end

group :production, :staging do
  gem 'unicorn'
end

gem 'dotenv-rails'

config/database.ymlのproduction環境の設定を以下のようにします。

config/database.yml
production:
  adapter: mysql2
  encoding: utf8
  reconnect: false
  database: sample_production (#アプリ名_productionにする)
  username: root
  password: <%= ENV['DATABASE_PRO_PASSWORD'] %>

既にキー流出防止策を取っている方はスキップして問題ありませんが、していない方はdatabaseのパスワードや、クラウドサービスのパスワードなどといった、重要な値は、流出を防ぐために環境変数として.envファイルへと移しておき、.envファイルは.gitignoreファイルに登録しておきましょう。gem 'dotenv-rails'を入れておけば、.envファイルから環境変数の値を読み込んでくれます。
gem 'dotenv-rails'の使い方について、詳しくはこちら

最後に、Githubへプッシュして完了です。(リモートリポジトリは既に作られていて、ローカルリポジトリとの紐付けも既にできているものとします。)

local
$ git add -A
$ git commit -m'preparing-for-deploy'
$ git push origin master

3.EC2インスタンスの置き場の作成

VPC、サブネット、インターネットゲートウェイ、ルートテーブル、セキュリティグループの設定をしていきます。
(下準備編)世界一丁寧なAWS解説。EC2を利用して、RailsアプリをAWSにあげるまでの手順通りに進めれば問題ありません。しかし、最後の段階でパブリックDNSが割り当てられていないことがあるので、こちらの記事を参考に、割当をしましょう。

4.EC2インスタンスの作成

AWS上に作った置き場に、EC2インスタンスを作ります。EC2インスタンス=仮想のパソコンというイメージを持つと、この後の作業が考えやすいと思います。
(DB・サーバー構築編)世界一丁寧なAWS解説。EC2を利用して、RailsアプリをAWSにあげるまでの、8. EC2インスタンスの作成 以降の手順通りに進めれば問題ありません。作成したEC2インスタンスにSSHログインできる段階まで進めてください。
(この記事では、RDSは利用しないので、上記記事のRDSの設定箇所は飛ばしても大丈夫です。)

5.EC2インスタンス内の環境構築

AWSに作ったEC2インスタンスの中に、rubyやその他railsアプリケーションを動かすために必要なものを構築していきます。(以降の例では、ログインユーザーをryotaとして進めています。世界一丁寧なAWS解説シリーズでのユーザー名naokiと読み替えてください。)

Gitなどのインストール

まずはgitなどのインストールをします。

remote
[ryota ~]$ sudo yum install git make gcc-c++ patch openssl-devel libyaml-devel libffi-devel libicu-devel libxml2 libxslt libxml2-devel libxslt-devel zlib-devel readline-devel mysql mysql-server mysql-devel ImageMagick ImageMagick-devel epel-release

インストール中に何回か以下のような確認を求められますが、気にせずyを押して進めてください。

総ダウンロード容量: 110 M
Is this ok [y/d/N]: 

もし以下のようなメッセージが出てきたら、言われたとおりコマンドをrunしてください。

remote
epel-release is available in Amazon Linux Extra topic "epel"

To use, run
# sudo amazon-linux-extras install epel

Node.jsのインストール

次にnode.jsをインストールします。コマンドラインで次のように入力して、ノードバージョンマネージャー (nvm) をインストールします。Node.jsのインストールはこちらの記事を参考にしました。

remote
[ryota ~]$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.34.0/install.sh | bash    (#nvmのインストール)
[ryota ~]$ . ~/.nvm/nvm.sh     (#nvmの有効化)
[ryota ~]$ source .bashrc
[ryota ~]$ nvm install node  (#nvm を使用して Node.js の最新バージョンをインストール。)
[ryota ~]$ node -v   (#node.jsのバージョン確認)

node.jsのバージョンが表示されればnode.jsのインストールは完了です。

Yarnのインストール

次にyarnをインストールします。yarnのインストールはこちらの記事を参考にしました。

remote
[ryota ~]$ curl -o- -L https://yarnpkg.com/install.sh | bash
[ryota ~]$ source .bashrc
[ryota ~]$ yarn -v (#yarnのバージョン確認)

yarnのバージョンが表示されればyarnのインストールは完了です。

Rubyのインストール

まずはrbenvをインストールしましょう。rbenvについて詳しくはこちら

remote
[ryota|~]$ git clone https://github.com/sstephenson/rbenv.git ~/.rbenv 
(#rbenvのインストール) 
[ryota|~]$ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile 
(#パスを通す)
[ryota|~]$ echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
[ryota|~]$ source .bash_profile  
(#.bash_profileの読み込み)
[ryota|~]$ git clone https://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build
(#ruby-buildのインストール)
[ryota|~]$ rbenv rehash  
(#rehashを行う) 

rbenvがインストールできたら、いよいよrubyのインストールです。

remote
[ryota ~]$ rbenv install -v 2.6.5 (#デプロイするrailsで使用するrubyのバージョンに合わせる)
[ryota|~]$ rbenv global 2.6.5
[ryota|~]$ rbenv rehash
[ryota|~]$ ruby -v

ruby -vコマンドで、rubyのバージョンが表示されれば成功です。

6.Git、Githubとの連携、railsアプリのクローン

デプロイしたいプロダクトをEC2インスタンス内にクローンします。まずはEC2インスタンス内のgitの設定をします。

remote
[ryota|~]$ vim .gitconfig

開かれた.gitconfigに以下を転記します。

remote
[user]
  name = your_name (#gitに登録した自分の名前)
  email = hoge@hoge.com (#git登録時の自分のメールアドレス)

[alias] (#普段のローカルでの開発で頻繁に使うgitのエイリアスがあったら登録するのもありです)
  a = add
  b = branch
  co = checkout
  s = status

[url "github:"] (#pull、pushのための設定)
    InsteadOf = https://github.com/
    InsteadOf = git@github.com:

もしgitで登録している名前とメールアドレスがわからなくなったら、以下のコマンドをローカルで打ち込めばわかります。

local
$ git config user.name    (#ユーザー名の確認)
$ git config user.email    (#登録しているメールアドレスの確認) 

.gitconfigファイルの編集が完了したらファイルを保存し、アプリを配置するディレクトリを作成していきます。

remote
[ryota|~]$ cd /
[ryota|/]$ sudo chown ryota var (#varフォルダの所有者をEC2ログイン中のユーザーにする)
[ryota|/]$ cd var
[ryota|var]$ sudo mkdir www
[ryota|var]$ sudo chown ryota www 
[ryota|var]$ cd www
(#wwwと同じ処理)
[ryota|www]$ sudo mkdir rails
[ryota|www]$ sudo chown ryota rails

chownやchmodについて曖昧な人はこちらの記事を、mkdirについて曖昧な人はこちらを参照してみてください。

GitとGithubの接続

いよいよgithubとの接続です。下記コマンドを打ち込んでください。

remote
[ryota|www]$ cd ~
[ryota|~]$ mkdir .ssh (#既に生成されている場合もあります。)
[ryota|~]$ chmod 700 .ssh
[ryota|.ssh]$ cd .ssh
[ryota|.ssh]$ ssh-keygen -t rsa
-----------------------------
Enter file in which to save the key ():aws_git_rsa 
(#ここでファイルの名前を記述して、エンター)
Enter passphrase (empty for no passphrase): 
(#何もせずそのままエンター)
Enter same passphrase again: 
(#何もせずそのままエンター)
-----------------------------
[ryota|.ssh]$ ls
(#「aws_git_rsa」と「aws_git_rsa.pub」が生成されたことを確認)
[ryota|.ssh]$ vim config

configファイルが開いたら、以下を追記。

config
Host github
  Hostname github.com
  User git
  IdentityFile ~/.ssh/aws_git_rsa (#秘密鍵の設定)

configファイルを保存したら、公開鍵の中身をコピーしてください。

remote
[ryota|.ssh]$ cat aws_git_rsa.pub

(#catコマンドの結果、公開鍵の内容が表示されるのでメモ帳などにコピーしておく)
ssh-rsa sdfjerijgviodsjcIKJKJSDFJWIRJGIUVSDJFKCNZKXVNJSKDNVMJKNSFUIEJSDFNCJSKDNVJKDSNVJNVJKDSNVJKNXCMXCNMXNVMDSXCKLMKDLSMVKSDLMVKDSLMVKLCA ryota@ip-10-0-1-10

Githubに公開鍵を登録します。
鍵の登録ページの右上にある、New SSH Key を開いて、Titleに鍵の名前、Keyに先程コピーした公開鍵の中身をペーストします。
スクリーンショット 2020-04-08 13.18.42.png

ペーストが終わったら、以下のコマンドでgithubへの接続の確認をしてください。

remote
[ryota|.ssh]$ chmod 600 config
[ryota|.ssh]$ ssh -T github

(途中で、Are you sure you want to continue connecting (yes/no)? と聞かれるので、yesと入力)
(#上手く接続できれば、以下のメッセージが表示される。
Hi Ryota! You've succwwwessfully authenticated, but GitHub does not provide shell access.

詳しく追求したい人はGitHubでssh接続する手順~公開鍵・秘密鍵の生成から~を参照してください。

Railsアプリのclone

いよいよGithubからデプロイしたいRailsアプリをcloneします。clone後にlsコマンドでcloneを試みたアプリ名が表示されれば完了です。(以下、例ではアプリ名はsampleとして進めていきます。)

remote
[ryota|.ssh]$ cd /var/www/rails
[ryota|rails]$ git clone git@github.com:~~~~~~~~~~~~
[ryota|rails]$ ls 

sample

cloneが成功したら、railsファイルのconfig/secrets.ymlに記述されている、secrets_key_baseを変更します。本番環境用の設定を変更するので、productの欄のみ編集すれば大丈夫です。

デプロイしたいアプリのローカル環境でのディレクトリで、以下コマンドを実行し、出力されたキーをコピーしておきます。

local
$: bundle exec rake secret
(#シークレットキーを生成)

jr934ugr89vwredvu9iqfj394vj9edfjcvnxii90wefjc9weiodjsc9o i09fiodjvcijdsjcwejdsciojdsxcjdkkdsv
(#表示されるキーは人によって違います。表示されるkeyをコピーしておきましょう)

続いてEC2インスタンス内のrailsアプリのsecrets.ymlを編集します

remote
[ryota|rails]$ cd sample
[remote|sample] $ vim config/secrets.yml

(#config/database.ymlを開く)
config/secrets.yml
production:
  secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>

コマンドでSECRET_KEY_BASEにキーをセットしましょう。

remote
[ryota|sample] $ export SECRET_KEY_BASE='jr934ugr89vwredvu9iqfj394vj9edfjcvnxii90wefjc9weiodjsc9o i09fiodjvcijdsjcwejdsciojdsxcjdkkdsv'

7.Nginx、Unicorn、MySQLの設定

Railsアプリを本番環境で動かすために必要な、Webサーバー、WebサーバーとRackをつなぐ部分、DBの設定をします。
なぜRailsの本番環境にNginxとUnicornを採用するのか、解りやすい 記事があったので興味ある方はぜひ御覧ください。

Unicornの設定

remote
[ryota|sample] $ gem install bundler
[ryota|sample] $ bundle install --without development test
[ryota|sample] $ vi config/unicorn.conf.rb 
(#config/unicorn .conf.rbの作成)

Unicornの設定ファイルの編集です。下記を転記してください。

unicorn.conf.rb
  # set lets
  $worker  = 2
  $timeout = 30
  $app_dir = "/var/www/rails/sample" #自分のアプリケーション名にする
  $listen  = File.expand_path 'tmp/sockets/.unicorn.sock', $app_dir
  $pid     = File.expand_path 'tmp/pids/unicorn.pid', $app_dir
  $std_log = File.expand_path 'log/unicorn.log', $app_dir
  # set config
  worker_processes  $worker
  working_directory $app_dir
  stderr_path $std_log
  stdout_path $std_log
  timeout $timeout
  listen  $listen
  pid $pid
  # loading booster
  preload_app true
  # before starting processes
  before_fork do |server, worker|
    defined?(ActiveRecord::Base) and ActiveRecord::Base.connection.disconnect!
    old_pid = "#{server.config[:pid]}.oldbin"
    if old_pid != server.pid
      begin
        Process.kill "QUIT", File.read(old_pid).to_i
      rescue Errno::ENOENT, Errno::ESRCH
      end
    end
  end
  # after finishing processes
  after_fork do |server, worker|
    defined?(ActiveRecord::Base) and ActiveRecord::Base.establish_connection
  end

Nginxの設定

remote
[ryota|~]$ cd ~
[ryota|~]$ sudo yum install nginx (#nginxのインストール)
[ryota|~]$ cd /etc/nginx/conf.d/
[ryota|conf.d]$ sudo vi sample.conf (#自分のアプリケーション名でファイル名変更)

作成したnginxの設定ファイルに以下を転記

remote
# log directory
error_log  /var/www/rails/sample/log/nginx.error.log; #自分のアプリケーション名に変更する箇所あり
access_log /var/www/rails/sample/log/nginx.access.log; #自分のアプリケーション名に変更する箇所あり
# max body size
client_max_body_size 2G;
upstream app_server {
  # for UNIX domain socket setups
  server unix:/var/www/rails/sample/tmp/sockets/.unicorn.sock fail_timeout=0; #自分のアプリケーション名に変更する箇所あり
}
server {
  listen 80;
  server_name ~~~.~~~.~~~.~~~;(#アプリのElastic IPに変更してください)
  # nginx so increasing this is generally safe...
  keepalive_timeout 5;
  # path for static files
  root /var/www/rails/sample/public; #自分のアプリケーション名に変更する箇所あり
  # page cache loading
  try_files $uri/index.html $uri.html $uri @app;
  location @app {
    # HTTP headers
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_redirect off;
    proxy_pass http://app_server;
  }
  # Rails error pages
  error_page 500 502 503 504 /500.html;
  location = /500.html {
    root /var/www/rails/sample/public; #自分のアプリケーション名に変更する箇所あり
  }
}

MySQLの設定と起動

本番環境用にmysqlをインストールします。

remote
[ryota|conf.d]$ cd ~
[ryota|~]$ yum list installed | grep mariadb
mariadb-libs.x86_64                   1:5.5.56-2.amzn2                 installed

#mysqlがインストールされているか確認
[ryota|~]$ yum list installed | grep mysql

#mysqlがインストールできるか確認
[ryota|~]$ yum info mysql
読み込んだプラグイン:extras_suggestions, langpacks, priorities, update-motd
エラー: 表示するパッケージはありません

#mysql8.0リポジトリの追加(このリポジトリに5.7も含まれています)
[ryota|~]$ sudo yum localinstall https://dev.mysql.com/get/mysql80-community-release-el7-1.noarch.rpm -y

#mysql8.0リポジトリの無効化
[ryota|~]$ sudo yum-config-manager --disable mysql80-community

#mysql5.7リポジトリの有効化
[ryota|~]$ sudo yum-config-manager --enable mysql57-community

#mysql5.7がインストールできるか確認
[ryota|~]$ yum info mysql-community-server

#mysqlインストール
[ryota|~]$ sudo yum install mysql-community-server -y
[ryota|~]$ mysqld --version
mysqld  Ver 5.7.29 for Linux on x86_64 (MySQL Community Server (GPL))

つづいて、mysqlの初期設定を行います。

remote
[ryota|~]$ sudo service mysqld start #mysqlの起動

[ryota|~]$ sudo cat /var/log/mysqld.log | grep password  (#rootパスワードを確認)
A temporary password is generated for root@localhost: rootの初期パスワード

#初期設定
[ryota|~]$ mysql_secure_installation

Enter password for user root: 初期パスワード
New password: 新しいパスワード
Re-enter new password: 新しいパスワード
Change the password for root ? ((Press y|Y for Yes, any other key for No) : No
Remove anonymous users? (Press y|Y for Yes, any other key for No) : y
Disallow root login remotely? (Press y|Y for Yes, any other key for No) : y
Remove test database and access to it? (Press y|Y for Yes, any other key for No) : y
Reload privilege tables now? (Press y|Y for Yes, any other key for No) : y

remote
[ryota|~]$ cd /
[ryota|/]$ cd var/www/rails/sample  (#アプリのディレクトリまで移動)
[ryota|sample]$ export DATABASE_PRO_PASSWORD='mysqlの新しいパスワード' (#MySQLのパスワードのセット)
[ryota|sample]$ ln -s /var/lib/mysql/mysql.sock /tmp/mysql.sock
[ryota|sample]$ rake db:create RAILS_ENV=production
[ryota|sample]$ rake db:migrate RAILS_ENV=production

これでMYSQLの設定は完了です。
その他環境変数のセットが必要な場合はこのタイミングでしておきましょう。

Unicornの起動

Unicornの起動の前に、念の為プリコンパイルもしておきましょう。

remote
[ryota|sample]$ bundle exec rake assets:precompile RAILS_ENV=production
[ryota|sample]$ bundle exec unicorn_rails -c /var/www/rails/sample(アプリの名前)/config/unicorn.conf.rb -D -E production (#unicornを起動させる)

Nginxの起動

remote
[ryota|sample]$ sudo service nginx start 

8.表示の確認

さて、URL欄に、登録したElastic IPを打ち込んでみましょう。アプリのトップページに設定した画面が表示されるはずです。もしエラーが起きている場合はGoogleで検索して耐えてください。

エラーの内容はconfig/unicorn.logの中に書いてあることが多いので、エラーが起きたときはここを確認すると良いかもしれません。
また、何かエラー対応策をした際は、念の為UnicornとNginxを再起動をすると良いでしょう。その際は以下のコマンドを使ってみてください。

remote
[ryota|sample]$ ps -ef | grep unicorn | grep -v grep
(#起動中のUnicornを確認)
ryota    27940     1  0 15:07 ?        00:00:00 unicorn_rails master -c 
/var/www/rails/article/config/unicorn.conf.rb -D -E production
ryota    27943 27940  0 15:07 ?        00:00:00 unicorn_rails worker[0] -c /var/www/rails/article/config/unicorn.conf.rb -D -E production
ryota    27944 27940  0 15:07 ?        00:00:00 unicorn_rails worker[1] -c /var/www/rails/article/config/unicorn.conf.rb -D -E production

[ryota|sample]$ kill -9 27940 (#プロセスの停止)
[ryota|sample]$ bundle exec unicorn_rails -c /var/www/rails/sample(アプリの名前)/config/unicorn.conf.rb -D -E production  (# Unicornの起動)
[ryota|sample]$ sudo nginx -s reload (# Nginxの再起動)

おわりに

以上で解説は終了です。
長い記事にお付き合い頂きありがとうございました。
筆者自身勉強が浅く、説明不足な点や間違った説明があるかもしれません。効率の良い方法や説明を発見次第、この記事を更新していきたいと思います。

参考記事

世界一丁寧なAWS解説シリーズ
AWSのEC2で行うAmazon Linux2(MySQL5.7)環境構築
チュートリアル: Amazon EC2 インスタンスでの Node.js のセットアップ
EC2にyarnをインストールする
なぜrailsの本番環境ではUnicorn,Nginxを使うのか?  ~ Rack,Unicorn,Nginxの連携について ~【Ruby On Railsでwebサービス運営】
Rails5をproduction(本番環境)で起動する時に嵌ったこと

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsチュートリアル9章攻略の第一歩を踏み出してみた。【セッション永続化編】

はじめに

9章の解説は私にとってあまりに複雑だったので
少しだけわかりやすく理解できるように、9章の中でもセッションの永続化に関する内容を簡単にまとめてみました。

説明と言うより、箇条書き形式にしてまとめております。

こんな人に読んで欲しい

Railsチュートリアルを1〜8章までをそれなりに理解し、尚且つ9章を一応は終えたが、それでも理解度は低めだと感じている方。

内容

セッション永続化機能の実装。
簡単に言うと、ブラウザを閉じてもログインを保持することができる機能です。
見たらわかるめちゃくちゃ便利な機能って事で絶対に物にしたいです。

実装手順

前置きが長くなりましたが、進めていきます。
ログイン保持機能を実装する手順を簡単にまとめると以下のようになります。

1.永続セッション作成の準備
2.永続セッション作成
3.上記を利用して、ログインユーザーのログインを保持する
4.永続セッションからログアウトできるようにする
5.上記機能実装により発生するバグを修正する

では、より詳しくまとめていきます。

1.永続セッション作成の準備

手順1の、永続セッション作成の準備についてです。
ログインを保持するには永続セッションが必要ですが、さらにその永続セッションを作成するための準備が必要になるので、手順1ではその準備を行っていきます。

永続セッション作成準備でやること

 ・ユーザーを記憶するための、記憶トークンなる物を作成する。
 ・その記憶トークンをハッシュ化し、データベースに保存させる機能を実装する。


1-1.トークンの作成。
 ❶Userモデル内に、クラスメソッドとして生成。
   ・Userモデル内のdigestメソッドが、ユーザーオブジェクトを必要としないため。
 ❷トークンを生成するメソッドを定義。
   ・new_tokenとする。
 ❸トークンは、長いランダムな文字列にすること。
   ・SecureRandomモジュールにある、urlsage_base64メソッドを使用。
   ・22文字のランダムな文字列が生成される。
 ❹このトークンを使って、記憶トークンを生成する。

1-2.記憶トークンをユーザーと関連付け、記憶ダイジェストに保存されるようにする。
 ❶記憶ダイジェスト属性をUserモデルに追加する。
   ・カラム名はremember_digest、データ型は文字列とする。
   ・コマンドでDBへその変更を反映。
   ・このデータにユーザーが触れることはないので、インデックスは不要。
   ・記憶トークンは、このカラムに保存されることになる。
 ❷記憶トークン属性を追加する。
   ・DBには保存しないような属性を追加する。
   ・attr_accessorを使い、仮想の属性とする。
   ・Userモデル内に記述する。
   ・属性名はremember_tokenとする。
   ・この記憶トークンの値が、記憶ダイジェストカラムに追加されることになる。
 ❸ユーザーを記憶し、記憶ダイジェストに保存するメソッドを定義する。
   ・Userモデル内に作成。
   ・メソッド名をrememberとする。
   ・1-1で生成したランダムな文字列のトークン(new_token)を、remember_token属性として取得する処理の記述。
     →remember_token属性にはselfを付け、属性として正しく認識させる。
     →付けないとローカル変数として定義される。
   ・記憶トークンを、記憶ダイジェストに保存する処理の記述。
     →update_attributeを使う。
     →remember_tokenで取得した値をハッシュ化して、記憶ダイジェストに保存する。
       →パスワード実装の際に使用した、digestメソッドによりハッシュ化する。
   ・このメソッドは、バリデーションを素通りさせる。
   ・これにより、ユーザーと関連付けられた記憶トークンが、記憶ダイジェストに保存されるようになった。
     →例えば、user.rememberのようにして、このメソッドを使ってユーザーを呼び出すと、呼び出したユーザーのremember_digestカラムに、ランダムな文字列で生成された記憶トークンが保存されるようになる。

models/user.rb
#1-1
 def User.new_token #Userを付することでクラスメソッドであることを明示的に示している。
    SecureRandom.urlsafe_base64
 end
1-2
$ rails g migration add_remember_digest_to_users remember_digest:string
$ rails db:migrate
models/user.rb
#1-2
attr_accessor :remember_token #仮想の記憶トークン属性を作成。
models/user.rb
#1-2
#記憶トークンをハッシュ化し、呼び出したユーザーの記憶ダイジェストに保存する。
def remember
  self.remember_token = User.new_token
  update_attribute(:remember_digest, User.digest(remember_token))
end

1-3.準備完了
  ・以上で、ユーザーと一緒にrememberメソッドを呼び出すと、そのユーザーの記憶ダイジェストにランダムな文字列が保存されるようになりました。

2.永続セッション作成

続きまして、手順2の永続セッションの作成です。
手順1で作成したメソッドなどは、ここで使用していきます。

永続セッション作成でやること

・ユーザーIDを暗号化する
・暗号化したユーザーIDと記憶トークンを、ブラウザの永続cookiesに保存する。
・以上の機能を実装するために、cookiesメソッドを使用する。


2-1.記憶トークンをcookiesに保存する。
 ❶cookiesメソッドを使用することで保存できる。
   ・記憶トークンをcookieに保存する。
     →cookies.permanent[:remember_token] = remember_token
       →cookiesメソッドはsessionメソッド同様、ハッシュで扱う。
       →permanentメソッドを使用すれば、cookiesの有効期限が20年後に設定される。
       →permanentメソッドにより永続化完了。

2-2.ユーザーIDを暗号化してcookiesに保存する。
 ❶cookiesメソッドでcookieにユーザーを保存する。
   ・cookies[:user_id] = user.id
   ・ただこのままでは、IDが生のテキストのまま保存されているので、奪い取られる可能性がある。
   ・署名付きcookieを使い、ユーザーIDを暗号化する。
     →cookies.signed[:user_id] = user.id
     →sigedメソッド付与により暗号化される。
   ・さらに、cookieも永続化してあげる。
     →cookies.permanent.signed[:user_id] = user.id
     →permanentとsignedはメソッドチェーンで繋ぐ。
   ・cookiesを設定したことで、cookiesからユーザーを取得できるようになった。
     →例えば、User.find_by(id: cookies.signed[:user_id])
       →ちなみに、cookies.signed[:user_id]では自動でユーザーIDの暗号化が解除される。

2-3.cookiesに保存された記憶トークンが、ユーザーの記憶ダイジェストと一致することを確認する。
 ❶この2つの一致を確認することにより、後々ログインを可能とする。
   →パスワードで例えると、passwordとpassword_digestの比較と同じ。
 ❶bcryptを使って確認。
  ・secure_passwordのソースコードを確認してみる。
    →BCrypt::Password.new(password_digest) == unencrypted_passwordというコードを参考にする。
      →bcryptでハッシュ化されたパスワードを比較する際にこのコードが使われているので、これを利用してやるという動機がある。
    →BCrypt::Password.new(remember_digest) == remember_tokenとして、当てはめてみる。
      →しかしこれだと、暗号化されたパスワードとトークンを直接比較している。
      →bcryptでは記憶ダイジェストは復号化されないので、直接比較はできない。
  ・bcrypt gemのソースコードの方を詳しく見てみる。
    →すると、==の部分がis_password?という論理値メソッドで再定義されている。
    →なので、BCrypt::Password.new(remember_digest).is_password?(remember_token)として比較してあげることにする。
  ・これにより、記憶トークンと記憶ダイジェストを比較してあげることにする。

2-4.2-3より、記憶トークンと記憶ダイジェストを比較するためのメソッドの定義をする。
 ❶Userモデルの中に作成。
 ❷メソッド名はauthenticated?とする。
 ❸渡された記憶トークンが、記憶ダイジェストと一致したらtrueを返す処理を記述する。
 ❹ここでの記憶トークンとは、cookiesに保存されている物で、仮想の属性であるアクセサとは別物。
 ※このメソッドは、手順3の3-2で使用。

 #2-1.記憶トークンをcookiesに保存する。
 cookies.permanent[:remember_token] = user.remember_token
 #2-2.ユーザーIDを暗号化してcookiesに保存する
 cookies.permanent.signed[:user_id] = user.id 
models/user.rb
 #2-4.記憶トークンと記憶ダイジェストを比較するためのメソッドの定義をする
  def authenticated?(remember_token)
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
  end

2-5.これにて、永続セッションの作成が完了しました。

3.ログインしたユーザーのログインを保持する

続きまして、手順3に移ります。
手順2で作成した永続セッションの動作を利用して、ログインを保持する機能を実装していきます。

3-1.ログイン時にユーザーを保持させる。
 ❶ログインを保持するためのメソッドを定義する。
   ・メソッド名をrememberとし、ユーザーを引数に取るヘルパーメソッドを定義。
     →Sessionsヘルパー内に記述。
     →記憶トークンを生成し、DBの記憶ダイジェストに保存させる処理を記述。
       →これに関しては、Usersモデルで定義したrememberメソッドを呼び出せばいい。
     →暗号化したユーザーIDを、永続cookiesに保存する処理を記述。
     →ユーザーの記憶トークンを、永続cookiesに保存する処理を記述。
   ・そして、アクション内でこのメソッドを呼び出すと…
     →渡されたユーザーの記憶トークンが記憶ダイジェストにが保存される。
     →暗号化したユーザーIDと記憶トークンが、永続cookiesに保存される。
     →その結果、ログインが保持されることになる。
 ❷最後に、Sessionsコントローラ内でrememberヘルパーメソッドを呼び出す。
   ・ログインの保持は、ログインした時に実行されるのでlog_inメソッドとセットで定義してあげる。
 ❸これでログインしたユーザーは、ブラウザに正しく記憶されるようになったので、ログイン保持機能の実装は完了した。

3-2.問題点
 しかしながら現状では、このログイン保持機能には、問題点が1つ存在します。それは、Sessionsヘルパー内で定義されているcurrent_user内にあり、このメソッド内でsessionメソッドによる一時セッションしか扱っていないため、ログイン保持機能が正常に動作しない。

【解決策】
 ❶永続セッションを動作させる場合には、ユーザーの取得方法に関して条件がある。
   ・session[:user_id]が存在する場合は、一時セッションからユーザーを取得する必要がある。
     →sessionにユーザーIDが存在するかどうかの条件式を記述。
   ・それ以外の場合は、cookies[:user_id]からユーザーを取得する必要がある。
     →cookiesに、暗号化されたユーザーIDが存在するかどうかの条件式を記述。
       →存在する場合、cookiesに保存されたIDと一致するIDを持つユーザーを取得する処理を記述。
       →「上記のユーザーが存在し」、かつ、「そのユーザーの記憶トークンと記憶ダイジェストが一致する(2-4で定義したメソッドを使用)」かどうかの条件式を記述。
         →どちらの条件も満たすならば、そのユーザーでログインする処理を記述。
         →ログイン中のユーザーをインスタンス変数で返す処理を記述。
   ・リファクタリング
     →ローカル変数を用いて、同じ記述をしている部分を修正。
     →重複している記述を、条件式内でローカル変数として一気に定義してあげる。
     →ユーザーIDのセッションが存在する場合=if(user_id = session[:user_id])とする。
     →ユーザーIDのクッキーが存在する場合=if(user_id = cookies.signed[:user_id])とする。
     →ローカル変数user_idを利用する。
 ❷これにより、ログインしたユーザーが正しく記憶されるようになった。

helpers/sessions_helper.rb
 #3-1の❶.ログイン時にユーザーを保持させるメソッド。
  def remember(user)
    user.remember
    cookies.permanent.signed[:user_id] = user.id
    cookies.permanent[:remember_token] = user.remember_token
  end
controllers/sessions_controller.rb
 def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      log_in user
      remember user #3-1の❷.ログイン時に、ログインを保持する。
      redirect_to user
    else
      flash.now[:danger] = "失敗"
      render 'new'
    end
 end
helpers/sessions_helper.rb
 #3-2.記憶トークンcookieに対応するユーザーを返す
  def current_user
    if (user_id = session[:user_id]) #sessionを持つユーザーの存在。
      @current_user ||= User.find_by(id: user_id )
    elsif (user_id = cookies.signed[:user_id]) #暗号化IDを持つユーザーの存在。
      user = User.find_by(id: user_id ) #ユーザーの取得
      if user && user.authenticated?(cookies[:remember_token]) #ユーザーの存在と、記憶トークンと記憶ダイジェストが一致するかどうか(2-4で定義)。
        log_in user
        @current_user = user
      end
    end
  end


4.永続セッションからログアウトできるようにする

4-1.完全にログアウトできるように、ユーザーを忘れるためのメソッドを定義する必要がある。
 ・Userモデルに追加。
 ・メソッド名をforgetとして定義する。
 ・DBの記憶ダイジェストをnilで更新する処理を記述をする。
   →update_attributeを使用。
 ・ユーザーのログイン情報を破棄する処理を記述する。
 ・永続セッションを終了させる準備が完了。

4-2.永続的セッションを破棄するメソッドを記述。
 ・Sessionsヘルパー内に追加。
 ・メソッド名をforgetとする。
 ・渡されたユーザーのログイン情報を破棄する処理を記述。
 ・ブラウザに保存されている、ユーザーIDのクッキーを削除する処理を記述。
 ・ブラウザに保存されている、記憶トークンのクッキーを削除する処理を記述。

4-3.現在のユーザーのログアウトする。
 ・log_outヘルパーメソッド内で、永続セッションを破棄するメソッドを呼び出す。
   →ログイン中のユーザーを破棄するので、引数はcurrent_user。

4-4.全てのテストスイートがGREENになる。

models/user.rb
 #4-1.ユーザーのログイン情報を破棄する。
  def forget
    update_attribute(:remember_digest, nil)
  end
helpers/sessions_helper.rb
 #4-2.ユーザーの永続的セッションを破棄する。
 def forget(user)
    user.forget #4-1のメソッドを呼び出してる。
    cookies.delete(:user_id)
    cookies.delete(:rememeber_token)
 end
helpers/sessions_helper.rb
 #4-3.現在のユーザーをログアウトする。
 def log_out
    forget(current_user) #4-2のメソッドを呼び出している。
    session.delete(:user_id)
    @current_user = nil
 end


5.上記機能実装により発生するバグを修正する

まず、上記機能実装によるバグが2つ存在するので、その詳細から挙げていく。

【1つ目】:複数のタブを開きながらログインしているユーザーが、一方のタブでログアウトをして、もう一つのタブで再度ログアウトしようとするとエラーになる。
  【原因】:ログアウトリンクをクリックすると、current_userがnilになり、log_outメソッド内のforget(current_user)が失敗してしまうため。
  【解決策】:ユーザーが、ログイン中の場合にのみログアウトさせる。

【2つ目】:複数のブラウザ(FirefoxやChromeなど)でログインしているユーザーが、一方のブラウザ(Firefoxとする)ではログアウトして、
一方(Chromeとする)のブラウザではログアウトはせずに、ブラウザを終了させてから再度同じブラウザで同じページへ行くと、問題が発生する。
  【具体例】
    ・Firefoxでログアウトした時。
      →user.forgetメソッドによってremember_digestがnilになる。
      →この時同じタイミングで、log_outメソッドによりユーザーIDが削除され、current_userメソッド内の2つの条件文がfalseになる。
      →結果、current_userメソッドの戻り値は期待通りnilになる。
      →ここまでは正常に動作している。
    ・問題は、Chromeをログアウトせずに閉じた時。
      →current_userメソッド内のsession[:user_id]はnilになる(セッションの有効期限切れによる)。
      →しかし、cookiesはブラウザに残り続けている。
      →そのためChromeを再起動して、アプリにアクセスすると、DBからそのユーザーを見つけることができてしまうため、セキュリティ上の問題が存在する。
      →クッキーが存在するせいで、current_userメソッド内のuser && user.authenticated?(cookies[:remember_token])が評価される。
      →結果的に、記憶ダイジェストがnilなので、user.authenticated?(cookies[:remember_token])の部分でエラーが発生する。
  【原因】
    ・Firefoxログアウト時に、remember_digestが削除されるにもかかわらず、Chromeアクセス時、BCrypt::Password.new(remember_didest).is_password?(remember_token)が実行されてしまうため。
      →要するにremember_digestがnilとなるため、bcrypt内で例外が発生してしまう。
  【解決策】
    ・remember_digestが存在しない場合は例外ではなく、falseを返す処理を、authenticated?メソッドに追加する。

5-1.まず、上記2つのエラーをキャッチするテストを記述していく。

【1つ目の問題についてのテスト】
 ❶2番目のウィンドウでログアウトをクリックするユーザーを検証する。
   ・integration/users_login_test.rb内のログアウトテストに追加で記述。
   ・1個目のログアウトの後に、もう一つログアウトテストを追加する。
     →delete logout_path
   ・current_userがいないので、2度目の呼び出しはエラーが発生するため、テストスイートはRED。
 ❷テストをパスさせるために機能を追加していく。
   ・ユーザーがログイン中の場合のみ、ログアウトできるようにする機能を記述する。
     →Sessionsコントローラ内に処理を記述。
     →logged_in?メソッド(ログインしているユーザーがいればtrueを返すメソッド)が、trueの場合にのみ、log_outを呼び出すようにする。
     →destroyアクション内に処理を記述。
       →「 log_out if logged_in? 」
   ・テストスイートがパス。GREEN。

【2つ目の問題についてのテスト】
 ❸ダイジェストが存在しない場合のauthenticated?をテストする。
   ・Userモデルで直接テストする。
   ・記憶ダイジェストを持たないユーザーを用意する。
   ・authenticated?メソッドを呼び出す。(渡されたトークンが、ダイジェストと一致したらtrueを返すメソッド)
   ・assert_not @user.authenticated?('')
     →結果がfalseならパス。
     →記憶トークンが使われる前に記憶ダイジェストでエラーになるので、記憶トークン部分の値は何でもいい。
   ・BCrypt::Password.new(nil)でエラーが発生するため、テストスイートはRED。

 ❹テストをパスさせる。
   ・authenticated?メソッドで、記憶ダイジェストがnilの場合はfalseを返すようにする処理を記述。
     →return false if remember_digest.nil?
     →returnキーワードによって、記憶ダイジェストがnilの場合、即座にメソッドを終了させる。
     →処理を途中で終わらせるのに用いられるテクニック。
   ・テストスイートGREEN。

integration/users_login_test.rb
test "login with valid information followed by logout" do
    get login_path
    post login_path, params: { session: { email: @user.email,
                                          password: 'password'
    }}
    assert is_logged_in?
    assert_redirected_to @user
    follow_redirect!
    assert_template 'users/show'
    assert_select "a[href=?]", login_path, count: 0
    assert_select "a[href=?]", logout_path
    assert_select "a[href=?]", user_path(@user)
    delete logout_path
    assert_not is_logged_in?
    assert_redirected_to root_url
    delete logout_path #5-1の❶.2度目のログアウトを検証(RED)。
    follow_redirect!
    assert_select "a[href=?]", login_path
    assert_select "a[href=?]", logout_path, count: 0
    assert_select "a[href=?]", user_path(@user), count: 0
  end
controllers/sessions_controller.rb
 def destroy
    log_out if logged_in? #5-1の❷.ログインしているユーザーがいる場合のみログアウトさせる。
    redirect_to root_url
 end
test/models/user_test.rb
 #5-1の❸.authenticated?メソッドの結果がfalseならテストをパスする。
 test "authenticated? should return false for a user with nil digest" do
    assert_not @user.authenticated?('')
 end
models/user.rb
 def authenticated?(remember_token)
    return false if remember_digest.nil? #5-1の❹.記憶ダイジェストがnilの場合はfalseを返す。
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
 end


最後に

以上で、セッションの永続化機能の実装が完了しました。
あとは、この機能をユーザーが選択可能にする[Remember me]チェックボックス機能を実装すれば、9章の内容が完了します。
チェックボックス編に関しては、別でまた記事を投稿しようと思います。

参考

 Railsチュートリアル 第9章発展的なログイン機構

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsチュートリアル9章攻略の第一歩を踏み出す。【セッション永続化編】

はじめに

9章の解説は私にとってあまりに複雑だったので
ほんの少しでもわかりやすく理解できるよう、9章の中でもセッションの永続化に関する内容を簡単にまとめてみました。

対象者

Railsチュートリアルにてログイン機能を実装しており、尚且つ9章を終えたが理解度が足りていないと感じている方。
あと私自身。

内容

セッション永続化機能の実装。
簡単に言うと、ブラウザを閉じてもログインを保持することができる機能です。
見たらわかるめちゃくちゃ便利な機能って事で絶対に物にしたいです。

ほぼ箇条書きでまとめてあります。
キリの良いポイントまで一旦まとめてから、コードの記述を行っています。

実装手順

前置きが長くなりましたが、進めていきます。
ログイン保持機能を実装する手順を簡単にまとめると以下のようになります。

1.永続セッション作成の準備
2.永続セッション作成
3.上記を利用して、ログインユーザーのログインを保持する
4.永続セッションからログアウトできるようにする
5.上記機能実装により発生するバグを修正する


では、より詳しくまとめていきます。

1.永続セッション作成の準備

手順1の、永続セッション作成の準備についてです。
ログインを保持するには永続セッションが必要ですが、さらにその永続セッションを作成するための準備が必要になるので、手順1ではその準備を行っていきます。


永続セッション作成準備でやること
 ●ユーザーを記憶するための、記憶トークンなる物を作成する。
 ●その記憶トークンをハッシュ化し、データベースに保存させる機能を実装する。

1-1.トークンの作成。

 ●Userモデル内に、クラスメソッドとして生成。
   ➡︎Userモデル内のdigestメソッドが、ユーザーオブジェクトを必要としないため。
 ●トークンを生成するメソッドを定義。
   ➡︎new_tokenとする。
 ●トークンは、長いランダムな文字列にすること。
   ➡︎SecureRandomモジュールにある、urlsage_base64メソッドを使用。
   ➡︎22文字のランダムな文字列が生成される。
 ●このトークンを使って、記憶トークンを生成する。

1-2.記憶トークンをユーザーと関連付け、記憶ダイジェストに保存されるようにする。

 ●記憶ダイジェスト属性をUserモデルに追加する。
   ➡︎カラム名はremember_digest、データ型は文字列とする。
   ➡︎コマンドでDBへその変更を反映。
   ➡︎このデータにユーザーが触れることはないので、インデックスは不要。
   ➡︎記憶トークンは、このカラムに保存されることになる。
 ●記憶トークン属性を追加する。
   ➡︎DBには保存しないような属性を追加する。
   ➡︎attr_accessorを使い、仮想の属性とする。
   ➡︎Userモデル内に記述する。
   ➡︎属性名はremember_tokenとする。
   ➡︎この記憶トークンの値が、記憶ダイジェストカラムに追加されることになる。
 ●ユーザーを記憶し、記憶ダイジェストに保存するメソッドを定義する。
   ➡︎Userモデル内に作成。
   ➡︎メソッド名をrememberとする。
   ➡︎1-1で生成したランダムな文字列のトークン(new_token)を、remember_token属性として取得する処理の記述。
     →remember_token属性にはselfを付け、属性として正しく認識させる。
     →付けないとローカル変数として定義される。
   ➡︎記憶トークンを、記憶ダイジェストに保存する処理の記述。
     →update_attributeを使う。
     →remember_tokenで取得した値をハッシュ化して、記憶ダイジェストに保存する。
       →パスワード実装の際に使用した、digestメソッドによりハッシュ化する。
   ➡︎このメソッドは、バリデーションを素通りさせる。
   ➡︎これにより、ユーザーと関連付けられた記憶トークンが、記憶ダイジェストに保存されるようになった。
     →例えば、user.rememberのようにして、このメソッドを使ってユーザーを呼び出すと、呼び出したユーザーのremember_digestカラムに、ランダムな文字列で生成された記憶トークンが保存されるようになる。

1-3.準備完了

  ●ユーザーをレシーバにrememberメソッドを呼び出すと、そのユーザーの記憶ダイジェストにランダムな文字列が保存されるようになりました。
  ●以上の機能を利用して、永続セッションを作成していきます。

models/user.rb
#1-1
 def User.new_token #Userを付することでクラスメソッドであることを明示的に示している。
    SecureRandom.urlsafe_base64
 end
1-2
$ rails g migration add_remember_digest_to_users remember_digest:string
$ rails db:migrate
models/user.rb
#1-2
attr_accessor :remember_token #仮想の記憶トークン属性を作成。
models/user.rb
#1-2
#記憶トークンをハッシュ化し、呼び出したユーザーの記憶ダイジェストに保存する。
def remember
  self.remember_token = User.new_token
  update_attribute(:remember_digest, User.digest(remember_token))
end


2.永続セッション作成

次に、手順2の永続セッションの作成です。
手順1で作成したメソッドなどは、ここで使用していきます。


永続セッション作成でやること
・ユーザーIDを暗号化する
・暗号化したユーザーIDと記憶トークンを、ブラウザの永続cookiesに保存する。
・以上の機能を実装するために、cookiesメソッドを使用する。

2-1.記憶トークンをcookiesに保存する。

 ●cookiesメソッドを使用することで保存できる。
   ➡︎記憶トークンをcookieに保存する。
     →cookies.permanent[:remember_token] = remember_token
       →cookiesメソッドはsessionメソッド同様、ハッシュで扱う。
       →permanentメソッドを使用すれば、cookiesの有効期限が20年後に設定される。
       →permanentメソッドにより永続化完了。

2-2.ユーザーIDを暗号化してcookiesに保存する。

 ●cookiesメソッドでcookieにユーザーを保存する。
   ➡︎cookies[:user_id] = user.id
   ➡︎ただこのままでは、IDが生のテキストのまま保存されているので、奪い取られる可能性がある。
   ➡︎署名付きcookieを使い、ユーザーIDを暗号化する。
     →cookies.signed[:user_id] = user.id
     →sigedメソッド付与により暗号化される。
   ➡︎さらに、cookieも永続化してあげる。
     →cookies.permanent.signed[:user_id] = user.id
     →permanentとsignedはメソッドチェーンで繋ぐ。
   ➡︎cookiesを設定したことで、cookiesからユーザーを取得できるようになった。
     →例えば、User.find_by(id: cookies.signed[:user_id])
       →ちなみに、cookies.signed[:user_id]では自動でユーザーIDの暗号化が解除される。

2-3.cookiesに保存された記憶トークンが、ユーザーの記憶ダイジェストと一致することを確認する。

 ●この2つの一致を確認することにより、後々ログインを可能とする。
   ➡︎パスワードで例えると、passwordとpassword_digestの比較と同じ。
 ●bcryptを使って確認。
   ➡︎secure_passwordのソースコードを確認してみる。
    →BCrypt::Password.new(password_digest) == unencrypted_passwordというコードを参考にする。
      →bcryptでハッシュ化されたパスワードを比較する際にこのコードが使われているので、これを利用してやるという動機がある。
    →BCrypt::Password.new(remember_digest) == remember_tokenとして、当てはめてみる。
      →しかしこれだと、暗号化されたパスワードとトークンを直接比較している。
      →bcryptでは記憶ダイジェストは復号化されないので、直接比較はできない。
  ➡︎bcrypt gemのソースコードの方を詳しく見てみる。
    →すると、==の部分がis_password?という論理値メソッドで再定義されている。
    →なので、BCrypt::Password.new(remember_digest).is_password?(remember_token)として比較してあげることにする。
  ➡︎これにより、記憶トークンと記憶ダイジェストを比較してあげることにする。

2-4.記憶トークンと記憶ダイジェストを比較するためのメソッドの定義をする。(2-3より)

 ●Userモデルの中に作成。
 ●メソッド名はauthenticated?とする。
 ●渡された記憶トークンが、記憶ダイジェストと一致したらtrueを返す処理を記述する。
 ●ここでの記憶トークンとは、cookiesに保存されている物で、仮想の属性であるアクセサとは別物。
 ※このメソッドは、手順3の3-2で使用。

2-5.永続セッションの作成が完了しました。


 #2-1.記憶トークンをcookiesに保存する。
 cookies.permanent[:remember_token] = user.remember_token
 #2-2.ユーザーIDを暗号化してcookiesに保存する
 cookies.permanent.signed[:user_id] = user.id 
models/user.rb
 #2-4.記憶トークンと記憶ダイジェストを比較するためのメソッドの定義をする
  def authenticated?(remember_token)
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
  end


3.ログインしたユーザーのログインを保持する

続きまして、手順3に移ります。
手順2で作成した永続セッションの動作を利用して、ログインを保持する機能を実装していきます。

3-1.ログイン時にユーザーを保持させる。

 ●ログインを保持するためのメソッドを定義する。
   ➡︎メソッド名をrememberとし、ユーザーを引数に取るヘルパーメソッドを定義。
     →Sessionsヘルパー内に記述。
     →記憶トークンを生成し、DBの記憶ダイジェストに保存させる処理を記述。
       →これに関しては、Usersモデルで定義したrememberメソッドを呼び出せばいい。
     →暗号化したユーザーIDを、永続cookiesに保存する処理を記述。
     →ユーザーの記憶トークンを、永続cookiesに保存する処理を記述。
   ➡︎そして、アクション内でこのメソッドを呼び出すと…
     →渡されたユーザーの記憶トークンが記憶ダイジェストにが保存される。
     →暗号化したユーザーIDと記憶トークンが、永続cookiesに保存される。
     →その結果、ログインが保持されることになる。
 ●最後に、Sessionsコントローラ内でrememberヘルパーメソッドを呼び出す。
   ・ログインの保持は、ログインした時に実行されるのでlog_inメソッドとセットで定義してあげる。
 ●これでログインしたユーザーは、ブラウザに正しく記憶されるようになったので、ログイン保持機能の実装は完了した。

3-2.問題点

 ●しかしながら現状では、このログイン保持機能には、問題点が1つ存在。
 ●それは、Sessionsヘルパー内で定義されているcurrent_user内にあり、このメソッド内でsessionメソッドによる一時セッションしか扱っていないため、ログイン保持機能が正常に動作しないこと。

3-3.解決策

 ●永続セッションを動作させる場合には、ユーザーの取得方法に関して条件がある。
   ➡︎session[:user_id]が存在する場合は、一時セッションからユーザーを取得する必要がある。
     →sessionにユーザーIDが存在するかどうかの条件式を記述。
   ➡︎それ以外の場合は、cookies[:user_id]からユーザーを取得する必要がある。
     →cookiesに、暗号化されたユーザーIDが存在するかどうかの条件式を記述。
       →存在する場合、cookiesに保存されたIDと一致するIDを持つユーザーを取得する処理を記述。
       →「上記のユーザーが存在し」、かつ、「そのユーザーの記憶トークンと記憶ダイジェストが一致する(2-4で定義したメソッドを使用)」かどうかの条件式を記述。
         →どちらの条件も満たすならば、そのユーザーでログインする処理を記述。
         →ログイン中のユーザーをインスタンス変数で返す処理を記述。
   ➡︎リファクタリング
     →ローカル変数を用いて、同じ記述をしている部分を修正。
     →重複している記述を、条件式内でローカル変数として一気に定義してあげる。
     →ユーザーIDのセッションが存在する場合=if(user_id = session[:user_id])とする。
     →ユーザーIDのクッキーが存在する場合=if(user_id = cookies.signed[:user_id])とする。
     →ローカル変数user_idを利用する。
 ●これにより、ログインしたユーザーが正しく記憶されるようになった。

helpers/sessions_helper.rb
 #3-1.ログイン時にユーザーを保持させるメソッド。
  def remember(user)
    user.remember
    cookies.permanent.signed[:user_id] = user.id
    cookies.permanent[:remember_token] = user.remember_token
  end
controllers/sessions_controller.rb
 def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      log_in user
      remember user #3-1.ログイン時に、ログインを保持する。
      redirect_to user
    else
      flash.now[:danger] = "失敗"
      render 'new'
    end
 end
helpers/sessions_helper.rb
 #3-2.記憶トークンcookieに対応するユーザーを返す
  def current_user
    if (user_id = session[:user_id]) #sessionを持つユーザーの存在。
      @current_user ||= User.find_by(id: user_id )
    elsif (user_id = cookies.signed[:user_id]) #暗号化IDを持つユーザーの存在。
      user = User.find_by(id: user_id ) #ユーザーの取得
      if user && user.authenticated?(cookies[:remember_token]) #ユーザーの存在と、記憶トークンと記憶ダイジェストが一致するかどうか(2-4で定義)。
        log_in user
        @current_user = user
      end
    end
  end


4.永続セッションからログアウトできるようにする

4-1.完全にログアウトできるように、ユーザーを忘れるためのメソッドを定義する必要がある。

 ●Userモデルに追加。
 ●メソッド名をforgetとして定義する。
 ●DBの記憶ダイジェストをnilで更新する処理を記述をする。
   ➡︎update_attributeを使用。
 ●ユーザーのログイン情報を破棄する処理を記述する。
 ●永続セッションを終了させる準備が完了。

4-2.永続的セッションを破棄するメソッドを記述。

 ●Sessionsヘルパー内に追加。
 ●メソッド名をforgetとする。
 ●渡されたユーザーのログイン情報を破棄する処理を記述。
 ●ブラウザに保存されている、ユーザーIDのクッキーを削除する処理を記述。
 ●ブラウザに保存されている、記憶トークンのクッキーを削除する処理を記述。

4-3.現在のユーザーのログアウトする。

 ●log_outヘルパーメソッド内で、永続セッションを破棄するメソッドを呼び出す。
   ➡︎ログイン中のユーザーを破棄するので、引数はcurrent_user。

4-4.全てのテストスイートがGREENになる。


models/user.rb
 #4-1.ユーザーのログイン情報を破棄する。
  def forget
    update_attribute(:remember_digest, nil)
  end
helpers/sessions_helper.rb
 #4-2.ユーザーの永続的セッションを破棄する。
 def forget(user)
    user.forget #4-1のメソッドを呼び出してる。
    cookies.delete(:user_id)
    cookies.delete(:rememeber_token)
 end
helpers/sessions_helper.rb
 #4-3.現在のユーザーをログアウトする。
 def log_out
    forget(current_user) #4-2のメソッドを呼び出している。
    session.delete(:user_id)
    @current_user = nil
 end


5.上記機能実装により発生するバグを修正する

まず、上記機能実装によるバグが2つ存在するので、その詳細から挙げていく。

【1つ目】:複数のタブを開きながらログインしているユーザーが、一方のタブでログアウトをして、もう一つのタブで再度ログアウトしようとするとエラーになる。

  【原因】:ログアウトリンクをクリックすると、current_userがnilになり、log_outメソッド内のforget(current_user)が失敗してしまうため。

  【解決策】:ユーザーが、ログイン中の場合にのみログアウトさせる。

【2つ目】:複数のブラウザ(FirefoxやChromeなど)でログインしているユーザーが、一方のブラウザ(Firefoxとする)ではログアウトして、一方(Chromeとする)のブラウザではログアウトはせずに、ブラウザを終了させてから再度同じブラウザで同じページへ行くと、問題が発生する。

  【具体的なエラーの挙動】
    ●Firefoxでログアウトした時。
      ➡︎user.forgetメソッドによってremember_digestがnilになる。
      ➡︎この時同じタイミングで、log_outメソッドによりユーザーIDが削除され、current_userメソッド内の2つの条件文がfalseになる。
      ➡︎結果、current_userメソッドの戻り値は期待通りnilになる。
      ➡︎ここまでは正常に動作している。
    ●問題は、Chromeをログアウトせずに閉じた時。
      ➡︎current_userメソッド内のsession[:user_id]はnilになる(セッションの有効期限切れによる)。
      ➡︎しかし、cookiesはブラウザに残り続けている。
      ➡︎そのためChromeを再起動して、アプリにアクセスすると、DBからそのユーザーを見つけることができてしまうため、セキュリティ上の問題が存在する。
      ➡︎クッキーが存在するせいで、current_userメソッド内のuser && user.authenticated?(cookies[:remember_token])が評価される。
      ➡︎結果的に、記憶ダイジェストがnilなので、user.authenticated?(cookies[:remember_token])の部分でエラーが発生する。

  【原因】
    ●Firefoxログアウト時に、remember_digestが削除されるにもかかわらず、Chromeアクセス時、BCrypt::Password.new(remember_didest).is_password?(remember_token)が実行されてしまうため。
      ➡︎要するにremember_digestがnilとなるため、bcrypt内で例外が発生してしまう。

  【解決策】
    ●remember_digestが存在しない場合は例外ではなく、falseを返す処理を、authenticated?メソッドに追加する。

5-1.まず、上記2つのエラーをキャッチするテストを記述していく。

【1つ目の問題についてのテスト】
 ●2番目のウィンドウでログアウトをクリックするユーザーを検証する。
   ➡︎integration/users_login_test.rb内のログアウトテストに追加で記述。
   ➡︎1個目のログアウトの後に、もう一つログアウトテストを追加する。
     →delete logout_path
   ➡︎current_userがいないので、2度目の呼び出しはエラーが発生するため、テストスイートはRED。
 ●テストをパスさせるために機能を追加していく。
   ➡︎ユーザーがログイン中の場合のみ、ログアウトできるようにする機能を記述する。
     →Sessionsコントローラ内に処理を記述。
     →logged_in?メソッド(ログインしているユーザーがいればtrueを返すメソッド)が、trueの場合にのみ、log_outを呼び出すようにする。
     →destroyアクション内に処理を記述。
       →「 log_out if logged_in? 」
   ➡︎テストスイートがパス。GREEN。

【2つ目の問題についてのテスト】
 ●ダイジェストが存在しない場合のauthenticated?をテストする。
   ➡︎Userモデルで直接テストする。
   ➡︎記憶ダイジェストを持たないユーザーを用意する。
   ➡︎authenticated?メソッドを呼び出す。(渡されたトークンが、ダイジェストと一致したらtrueを返すメソッド)
   ➡︎assert_not @user.authenticated?('')
     →結果がfalseならパス。
     →記憶トークンが使われる前に記憶ダイジェストでエラーになるので、記憶トークン部分の値は何でもいい。
   ➡︎BCrypt::Password.new(nil)でエラーが発生するため、テストスイートはRED。

 ●テストをパスさせる。
   ➡︎authenticated?メソッドで、記憶ダイジェストがnilの場合はfalseを返すようにする処理を記述。
     →return false if remember_digest.nil?
     →returnキーワードによって、記憶ダイジェストがnilの場合、即座にメソッドを終了させる。
     →処理を途中で終わらせるのに用いられるテクニック。
   ➡︎テストスイートGREEN。

integration/users_login_test.rb
test "login with valid information followed by logout" do
    get login_path
    post login_path, params: { session: { email: @user.email,
                                          password: 'password'
    }}
    assert is_logged_in?
    assert_redirected_to @user
    follow_redirect!
    assert_template 'users/show'
    assert_select "a[href=?]", login_path, count: 0
    assert_select "a[href=?]", logout_path
    assert_select "a[href=?]", user_path(@user)
    delete logout_path
    assert_not is_logged_in?
    assert_redirected_to root_url
    delete logout_path #5-1.2度目のログアウトを検証(RED)。
    follow_redirect!
    assert_select "a[href=?]", login_path
    assert_select "a[href=?]", logout_path, count: 0
    assert_select "a[href=?]", user_path(@user), count: 0
  end
controllers/sessions_controller.rb
 def destroy
    log_out if logged_in? #5-1.ログインしているユーザーがいる場合のみログアウトさせる。
    redirect_to root_url
 end
test/models/user_test.rb
 #5-1.authenticated?メソッドの結果がfalseならテストをパスする。
 test "authenticated? should return false for a user with nil digest" do
    assert_not @user.authenticated?('')
 end
models/user.rb
 def authenticated?(remember_token)
    return false if remember_digest.nil? #5-1.記憶ダイジェストがnilの場合はfalseを返す。
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
 end


最後に

以上で、セッションの永続化機能の実装が完了しました。
あとは、この機能をユーザーが選択可能にする[Remember me]チェックボックス機能を実装すれば、9章の内容が完了します。
チェックボックス編に関しては、別でまた記事を投稿しようと思います。

参考

 Railsチュートリアル 第9章発展的なログイン機構

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む