20191203のRubyに関する記事は16件です。

B - ROT N

過去問やっていきます
AtCoder Beginner Contest 146
Aは流石にやらなくていいかな

問題

https://atcoder.jp/contests/abc146/tasks/abc146_b
スクリーンショット 2019-12-03 22.57.29.png

1回目

回答

N = gets.to_i
S = gets.chomp

alf = ['A','B','C','D','E','F','G',
  'H','I','J','K','L','M','N','O','P',
  'Q','R','S','T','U','V','W','X','Y','Z']

result = ''
for c in S.chars
  x = alf.index(c)
  x = ( x + N ) % 26
  result += alf[x]
end

puts result

結果

スクリーンショット 2019-12-03 22.59.15.png

2回目

回答

N = gets.to_i
S = gets.chomp

N.times do |i|
  S = S.tr('A-Z', 'B-ZA')
end
puts S

結果

スクリーンショット 2019-12-03 23.00.44.png

感想

流石にAからZ書かなくてもいい方法あるよな〜と思ったけど、書き方が分からなかった。。。
メモリ使用量9割くらい減ったな。。。

追記

コメントを頂いて、もう一回やりました!ありがとうございます!
あと alphabet の頭をとったつもりが alf となっていたのめっちゃ恥ずかしい
(alfabet はポーランド語らしい。へえ〜)

回答

N = gets.to_i
S = gets.chomp
a = [*"A".."Z"]
puts S.tr(a.to_s, a.rotate(N).to_s)

追記2

to_s では配列の文字を繋げられないとのご指摘を頂きました。
今回はたまたま上手くいきましたが、
本来の想定では以下のコードの挙動になる予定でしたので、
追記しておきます!

N = gets.to_i
S = gets.chomp
a = [*"A".."Z"]
puts S.tr(a.join, a.rotate(N).join)

結果

スクリーンショット 2019-12-04 13.02.55.png

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

railsアプリではクラスインスタンス変数の注意する

クラスインスタンス変数の注意

右辺の実行結果をキャッシュするために以下の手法が使われる事があると思います。
1回目のメソッド実行時に実行結果をインスタンス変数@fooでキャッシュしておいて、2回目のメソッド実行時にはインスタンス変数の中身を返しています。

Class Foo
  def
    @foo ||= bar
  end
end

インスタンスメソッドの場合、インスタンス変数はFooクラスのオブジェクト毎に保持されます。
railsアプリ内のコードでこれをクラスメソッド内で使用する場合は注意が必要です。

Class Foo
  self.def
    @foo ||= bar
  end
end

クラスメソッド内でインスタンス変数はClassクラスのオブジェクト毎に保持され、
Classクラスのオブジェクトはrailsアプリケーション起動時から同じものを使い続けます。
その為、DB問合せ結果をキャッシュしてしまったりすると、アプリケーション起動後DBに初めて問い合わせした値を保持し続けてしまう為、その後別のリクエストでDBを更新しても、反映されない問題が発生します。

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

rubyワンライナーでwebサーバーを立てるTips

mockサーバとかサッと作成したい時に使うTipsです。
rubyコマンドを使ってワンライナーでwebサーバーを起動します。

手順

1. レスポンスボディのファイルを用意します。

response.rb
"Hello"

2. response.rbファイルが配置されているパスでruby -run -e httpd ./response.rb -p 3333実行します。

$ ls -la
response.rb

$ ruby -run -e httpd ./response.rb -p 3333

-> これでwebサーバーが起動します。

3. 確認してみる

curlで確認してみます。
ブラウザでもlocalhost:3333にアクサスしたらresponse.rbの中身が返ってきます。

$ curl localhost:3333
"hello"
::1 - - [03/Dec/2019:20:18:36 JST] "GET / HTTP/1.1" 200 8
- -> /
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

#Rails OR #Ruby + ActiveSupport の String#in_time_zone で 2月29日 30日 31日 が3月にずれるのだが

Rubyとか言語使用に関わる、深い事情ありの話かと思った。

Date.new を利用した方が良いかな。

require 'active_support/core_ext'

'2019-2-29'.in_time_zone('Tokyo').to_s
# => "2019-03-01 00:00:00 +0900"

'2019-2-30'.in_time_zone('Tokyo').to_s
# => "2019-03-02 00:00:00 +0900"

'2019-2-31'.in_time_zone('Tokyo').to_s
# => "2019-03-03 00:00:00 +0900"

'2019-2-32'.in_time_zone('Tokyo').to_s
# ArgumentError: argument out of range


Date.new(2019, 02, 28).in_time_zone('Tokyo')
# => Thu, 28 Feb 2019 00:00:00 JST +09:00

Date.new(2019, 02, 29)
# ArgumentError: invalid date

Original by Github issue

https://github.com/YumaInaura/YumaInaura/issues/2804

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

さくらVPSでCentOS7 11.RailsプロジェクトをGitで共同開発

はじめに

自由にテスト出来るLinuxのサーバーがほしくて、さくらVPSで構築してみました。
順次手順をアップしていく予定です。

前回インストールしたRuby On Railsを共同開発できるようにGitで管理したいと思います。

目次

  1. 申し込み
  2. CentOS7インストール
  3. SSH接続
  4. Apache・PHPインストール
  5. MariaDBインストール
  6. FTP接続
  7. sftp接続
  8. phpMyAdminインストール
  9. 環境のバックアップ
  10. Ruby On Railsインストール
  11. RailsプロジェクトをGitで共同開発

11.RailsプロジェクトをGitで共同開発

前回、Ruby On Railsインストール時に作成したテスト用プロジェクトHelloWorkdを共同開発できるようにGitで管理したいと思います。

共同開発用のマシンは、ローカルのWindows10のマシンにします。
構成は、こんな感じです。

共有リポジトリは、GitHub等を使う方法もありますが、今回はプロジェクトの有るさくらVPSサーバー内に作成します。

Gitでは、開発マシンのソースを直接本番ソースにアップするのではなく、一度共有リポジトリにアップしたものを本番ソースから取りに行く形になります。
本番ソースも開発と同様ローカルポジトリですので、開発側がアップした変更は手動で本番ソースに取り込む必要があります。
これだと不便ですので、共有リポジトリが変更を受け取った時自動的に本番ソースが取りに行くように設定します。

さくらVPSにGitインストール

インストール

$ sudo yum install git-all

インストール後の設定

ユーザ名とメールアドレスを設定します

$ git config --global user.name "sakura"
$ git config --global user.email sakura@kogueisya.com

gitの出力をカラーリング

$ git config --global color.ui auto

ディフォルトエディタの設定

$ git config --global core.editor vim

エイリアス設定(「checkout」を「co」に設定)

$ git config --global alias.co checkout

確認

$ git config user.name
sakura

$ git config -l
user.name=sakura
user.email=sakura@kougeisya.com
color.ui=auto
core.editor=vim
alias.co=checkout
core.repositoryformatversion=0
core.filemode=true
core.bare=false
core.logallrefupdates=true

$ cd
$ cat .gitconfig
[user]
        name = sakura
        email = sakura@kougeisya.com
[color]
        ui = auto
[core]
        editor = vim
[alias]
        co = checkout

共有リポジトリ(ベアリポジトリ)作成

さくらVPSサーバーに共有リポジトリを作成します。

共有リポジトリ用ディレクトリ作成

/optの中に「プロジェクト名.git」というフォルダを作成します。

$ sudo mkdir -p /opt/helloworld.git/

-pは/optフォルダがなければ作成します。

所有者変更

フォルダの所有者を 「4.Apache・PHPインストール」で作成したWebコンテンツ操作用のグループ(webadmin)にします。

$sudo chown root:webadmin /opt/helloworld.git/
$sudo chmod 2775 /opt/helloworld.git/ -R

ベアリポジトリを作成

$ cd /opt/helloworld.git
$ git init --bare --share
Initialized empty shared Git repository in /opt/helloworld.git/

---bareは、ベアリポジトリを作成するオプション
--shareは、ベアリポジトリを複数のユーザによって共有可能にするオプション

本番ソース

前回「10.Ruby On Railsインストール」で作成したHelloWorldプロジェクトにローカルリポジトリを作成し、リモートリポジトリにプッシュします。

リポジトリのセットアップ

$ cd /var/www/app/HelloWorld
$ git init
Reinitialized existing Git repository in /var/www/app/HelloWorld/.git/

除外ファイル指定

railsコマンド実行時に作成される.gitignoreファイルにリポジトリから除外するファイルを指定するためのルールが記載されています。
このファイルに以下を追加します。

$ vi .gitignore
# Ignore other unneeded files.
doc/
*.swp
*~
.project
.DS_Store
.idea
.secret

ファイルをインデックスに追加(addコマンド)

プロジェクトのファイルをコミット待ちの変更が格納される「ステージングエリア」という一種の待機場所に追加

$ git add .

確認
ステージングエリアにあるファイルのリスト表示

$ git status

コミット

ローカルリポジトリへの変更反映

$ git commit -m "Initalize repository"

コミットメッセージの履歴参照

$ git log
commit d3f7327c74c7175b75ca7d78f4a1cd576f5b6d9a
Author: sakura <sakura@kougeisya.com>
Date:   Mon Dec 2 15:49:37 2019 +0900

    Initialize repository

リモートリポジトリにプッシュ

リモートリポジトリのアドレスに「origin」という名前を付けて記録
(「origin」にしたのは、pushやpullコマンドは実行時にリモートリポジトリ名を省略するとoriginという名前を使用する為)

$ git remote add origin /opt/helloworld.git

リポジトリをプッシュ

$ git push -u origin master
Counting objects: 6674, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (6272/6272), done.
Writing objects: 100% (6674/6674), 29.86 MiB | 8.65 MiB/s, done.
Total 6674 (delta 909), reused 0 (delta 0)
To /opt/helloworld.git
 * [new branch]      master -> master
Branch master set up to track remote branch master from origin.

-uオプションは、「git push -u origin master」とすると次回から「git push」だけでpushしてくれます。

本番ソース反映の自動化

本番ソースも開発と同様ローカルポジトリですので、開発側がアップした変更は手動で本番ソースに取り込む必要があります。
これだと不便ですので、共有リポジトリが変更を受け取った時自動的に本番ソースが取りに行くように自動化します。

自動化にはフックを使います。
フックは、Gitでコマンドを実行する直前もしくは実行後に特定のスクリプトを実行する為の仕組みです。

フックの種類(サーバーサイド)

フック 概要
pre-receive クライアントからpushを受け取った直後に起動。
更新内容の確認が可能。
post-receive push内容の適用終了後起動。
通知を行う場合に便利。
update pushによる更新対称ブランチごとに起動。

共有リポジトリの中にある「hooks」の中に、フック名のスクリプトファイルを作成することで実現できます。

今回は、開発のローカルリポジトリからプッシュされ、その内容が適用された後に本番のローカルリポジトリからプルするスクリプトを実行したいので、「/opt/helloworld.git/hokks/post-receive」というスクリプトファイルを作成します。
スクリプトの内容は、本番ソースのドキュメントルートに移動し、publlを実行します。

$ vi /opt/helloworld.git/hooks/post-receive
#!/bin/sh
cd /var/www/app/HelloWorld/
git --git-dir=.git pull

スクリプトファイルを他のユーザから実行できるようにパーミッションを設定します。

$ chmod 755 /opt/helloworld.git/hooks/post-receive

以上でサーバー側の設定は完了です。

開発用ローカルマシン(Windows10)の設定

各ツールのインストール

Ruby、Rails、Git等をインストールしていきます。

Rubyインストール

下記サイトからダウンロードします。
最新版の「Ruby+Devkit 2.6.5-1 (x64)」をダウンロードしました。
https://rubyinstaller.org/downloads/

ダウンロードしたファイルを実行すると下記の画面が表示されます。
「◎I accept the License」を選択し[Next>]をクリックします。

下記画面が表示されたら、チェックボックス3つともチェックをいれ、[Install]をクリックします。

下記画面が表示されたらそのまま[Next>]をクリックします。

下記画面のようにインストールが開始されますので、終わるまでしばらく待ちます。

インストールが終了すると、下記画面が表示されます。
そのまま[Finish]をクリックすると画面が閉じ、インストールが完了します。

引き続き下記の画面が開きます。
1,2,3と入力して[Enter]キーを押すとインストールが開始されます。

インストールが完了すると下記画面のように「Which components shall be installed? If unsure press ENTER []」と表示されますので、[ENTER]キーを押すと画面が閉じます。

Railsに必要なgemをインストール

SQLite3インストール

コマンドプロンプトを開き、以下を実行します。

> ridk exec pacman -S mingw-w64-x86_64-sqlite3

下記画面のように「インストールを行いますか? [Y/n]」と聞いてきますので「Y」を入力します。

プロンプトに戻ったらインストール終了です。
引き続き以下のコマンドを実行します。

> gem install sqlite3 --platform ruby

下記画面のようにプロンプトに戻ったら完了です。

nokogiriインストール

コマンドプロンプトで以下を実行します。

> ridk exec pacman -S mingw-w64-x86_64-libxslt

下記画面のように「インストールを行いますか? [Y/n]」と聞いてきますので「Y」を入力します。

プロンプトに戻ったらインストール終了です。
引き続き以下のコマンドを実行します。

> gem install nokogiri --platform ruby -- --use-system-libraries

下記画面のようにプロンプトに戻ったら完了です。

Node.jsインストール

下記サイトからダウンロードします。
https://nodejs.org/ja/download/
「LTS推奨版」「Windows Installer (.msi)」の「64-bit」をダウンロードしました。

ダウンロードしたファイルを実行すると下記の画面が表示されますので[Next]をクリックします。

下記画面が表示されたら「□I accept the terms in the License Agreement」にチェックを入れ[Next]をクリックします。

下記画面が表示されますので、そのまま[Next]をクリック。

下記画面もそのまま[Next]クリック。

下記画面が表示されたら「Automatically install the ・・・」にチェックをいれて[Next]をクリック。

下記画面の[Install]クリックで、インストールが開始されます。

インストールが終わるまでしばらく待ちます。

インストールが終わると下記画面が開きますので[Finish]をクリックすると画面が閉じます。

引き続き下記の画面が開きます。
「継続するには何かキーを押してください ...」と2回表示されますので都度[Enter]キーを押してください。

下記の画面が開き、インストールが開始されます。

下記のように「Tyoe ENTER to ext:」と表示されたら完了です。
[Enter]キーを押すと画面が閉じます。

Bundlerインストール

コマンプロンプトを開き下記のコマンドを実行します。

> gem install bundler

Gitインストール

GUIでGitが使えるTortoiseGitを使います。
画面のキャプチャは取っていませんでしたm(__)m

Git For Windows

下記サイトの[Download]ボタンよりダウンロード
ダウンロードしたファイルを実行し、画面に従いインストール
https://gitforwindows.org/

TortoiseGit

下記サイトのfro 64-bit Windowsの「Download TortoiseGit 2.9.0-64.bit(~19.5Mib)」をクリックしてダウンロード
ダウンロードしたファイルを実行し、画面に従いインストール
https://tortoisegit.org/download/

メニュー等を日本語に設定

下記サイトからLanguage PacksのJapanese 64Bitの[Setup]をクリックしてダウンロード
ダウンロードしたファイルを実行し、画面に従いインストール
https://tortoisegit.org/download/

インストール後、デスクトップの適当な場所で右クリックし表示されるメニューから[TortoisGit]-[設定]を選択

開いた画面の[全般][TotoiseGit][言語(Language)]より「日本語(日本)」を選択、[OK]ボタンをクリック

以上でWIndows10の設定は完了です。

クローンを作成

共有リポジトリからクローンを作成します。
作成場所は、C:\Prj\HelloWorldにします。

TortoiseGitを利用してクローン作製

デスクトップ等適当な場所で右クリックし、表示されたメニューから[Gitクローン (複製)...]を選択します。

下記の設定画面が開きます

URLには以下の値を設定します。
"ssh://" + ユーザー名 + "@" + さくらVPSサーバーのアドレス + ":" + SSHポート番号 + "共有リポジトリディレクトリ"

ディレクトリは、クローンを作成するフォルダ名を設定します。
ここでは、「C:\Prj\HelloWorld」にしました。
フォルダは無ければ作成されます。

Putty鍵のロードにチェックを入れ、[...]をクリックし、秘密鍵を選択します。
ここで選択する秘密鍵は、「 7.sftp接続」の設定時に作成した秘密鍵ファイルです。

[OK]をクリックすると、秘密鍵のパスフレーズを聞いてきますので、「 7.sftp接続」で設定したパスフレーズを入力し[OK]をクリックします。

クローン作製中

無事終了しました

フォルダ確認

動作確認

サーバーを起動してみる

コマンドプロンプトを起動し、ディレクトリの移動。

> cd \Prj\HelloWorld

とりあえずサーバーを立ち上げてみたら、railsコマンドがみつからないので、「bundle install」しろと出ました。

>bundle exec rails s
bundler: command not found: rails
Install missing gem executables with `bundle install`

「bundle install」を実行すると、派手にエラーが出ました(T0T)

>bundle install
Fetching gem metadata from https://rubygems.org/.
Retrying fetcher due to error (2/4): Bundler::PermissionError There was an error while trying to read from `C:/Users/sugi/.bundle/cache/compact_index/rubygems.org.443.29b0360b937aa4d161703e6160654e47/info/mini_portile2`. It is likely that you need to grant read permissions for that path.
.
Retrying fetcher due to error (3/4): Bundler::PermissionError There was an error while trying to read from `C:/Users/sugi/.bundle/cache/compact_index/rubygems.org.443.29b0360b937aa4d161703e6160654e47/info/thread_safe`. It is likely that you need to grant read permissions for that path.
.
Retrying fetcher due to error (4/4): Bundler::PermissionError There was an error while trying to read from `C:/Users/sugi/.bundle/cache/compact_index/rubygems.org.443.29b0360b937aa4d161703e6160654e47/info/websocket-driver`. It is likely that you need to grant read permissions for that path.
.
There was an error while trying to read from
`C:/Users/sugi/.bundle/cache/compact_index/rubygems.org.443.29b0360b937aa4d161703e6160654e47/info/globalid`.
It is likely that you need to grant read permissions for that path.

どうもファイルの読み取り権限に問題があるようです。
調べてもよくわからなかったので、ちと乱暴ですが管理者としてコマンドプロンプトを実行してみました。

再度「bundle install」したら、無事動きました。

サーバー起動に再チャレンジ。

>bundle exec rails server -b 0.0.0.0
=> Booting Puma
=> Rails 5.2.4 application starting in development
=> Run `rails server -h` for more startup options
  Please add the following to your Gemfile to avoid polling for changes:
    gem 'wdm', '>= 0.1.0' if Gem.win_platform?
  Please add the following to your Gemfile to avoid polling for changes:
    gem 'wdm', '>= 0.1.0' if Gem.win_platform?
*** 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.1 (ruby 2.6.5-p114), codename: Llamas in Pajamas
* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://0.0.0.0:3000
Use Ctrl-C to stop

ブラウザで確認。
動いているようです。

コントローラーを作成しプッシュしてみる

コントローラを作成

>bundle exec rails generate controller hello
  Please add the following to your Gemfile to avoid polling for changes:
    gem 'wdm', '>= 0.1.0' if Gem.win_platform?
  Please add the following to your Gemfile to avoid polling for changes:
    gem 'wdm', '>= 0.1.0' if Gem.win_platform?
      create  app/controllers/hello_controller.rb
      invoke  erb
      create    app/views/hello
      invoke  test_unit
      create    test/controllers/hello_controller_test.rb
      invoke  helper
      create    app/helpers/hello_helper.rb
      invoke    test_unit
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/hello.coffee
      invoke    scss
      create      app/assets/stylesheets/hello.scss

app/controllers/hello_controller.rbにアクションメソッドindexを追加

class HelloController < ApplicationController
  def index
    render plain: 'こんにちは、世界!'
  end
end

config/routes.rbにルーティング設定

Rails.application.routes.draw do
  # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
  get 'hello/index', to: 'hello#index'
end

ローカルサーバーを起動し動作確認

> bundle exec rails s

プッシュ

変更したファイルをさくらVPSサーバの共有リポジトリにプッシュします。
その前に、ローカルリポジトリに変更をコミットします。

HelloWorldフォルダの上で右クリックし、表示されたメニューより[Gitコミット(C)->"master"...]を選択。

下記のようにコミット画面が表示されますので、「メッセージ」を入力し、追加ファイルにチェックを入れ[コミット]ボタンをクリックします。

下記画面のようにコミットが実行され、成功と表示されるとコミットは成功です。

コミットが成功したら、共有リポジトリへプッシュします。
上記画面の[プッシュ]ボタンをクリックすると下記のプッシュ画面が開きます。
[OK]をクリックし、サーバーの共有リポジトリにプッシュします。

プッシュが実行されます。
無事成功しました。

さくらVPSサーバーでの動作確認

Git確認

共有リポジトリへのプッシュ確認

$ cd /opt/helloworld.git
$ git log
commit 941e28b8e44d6dfcbe2ba2bdf5189cba76b92b38
Author: Kouichi Sugimoto <sugi@kougeisya.com>
Date:   Tue Dec 3 18:06:20 2019 +0900

    Windowsからのプッシュテスト

commit d3f7327c74c7175b75ca7d78f4a1cd576f5b6d9a
Author: sugi <sugi@kougeisya.com>
Date:   Mon Dec 2 15:49:37 2019 +0900

    Initialize repository

プッシュはうまくいっているようです。
開発リポジトリから共有リポジトリにプッシュがあると、自動的に本番サーバーがプルするように設定していました。
これが動いているかログをチェックします。

$ cd /var/www/app/HelloWorld
$ git log
commit 941e28b8e44d6dfcbe2ba2bdf5189cba76b92b38
Author: Kouichi Sugimoto <sugi@kougeisya.com>
Date:   Tue Dec 3 18:06:20 2019 +0900

    Windowsからのプッシュテスト

commit d3f7327c74c7175b75ca7d78f4a1cd576f5b6d9a
Author: sugi <sugi@kougeisya.com>
Date:   Mon Dec 2 15:49:37 2019 +0900

    Initialize repository

hello_controller.rbが出来ているかチェック

$ cd /var/www/app/HelloWorld/app/controllers
$ ls
application_controller.rb  concerns  hello_controller.rb

動作確認

本番ソースのサーバーを起動します。

$cd /var/www/app/HelloWorld
$ rails server -b 0.0.0.0
=> Booting Puma
=> Rails 5.2.4 application starting in development
=> Run `rails server -h` for more startup options
Puma starting in single mode...
* Version 3.12.1 (ruby 2.6.5-p114), codename: Llamas in Pajamas
* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://0.0.0.0:3000
Use Ctrl-C to stop

ブラウザで確認

無事動きました。
これでGitを使った共同開発の環境が出来ました。

次回

次回は、Pythonのインストールの予定です。

前回:Ruby On Railsインストール

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

rails-tutorial第9章

8章補足。

sessionを変数と考えていたが、正確には
sessionメソッドを使うと、ユーザーIDなどをブラウザに一時的に保存できるということである。

発展的なログイン機構

sessionだけを使ったログインだとサーバーやブラウザを閉じてしまうと、またログインが必要となる。そこを改善できないものだろうか。

ベターなのは、ユーザーの意思で、期限のあるセッションか、永続的なセッションかを選べるようにするといい。

cookiesとsessionの違い。

超わかりやすい説明。

cookiesは診察券、session idは整理番号と考えるとわかりやすい。
cookiesはクライアント側に保存されるので、その情報から、以前何を買ったとかショッピングカートに入れたとかがわかる。

session idもcookieに保存されるのだが、session idはブラウザとサーバーの通信状態を呼ぶから、
別のページに移動したり、別のデバイスから入ろうとすると、その時点でsession idはsessionというハッシュから削除されてしまう。

なので、たとえcookieからsessionidを盗み出せたとしても、別のページに移動したり、別のデバイスから入ろうとすると、その時点でsession idはsessionというハッシュから削除されてしまうという理由から、そんなsession idはそもそもハッシュに保存されていませんよーとなってしまう。

ただ、cookieは診察券みたいなものなので、「あ、前回はこの病気を見てもらったんですねー」というようなことができる。

以上!!!!!!!!!!!!!!!!!!!!!!

sessionはサーバとブラウザが相互にやり取りをして、どちらかが切れたら終了というものだった。

cookieの実装方法

cookieはクライアント側に記憶トークン(rememberトークンともいう)を付与し、それをパスワードダイジェストのように、ハッシュ化したものをDBに保存する。

そのため、まずは記憶トークンをハッシュ化したものを保存する場所をDBに作っていこう。

$ rails generate migration add_remember_digest_to_users remember_digest:string

ここでも、rails g migration add_追加するカラム名toテーブル名 カラム名:データ型
という風に指定してあげると、rails側で以下のようなファイルを勝手に作ってくれる。

db/migrate/[timestamp]_add_remember_digest_to_users.rb
class AddRememberDigestToUsers < ActiveRecord::Migration[5.0]
  def change
    add_column :users, :remember_digest, :string
  end
end

これを確認したら rails db:migrate

記憶トークンに使われるランダムな文字列をどうやって作るか?

Ruby標準ライブラリのSecureRandomモジュールにあるurlsafe_base64メソッドなら、この用途にぴったり合いそうです3。このメソッドは、A–Z、a–z、0–9、"-"、"_"のいずれかの文字 (64種類) からなる長さ22のランダムな文字列を返します (64種類なのでbase64と呼ばれています)。典型的なbase64の文字列は、次のようなものです。

$ rails console
>> SecureRandom.urlsafe_base64
=> "q5lt38hQDc_959PVoo6b7A"

SecureRandomクラスのクラスメソッドってことだよね。

この文字列をクライアント側に送って、さらに、この文字列をハッシュ化したものをDBに保存する。

トークン生成用のメソッドの定義

app/models/user.rb
class User < ApplicationRecord
  before_save { self.email = email.downcase }
  validates :name,  presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
  has_secure_password
  validates :password, presence: true, length: { minimum: 6 }

  # 渡された文字列のハッシュ値を返す
  def User.digest(string)
    cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
                                                  BCrypt::Engine.cost
    BCrypt::Password.create(string, cost: cost)
  end

  # ランダムなトークンを返す
  def User.new_token
    SecureRandom.urlsafe_base64
  end
end

ランダムなトークンを作るためだけに、インスタンスを生成するのは勿体無い。
なので、user.rbにクラスメソッドとして定義すれば良い。

ただ、今の状態だと、記憶トークンdigestを参照することができるが、記憶トークンの平文を参照することができない。
記憶トークンの平文は、password_digest実装時のpasswordやpassword_confirmationのような仮想的な属性である。

これを実装するにはどうすればいいだろうか?

実は上記のような仮想的な属性はゲッター、セッターの実装ができる。一時的に保存できるがDBに保存はされない。

これは、

attr_accessor :remember_token

とすることで、自動的にゲッターとセッターを実装してくれる。
言い換えると、attr_accessorはメソッドを定義するメソッドと言える。

attr_accessor :remember_tokenを実装

app/models/user.rb
class User < ApplicationRecord
  attr_accessor :remember_token
  before_save { self.email = email.downcase }
  validates :name,  presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
  has_secure_password
  validates :password, presence: true, length: { minimum: 6 }

  # 渡された文字列のハッシュ値を返す
  def User.digest(string)
    cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
                                                  BCrypt::Engine.cost
    BCrypt::Password.create(string, cost: cost)
  end

  # ランダムなトークンを返す
  def User.new_token
    SecureRandom.urlsafe_base64
  end

  # 永続セッションのためにユーザーをデータベースに記憶する
  def remember
    self.remember_token = User.new_token
    update_attribute(:remember_digest, User.digest(remember_token))
  end
end

次は上記の最後に定義されているrememberメソッドを見ていこう

rememberメソッドはユーザーがチェックボックスにチェックを入れてログインをした。
その時に呼び出されるメソッドである。

ここで注意点
rememberはインスタンスメソッド。これが呼び出されてる時は必ず呼び出し元がいるということ。

で、self.remember_tokenのselfには rememberメソッドの呼び出し元が代入される。

selfの省略について

update_attributeはself.update_attributeの省略形である。
しかし、一つ前のself.remember_tokenのselfは省略してはいけない。

どのようなルールがあるのだろうか?

省略してはいけないのは、

def remember
    self.remember_token = User.new_token
    update_attribute(:remember_digest, User.digest(remember_token))
  end

1文目は、selfを省略してしまうと、remember_tokenというローカル変数にUser.new_tokenを代入するという意味になってしまう。

つまり、代入文であり、代入文の左辺だった時はselfが必要。

update_attributeはメソッド(インスタンスメソッド)であることが明白なので、selfを省略してもOK

次は、

cookieからsessionの状態を復元する機能を実装しよう。

ログインした時にはユーザー自身が入力したemailからユーザーインスタンスをfindし、入力したパスワードを元に@user.authenticateをして認証することができた。

しかし、cookieの場合は、emailが存在しないので、どうやってユーザーインスタンスを引っ張ってくるかが課題になる。

これを解決するために、署名付きユーザーidというものを使う。

これは、cookieを送る時に、@user.idを暗号化したものを一緒に送る。

これを、元の@user.idに復号化してあげて、

そこから得たuser.idを使って、find_byしてインスタンスを引っ張ってくる。

で、authenticateメソッドはパスワードを比較するためのメソッドなので、記憶トークンには使えない。
なので、自分で定義する必要がある。

app/models/user.rb
class User < ApplicationRecord
  attr_accessor :remember_token
  before_save { self.email = email.downcase }
  validates :name,  presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
  has_secure_password
  validates :password, presence: true, length: { minimum: 6 }

  # 渡された文字列のハッシュ値を返す
  def User.digest(string)
    cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
                                                  BCrypt::Engine.cost
    BCrypt::Password.create(string, cost: cost)
  end

  # ランダムなトークンを返す
  def User.new_token
    SecureRandom.urlsafe_base64
  end

  # 永続セッションのためにユーザーをデータベースに記憶する
  def remember
    self.remember_token = User.new_token
    update_attribute(:remember_digest, User.digest(remember_token))
  end

  # 渡されたトークンがダイジェストと一致したらtrueを返す
  def authenticated?(remember_token)
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
  end
end

rememberメソッドをsession_controllerのcreateアクションに実装する。

app/controllers/sessions_controller.rb
class SessionsController < ApplicationController

  def new
  end

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      log_in user
      remember user
      redirect_to user
    else
      flash.now[:danger] = 'Invalid email/password combination'
      render 'new'
    end
  end

  def destroy
    log_out
    redirect_to root_url
  end
end

これは、ユーザーがメールとパスワードを入れてログインしたら、記憶トークンをハッシュ化したものをDBに保存するよーって処理。

ただ、ここでremember userというように引数を取っていることにお気づきだろうか?

実はこのメソッドはsession_helperに定義された別のメソッドだったのだ!!!

実はこのremember(user)メソッドで記憶トークンをクライアント側に送るなどの処理もしている。

このメソッドを定義していこう

app/helpers/sessions_helper.rb
module SessionsHelper

  # 渡されたユーザーでログインする
  def log_in(user)
    session[:user_id] = user.id
  end

  # ユーザーのセッションを永続的にする
  def remember(user)
    user.remember
    cookies.permanent.signed[:user_id] = user.id
    cookies.permanent[:remember_token] = user.remember_token
  end

  # 現在ログインしているユーザーを返す (いる場合)
  def current_user
    if session[:user_id]
      @current_user ||= User.find_by(id: session[:user_id])
    end
  end

  # ユーザーがログインしていればtrue、その他ならfalseを返す
  def logged_in?
    !current_user.nil?
  end

  # 現在のユーザーをログアウトする
  def log_out
    session.delete(:user_id)
    @current_user = nil
  end
end

sessionメソッドと同様、:user_idをキーにして、user.idを代入できる。
この場合、ユーザーIDが生のテキストとしてcookieに保存される。

署名付きcookieを使うためには、cookies.signedメソッドを使用する。
cookieをブラウザに保存する前に暗号化を行う。

じゃあ、結局どうやってsession状態を復元するんだよ。

current_userメソッドを変える。

app/helpers/sessions_helper.rb
module SessionsHelper

  # 渡されたユーザーでログインする
  def log_in(user)
    session[:user_id] = user.id
  end

  # ユーザーのセッションを永続的にする
  def remember(user)
    user.remember
    cookies.permanent.signed[:user_id] = user.id
    cookies.permanent[:remember_token] = user.remember_token
  end

  # 記憶トークンcookieに対応するユーザーを返す
  def current_user
    if (user_id = session[:user_id])
      @current_user ||= User.find_by(id: user_id)
    elsif (user_id = cookies.signed[:user_id])
      user = User.find_by(id: user_id)
      if user && user.authenticated?(cookies[:remember_token])
        log_in user
        @current_user = user
      end
    end
  end

  # ユーザーがログインしていればtrue、その他ならfalseを返す
  def logged_in?
    !current_user.nil?
  end

  # 現在のユーザーをログアウトする
  def log_out
    session.delete(:user_id)
    @current_user = nil
  end
end

簡単にいうと、
sessionでログインできればそれでログインし、
できなければ、cookieを使ってログインしてというメソッド。

if (user_id = session[:user_id])は
user_idにsession[:user_id]を代入した結果、値が存在すればという条件式になる。

elsif (user_id = cookies.signed[:user_id])
また、このコードのsignedは暗号化された文字列を復号化する役割がある。
つまり、signedは暗号化もできるし、復号化することもできる。

ちなみに
if user && user.authenticated?(cookies[:remember_token])

のcookies[:remember_token]は、クライアント側に保存されているもの。

つまり、DBに保存されているユーザーインスタンスの記憶トークンのハッシュ化された値と、クライアント側に保存されている記憶トークンをハッシュ化したものを比較してくれている。

もし、if文もelsif文も失敗したら、nilが返ってくるという仕様。
そのため、logged_in?メソッドをそのまま使える。

この時点でテストは失敗している。

ユーザーを忘れる。

これはrememberメソッド、remember(user)メソッドの全く逆のことを実装すれば良い

app/models/user.rb
class User < ApplicationRecord
  attr_accessor :remember_token
  before_save { self.email = email.downcase }
  validates :name,  presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
  has_secure_password
  validates :password, presence: true, length: { minimum: 6 }

  # 渡された文字列のハッシュ値を返す
  def User.digest(string)
    cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
                                                  BCrypt::Engine.cost
    BCrypt::Password.create(string, cost: cost)
  end

  # ランダムなトークンを返す
  def User.new_token
    SecureRandom.urlsafe_base64
  end

  # 永続セッションのためにユーザーをデータベースに記憶する
  def remember
    self.remember_token = User.new_token
    update_attribute(:remember_digest, User.digest(remember_token))
  end

  # 渡されたトークンがダイジェストと一致したらtrueを返す
  def authenticated?(remember_token)
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
  end

  # ユーザーのログイン情報を破棄する
  def forget
    update_attribute(:remember_digest, nil)
  end
end

これはrememberメソッドと対になるメソッド。
DBの値をnilにしたので、
次は焼いたクッキーを消すメソッドを実装しよう。

app/helpers/sessions_helper.rb
module SessionsHelper

  # 渡されたユーザーでログインする
  def log_in(user)
    session[:user_id] = user.id
  end
  .
  .
  .
  # 永続的セッションを破棄する
  def forget(user)
    user.forget
    cookies.delete(:user_id)
    cookies.delete(:remember_token)
  end

  # 現在のユーザーをログアウトする
  def log_out
    forget(current_user)
    session.delete(:user_id)
    @current_user = nil
  end
end

forget(user)はremember(user)の対になるメソッド。
また、log_outメソッドに forget(current_user)を追加しないと、cookieを使ってまたログインできてしまう。

さっきテストで失敗してしまったのは、ログアウトしたらログアウトパスが本来0個のはずなのに、cookieをつかったログインが成功してしまい、1つ発見されてしまったからだろう。

実はこれだけじゃあ実装は終わらないぜ

目立たないバグ潰し

二つのログイン済みのタブがあり、どちらか一方をログアウトさせ、もう片方もログアウトさせようとするとエラーが起こる。
これは1回目でcurrent_userがなくなり、2回目で、nilにforgetメソッドを呼び出そうとしているため、NoMethodErrorが起こってしまうからだ。

これを解決するには、log_outメソッドを使えるのはlog_inしている時のみという条件をつける。

app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  .
  .
  .
  def destroy
    log_out if logged_in?
    redirect_to root_url
  end
end

これで1つ目のバグは解決。

2つ目のバグを解決しよう!!!

cookieの暴走問題

1つはSafari、もう1つはChromeでログインする。
そうするとどちらにもcookieが付与された状態になる。

これで、Chromeのタブでログアウトを実行。

さらに、Safariのタブを消してしまう。

それで、Safariでもう一度アプリのページを開こうとするとエラーになってしまう。

これは、Chromeのログアウトの時点でDBのremember_digestはnilになっている。
そしてSafariのcookie情報でcurrent_userを見つけようとするもDBの値がnilになっているので見つけられず、例外を出してエラーを出すようになってしまうかららしい。bcryptによるもの。

じゃあ、どうやって解決する?

まずは回帰バグを防ぐためにテストコードを書こう。

test/models/user_test.rb
require 'test_helper'

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "user@example.com",
                     password: "foobar", password_confirmation: "foobar")
  end
  .
  .
  .
  test "authenticated? should return false for a user with nil digest" do
    assert_not @user.authenticated?('')
  end
end

最後のテストは、authenticated?()メソッドの引数にnilや空文字を入れたらfalseを返すでしょ?というテストである。ちなみにcookieの暴走バグはfalseすら返さず例外を出しているために起きている。

テスト結果は以下。

ERROR["test_authenticated?_should_return_false_for_a_user_with_nil_digest", UserTest, 0.47229603000005227]
 test_authenticated?_should_return_false_for_a_user_with_nil_digest#UserTest (0.47s)
BCrypt::Errors::InvalidHash:         BCrypt::Errors::InvalidHash: invalid hash
            app/models/user.rb:32:in `new'
            app/models/user.rb:32:in `authenticated?'
            test/models/user_test.rb:70:in `block in <class:UserTest>'

  21/21: [===========================================================================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.57426s
21 tests, 50 assertions, 0 failures, 1 errors, 0 skips

ちなみに、failuresは期待された値にならなかった時。
errorsは期待された値とか関係なく、例外などが出た時に表示される。

cookieの暴走バグ解決法

これの解決方法は、remember_digestがnilの時はbcryptを実行せずに、falseを返してあげれば良い

app/models/user.rb
class User < ApplicationRecord
  .
  .
  .
  # 渡されたトークンがダイジェストと一致したらtrueを返す
  def authenticated?(remember_token)
    return false if remember_digest.nil?
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
  end

  # ユーザーのログイン情報を破棄する
  def forget
    update_attribute(:remember_digest, nil)
  end
end

これで、cookieの暴走バグを解決できる。
returnを実行されると、それがメソッドの戻り値になるので、以降のメソッド処理は実行されなくなる。

チェックボックスの実装

まずはチェックボックスをログインフォームに実装しよう

app/views/sessions/new.html.erb
<% provide(:title, "Log in") %>
<h1>Log in</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_for(:session, url: login_path) do |f| %>

      <%= f.label :email %>
      <%= f.email_field :email, class: 'form-control' %>

      <%= f.label :password %>
      <%= f.password_field :password, class: 'form-control' %>

      <%= f.label :remember_me, class: "checkbox inline" do %>
        <%= f.check_box :remember_me %>
        <span>Remember me on this computer</span>
      <% end %>

      <%= f.submit "Log in", class: "btn btn-primary" %>
    <% end %>

    <p>New user? <%= link_to "Sign up now!", signup_path %></p>
  </div>
</div>

チェックボックスを実装すると、
params[:session][:remember_me]

の値が、チェックされてる時は'1'
チェックされてない時は'0'となる。

これを利用していこう。

if params[:session][:remember_me] == '1'
  remember(user)
else
  forget(user)
end

このように条件分岐していけばいいのだが、
これを三項演算子を使うとスマートに実装することができる。

app/controllers/sessions_controller.rb
class SessionsController < ApplicationController

  def new
  end

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      log_in user
      params[:session][:remember_me] == '1' ? remember(user) : forget(user)
      redirect_to user
    else
      flash.now[:danger] = 'Invalid email/password combination'
      render 'new'
    end
  end

  def destroy
    log_out if logged_in?
    redirect_to root_url
  end
end

基本的にチェックボックスにチェックをつけないとcookieは焼かれないが、一応万全を期すためにforget()メソッドを呼び出している。

Remember meのテストを書こう。

まずテスト環境でログインしたユーザーを作るためにtest_helperにメソッドを定義していく

test/test_helper.rb
ENV['RAILS_ENV'] ||= 'test'
.
.
.
class ActiveSupport::TestCase
  fixtures :all

  # テストユーザーがログイン中の場合にtrueを返す
  def is_logged_in?
    !session[:user_id].nil?
  end

  # テストユーザーとしてログインする
  def log_in_as(user)
    session[:user_id] = user.id
  end
end

class ActionDispatch::IntegrationTest

  # テストユーザーとしてログインする
  def log_in_as(user, password: 'password', remember_me: '1')
    post login_path, params: { session: { email: user.email,
                                          password: password,
                                          remember_me: remember_me } }
  end
end

下のクラス定義されてるやつはなんぞや??

統合テストは基本的にブラウザでできることができるようなテスト、

だからこのようにいちいち情報を入力してログインしてもらう必要がある。
そのためメソッドを分けて定義している

つまり、上のlog_in_asはケーステスト用のメソッド。
下のlog_in_asは統合テスト用のメソッドとなっている。

ちなみに password: 'password'と remember_me: '1'はデフォルト値として設定されている。

チェックボックスの統合テストをしよう

test/integration/users_login_test.rb
require 'test_helper'

class UsersLoginTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end
  .
  .
  .
  test "login with remembering" do
    log_in_as(@user, remember_me: '1')
    assert_not_empty cookies['remember_token']
  end

  test "login without remembering" do
    # クッキーを保存してログイン
    log_in_as(@user, remember_me: '1')
    delete logout_path
    # クッキーを削除してログイン
    log_in_as(@user, remember_me: '0')
    assert_empty cookies['remember_token']
  end
end

ここではテスト通る。

raiseを理解する。

raiseはテストの途中で例外を発生させる機能。

app/helpers/sessions_helper.rb
module SessionsHelper
  .
  .
  .
  # 記憶トークンcookieに対応するユーザーを返す
  def current_user
    if (user_id = session[:user_id])
      @current_user ||= User.find_by(id: user_id)
    elsif (user_id = cookies.signed[:user_id])
      raise       # テストがパスすれば、この部分がテストされていないことがわかる
      user = User.find_by(id: user_id)
      if user && user.authenticated?(cookies[:remember_token])
        log_in user
        @current_user = user
      end
    end
  end
  .
  .
  .
end

つまり、テストがパスしてしまうと、raise以下のテストが実行されていないことがわかる。

これは問題。

解決するには、raise以下の使うテストを追加してあげれば良い

本当にここテストされてるかなー?って思ったらraiseを使ってみよう

メンテナンスモードについて

開発者側からはアプリに入れてクライアント側からは入れなくしたい時は、メンテナンスモードをonにする。

heroku maintenance:on

これを解除するには、

$ heroku maintenance:off

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

Macでrbenvが使えない!【binutilsの悪夢シリーズ】

以下のことがしたかった

  • rbenvでruby 2.6.5をインストール
  • 結論としては,binutilsが色々と悪さをする

環境

  • Mac Catalina 10.15.1

普通に入れようとした記録

$ brew install rbenv
... 略
$ rbenv install 2.6.5
Downloading openssl-1.1.1d.tar.gz...
-> https://dqw8nmjcqpjn7.cloudfront.net/1e3a91bc1f9dfce01af26026f856e064eab4c8ee0a8f457b5ae30b40b8b711f2
Installing openssl-1.1.1d...

BUILD FAILED (OS X 10.15.1 using ruby-build 20191124)

Inspect or clean up the working tree at /var/folders/df/0q1vdcy17j32kzq9xlb5zgg40000gn/T/ruby-build.20191203170740.4037.sKb82m
Results logged to /var/folders/df/0q1vdcy17j32kzq9xlb5zgg40000gn/T/ruby-build.20191203170740.4037.log

Last 10 log lines:
      _s_server_main in s_server.o
  "_verify_stateless_cookie_callback", referenced from:
      _s_server_main in s_server.o
  "_wait_for_async", referenced from:
      _s_client_main in s_client.o
      _sv_body in s_server.o
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
make[1]: *** [apps/openssl] Error 1
make: *** [all] Error 2

はて,Rubyを入れる前のOpenSSLのビルドでコケているみたいだ...

解決策

ログが /var/folders/df/0q1vdcy17j32kzq9xlb5zgg40000gn/T/ruby-build.20191203170740.4037.log にあるようなので覗いてみる

${LDCMD:-clang} -arch x86_64 -O3 -Wall -L. -Wl,-search_paths_first -L/Users/hiragi/.rbenv/versions/2.6.5/lib \
                -o apps/openssl apps/asn1pars.o apps/ca.o apps/ciphers.o apps/cms.o apps/crl.o apps/crl2p7.o apps/dgst.o apps/dhparam.o apps/dsa.o apps/dsaparam.o apps/ec.o apps/ecparam.o apps/enc.o apps/engine.o apps/errstr.o apps/gendsa.o apps/genpkey.o apps/genrsa.o apps/nseq.o apps/ocsp.o apps/openssl.o apps/passwd.o apps/pkcs12.o apps/pkcs7.o apps/pkcs8.o apps/pkey.o apps/pkeyparam.o apps/pkeyutl.o apps/prime.o apps/rand.o apps/rehash.o apps/req.o apps/rsa.o apps/rsautl.o apps/s_client.o apps/s_server.o apps/s_time.o apps/sess_id.o apps/smime.o apps/speed.o apps/spkac.o apps/srp.o apps/storeutl.o apps/ts.o apps/verify.o apps/version.o apps/x509.o \
                 apps/libapps.a -lssl -lcrypto  
ld: warning: directory not found for option '-L/Users/hiragi/.rbenv/versions/2.6.5/lib'
ld: warning: ignoring file apps/libapps.a, building for macOS-x86_64 but attempting to link with file built for unknown-unsupported file format ( 0x21 0x3C 0x61 0x72 0x63 0x68 0x3E 0x0A 0x2F 0x20 0x20 0x20 0x20 0x20 0x20 0x20 )
Undefined symbols for architecture x86_64:

#{LDCMD:-clang} はたぶんldのこと,コンパイルはちゃんと通ってるけど,リンクするときにうまく行っていない.
で,apps/libapps.a とかいう多分shared-library的なもののフォーマットが合っていないといった旨のエラー.

それともうちょっと上側を見てみると,

ar: creating libcrypto.a
ranlib -c libcrypto.a || echo Never mind.
ranlib: invalid option -- c

invalid option -- cて...なんか別のコマンドが実行されている...?

ranlib error とかでググってみると,以下のページが出た

https://tetsuok.hatenablog.com/entry/2012/05/07/054356

なるほど,なんかOS標準のarranlibを使わないとなにか別のものができてしまう模様...
そして更に調べると

https://naoyat.hatenablog.jp/entry/2012/01/31/033312

こんな記事も見つけた,どうやらbrewのbinutilsを入れていると,そいつの依存でついてくるarranlibが勝手にパスを通してしまうらしい.... こんなのわかるわけ無いやろ(半ギレ)
そういえば以前もなにか自前でビルドしようとしたときになんとなくbinutilsを消していた気がする...恐るべしbinutils....

https://qiita.com/nagomiso/items/dc6021beb72d09f2128f
似たような記事がありました,ただ原因と問題が発生する場所が違いすぎて原因にたどり着きにくい...
本格的にLinuxを動かすことを検討しないと,こういう問題が起きる毎に無駄に時間を使ってしまう.

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

Excelって名前しか知らないPCに不慣れな感覚派22歳プログラミング完全未経験者がRubyを習得するまでの軌跡。Part4

【HTML-CSS編】

はじめはpart2,3のように用語の説明や文型をまとめようとおもったがprogateの説明だけで完成されており、理解することが可能。最低限大事なことだけまとめる。

1.HTMLとCSSってなんなのか CSSは色、大きさ、形などデザインに関するプログラミング HTMLが素材でCSSが職人。
2.CSSはHTMLとは別にファイルを作って記述する。
3.プロパティはCSSの機能 ex.color.font-size,background-color,width,height

やってて感じたこと

HTML-CSSはRubyと比べると内容自体はすごく簡単で意味がわからないということはほぼないと想う。ただRubyが詩だとするとHTML-CSSは単文といった感じで1文1文で完結していて面白みはあまりない。

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

Rails 6.0.x標準で Ajax+(jQuery+Partial) でHTML部分更新する世界一シンプルなサンプル

TL;DR (長い! 3行で!)

↓ の記事をRails 6.0.1 版に書き直したものです.
Rails 5.x標準で Ajax+(jQuery+Partial) でHTML部分更新する世界一シンプルなサンプル

Front EndがWebpackに変わっており,
・ Coffee Script → JavaScript への変更
・ jQuery導入方法の変更
が大きな変更点です.

動作確認環境

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 14.04.6 LTS ← 古いLTSなのでわりとダメ,できれば最新LTS使ってください
Release:        14.04
Codename:       trusty

$ ruby --version
ruby 2.6.3p62 (2019-04-16 revision 67580) [x86_64-linux]

$ rails --version
Rails 6.0.1

$ yarn --version
1.16.0

$ nvm --version
0.35.1

$ node --version
v12.13.1

NOTICE

nodeのversionが古いと(LTSじゃないと?),$ rails newの途中で ↓ のようなErrorが出ます.

error get-caller-file@2.0.5: The engine "node" is incompatible with this module. 
Expected version "6.* || 8.* || >= 10.*". Got "9.10.1"
error Found incompatible module.

これ以降,webpackが使えなくなります.
(error Command "webpack" not found.)
$ rails new自体は "Webpacker successfully installed" のメッセージがでて正常終了してるように見えてしまうため気付けない...

ざっくりシーケンス

だいたい ↓ のようなシーケンスを実現します.
Client-Server間でSession張って双方向通信とかそういうところはRails Frameworkが全部やってくれます.

Front End : UserがInputしたTextをAjaxでBack End側にPost
↓
Back End : PostされたTextとPartialを使って部分的に差し替えるHTMLをRendering
↓
Front End : JavaScriptでAjaxのCallback(部分的に差し替えるHTML)を受け取る
↓
Front End : jQueryでHTMLの部分差し替え


Rails 6 でFront EndがWebpack化されたことで,Rails 5 に比べてReactなども導入しやすくなっています.そのため,Back Endから返すCallbackはpre-renderしたHTMLでなく,Jsonなどでデータを返して,Front End側でrenderする,という方法もあります.

Project初期化 + Default動作確認

$ rails new ajax-test

でRails Projectを初期化します.$ cd ajax-testでRailsのrootに移動して,

$ rails server

でサーバを起動して,
http://localhost:3000/
にアクセスして "Yay! You're on Rails!" のDefaultページが出たらOKです.

Static Page作成

Ajax動作のベースになるStatic Page部分を実装します.

Controller

$ rails generate controller AjaxTest

でControllerのTemplateを作成後,Action(Method)を追加します.

app/controller/ajax_test_controller.rb
class AjaxTestController < ApplicationController
  def top
    # NOP.
  end

  def update
    # TBD.
  end
end

top : Topページ表示用,特に処理はありません.
update : Ajaxリクエストを受ける用,実装はあとで追加します.

Route Config

ControllerのActionに繋げるためのRoute定義を追加します.

config/routes.rb
get  'ajax_test/top',    to: 'ajax_test#top',    as: 'ajax_test_top'
post 'ajax_test/update', to: 'ajax_test#update', as: 'ajax_test_update'

GET : Topページ表示用.
POST : Ajaxリクエスト用.

View

ajax/test/top にアクセスしたときに表示するViewを追加します.

app/views/ajax_test/top.html.erb
<div id="request_ajax_update" >
  <%= form_tag(ajax_test_update_path, method: :post, remote: true) { %>
    <%= text_field :data, :text %>
    <%= submit_tag 'Post AJAX' %>
  <% } %>
</div>

<hr>

<div id="updated_by_ajax" >
  DEFAULT
</div>

<hr>

大きく2つのブロックだけです.

<div id="request_ajax_update" >
Ajax RequestをPostするFormを持つブロック.
Userが入力したTextを params[:data][:text] に詰めて,
ajax_test_update_path ( = /ajax_test/update) にPOSTします.

<div id="updated_by_ajax" >
Ajax Callbackを受けてHTMLの部分更新をするブロック.

Static Page動作確認

この時点で,
http://localhost:3000/ajax_test/top
にアクセスすると,↓ のようなページが表示されると思います.
default.png

ただし,Ajaxの実装がまだ無いので,Post AJAX ボタンをClickしても見た目上は何も起こりませんが,$ rails server が動いているConsoleにText Fieldに入れた文字列がParametersとして通知されているのを確認できると思います.

Parameters: {"data"=>{"text"=>"Hello World !"}, "commit"=>"Post AJAX"}

Ajax実装

いままでに作ったStatic PageにAjax実装を組み込んでInteractiveな機能を追加します.

Partial View

部分的に更新するHTMLの部品を追加します.

app/views/ajax_test/_ajax_partial.html.erb
<div>
  Callback Msg = <%= results[:message] %>
</div>

このPartialで <div id="updated_by_ajax" > の中身を差し替えます.

Controller Action

さきほど作ったControllerの update の中身を実装します.

app/controller/ajax_test_controller.rb
class AjaxTestController < ApplicationController
  def top
    # NOP.
  end

  def update
    post_text = params[:data][:text]
    results = { :message => post_text }
    render partial: 'ajax_partial', locals: { :results => results }
  end
end

Controllerに渡ってきた Parameters の中に格納されているUserが入力したTextを使ってPartialをrenderしています.
Partialのファイル名 _ajax_partial.html.erbajax_partial で使えるあたりはRailsの規約に沿っています.

ここで,

res = render_to_string partial: 'ajax_partial', locals: { :results => results }
puts res

のようなCodeを書いておくとPartialをrenderしたときの実際のOutputが見れます.

<div>
  Callback Msg = Hello World !
</div>

JavaScript実装

Rails 6 からFront EndにWebpackが標準で使われることになり,Coffee ScriptがDefaultで使われなくなっています.

かわりに,
app/javascript/packs/application.js
のようなWebpackに対応したJavaScriptのDir構成に変わっています.
(Rails 5 でWebpack使っていた人にはおなじみの構成かも)

ViewにJavaScriptのEntry Pointを追加

ViewのHTMLにJavaScriptの読み込み部分を追加します.

app/views/ajax_test/top.html.erb
<div id="request_ajax_update" >
  <%= form_tag(ajax_test_update_path, method: :post, remote: true) { %>
    <%= text_field :data, :text %>
    <%= submit_tag 'Post AJAX' %>
  <% } %>
</div>

<hr>

<div id="updated_by_ajax" >
  DEFAULT
</div>

<hr>

<!-- ↓ 追加 -->
<%= javascript_pack_tag 'ajax_test' %>
<script>
  register_callback();
</script>

<%= javascript_pack_tag 'ajax_test' %>
app/javascript/packs/ 以下に置いてある ajax_test というファイル(の中身)を読み込む,という指示です.
実際にはWebpackがひとかたまりの.jsにしてしまうので,この名前のファイルを読み込んでいるわけではありません.

その後,<script></script> タグで register_callback() という関数を呼び出します.
この関数の実装は次で追加します.

JavaScript本体の実装

Webpack用Dir構成に沿って,Viewから呼び出せるように ↓ のJavaScriptを追加します.

app/javascript/packs/ajax_test.js
import * as $ from "jquery";

function register_callback() {

  $("#request_ajax_update").on(
      "ajax:complete",
      function(event) {
        var res = event.detail[0].response
        $('#updated_by_ajax').html(res)
      }
  );

}

window.register_callback = register_callback;

Viewから呼び出すregister_callback()関数の中で,AjaxリクエストをPOSTする #request_ajax_update Tagに ajax:complete のCallback関数を登録しています.

Viewから呼び出せるようにするため,
window.register_callback = register_callbcak;
の行でwindowの名前空間(global)にexportしています.
注) この方法は動作はしますがあまり行儀よくないはずです...

ただし,このままでTopページからPost AJAXをClickしても何も起こりません.
ChromeのDeveloper Tool等でConsole Logをみると,

Uncaught Error: Cannot find module 'jquery'
    at webpackMissingModule (ajax_test.js:1)
    at Module../app/javascript/packs/ajax_test.js (ajax_test.js:1)

のようなError Logがでていて,jquery の名前解決ができてないことがわかります.

jQueryを使えるようにする

Rails 5 から引き続きRails標準ではjQueryはサポートされていないため,WebpackからjQueryを使えるようにします.

$ yarn add jquery

のCommandでjqueryをInstallします.

必要かもしれない追加

"rails6 + jquery" とかのキーワードでぐぐると,↓ の2つの追加も必要,と出てきますが,手元の環境だと必要ありませんでした.
RailsのVersionによるのか,他の環境によるのか...

ref: https://www.botreetechnologies.com/blog/introducing-jquery-in-rails-6-using-webpacker

app/javascript/packs/application.js
// 他のrequireがいろいろ...

require("jquery") // 追加

config/webpack/environment.js
const { environment } = require('@rails/webpacker')

// 追加
const webpack = require('webpack')
environment.plugins.prepend('Provide',
  new webpack.ProvidePlugin({
    $: 'jquery/src/jquery',
    jQuery: 'jquery/src/jquery'
  })
)

module.exports = environment

Ajax動作確認

これで必要な変更はすべてです.
http://localhost:3000/ajax_test/top
にアクセスして,Text Fieldになにか文字列を入力してPost AJAXボタンを押したら,DEFAULT の文字列が更新されて ↓ のような画面になっていれば成功です.

ajax.png

ChromeのDeveloper ToolなどでDOM構造を見てみると,↓ のようになっていて,<div id="updated_by_ajax" >の中身が差し替わっているのが見えます.

dev_tool.png

おわり

Rails 6 でAjaxを使ったシーケンスを一本通すまでをできる限りDefaultのままで実現してみました.
環境依存でたまたまうまく動いている部分などあるかもしれません.もし何かおかしな点がありましたら教えていただけると助かります.

---///

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

CC0ライセンスの空白PDFファイル

はじめに

ライセンス的に問題のない「空白PDFファイル」が欲しかったのですが、すぐに見つからなかったので作りました。リポジトリはこちらです。

https://github.com/kaityo256/blank_pdf

空白PDFが欲しい

あるPDFファイルと別のPDFをpdftkで結合することを考えます。この時、二つ目のPDFを、いわゆる「奇数ページ起こし」にしたいことがあります。つまり、1つ目のファイル(A)が奇数ページ(例えば3ページ)で終わっており、二つ目のファイル(B)が偶数ページ(例えば4ページ)ある時、そのまま結合して両面印刷すると

A1,A2 | A3,B1 | B2, B3| B4

みたいになって読みづらくなります。これを、空白を一枚挟んで

A1,A2 | A3,空白 | B1, B2| B3, B4

としたくなりますね。もし空白のPDFblank.pdfがあれば、pdftkを使って

pdftk A.pdf blank.pdf B.pdf cat output output.pdf

でおしまいです。というわけで、PDFblank.pdfが欲しいわけですが・・・、空白のPDFってどうやって作るんだ?

で、ググって見ると、Rubyを使う方法も出てくるんですが、オンラインツール使う方法とか、Adobe Acrobatを使う方法とかが多い感じです。英語でググっても、なんかツールを使えみたいなのが多くて、うーん、という感じ。

たまに「白紙PDFをどうぞ」的なサイトもあるのですが、ライセンスが不明なのと、ダウンロードしてみると、ファイルに余計な情報が入っていたりして微妙だったりします。

というわけで、こちらでも紹介されている、Ruby + Prawnで空白PDFを生成することにします1

Ruby + Prawnで空白PDF

といっても、コードはこれだけ。

blank_pdf.rb
require "prawn"

doc = Prawn::Document.new(page_size: "A4")
doc.render_file "blank.pdf"

できあがるPDFも

%PDF-1.3
%????
1 0 obj
<< /Creator <feff0050007200610077006e>
/Producer <feff0050007200610077006e>
>>
endobj
2 0 obj
<< /Type /Catalog
/Pages 3 0 R
>>
endobj
3 0 obj
<< /Type /Pages
/Count 1
/Kids [5 0 R]
>>
endobj
4 0 obj
<< /Length 4
>>
stream
q
Q

endstream
endobj
5 0 obj
<< /Type /Page
/Parent 3 0 R
/MediaBox [0 0 595.28 841.89]
/CropBox [0 0 595.28 841.89]
/BleedBox [0 0 595.28 841.89]
/TrimBox [0 0 595.28 841.89]
/ArtBox [0 0 595.28 841.89]
/Contents 4 0 R
/Resources << /ProcSet [/PDF /Text /ImageB /ImageC /ImageI]
>>
>>
endobj
xref
0 6
0000000000 65535 f 
0000000015 00000 n 
0000000109 00000 n 
0000000158 00000 n 
0000000215 00000 n 
0000000268 00000 n 
trailer
<< /Size 6
/Root 2 0 R
/Info 1 0 R
>>
startxref
540
%%EOF

と最低限で、余計な情報が入っていなくていい感じです。ちなみに二行目の「????」は、このファイルがバイナリであることを知らせるための非ASCIIデータです。一応このファイルをCC0ライセンスでリポジトリに置いておくので、必要な人はダウンロードして使ってください。

まとめ

なんか空白PDFが必要な人が、その都度スクリプトその他で作ってる気がしてアレな気がしますね。pdftkに白紙PDFを生成する機能があると一番幸せな気がしますが、作ってプルリクするのがいいのかな。


  1. 最初、このページ見て「RubyとlibHaruを使っている」ところまで見て、自分でPrawnで書いてから、後で見返してみたら「追記」にPrawnを使う方法が書いてあることに気が付きました。 

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

よくあるデータ出力機能を二段階に分けて作ったら機能の汎用性がとても高くなったはなし

この記事は食べログ Advent Calendar 2019 7日目の記事です。
よくあるデータ出力機能をふたつに分けて作ったら、機能の汎用性がとても高くなったはなしをします。

おそらくよくあるデータ出力処理

ふだん、データ出力ってどのように作りますか?

自分は、こんな感じで
レコードの絞り込みとデータの取得を同時に行なって
1レコードずつ出力したい順に情報を並べ替えてファイルに出力
することが多いです。

おそらくよくあるデータ出力実装.rb
records = Employee.select(:age, :first_name, :last_name).where(job: :engineer)
CSV.open("engineer.csv") do |csv|
  csv << [:first_name, :last_name, :age] # headers
  records.each do |record|
    csv << [record.first_name, record.last_name ,record.age]
  end
end

いつものようにしたけれど、コードが長くなっちゃった

その日は出力したい情報の種類が多いデータ出力を作っていました。
いつものように作ってみたけれど、データの取得先だけで5種類もあってコードがとても長い、、、
なんとかしたい。
そのときふと
出力したいレコードの絞り込みと、出力したいデータの取得処理を
分けてみたらどうだろうと思いました。

レコードの絞り込みと、データの取得をわけてみた

そうして出来上がったのがこちらです。

欲しいレコードの検索条件と
知りたいデータの項目名を定義したファイルを渡します。

id name tel address station url reviews_url yoyaku

するとまず、レコード絞り込み機能が
レコードの特定条件で絞り込み検索をして出力対象レコードのidのみを出力します。

id name tel address station url reviews_url yoyaku
100
200
300

続いて、このファイルがデータ取得機能に渡されて

id name tel address station url reviews_url yoyaku
100 カフェ 03-0000-0000 恵比寿南 恵比寿駅 /100/ /100/dtlrvwlst なし
200 ラーメン屋 03-0000-0000 恵比寿南 恵比寿駅 /200/ /200/dtlrvwlst なし
300 イタ飯屋 03-0000-0000 恵比寿南 恵比寿駅 /300/ /300/dtlrvwlst あり

idをキーにデータを取ってきて、項目名列に対応した情報を付加してくれます。

表の項目名は、データ取得先ごと色分けしてみました。
データ取得先はDBだったりAPIだったりするので、単純に結合もできず
特にAPIはデータを取るときにページングや並列処理をしたりもするのでわりと手間がかかります。

データ取得機能の汎用性がとてもよかった

長いコードを整理するために機能を分割したのですが
思いがけず、データ取得機能の汎用性がとても高いことに気づきました。

たとえば
レコードを絞り込む条件は違うけど、出力したいデータが似ている機能を新しく作りたいときは、
レコードの絞り込み部分だけ作れば、あとはデータ取得機能に渡すだけ!

あるいは
既にレコードは特定できていて、詳細情報を知りたいとき
id列を埋めたファイルをデータ取得機能に渡すだけ!

渡すだけ!これはなかなか汎用性があるぞう。

単純なデータ出力ではあまり恩恵はなさそうですが
今回ご紹介したように、データ出力のために用意する種類が多くて手間がかかるケースで
それを利用したいシーンが複数あると、たくさんの恩恵を受けられそうです。

ドメイン間は疎結合のまま、情報満載なデータ出力ができそうな予感

今回はひとつのドメインのデータ取得機能だけ作りましたが、別のドメインにもデータ取得機能を作り
知りたいデータの項目定義ファイルに、いろんなドメインのデータ取得機能を旅させることで
情報満載なCSVファイルを作ることもできそうです。

感想

ふだん何気なくやっていたことを少し変えてみたら新しい発見につながったので
今後も何気ないことを改めて考えてみて、いろんな発見をしていきたいですね!

明日は @hiroteru_ さんによる「UIViewの角丸と影のおはなし」です。
お楽しみに!!

最後までご覧いただきありがとうございました!

おまけ

データ出力機能を作るときに役立ちそうなコード集です。

最近のExcelってUTF-8を理解できるんですって(ただしBOMつきに限る)

最近のExcelは、UTF-8でもBOMつきであれば文字化けせずにcsvファイルを開いてくれるようです。

BOMつきUTF-8のCSVを作る
File.write(filepath, "\xEF\xBB\xBF") # excelで直接UTF-8を開けるようにするためにBOMをつける
CSV.open(filepath, 'a') do |csv|     # 追加書き込みモードで開く

これで「ファイルをインポート」機能とお別れできます。

この方法で作成したCSVを読み込むときは、BOMついてるかもよ。と教えてあげる必要があります。

BOMつきUTF-8のCSVを読み込む
CSV.open(filepath, encoding: "BOM|UTF-8") do |csv|

データ取得機能で活躍した遅延ロード

不必要なデータまで取得していると、処理に無駄な時間がかかってしまいます。
1つの取得先のデータだけあれば済むときに、5つの取得先からデータを取得したくないですよね。
こんなときに遅延ロードが大活躍しました。

class Hoge
  def initialize(id)
    @id = id
  end

  def name
    data.name
  end

  def tel
    data.tel
  end

  private

  def data
    return @data if defined? @data # メモ化
    @data = HogeData.select(:name, :tel).where(id: @id).take
  end
end

header = csv.headers # 出力したいと指定されている項目名を取得
-> ["name", "tel"]

hoge = Hoge.new(id)
header.each do |column|
  hoge.public_send(column.to_sym) # 初回にnameが呼び出されたときに、中のdataメソッドがデータを取得してくれる
  # 続いて tel が呼び出されたときは、メモ化された@dataを見るので改めてデータ取得はしない
end

このように、nameの情報を出力したい。と呼び出されたときにはじめてデータを取得することで不必要なデータは取得せずに済むようになりました。

あるクラスのメソッドをまとめてdelegateに定義する

delegateって明示的にメソッド名を定義する必要があって、面倒ですよね。
Hogeクラスのメソッド全部をdelegateしたいときは、こんな書き方で定義できました。

delegate *Hoge.instance_methods(false), to: :hoge

データ取得機能では、データの取得先毎にクラスをわけてオブジェクトを作っています。
メソッドが呼び出されたら、そのデータを持っているオブジェクトにdelegateするように、この方法で実現しています。

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

サンプルコードでわかる!Ruby 2.7の主な新機能と変更点 Part 1 - 番号指定パラメータ(numbered parameter)

はじめに

Rubyは毎年12月25日にアップデートされます。
Ruby 2.7については2019年11月23日にpreview3がリリースされました。

Ruby 2.7.0-preview3 リリース

この記事ではRuby 2.7で導入される変更点や新機能について、サンプルコード付きでできるだけわかりやすく紹介していきます。

ただし、Ruby 2.7は多くの新機能や変更点があり、1つの記事に収まらないのでいくつかの記事に分けて書いていきます。
本記事で紹介するのは番号指定パラメータ(numbered parameter)です。

本記事の情報源

本記事は以下のような情報源をベースにして、記事を執筆しています。

動作確認したRubyのバージョン

本記事は以下の環境で実行した結果を記載しています。

$ ruby -v
ruby 2.7.0preview3 (2019-11-23 master b563439274) [x86_64-darwin19]

フィードバックお待ちしています

本文の説明内容に間違いや不十分な点があった場合は、コメント欄や編集リクエスト等で指摘 or 修正をお願いします?

それでは以下が本編です!

ブロックの仮引数として番号指定パラメータが試験的に導入された

Ruby 2.7ではブロックの仮引数として番号指定パラメータ(numbered parameter)が試験的に導入されました。
これにより、|s| のように明示的に引数名を指定する代わりに、_1 のような連番でブロックの仮引数を受け取ることができます。

# 番号指定パラメータを使わない場合
%w(1 20 300).map { |s| s.rjust(3, '0') }
#=> ["001", "020", "300"]

# 番号指定パラメータを使う場合
%w(1 20 300).map { _1.rjust(3, '0') }
#=> ["001", "020", "300"]
# 番号指定パラメータを使わない場合
[1, 2, 3, 4].inject(0) { |memo, n| memo + n }
#=> 10

# 番号指定パラメータを使う場合
[1, 2, 3, 4].inject(0) { _1 + _2 }
#=> 10

(Ruby初心者さん向けの補足)
上のコードに登場する%w()という構文は、文字列の配列を作成するための構文です。

%w(1 20 300)
# ↑のコードは、↓と書いているのと同じ
["1", "20", "300"]

割り当てられていない連番はnil

割り当てられない連番を指定するとnilが返ります。

# 9番目の仮引数は何も割り当てられないのでnil
%w(1 20 300).map { _9 }
#=> [nil, nil, nil]

使用できる連番は_1から_9まで

使用できる連番は_1から_9までです。
_10のような連番を指定するとNameErrorが発生します。

%w(1 20 300).map { _10 }
#=> NameError (undefined local variable or method `_10' for main:Object)

_1〜_9のようなローカル変数を宣言していると警告が出る

番号指定パラメータを使っているかどうかにかかわらず、_1_9のような変数名を宣言していると、コードを読み込んだタイミングで警告が出ます。

# 番号指定パラメータと同名の変数を宣言すると警告が出る
_1 = "999"
#=> warning: `_1' is used as numbered parameter

ですので、既存のコードをRuby 2.7環境で動かすと、場合によってはこのような警告が出る可能性があります。

同名のローカル変数があるとブロック内でも変数が優先される

ブロックの外で同名の変数が宣言されている場合、ブロック内では番号指定パラメータではなく、ブロックの外で宣言されたローカル変数として参照されます。

# 番号指定パラメータと同名の変数を宣言しておく
_1 = "999"

# _1はローカル変数の"999"として扱われる
%w(1 20 300).map { _1.rjust(3, '0') }
#=> ["999", "999", "999"]

ただし、同名のローカル変数がブロックの後ろで宣言されていた場合はこの限りではありません。

# _1が番号指定パラメータとして機能する
%w(1 20 300).map { _1.rjust(3, '0') }
#=> ["001", "020", "300"]

# 同名のローカル変数をブロックの後ろで宣言する
_1 = "999"

同名のメソッドがあると番号指定パラメータが優先される

_1のような名前のメソッドが定義されている場合は、番号指定パラメータが優先されます。

# 番号指定パラメータと同名のメソッドを定義しておく
def _1
  '123'
end

# _1は番号指定パラメータとして扱われる
%w(1 20 300).map { _1.rjust(3, '0') }
#=> ["001", "020", "300"]

# _1() または self._1 とすればメソッドを明示的に呼び出せる
%w(1 20 300).map { _1().rjust(3, '0') }
#=> ["123", "123", "123"]

%w(1 20 300).map { self._1.rjust(3, '0') }
#=> ["123", "123", "123"]

ちなみに、Ruby 2.6だと番号指定パラメータの構文が導入されていないため、メソッドの_1が呼ばれます。

def _1
  '123'
end

# Ruby 2.6の場合はメソッドの_1が呼ばれる(Ruby 2.7と挙動が異なる)
%w(1 20 300).map { _1.rjust(3, '0') }
#=> ["123", "123", "123"]

Ruby 2.6と2.7では挙動が異なりますが、この場合は警告も出ないため、運が悪いと「Ruby 2.7に上げたら、なぜかちゃんと動かなくなった!」というトラブルが起きるかもしれません。(まあ、こんなコードを書いている人は滅多にないと思いますが・・・)

ネストしたブロックで番号指定パラメータを使う際の注意点

番号指定パラメータをネストしたブロックの中で使おうとするとsyntax errorが発生します。

sum = 0
[*1..4].each_slice(3) do
  # 外側のブロックで番号指定パラメータを使う
  _1.each do
    # 内側のブロックでも番号指定パラメータを使おうとするとエラーになる
    sum += _1
  end
end
#=> numbered parameter is already used in (SyntaxError)
#   outer block here
#           sum += _1
#   ^~~~~~~~~~~~~~~~~

外側のブロックだけ、もしくは内側のブロックだけで使うのはOKです。

sum = 0
[1, 2, 3, 4].each_slice(3) do |arr|
  arr.each do
    # 内側のブロックでだけ番号指定パラメータを使う
    sum += _1
  end
end
sum #=> 10

従来のブロック仮引数と混在させると構文エラーになる

次のコードのように、従来のブロック仮引数と混在させると構文エラーが発生します。

# 従来のブロック仮引数 |memo, n| と、番号指定パラメータ _1 が混在すると構文エラー
[1, 2, 3, 4].inject(0) { |memo, n| _1 + n }
#=> ordinary parameter is defined (SyntaxError)

ただし、明示的に番号指定パラメータと同名のブロック仮引数を指定した場合は構文エラーになりません。

# 番号指定パラメータと同名のブロック仮引数を使えば構文エラーにならない
[1, 2, 3, 4].inject(0) { |_1, n| _1 + n }
#=> 10

コラム:濫用厳禁!?番号指定パラメータの使いどころ

タイプ量が減るので一見便利に見える番号指定パラメータですが、個人的には「濫用厳禁、用量と用法を守って使いましょう」な新機能だなと感じます。

なぜなら連番だとコードを書いた人の意図がまったく見えないので、可読性が落ちるからです。
(単純に「連番は脳への負担が大きい」という問題もあります)

# 引数名を見れば「1, 2, 3, 4の値が渡されるのはnの方だな」と推測できる
[1, 2, 3, 4].inject(0) { |memo, n| memo + n }

# ん?どっちがどっちだ!?わからん!!
[1, 2, 3, 4].inject(0) { _1 + _2 }

irbなどでサクッと実行結果を確認したりするときに使うのは便利だと思いますが、実務で書く寿命の長いコードの場合はなるべく使用を控えた方が良い気がします。(そもそもRuby 2.7では「試験的な新機能」の扱いですので!)

まとめ

本記事ではRuby 2.7で試験的に導入された番号指定パラメータ(numbered parameter)を紹介しました。

冒頭でも説明したように「サンプルコードでわかる!Ruby 2.7の主な新機能と変更点」はいくつかの記事に分けて執筆します。
新しい記事を書いたらQiitaの「変更通知」でお知らせしますので、いちはやくキャッチしたい方は本記事のストックをよろしくお願いします!

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

Fullstaq Ruby を投入してみた(結果編に続く)

こんにちは。

以前見かけた Fullstaq Ruby を試してみようと思います。

Fullstaq Ruby

触れ込みについて

公式サイト の触れ込みでは

Ruby, optimized for production
A new Ruby distribution, with the Ruby you know and love, but better.

本番環境向けに調整されたより優れたRubyとのことです。

Less memory — save 30-50%

メモリ消費を3~5割抑え

Faster

高速で

More secure

セキュアで

Fully open source, based on MRI

MRIベースの完全なオープンソース

とのこと。

作者について

サイトをみていると、創設者は Hongli Lai と Fullstaq という会社のようです。
Hongli Lai はそこに記載があるように、 Passenger や Ruby Enterprise Edition の作者だそうですね。

最近の方は Ruby Enterprise Edition をご存じないかもしれません。

Ruby 1.8系の時代にメモリ使用率が激減することで、よく本番環境で用いられていたRuby処理系でした。

Rubyおじさんとしては「まじか、使えるんじゃないか」という気持ちにもなってきますね。

しくみ

週刊Railsウォッチで取り上げられていたのでそちらをお読みください。

週刊Railsウォッチ(20190821-2/2後編)11のgemにバックドア、ruby-jp Slackがとてもアツい、Fullstaq Rubyでチューンアップ、HTTPサービス監視chaoほか

導入

Server Edition 、Container Edition 、Heroku Edition と提供する予定のようですが、2019年12月3日現在は Server Edition のみのようです。

GET STARTED - Installation に RHEL/CentOS と Debian/Ubuntu のパッケージマネージャを使った方法が載っているので、そちらを読んで導入してみます。

今自由にできる環境がDocker環境しかなかったので、既存のDockerfileを手直しして入れてみました。
2019年12月3日現在、最新の Debian 10 buster 向けのパッケージは用意されていないようだったので、Debian 9 stretch をベースに使いました。

FROM buildpack-deps:stretch

ENV RUBY_VERSION 2.6.5

# Install dependencies
RUN apt-get update -qq \
  && apt-get install -y gnupg apt-transport-https ca-certificates curl

RUN echo 'deb https://apt.fullstaqruby.org debian-9 main' > /etc/apt/sources.list.d/fullstaq-ruby.list \
  && curl -SLfO https://raw.githubusercontent.com/fullstaq-labs/fullstaq-ruby-server-edition/master/fullstaq-ruby.asc \
  && apt-key add fullstaq-ruby.asc \
  && apt-get update -qq \
  && apt-get install -y fullstaq-ruby-${RUBY_VERSION} \
  && apt-get clean

ENV PATH $PATH:/usr/lib/fullstaq-ruby/versions/${RUBY_VERSION}/bin

# (省略)

結果

まだデータ不足。
1、2週様子を見て載せます。

動作には問題なく、テストはすべてパスし、エラートラッカーには特に情報は上がってきていません。

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

三井住友信託銀行プログラミングコンテスト2019に参加してみた

感想

AtCoderのコンテスト初参加。結構疲れた。てかムズイ。普通に数学的な発想が求められるんだな〜。
解ければ快感、解けなければうつ病。
https://atcoder.jp/contests/sumitrust2019
以下自分の回答

A November 30

M1,D1 = gets.chomp.split.map(&:to_i)
M2,D2 = gets.chomp.split.map(&:to_i)

ret = 0

if M1 == 12
  if D1 == 31
    ret = 1
  end
end

if M1 + 1 == M2 
  ret = 1
end

puts ret

D2が1かどうか判定すれば良かっただけなのに、なにしてるんだろうか。ぐぬぬ。

B Tax Rate

N = gets.to_i

X = ":("

a = N * 100 / 108
if ( N * 100 ) % 108 != 0
  a += 1
end

if a * 108 / 100 == N
  X = a
end
puts X

結構迷ってしまった。てか正規のやり方なのか怪しい。
Xに文字列いれた上に数値入れてグロい。Ruby凄いな。

C 100 to 105

X = gets.to_i

N = X / 100

can = 0

for i in 0..N
  x = X - i * 100
  if 0 <= x && x <= 5 * i
    can = 1
    break
  end
end

puts can

特にコメントなし(実際に解いてる時は一瞬で解けた嬉しさで内心ウッキウキだった)

D Lucky PIN

N = gets.to_i
S = gets.chomp

count = 0
for i in 0..9
  a = S.index(i.to_s)
  if a != nil
    for j in 0..9
      b = S.index(j.to_s, a+1)
      if b != nil
        for k in 0..9
          c = S.index(k.to_s, b+1)
          if c != nil
            count += 1
          end
        end
      end
    end
  end
end

puts count

最初30000個を3重ループして時間オーバーになった(あたりまえやん)
ここまでは100分以内にできた。

E Colorful Hats 2

ここから後日解き直した。

N = gets.to_i
A = gets.split.map &:to_i

t = [ 0, 0, 0 ]

result = 1
for i in 0..N-1
  if t.count( A[i] ) == 0
    result = 0
    break
  end
  result = ( result * t.count( A[i] ) ) % 1000000007
  for j in 0..2
    if t[j] == A[i]
      t[j] += 1
      break
    end
  end
end
puts result

これみんな簡単って言ってるのにわからなくて泣いた。
それぞれの帽子の数を保持して、その値と一致した分だけ場合分けができるという発想が無かった。
ブログラミングじゃなくても解けなかったと思う。悲しい。理系とは何だったのか。

F Interval Running

T = gets.split.map(&:to_i)
A = gets.split.map(&:to_i)
B = gets.split.map(&:to_i)

b = ( T[0] * A[0] + T[1] * A[1] ) - ( T[0] * B[0] + T[1] * B[1] )
if b == 0
  puts "infinity"
else
  a = T[0] * ( A[0] - B[0] )
  if a * b > 0
    puts 0
  else
    c = ( a / b ) * -2
    if a % b != 0
      c -= 1
    end
    puts c
  end
end

E問題で精神がやられて変数名とか見づらいしif文もおかしいけど一応解けた。
実際のコーディングだったら絶対に修正する。
A問題では変数に1つずつ入れているのにF問題では配列にいれる謎。(表記揺れってやつになるのかな)
自分で書いといてなんだけど*-2とかいう箇所気持ち悪すぎる。
横軸時間、縦軸距離のグラフを書くのがわかりやすかった。

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

rails-tutorial第8章

ログイン機能を作ろう!!

モデルを使わないSessionリソースを扱う。

Sessionsコントローラを作ろう

$ rails generate controller Sessions new

ルーティング設定

config/routes.rb
Rails.application.routes.draw do
  root   'static_pages#home'
  get    '/help',    to: 'static_pages#help'
  get    '/about',   to: 'static_pages#about'
  get    '/contact', to: 'static_pages#contact'
  get    '/signup',  to: 'users#new'
  get    '/login',   to: 'sessions#new'
  post   '/login',   to: 'sessions#create'
  delete '/logout',  to: 'sessions#destroy'
  resources :users
end

別にresourcesを使ってないからといってrestfulじゃないわけではない。本質は同じurlにhttpリクエストメソッドで指定してアクションを分けるということ。

また、これにより、login_pathなどの名前付きルートが設定される。

ログインフォーム作る。

app/views/sessions/new.html.erb
<% provide(:title, "Log in") %>
<h1>Log in</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_for(:session, url: login_path) do |f| %>

      <%= f.label :email %>
      <%= f.email_field :email, class: 'form-control' %>

      <%= f.label :password %>
      <%= f.password_field :password, class: 'form-control' %>

      <%= f.submit "Log in", class: "btn btn-primary" %>
    <% end %>

    <p>New user? <%= link_to "Sign up now!", signup_path %></p>
  </div>
</div>

form_forの引数はsignupの時は@userを渡していたが、:sessionを渡すことにより、
params = {session: {email: ~~~, password: ~~~}}という形で情報を送ることができる。

で、urlオプションに名前付きルートを渡せば完成。

次にSessionsコントローラのcreateアクションを実装していこう。

app/controllers/sessions_controller.rb
class SessionsController < ApplicationController

  def new
  end

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      # ユーザーログイン後にユーザー情報のページにリダイレクトする
    else
      # エラーメッセージを作成する
      render 'new'
    end
  end

  def destroy
  end
end

find_byに注目。
find()だと、idでしか探すことができず、()の中に数字しか入れられない。

IDがわかっている場合は、findメソッド
IDが不明で、別の条件でレコード検索をしたい場合は、find_byメソッド
このように覚えておこう。

注意点!!

user = User.find_by(email: params[:session][:email].downcase)
if user.authenticate(params[:session][:password])
.
.
.

上記のように書いてしまうと、致命的な欠陥がある。

find_byはオブジェクトが見つからない時にnilを返すので、もし存在していないメールアドレスを渡してしまうと、、

nil.authenticateとなり、Nomethoderrorが起こってしまう。

なので、

user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])

ユーザーが存在するかつ、アドレスの認証が通れば、という条件をif文に当てている。

ちなみに userが存在しないとわかった時点で、右側の条件式は評価されないという特徴がある。
これにより、nil.authenticateは実行されなくなる。

ログイン失敗時のメッセージを表示する。

ActiveRecordを継承しているモデルと、バリデーションを設定すれば@user.errors.full_messagesに実際のエラーメッセージが表示されたが、

Sessionsの場合、モデルでもなければバリデーションもないので、

flashを使ってメッセージを表示する必要がある。

app/controllers/sessions_controller.rb
class SessionsController < ApplicationController

  def new
  end

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      # ユーザーログイン後にユーザー情報のページにリダイレクトする
    else
      flash[:danger] = 'Invalid email/password combination' # 本当は正しくない
      render 'new'
    end
  end

  def destroy
  end
end

上記の場合、ちょっとしたバグがある。
それは、flashメッセージがhome画面やhelp画面に移動しても表示されてしまうことだ。

なぜこれが起きるのか?

redirect_toの場合は、それでリクエストが一回。
別のページに飛ぼうとすると2回目なので飛ぶ寸前でflashメッセージは消える。

しかし、今回は、
render 'new'となっている。
renderはリクエストには入らないため、
例えば、home画面に飛ぶので一回目、
再度リロードすると、2回目なので消える、という流れになってしまう。

回帰バグを防ぐためにテストを書こう。

今回もブラウザを行ったり来たりしているテストなので、インテグレーションテストで行う。

$ rails generate integration_test users_login

test/integration/users_login_test.rb
require 'test_helper'

class UsersLoginTest < ActionDispatch::IntegrationTest

  test "login with invalid information" do
    get login_path
    assert_template 'sessions/new'
    post login_path, params: { session: { email: "", password: "" } }
    assert_template 'sessions/new'
    assert_not flash.empty?
    get root_path
    assert flash.empty?
  end
end

この状態だとテストは落ちる。

じゃあ、どうやってflashメッセージを1回だけ表示するの?

flashはメソッドなので、flash.now[:danger] = 'Invalid email/password combination'
というようにできる。
.nowは1度目のリクエストが来たらflashメッセージを消す。
これはflashメソッドがrailsで元から設定されたメソッドだからこういうことができる。

これで失敗時の実装は完了

成功した時の処理を実装しよう

ユーザーが存在し、アドレスの認証が通れば、ログインしている状態を作り出さないといけない。

sessionという特殊な変数を使ってそれを実現する。

session[:user_id] = user.id
ここに何か値が入れば、ログイン中とし、nilになればログアウトとする。

それを実現するためにhelperにloginメソッドを定義しよう。

app/helpers/sessions_helper.rb
module SessionsHelper

  # 渡されたユーザーでログインする
  def log_in(user)
    session[:user_id] = user.id
  end
end

このhelperメソッドを使ってログイン機能を実装

app/controllers/sessions_controller.rb
class SessionsController < ApplicationController

  def new
  end

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      log_in user
      redirect_to user
    else
      flash.now[:danger] = 'Invalid email/password combination'
      render 'new'
    end
  end

  def destroy
  end
end

最後に、loginしているかどうかは、様々なコントローラで使うので、loginメソッドを書いたhelperをどのコントローラでも使えるようにしておこう。

app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  include SessionsHelper
end

そのためには、全コントローラの親クラスであるapplication コントローラに

createアクションを実装したら

その次は、session[:user_id]に値が入っていれば、〜〜〜〜、入っていなければ、〜〜〜〜というようにしていく。

また、今どのユーザーがログインしているかがわからないと、showアクションでどのユーザーページを表示するべきかなどの問題が発生してしまうので、なんとかして、sessionの情報から現在ログインしているユーザーを参照する必要がある。

では、現在ログインしているユーザーを返すメソッドをhelperに定義しよう。

app/helpers/sessions_helper.rb
module SessionsHelper

  # 渡されたユーザーでログインする
  def log_in(user)
    session[:user_id] = user.id
  end

  # 現在ログイン中のユーザーを返す (いる場合)
  def current_user
    if session[:user_id]
      @current_user ||= User.find_by(id: session[:user_id])
    end
  end
end

@current_userとなっているのは、インスタンス変数をview側で使いたいから。
また、@current_userを使いたいたびにfind_byをするのはパフォーマンス上あまり良くない。
なので、 ||= を使って、存在すれば@current_userを返す。 なかったら、find_byを使うというようにしている。

これで、1リクエストで最大で1問い合わせにできる。

view側でログインユーザーと非ログインユーザーを分けるには

<% if logged_in? %>
  # ログインユーザー用のリンク
<% else %>
  # ログインしていないユーザー用のリンク
<% end %>

というようにrubyのコードを使ってあげれば良い。

そのために、上記のlogged_in?メソッドを定義しよう。

app/helpers/sessions_helper.rb
module SessionsHelper

  # 渡されたユーザーでログインする
  def log_in(user)
    session[:user_id] = user.id
  end

  # 現在ログイン中のユーザーを返す (いる場合)
  def current_user
    if session[:user_id]
      @current_user ||= User.find_by(id: session[:user_id])
    end
  end

  # ユーザーがログインしていればtrue、その他ならfalseを返す
  def logged_in?
    !current_user.nil?
  end
end

logged_in?メソッドはcurrent_userメソッドを呼び出して、インスタンス変数があれば、trueを返し、なければfalseを返すようにする。

このままだと、nilの時にtrueを返してしまう。

!は否定演算子である。true falseが逆になる。

これで、nilの時はfalseをnilじゃない時はtrueを返すようにする。

ログインしているかでviewを分ける

app/views/layouts/_header.html.erb
<header class="navbar navbar-fixed-top navbar-inverse">
  <div class="container">
    <%= link_to "sample app", root_path, id: "logo" %>
    <nav>
      <ul class="nav navbar-nav navbar-right">
        <li><%= link_to "Home", root_path %></li>
        <li><%= link_to "Help", help_path %></li>
        <% if logged_in? %>
          <li><%= link_to "Users", '#' %></li>
          <li class="dropdown">
            <a href="#" class="dropdown-toggle" data-toggle="dropdown">
              Account <b class="caret"></b>
            </a>
            <ul class="dropdown-menu">
              <li><%= link_to "Profile", current_user %></li>
              <li><%= link_to "Settings", '#' %></li>
              <li class="divider"></li>
              <li>
                <%= link_to "Log out", logout_path, method: :delete %>
              </li>
            </ul>
          </li>
        <% else %>
          <li><%= link_to "Log in", login_path %></li>
        <% end %>
      </ul>
    </nav>
  </div>
</header>

ここで注意しなければいけないのは、

<li><%= link_to "Profile", current_user %></li>

これは本来

<li><%= link_to "Profile", user_path(@current_user) %></li>

となっている。

ただ、これは @current_userに省略することができたよね。

だから、current_userメソッドを書くだけで良い。

もう一つ注意する点が、

<%= link_to "Log out", logout_path, method: :delete %>

link_toメソッドはデフォルトではgetリクエストを送るのでmethod: :deleteを書かないと、

/logout に getリクエストを送ってしまう。

なので、link_toでgetリクエスト以外を指定するときは、
method: :deleteのようにオプションを追加しなければいけない。

注意点としては、method: と :deleteどちらも:が必要だということ。

bootstrapのドロップダウンを使えるようにするために

app/assets/javascripts/application.js
//= require rails-ujs
//= require jquery
//= require bootstrap
//= require turbolinks
//= require_tree .

2,3行目を書き加えるとドロップダウンが使えるようになる。

ユーザーログインのテスト

テスト時に登録済みユーザーとしてログインしておく必要があります。当然ながら、データベースにそのためのユーザーが登録されていなければなりません。Railsでは、このようなテスト用データをfixture (フィクスチャ) で作成できます。

test/fixtures/users.yml
michael:
  name: Michael Example
  email: michael@example.com
  password_digest: <%= User.digest('password') %>

でもこれじゃあdigestメソッドがないから、ハッシュ化できない。

digestメソッドはUserに関連するときしか使わないので、userモデルに定義する。

app/models/user.rb
  before_save { self.email = email.downcase }
  validates :name,  presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
  has_secure_password
  validates :password, presence: true, length: { minimum: 6 }

  # 渡された文字列のハッシュ値を返す
  def User.digest(string)
    cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
                                                  BCrypt::Engine.cost
    BCrypt::Password.create(string, cost: cost)
  end
end

三項演算子について

condition ? expr1 : expr2

condition
trueかfalseかを評価する式です。
expr1, expr2
各々の値の場合に実行する式です。 conditionがtrueの場合、演算子はexpr1の値を返します。そうでない場合はexpr2の値を返します。

ユーザーログインテストコード

test/integration/users_login_test.rb
require 'test_helper'

class UsersLoginTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end
  .
  .
  .
  test "login with valid information" do
    get login_path
    post login_path, params: { session: { email:    @user.email,
                                          password: 'password' } }
    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)
  end
end

@user = users(:michael)はtest/fixtures/users.ymlで作った:michaelを@userに代入している。

また、

assert_redirected_to @user
follow_redirect!

1行目は、 @userにリダイレクトされますよね?(行き先は〜〜駅ですよね)
2行目は、 assert_redirected_to @userが通った上で@userにリダイレクトされる。

ちなみにこの状態でテストは通る。

signup後はそのままログイン済みにしよう。

app/controllers/users_controller.rb
class UsersController < ApplicationController

  def show
    @user = User.find(params[:id])
  end

  def new
    @user = User.new
  end

  def create
    @user = User.new(user_params)
    if @user.save
      log_in @user
      flash[:success] = "Welcome to the Sample App!"
      redirect_to @user
    else
      render 'new'
    end
  end

  private

    def user_params
      params.require(:user).permit(:name, :email, :password,
                                   :password_confirmation)
    end
end

log_in @userでセッション変数に@user.idを保存する。
ちなみに、log_in @userが使えるのはsessions_helperがapplication controllerにincludeされてるから。

ログインのテスト

テストにもテストのhelperが存在する。
そこに、ログインしているかどうかを判断するメソッドを定義しておこう。

test/test_helper.rb
ENV['RAILS_ENV'] ||= 'test'
.
.
.
class ActiveSupport::TestCase
  fixtures :all

  # テストユーザーがログイン中の場合にtrueを返す
  def is_logged_in?
    !session[:user_id].nil?
  end
end

ユーザー登録後ログイン状態になっているかテスト。

test/integration/users_signup_test.rb
require 'test_helper'

class UsersSignupTest < ActionDispatch::IntegrationTest
  .
  .
  .
  test "valid signup information" do
    get signup_path
    assert_difference 'User.count', 1 do
      post users_path, params: { user: { name:  "Example User",
                                         email: "user@example.com",
                                         password:              "password",
                                         password_confirmation: "password" } }
    end
    follow_redirect!
    assert_template 'users/show'
    assert is_logged_in?
  end
end

先ほどymlにログインしたユーザー、test_helperにis_logged_in?メソッドを定義したので、テストは通る。

ログアウト機能を実装

ログアウトメソッドもログインメソッドと同じようにsessions_helperに定義しておく。

app/helpers/sessions_helper.rb
module SessionsHelper

  # 渡されたユーザーでログインする
  def log_in(user)
    session[:user_id] = user.id
  end
  .
  .
  .
  # 現在のユーザーをログアウトする
  def log_out
    session.delete(:user_id)
    @current_user = nil
  end
end

ここで気をつけるのが、

sessionというのはハッシュな訳で、session[:user_id] = user.id

session = {user_id: user.id}という形になっている。

で、ハッシュの値を消すときは、

hash.delete(key)

というdeleteメソッドにキーを引数に渡すと実現できる。

で、今回sessionでも同じことが起きている。

session.delete(:user_id)

これにより、sessionというハッシュに格納されていた{user_id: user.id}が消える。

じゃあ、なんで@current_user = nilまでする必要があるの?

これは、なるべくサーバーの問い合わせ?的なものを無くして負荷を少なくするためらしい。

destroyアクションの実装

app/controllers/sessions_controller.rb
class SessionsController < ApplicationController

  def new
  end

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      log_in user
      redirect_to user
    else
      flash.now[:danger] = 'Invalid email/password combination'
      render 'new'
    end
  end

  def destroy
    log_out
    redirect_to root_url
  end
end

ここで注目したいのが、destroyアクションはviewがないので、最後にどのviewに飛ぶかを指定してあげる必要があるということ。これはupdateアクションとかにも言えそうだよね。

ログアウトのテスト

統合テストのストーリーを拡張しよう。
具体的には、ログインテストの中で、ログアウトのテストのアサーションも書いちゃおうというもの。

test/integration/users_login_test.rb
require 'test_helper'

class UsersLoginTest < ActionDispatch::IntegrationTest
  .
  .
  .
  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
    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
end

assert_not is_logged_in?
is_logged_in?はtest_helperに定義したから使える。

なぜsession変数はなくならない?なくなる?

調べろ。ブラウザとrails sに保存されるから?

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

ログイン中ユーザーの投稿内容をshowアクション以外で出力する方法

SNSっぽいアプリケーションのマイページで、ログイン中ユーザの投稿一覧を取得したかっただけなのにかなり詰まったので備忘録として残しておきます。

アプリの仕様上showアクションやshow.html.erbファイルを使用できず、index.html.erbで表示させるというレア?パターンではありますが。

環境

Rails 5.2.3
Ruby 2.5.1
gemのdeviseを使用
当記事で使用しているモデル名はplace

詰まった所

homes/index.html.erbファイル内にてeach doを利用し投稿内容を全部出力したい。

homes/index.html.erb
<% @myplaces.each do |myplace| %>
  #省略
<% end %>

全部のplaceを取得するのはhomesコントローラー内のindexアクションで

./controllers/homes_controller
@places = Place.all

こう書けばいい。そりゃそうだ。
問題はログイン中ユーザの投稿内容だけを取得したい時。

./controllers/homes_controller
@myplaces = Place.find(current_user.id)

と入力するとundefined method 'each' for nil:NilClassでエラーが発生する。これは@myplacesが空だからeachする元ネタがありませんよっていう怒られ方。できそうな気もするのに。

試しにターミナルでfind(1)として出力してみる。

ターミナル
pry(main)> Place.find(1)
   (0.5ms)  SET NAMES utf8,  @@SESSION.sql_mode = CONCAT(CONCAT(@@sql_mode, ',STRICT_ALL_TABLES'), ',NO_AUTO_VALUE_ON_ZERO'),  @@SESSION.sql_auto_is_null = 0, @@SESSION.wait_timeout = 2147483
  Place Load (0.3ms)  SELECT  `places`.* FROM `places` WHERE `places`.`id` = 1 LIMIT 1
=> #<Place:0x00007f831ad85618
 id: 1,
 name: #以下省略

問題ない。
idが1であるcurrent_user.idを適当な変数に代入して出力してみる

./controllers/homes_controller
@user = current_user.id
homes/index.html.erb
<%= @user %>
#=> 1

こっちも問題がない。
なんで出力されないのか不思議で仕方がなかったんですが、驚いた事にこんな書き方だけで解決しました。

./controllers/homes_controller
@places = Place.all
places = @places
@myplaces = current_user.places
homes/index.html.erb
<% @myplaces.each do |myplace| %>
  #省略
<% end %>

目ン玉飛び出そうになりました。こんな簡単な記述でよかったんかと。。。普通にidを指定すれば出来るやろって余裕こいてたら痛い目見ました。

原因は今度時間を見つけて探してみます。今度作る時にはハマらないように…

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