write ahead log

ロールフォワード用

Rails5でJSON APIをテストする

知らないとハマる.

JSONAPIに以下の様にしてアクセスしようとすると.

get order_detail_path(@buy_new_computer)
assert_response :success

UnknownFormatと言われる.

ActionController::UnknownFormat: ActionController::UnknownFormat
    app/controllers/order_details_controller.rb:7:in `show'
        test/controllers/order_details_controller_test.rb:30:in `block in <class:OrderDetailsControllerTest>'

調べるとjsonを取得する際は asでの指定が必要らしい.

#get order_detail_path(@buy_new_computer), as: :json
get order_detail_path(@buy_new_computer)
assert_response :success

参考

助かりました....

RailsのテストでJSON形式のデータを取得する

Rails5でcoffeescriptを途中から排除した時にハマったのでログ

個人でコツコツ触っているRailsですが, coffeescriptの扱いは困った感じでした.

最初はデフォルトという理由で頑張って勉強しようかと思ったのですが, もうjsもES2016とか2017とかも話題になっているし流石に良いかなと排除することに.

ところが消すのも以外と苦労したのでメモっておきます.

手順は以下でやりました.

  1. .coffeeのファイルを.jsへリネーム
  2. 中身をcoffeeからjsへ変更
  3. Gemfileのcoffee-railsコメントアウト
  4. キャッシュを削除

以下1つずつ.

.coffeeのファイルを.jsへリネーム

これはコマンドですぐ終わります.

$ cd app/assets/javascripts
$ for nm in *.coffee; do
    git mv $nm ${nm%.coffee}.js;
done

中身をcoffeeからjsへ変更

これが恐らく一番面倒ですが, 今回はほとんどjsを書いていなかったのですぐ終わりました.

ただし, 中にはcoffeescriptのコメントだけのファイルがあったりして油断すると引っかかるので注意かも.

Gemfileのcoffee-railsコメントアウト

これは消すだけです.

#gem 'coffee-rails', '~> 4.2'

これでgenerateコマンドでもjsを吐き出すようになります.

キャッシュを削除

ここまでやってもう大丈夫かなと思ったら以下のエラーが出てテストが通らなくなりました.

LoadError (cannot load such file -- coffee_script):

困ってたんですが, githubにissueがあったので救われています.

LoadError: cannot load such file -- coffee_script

以下のコマンドで解決できました.

bin/rake tmp:cache:clear

キャッシュが残るんですね.

gemも変えているのでサーバの再起動も忘れないように.

vimのステータスラインに出てくるbotって何?

ってふと思ったので調べてみた.

StackOverflowで同じ質問をしている人がいた.

stack overflow - what do “All” and “Bot” mean in vim status line?

どうやら

  • 行頭を表示している => Top
  • 行頭も行末も表示している => All
  • 行末を表示している => Bot

ということらしい.

確かにこの文章を書いている間もそういう挙動をしている.

10年近く使っているのに考えもしなかった.

もう少し色々気を配って生きた方が良いかもしれない.

ubuntu 18.04でホームディレクトリのディレクトリ名を日本語から英語にする

日本語環境でインストールするとデフォルトで日本語になってしまうので.

ターミナルで以下を実行.

$ LANG=C xdg-user-dirs-update --force

実行後, 日本語と英語両方のディレクトリがホームに残る形になる.

この時点ではちゃんと機能していないので再起動する.

再起動するとubuntuから日本語に変えるか提案されるが, もちろん無視して問い合わせさせないようにする.

あとはターミナルでホームを開いて日本語ディレクトリを消す.

ubuntu 18.04でcapslockをctrlにしたい

メモ.

以下のファイルを編集.

sudo vi /etc/default/keyboard

以下の箇所を変更.

...
XKBOPTIONS="ctrl:nocaps" 
...

再起動する.

Rails5でDeviseを使って認証機能を作る

えらく多機能なので試しながらメモる.

とりあえず使う

とりあえず使ってみる.

認証をするからにはサンプルが欲しいので用意しよう.

サンプルプロジェクトを考える

メモを管理するサービスということにしよう.

コントローラは

  • StaticPages(トップページ管理)
  • Notes(メモの管理)

の2つを用意する.

機能は認証とメモ管理のみ.

プロジェクトを作る

$ rails new notes

Deviseのインストール

gem 'devise'

をGemfileへ書いて

$ bundle install

とりあえずトップページとメモ機能を作る

とりあえずなのでscaffoldで.

$ rails g controller StaticPages
$ rails g scaffold note subject:string body:string
$ rails db:migrate

StaticPagesへはconfig/routes.rbを編集してルートも追加.

Rails.application.routes.draw do
   root to: 'static_pages#index'  #追加
   resources :notes
end

雑にトップページも作っておく.

<!-- app/views/static_pages/index.html.erb  -->
<h1>Top</h1>

ここまででメモ機能は動作する.

Deviseのセットアップ

ここからDeviseを使っていく.

Deviseを使うにはまず以下のコマンドでセットアップする.

$ rails g devise:install

そうすると丁寧に以下のメッセージを出してくれる.

===============================================================================

Some setup you must do manually if you haven't yet:

  1. Ensure you have defined default url options in your environments files. Here
     is an example of default_url_options appropriate for a development environment
     in config/environments/development.rb:

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

     In production, :host should be set to the actual host of your application.

  2. Ensure you have defined root_url to *something* in your config/routes.rb.
     For example:

       root to: "home#index"

  3. Ensure you have flash messages in app/views/layouts/application.html.erb.
     For example:

       <p class="notice"><%= notice %></p>
       <p class="alert"><%= alert %></p>

  4. You can copy Devise views (for customization) to your app by running:

       rails g devise:views

===============================================================================

1はメーラの設定, Deviseは認証やパスワードリマインダーにメールを使えるのでそのため. 2はルートページURLの設定, これは認証時の遷移先で必要になるだろう. 3はエラーメッセージなどを表示するタグをviewに入れてくれということ. 4はカスタムのviewが欲しい時に使えという事.

Deviseはデフォルトでviewを用意するのでカスタムする時には4のコマンドでまずはviewのファイルを生成してやらないとならない.
(viewをカスタムしない場面もあまりなさそうなものだけど)

2のルートページの設定は上述で行ったので, 1, 3を行います.

カスタムviewは後回し.

1. メーラの設定

# config/environments/development.rb
# 追加
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

3.エラーメッセージタグのviewへの追加

app/views/layout/application.html.erb

<!-- ここから -->
<div class="notice">
  <%= notice %>
</div>
<div class="alert">
  <%= alert %>
</div>
<!-- ここまで -->
<%= yield %>

認証に使うモデルの作成

認証するためのアカウント情報を保持するためのモデルを作成します.

作成にはDeviseが用意しているコマンドを使います.

$ rails g devise User

作成されたファイルを少し覗いてみます.

# app/models/user.rb
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable

deviseの利用するモジュールがわらわら書かれています(後述)

マイグレーションファイルも見てみます.

# frozen_string_literal: true

class DeviseCreateUsers < ActiveRecord::Migration[5.1]
  def change
    create_table :users do |t|
      ## Database authenticatable
      t.string :email,              null: false, default: ""
      t.string :encrypted_password, null: false, default: ""

      ## Recoverable
      t.string   :reset_password_token
      t.datetime :reset_password_sent_at

      ## Rememberable
      t.datetime :remember_created_at

      ## Trackable
      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

      ## Confirmable
      # t.string   :confirmation_token
      # t.datetime :confirmed_at
      # t.datetime :confirmation_sent_at
      # t.string   :unconfirmed_email # Only if using reconfirmable

      ## Lockable
      # t.integer  :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts
      # t.string   :unlock_token # Only if unlock strategy is :email or :both
      # t.datetime :locked_at


      t.timestamps null: false
    end

    add_index :users, :email,                unique: true
    add_index :users, :reset_password_token, unique: true
    # add_index :users, :confirmation_token,   unique: true
    # add_index :users, :unlock_token,         unique: true
  end
end

すごいわらわら出来てます...

コメントアウトされているものをアンコメントするとConfirmableなどのモジュールも利用できそうな気配.

とりあえずデフォルトの認証機能さえ使えれば良いのでマイグレーション.

$ rails db:migrate

サインインとサインアウトのリンクをつける

サインインとサインアウトの画面へのリンクをトップページに作りたいので, routesを確認してみます.

imai@IMAI-PC:~/lab/projects/notes$ rails routes
                  Prefix Verb   URI Pattern                    Controller#Action
        new_user_session GET    /users/sign_in(.:format)       devise/sessions#new
            user_session POST   /users/sign_in(.:format)       devise/sessions#create
    destroy_user_session DELETE /users/sign_out(.:format)      devise/sessions#destroy
       new_user_password GET    /users/password/new(.:format)  devise/passwords#new
      edit_user_password GET    /users/password/edit(.:format) devise/passwords#edit
           user_password PATCH  /users/password(.:format)      devise/passwords#update
                         PUT    /users/password(.:format)      devise/passwords#update
                         POST   /users/password(.:format)      devise/passwords#create
cancel_user_registration GET    /users/cancel(.:format)        devise/registrations#cancel
   new_user_registration GET    /users/sign_up(.:format)       devise/registrations#new
  edit_user_registration GET    /users/edit(.:format)          devise/registrations#edit
       user_registration PATCH  /users(.:format)               devise/registrations#update
                         PUT    /users(.:format)               devise/registrations#update
                         DELETE /users(.:format)               devise/registrations#destroy
                         POST   /users(.:format)               devise/registrations#create
                         [中略]

とりあえずusersの部分だけ見てもたくさん生成されています.

とはいえリンクに使えそうなものは以下でしょう.

  • new_user_registration
  • new_user_session
  • destroy_user_session

これらを使ってリンクを用意します.

トップページからのリンクを作る

ユーザ登録(sign up)とログイン(sign in)は必要でしょう.

トップページ(app/views/static_pages/index.html.erb)にリンクを加えます.

<h1>Top</h1>

<%= link_to 'sign in', new_user_session_path %>

<%= link_to 'sign up', new_user_registration_path %>

ログアウトのリンクを作る

ログインしたらログアウトのリンクも欲しいのでレイアウト(app/views/layouts/application.html.erb)へ加えます.

  <body>
  <!-- ここから -->
    <div class="session-control-box">
      <% if signed_in? %>
        <%= link_to 'sign out', destroy_user_session_path, method: :delete %>
      <% end %>
    </div>
  <!-- ここまで -->

signed_in?はdeviseが用意してくれるヘルパーメソッドです.

その名の通りログイン中だったらtrueを返します.

ログイン後のリダイレクトを設定する

ログイン後にはメモの一覧へ移動してほしいのでヘルパーメソッドの利用を追加します.

app/controllers/application_controller.rbに

  • after_sign_in_path_for
  • after_sign_out_path_for

の利用を追加します.

名前の通りこのメソッドの戻り値のパスへリダイレクトされます.

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

  def after_sign_in_path_for(resource)
    notes_path
  end

  def after_sign_out_path_for(resource)
    root_path
  end
end

これでログイン後とログアウト後の挙動を変更できます.

ログイン前はアクセスできないようにする

リダイレクトされてもログイン前にメモ管理のエリアにアクセスできてしまっては本末転倒なので, 認証機能を追加します.

class NotesController < ApplicationController
  before_action :authenticate_user!  #追加
  before_action :set_note, only: [:show, :edit, :update, :destroy]

authenticate_[モデル名]メソッドがdeviseで用意されているのでこれを使うだけです.

未認証の場合はログイン画面へリダイレクトされます.

ログインした人間のノートだけ参照できるようにする

このままではログインさえすれば誰のメモでも見れてしまうので, メモに所有者情報を持たせてログインしているユーザのメモだけが見れるように変更します.

まずはmigrationでnoteへuserへの外部キーを追加します.

$ rails g migration AddUserToNotes user:references
$ rails db:migrate

モデルへアソシエーションもつけておきます.

ユーザ側.

# app/models/user.rb
class User < ApplicationRecord
  has_many :notes

メモ側.

# app/models/note.rb
class Note < ApplicationRecord
  belongs_to :user
end

次にscaffoldで作ったコントローラをちょっと変更.

  def index
    #[変更]
    #@notes = Note.all
    @notes = current_user.notes
  end
    [中略]

  def new
    #[変更]
    #@note = Note.new
    @note = current_user.notes.build
  end
    [中略]

  def create
    #[変更]
    #@note = Note.new(note_params)
    @note = current_user.notes.build(note_params)
    [中略]

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_note
      #[変更]
      #@note = Note.find(params[:id])
      @note = current_user.notes.find_by(id: params[:id])
      redirect_to notes_url if @note.nil?
    end

current_[モデル名]ヘルパーメソッドがdeviseによって用意されるのでログイン中のユーザを取得するのは簡単です.

ここまでやると普通にアプリケーションっぽくなります.

i18nに対応する

deviseそのままでは英語メッセージが表示されるのでI18N対応してやる.

なんとそのままの名前のgemがあった.

tigrish/devise-i18n - GitHub

余談だがこの方, kaminariとか色々なgemのi18n対応をやってるみたいで地味にすごい.

Gemfileに追記して更新する.

# 追記
gem 'devise-i18n'
$ bundle install

デフォルトロケールを設定していない場合はこちらも設定.

# config/application.rb

# 以下を記述
config.i18n.default_locale = :ja

これだけでメッセージが日本語化される.ありがたい.

公式にも一応色々記載があるので込み入ったことをしたい場合は読むとよいかも.

専用のwikiもある.

ViewとControllerをカスタマイズする

ちょっと試すぐらいならデフォルトでも良いけど, 流石にこのままのviewでは実用に耐えない.

モデルもちょっといじって, 登録時にユーザ名も登録するようにしてみる.

Userモデルの変更

migrationでユーザ名を追加する.

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

deviseのviewの用意

deviseでviewをカスタマイズするにはまず以下のコマンドでビューを生成する.

$ rails g devise:views users(モデル名)

するとdeviseで使うビューが用意される.

これをカスタマイズしていく.

deviseのcontrollerの用意

controllerもviewと同様で, 下記のコマンドで生成する.

$ rails g devise:controllers users

親切にroutes.rbを変更しろと教えてくれる.

      create  app/controllers/users/confirmations_controller.rb
      create  app/controllers/users/passwords_controller.rb
      create  app/controllers/users/registrations_controller.rb
      create  app/controllers/users/sessions_controller.rb
      create  app/controllers/users/unlocks_controller.rb
      create  app/controllers/users/omniauth_callbacks_controller.rb
===============================================================================

Some setup you must do manually if you haven't yet:

  Ensure you have overridden routes for generated controllers in your routes.rb.
  For example:

    Rails.application.routes.draw do
      devise_for :users, controllers: {
        sessions: 'users/sessions'
      }
    end

===============================================================================

生成されたコントローラがapp/controllers/users/に用意されるが, これらは全てコメントアウトされている.

なのでこれをベースに使うものをアンコメントしていかないとならない.

各コントローラと機能の対応は多分以下の感じじゃないかな.

コントローラ名 機能
confirmations_controller.rb 登録後のメールでの確認
passwords_controller.rb パスワードリマインダー
registrations_controller.rb ユーザ登録
sessions_controller.rb セッション(ログイン/ログアウト)
omni_auth_callbacks_controller.rb omni_authによるOAuth制御のコールバック
unlocks_controller.rb ログインに一定回数失敗したらアカウントロックするアレ

とりあえず

  • ユーザ登録
  • セッション管理

だけは使いたいので以下の2つのファイルの中身をアンコメントする.

  • registrations_controller
  • sessions_controller

routesを変更

コンソールで出た指示通りに.

  #devise_for :users
  devise_for :users, controllers: {
    registrations: 'users/registrations',
    sessions: 'users/sessions'
  }

ここまででコントローラとビューをカスタムする前と同じ挙動になる.

viewの変更

app/views/users/registrations/new.html.erbを変更して名前欄を追加する.

<!-- ここから -->
  <div class="field">
    <%= f.label :name %><br />
    <%= f.text_field :name, autofocus: true %>
  </div>
<!-- ここまで -->

  <div class="field">

ついでにログイン中のユーザ名を見えるようにしておく.

<!-- app/views/layout/application.html.erbへ追加 -->
        <p><%= current_user.name %></p>

controllerの変更

strong parametersを追加するだけですが.

  def configure_sign_up_params
    #devise_parameter_sanitizer.permit(:sign_up, keys: [:attribute])
    devise_parameter_sanitizer.permit(:sign_up, keys: [:name])
  end

公式にstrong parametersに関する記載があったので, 困ったら読めば良いかな.

localeファイルの編集

普通にjaのymlを用意してActiveRecordの部分に書いた.

# config/locales/ja.yml
ja:
  activerecord:
    attributes:
      user:
        name: ユーザ名

これだけでうまく動作します.素敵ですね.

testをしたい

公式にも章が組まれている.

ControllerのテストもIntegrationテストもどちらでもIntegrationHelpersをincludeすると

  • sign_in
  • sign_out

の両メソッドが利用できるので, これで困ることはほぼないと思う.

# test/controllers/note_controller_test.rb
class NotesControllerTest < ActionDispatch::IntegrationTest
  include Devise::Test::IntegrationHelpers

  setup do
    sign_in users(:yamada)
  end

ちょっと調べる必要があったのがFixturesの書き方で, 以下の様に書いた.

# test/fixtures/users.yml
yamada:
    email: yamada@example.com
    encrypted_password: <%= Devise::Encryptor.digest(User, 'password') %>

モジュールの一覧

deviseには以下の10個のモジュールがあり, それぞれ用途に応じて使い分ける事ができる.

モジュール名 機能 デフォルト
database_authenticatable DBに保存するパスワードの暗号化 有効
registerable サインアップ処理 有効
recoverable パスワードリセット機能 有効
rememberable クッキーにログイン情報を保持する 有効
trackable サインイン回数・時刻・IPアドレスを保存 有効
validatable メールアドレスとパスワードでのバリデーション 有効
confirmable メール送信による登録確認
lockable 一定回数ログインに失敗した際のアカウントロック
timeoutable 一定時間でセッションを削除する
omniauthable OmniAuthのサポート

ちょっとすぐに全部は試せそうにない.心折れた.

とはいえこれだけでかなり使えそうです.

参考

Documentation for plataformatec/devise (master)

letter_opener_webを使ってメール送信をプレビューする

letter_opener_webはRailsで開発中のメールをブラウザで見れる便利なgem.

WSL環境やvagrant上での開発などではブラウザでメールが見られるのは非常に便利.

インストール

Gemfileを編集.

開発時のみ入れるように.

gem 'letter_opener_web', :group => :development

インストール

$ bundle install

設定

config/environments/development.rbを編集.

  # 以下2行を追加
  config.action_mailer.default_url_options = { host: 'localhost:3000' }
  config.action_mailer.delivery_method = :letter_opener_web

config/routes.rbを編集.

  # 以下3行を追加
  if Rails.env.development?
    mount LetterOpenerWeb::Engine, at: "/letter_opener"
  end

これで

http://localhost:3000/letter_opener

へブラウザでアクセスするとブラウザ上でメールが確認できます.

試してみる

mailerとcontrollerを適当に作って試してみる.

mailerを作る

$ rails g mailer HelloMailer

挨拶メールを送るだけ.

class HelloMailer < ApplicationMailer
  default from: "foo@example.com"

  def greeting
    to = "bar@example.com"
    subject = "greeting!"

    mail(subject: subject, to: to) do |format| 
      format.text
    end
  end
end

Controllerを作る

$ rails g controller send_mails

indexへアクセスしたらメールを送るようにする.

class SendMailsController < ApplicationController

  def index
    HelloMailer.greeting.deliver
  end

end

config/routesも追加.

  get '/send_mails', to: 'send_mails#index'

ビューを作る

ないと怒るので.

<!-- app/views/send_mails/index.html.erb -->
sent

ここまでで

localhost:3000/send_mails

へアクセスした後に

localhost:3000/letter_opener

へアクセスするとメールが見れた.

これは便利.