20200120のRubyに関する記事は17件です。

devise ユーザーのプロフィール画面作成と編集(デフォルトをカスタマイズ)

はじめに

deviseを導入の仕方を学び、デフォルトのままだとユーザーフレンドではないと思い、deviseのカスタマイズをしようと思います。

・sign up(アカウント登録)時にユーザーの名前も一緒に登録!
・users/showで「名前」「メールアドレス」「プロフィール」の表示!
・users/editで簡単編集!

上記の3つについて、rails初心者目線で書いていきたいと思います。

完成イメージ

スクリーンショット 2020-01-20 17.57.35.png
スクリーンショット 2020-01-20 18.10.25.png

Qiita初めての記事で緊張しますが、分かりやすく書いていきます!

環境

ruby 2.5.6
rails 5.2.3
devise 4.7.1

前提

今回、deviseは導入済みでその後どうやってカスタマイズしていくか進めます。

deviseの導入がまだの方用に参考URL貼っておきます。

devise導入方法URL
(1)公式ドキュメント
(2)[Rails] deviseの使い方(rails4版)
※(2)だと「1.deviseの導入」まで進めてください。

MVC(model/ view/ controller )設定

modelの生成

ターミナル.
$ rails g devise user

テキストエディターのapp/models見てみると一番下にuser.rbがあります。
スクリーンショット 2020-01-20 20.51.54.png
これでuser modelで出来ました。

viewの生成

ターミナル.
$ rails g devise:views

テキストエディターのapp/views/devise見てみるとこんな感じになります。

スクリーンショット 2020-01-20 20.57.17.png
確認ができたらdeviseのviewsが生成されました。

controllerの生成

ターミナル.
$ rails g devise:controllers users

テキストエディターのapp/controllers/users見てみるとこんな感じになります。

スクリーンショット 2020-01-20 21.00.18.png
確認できたら、deviseのcontroller生成されました!

各種ファイルの設定

この章の目的:
(1)sign upでメールアドレスとパスワードの他に名前もを登録
(2)ユーザーのプロフィール画像の作成
そのために各種の設定していきます。

userテーブルにカラムを追加しよう

ターミナル.
$ rails g migration add_name_profile_to_users

テキストエディターでdb/migrateの中を確認すると
その時作った「日時add_name_profile_users」と表示されたファイルが出来ます。

例えばこんな感じ「20200120053617_add_name_profile_to_users.rb」です。

〇〇_add_name_profile_to_users.rb
class AddNameProfileUsers < ActiveRecord::Migration[5.2]
  def change 
    add_column :users, :name, :string #追記
    add_column :users, :profile, :text #追記
  end
end
ターミナル.
$ rails db:migrate

解説(なぜ、 userテーブルに「name」「profile」カラムを?)

※なぜ、 userテーブルに「name」「profile」カラムを追加したか解説します。
結論、deviseの初期状態でのカラムに「name」「profile」カラムがないからです。

userテーブルが持っているカラムをどこで確認するのか?
db/schema.rbで確認
スクリーンショット 2020-01-20 21.54.09.png
これはカラム追加した後ですが、本来は「name」「profile」カラムがない状態です。

rails cで確認
ターミナルでrails cをして
User.column_namesをすると確認できます。

モデル

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

  validates :name, presence: true #追記
  validates :profile, length: { maximum: 200 } #追記
end

nameに空欄は許しませんよ!
profileは200文字に抑えてくさだいね!って制限をかけます。

コントローラー

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

  # ログイン済ユーザーのみにアクセスを許可する
  before_action :authenticate_user!

  # deviseコントローラーにストロングパラメータを追加する          
  before_action :configure_permitted_parameters, if: :devise_controller?

  protected
  def configure_permitted_parameters
    # サインアップ時にnameのストロングパラメータを追加
    devise_parameter_sanitizer.permit(:sign_up, keys: [:name])
    # アカウント編集の時にnameとprofileのストロングパラメータを追加
    devise_parameter_sanitizer.permit(:account_update, keys: [:name, :profile])
  end

end

後はviewで「sign up時にユーザーの名前も一緒に登録」「users/showで「名前」「メールアドレス」「プロフィール」の表示」を表示していきます。

ユーザー情報を記入

app/views/devise/registrations/new.html.erb
<h2>Sign up</h2>

<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
  <%= render "devise/shared/error_messages", resource: resource %>

    <!--サインアップ時に名前を入力できるようにフォームを追加-->
  <div class="field">
    <%= f.label :name %><br />
    <%= f.text_field :name, autofocus: true %>
  </div>

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

  <div class="field">
    <%= f.label :password %>
    <% if @minimum_password_length %>
    <em>(<%= @minimum_password_length %> characters minimum)</em>
    <% end %><br />
    <%= f.password_field :password, autocomplete: "new-password" %>
  </div>

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

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

<%= render "devise/shared/links" %>

ユーザー情報を編集

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 :name %><br />
    <%= f.text_field :name, autofocus: true %>
  </div>

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

  <!--アカウント編集時にプロフィールを入力できるようにフォームを追加-->
  <div class="field">
    <%= f.label :profile %><br />
    <%= f.text_area :profile, autofocus: true %>
  </div>

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

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

プロフィール画面の作成

ここでの目的
・プロフィール画面の作成

手順
①コントローラーとviewファイルの生成と設定
②ルーティング設定
の手順で進めていきます。

コントローラーとviewファイルの生成

ターミナル.
$ rails g controller Users show

deviseとまた別にコントローラーとshow.html.erbを作成します。

app/controllers/users_controller.rb
class UsersController < ApplicationController

  def show
    @user = current_user
  end
end

current_userは現在ログインしているユーザーのことを指します。

app/views/users/show.html.erb
<h1>about me</h1>

<h3>ユーザー名</h3>
<%= @user.name %>
<h3>メールアドレス</h3>
<%= @user.email %>
<h3>プロフィール</h3>
<%= @user.profile%>

ルーティング設定

route.rb
devise_for :users, controllers: { registrations: 'users/registrations' }
get "users/show" => "users#show"

パスワードを入力せずにユーザー情報を編集

編集画面で入力した情報をsubmitすると
Current password can't be blankが出てくると思います。

これはパスワードを入れて情報を更新してください。ってことなので、
パスワードを入れずにユーザー情報を編集できるようにします。

registrations_controller.rbの追記

registrations_controller.rb
class RegistrationsController < Devise::RegistrationsController

  protected
  def update_resource(resource, params)
    resource.update_without_password(params)
  end
end

ユーザー情報を編集する際にパスワード無しで編集可能になりました!

参考URL

[Devise] パスワードを入力せずにユーザー情報を編集する
devise導入からユーザ-のプロフィール画面を作成するまで

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

Dockerを使ってRuby2.7&Rails6を構築

はじめに

2020/01/20時点での最新安定版のrails開発環境の構築の記事がなかったので。

【Imagemagick対応】Dockerを利用して、Rails環境を作成

基本上記の記事のままです。
ただ、上記の記事はRuby2.6&Rails5.2.2で、そのままバージョンを書き換えるだけではうまく動作しなかったので、動作できるようにしたインストール資材を記載しておきます。
参考までに上記記事からの変更点も記載しておきます。

コンテナ立ち上げ以降は参考記事の手順のまま実施できます。

インストール資材

Dockerfile

Dockerfile
FROM ruby:2.7

ENV RUNTIME_PACKAGES="linux-headers libxml2-dev libxslt-dev make gcc libc-dev nodejs tzdata postgresql-dev postgresql" \
    DEV_PACKAGES="build-base curl-dev" \
    HOME="/myapp"

WORKDIR $HOME

# Counter Measure to Error:"Autoprefixer doesn’t support Node v4.8.2. Update it"
RUN curl -sL https://deb.nodesource.com/setup_10.x | bash - \
        && apt-get install -y nodejs

# yarnパッケージ管理ツールインストール
RUN apt-get update && apt-get install -y curl apt-transport-https wget && \
    curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
    echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \
    apt-get update && apt-get install -y yarn

RUN apt-get update && \
    apt-get install -y default-mysql-client \
    postgresql-client \
    sqlite3 \
    --no-install-recommends && \
    rm -rf /var/lib/apt/lists/*

ADD Gemfile      $HOME/Gemfile
ADD Gemfile.lock $HOME/Gemfile.lock

RUN bundle install

ADD ./ $HOME
COPY ./ $HOME

CMD ["rails", "server", "-b", "0.0.0.0"]

変更点①:ruby2.6⇒2.7

変更点②:mysql-client⇒default-mysql-client

mysql-client はインストールできなくなっていたので、代わりにdefault-mysql-clientをインストールします。
mysql-clientがインストールできない理由は以下の記事が参考になります。

Circle CI で mysql-client が apt-get install できなくなってCI環境が壊れた話

変更点③:yarnを追加でインストール

エラーメッセージは控えていませんでしたが、参考記事のままインストールしようとしたらyarnがないと怒られました。そのため、# yarnパッケージ管理ツールインストール のところでインストールしています。

docker-compose.yml

docker-compose.yml
version: '3'
services:
  db:
    container_name: db
    image: postgres:latest
    environment:
      POSTGRES_USER: root
      POSTGRES_PASSWORD: password
      POSTGRES_INITDB_ARGS: "--encoding=UTF-8"
  web:
    container_name: app
    build: .
    command: bundle exec rails s -p 3000 -b '0.0.0.0'
    volumes:
      - .:/myapp
    ports:
      - "3000:3000"
    depends_on:
      - db

変更点①:passwordありユーザーの設定

セキュリティ的にpasswordありユーザーがほしかったので、environmentのところでrootユーザにpasswordを付与しています。

Gemfile

Gemfile
source 'https://rubygems.org'

gem 'rails', '~> 6.0.2', '>= 6.0.2.1'

Imagemagickは使用しないのでGemfileから削除しています。

Gemfile.lock

Gemfile.lock

こちらは空のまま。

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

【Ruby】3文字の時だけ「遊☆戯☆王」みたいに出力されるアルゴリズム

はじめに

最近、遊戯王のゲーム実況をみながら寝落ちするのが日課です。
ある日、Rubyいじってたら、な ぜ か 「遊☆戯☆王」みたいに文字の間に☆入れたいなーって思ったんです。
こんなことをアルゴリズムの記事とか言って書いている自分のアルゴリズム、コレガワカラナイ。

問題のソースコード

keywords = ["遊戯","海馬","木馬","羽蛾","城之内","本田","杏","エクゾディア"]

keywords.each do |keyword|
    sleep 0.2 # 1行だすごとに一旦休憩する(なんとなく)
    if keyword.length == 3 #3文字であればの判定をしている
        k = keyword.chars #1文字ずつ分割する
        puts k.join("☆") #文字を☆でつなぐ(オサレ)

    else
        puts keyword #3文字出ない場合はそのまま表示
    end
end

出力するとこんな感じになります。

遊戯
海馬
木馬
羽蛾
城☆之☆内
本田
杏
エクゾディア

3文字の城之内くんだけが☆入っていますね。

他の文字列でもやってみました

c6ca66de06b258395b100fc7e578dfd5.gif

151匹を出力してみました。
名前が3文字のポケモンだけ☆が入っていますね。
ポ☆ッ☆ポピ☆ッ☆ピはパワーワードに見えますね。

たくさんの文字列精査して☆入れたいときには、CSVに入れるのが便利です。

CSVから文字列を持ってくるときのソース

require "csv" #csvを使用する場合はこのコードを書く

keywords = []

# csvから情報を持ってきて配列に入れる
CSV.foreach("keywords.csv") do |keyword|
    keywords << keyword[0]
end

keywords.each do |keyword|
    sleep 0.2
    if keyword.length == 3
        k = keyword.chars #1文字ずつ分割する
        puts k.join("☆") #文字を☆でつなぐ(オサレ)
    else
        puts keyword
    end
end

※コメントにてスッキリした書き方教えていただきました!

keywords.csvは同階層に置き、そのCSVのA列を上から順に読み込んで、3文字チェックをかけています。
このkeyword[0]の部分でどの列から持ってくるかを判定しています。

/ A B
[0] [1]
1 フシギダネ くさ・どく
2 フシギソウ くさ・どく
3 フシギバナ くさ・どく
4 ヒトカゲ ほのお
5 リザード ほのお
6 リザードン ほのお・ひこう

上記のような感じで、[0]はA列、[1]はB列から情報を持ってきます。
タイプを文字数判定したかったらkeyword[1]として引っ張ります。

参考記事

RubyでCSVファイルの読み込み・書き込みをする

文字数をうまく判定できないときには

CSVファイルに、見えないけれど存在する、封印されし文字列が入っている可能性があります。
そのため、下記の方法でCSVファイルから封印されし文字列を墓地へ送りましょう。

参考記事

UTF-8 のファイルから BOM を削除する方法

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

初級者向け アルゴリズムを解説(1)

初級者向け アルゴリズムを解説

 これからアルゴリズムについて解説をしていきますが、こちらの解説はプログラミング初級者(「最近プログラミングを始めました!!」向けになります。また数回のシリーズに分けてプログラミングで使用されるアルゴリズムを解説していきます。なお、次回からRubyのコードが出てきますが、簡単なメソッドを使用してコーディングする予定です。
※このシリーズは「アルゴリズムを、はじめよう(著:伊藤静香)」を参考にしております。

アルゴリズムとは

 はじめにアルゴリズムについて、簡単に説明します。
 そらく、アルゴリズムと聞くと、プログラミング・難しい..と考える方もいるかと思いますが、アルゴリズムを端的に言えば「手順」です。但し、ただの手順ではなく、「問題や課題を解決するための手順を表現した考え方やアイディア」です。
 実は我々の身の回りにも様々なアルゴリズムが使用されています。例えば、「お菓子のレシピ」、これにもアルゴリズムが使用されており、必要な材料とその量、また調理法がレシピに記載されています。そのレシピ通りに作れば誰でもお菓子が作れるようになっているわけです。つまり、レシピは「ある料理を作りたい」という課題を解決するための手順で有り、これこそがアルゴリズムです。

アルゴリズムの基本形

 アルゴリズムの手順(構造)には3つの基本形があります。
  1. 順次構造
  2. 選択構造
  3. 反復構造

1. 順次構造

 順次構造とは、実行する処理を最初から順番に行っていくことです。
 例えば、友達の家に飲み物を買って行くシーンを思い浮かべてみましょう。
「1.自宅を出る」「2.お店に行く」「3.飲み物を買う」「4.お店を出る」「5.友達の家に行く」の順番に物事が進むはずです。この1〜5までの処理を順番に行うことが順次構造となります。

2. 選択構造

 選択構造とは、その名の通り、処理を選んで実行する手順です。
 例えば、先ほど友達に飲み物を買う際に、その友達が炭酸が好きで、特にコーラが大好きだとしましょう。おそらく、あなたはお店に入って、まずコーラがあれば、コーラを買い、なければ、その他の炭酸飲料を買うと思います。
 この、ある条件下で(今であればコーラがあるかないか)、その答えがYESならその条件判断の処理を行い(今であればコーラを買う)、もしその答えがNOであればその他の処理を行う(今であればその他の炭酸飲料を買う)、これが選択構造となります。

3. 反復構造

 反復構造とは、同じ処理を繰り返す手順になります。
 例えば、先ほど友達に飲み物を買うための予算を500円としてお店に行った際、たまたまそのお店が閉店セールをやっていたことで、予算が300円余ったとしましょう。そこで、あなたは予算がなくなるまで、1つ100円のお菓子を買うことにしました。このときの処理を細かくと次のようになります。
 ・予算が300円 (お菓子が買える)
 ・お菓子を1つ買う。(100円支出する)
 ・予算が200円になる。(まだ買える)
 ・お菓子を1つ買う。(100円支出する)
 ・予算が100円になる。(まだ買える)
 ・お菓子を1つ買う。(100円支出する)
 ・予算が0円になる。(もう買えない)
 ・お店を出る。
 この、予算を確認して、予算がなくなるまでお菓子を買う(同じ処理を繰り返す)ことが反復構造になります。

次回予告

 今回はここで終了です。
 次回は本格的にプログラミングで使用される基本的なアルゴリズムについて説明していきたいと思います。

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

秀丸からRubyを呼び出して、選択範囲を加工する

秀丸からRubyを呼び出して、選択範囲を加工する

利用環境

秀丸エディタ 32bit Ver8.89 (2020/1/20時点最新)
RubyEval for 秀丸マクロ Ver.0.02

概要

筆者は、25年らいの秀丸エディタのユーザーだ。
今は、フリーでも同等機能のエディタがあるらしいし、unix畑の人はViライクをお使いであろうか
さて、上でRubyEval for 秀丸マクロ Ver.0.02は、2005年に公開されたマクロで15年前のものであるが
いまだ現役バリバリに愛用させていただいている。
秀丸エディタ上で選択した文字列を、Rubyのソースコードロジックで変換して返す
いろいろ応用範囲が広い。
是非、秀丸ユーザーには使ってみて欲しい
いくつかサンプルを紹介する。

ソースコード例

重複行を削除して排除、ユニーク行をゲットする 秀丸連携

uniqueGet.rb
#-------------------------------------------------------------------------------
# 重複行を削除して排除、ユニーク行をゲットする 秀丸連携
# uniqueGet.rb
#-------------------------------------------------------------------------------

unless $HIDEMARU_MACRO
#非秀丸テスト時 ヒアドキュメントを利用
from_hide = <<-ENDOFSRC
テスト-----------------M
テスト-----------------M
aaa
bbb
bbb
aaa
ccc
ENDOFSRC
else
      from_hide = $RUBY_EVAL_INSTR
end



#秀丸から受け取った文字列を、改行ごとに、Arrayへ格納
ary = Array.new 
ary = from_hide.split(/\n/)

#重複管理Array
uniqueAry = Array.new

#リターン
out_hide = ""

#重複しない行のみ、out_hideへ格納 (改行も)
ary.each{ |str|
  unless uniqueAry.index(str)
    out_hide = out_hide + str + "\n"
    #重複管理へ格納
    uniqueAry.push(str)
  end
}

unless $HIDEMARU_MACRO
    #非秀丸テスト時 print確認
    print out_hide
else
  #最後に評価した文字を返す
    out_hide
end

上記とは逆、重複行を取得 秀丸連携

duplicationGet.rb
#-------------------------------------------------------------------------------
# 重複行取得 秀丸連携
# duplicationGet.rb
#-------------------------------------------------------------------------------

unless $HIDEMARU_MACRO
#非秀丸テスト時 ヒアドキュメントを利用
from_hide = <<-ENDOFSRC
テスト-----------------M
テスト-----------------M
aaa
bbb
bbb
aaa
ccc
ENDOFSRC
else
      from_hide = $RUBY_EVAL_INSTR
end



#秀丸から受け取った文字列を、改行ごとに、Arrayへ格納
ary = Array.new 
ary = from_hide.split(/\n/)

#重複管理Array
uniqueAry = Array.new

#リターン
out_hide = ""

#重複する行のみ、out_hideへ格納 (改行も)
ary.each{ |str|
  unless uniqueAry.index(str)
    #重複管理へ格納
    uniqueAry.push(str)
  else
    out_hide = out_hide + str + "\n"
  end
}

unless $HIDEMARU_MACRO
    #非秀丸テスト時 print確認
    print out_hide
else
  #最後に評価した文字を返す
    out_hide
end

ロジックで、代入 入れ替え 秀丸連携
a = b → b = a / c = d → d = c

csSubstitutionChange.rb
#-------------------------------------------------------------------------------
# 代入 入れ替え 秀丸連携
# csSubstitutionChange.rb
#-------------------------------------------------------------------------------

unless $HIDEMARU_MACRO
#非秀丸テスト時 ヒアドキュメントを利用
from_hide = <<-ENDOFSRC
                a = b;
                c = d;
ENDOFSRC
else
      from_hide = $RUBY_EVAL_INSTR
end



#秀丸から受け取った文字列を、改行ごとに、Arrayへ格納
ary = Array.new 
ary = from_hide.split(/\n/)

#重複管理Array
uniqueAry = Array.new

#リターン
out_hide = ""

#重複しない行のみ、out_hideへ格納 (改行も)
ary.each{ |str|

  #代入前をマッチさせる。
  str =~ /(\t*)(.*) = (.*);/

  tab = $1
  lefth = $2
  right = $3
    out_hide = out_hide + tab + right + " = " + lefth +";\n"
}

unless $HIDEMARU_MACRO
    #非秀丸テスト時 print確認
    print out_hide
else
  #最後に評価した文字を返す
    out_hide
end

秀丸のマクロでも同等のことは、可能であろうけど
やはり一般的なスクリプト言語で実装していた方が、いろいろ応用範囲は広い。
今は、Pythonでも同等のことができるマクロもあるようなので、また調べて紹介したい。

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

Erubi とは何か

Qiita には Ruby on Rails の記事が 2 万本以上あるのに,Rails で使われているライブラリー Erubi の記事ほぼゼロ。
Erubi とは何なのか。

まず eRuby について

Rails を少しでもかじったことがあれば,eRuby というテンプレート言語を知っていると思う。いや「eRuby」という名称に馴染みがなくても,拡張子が .erb のアレと言えば分かるだろう。テキストに Ruby のコードを埋め込むテンプレート言語の名称だ。

読み方はおそらく「イー・ルビー」で,「e」は embedded(埋め込まれた)の頭文字。

データに基づいて HTML を生成するのによく使われるが,対象となるテキストは HTML に限らない。テキストであれば何でもいい。

実際,Rails では,例えば config/database.yml なんかは eRuby で記述されている。拡張子は単に .yml だが,

  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>

なんて行があるのを見たことがあると思う。環境変数 RAILS_MAX_THREADS の値を取得し(もし定義されてなければ 5 を採用し),それを pool の値にしているわけだ。
こんなふうに YAML テキストを生成するために eRuby テンプレートシステムが使われている。

ところで,この言語名について,「それ,『eRuby』じゃなくて『ERB』じゃないの?」と思った人も少なくないだろう。名称は本質ではないが,あとで触れよう。

Erubi とは

さて,一般にテンプレートを処理するプログラム部品を「テンプレートエンジン」などと言うが,Erubi は eRuby 用テンプレートエンジンの一つだ。

eRuby テンプレートを処理するエンジンには,ERB,Erubis,Erubi などがある。歴史的にはこの順に登場した1。これらは少し仕様が違うが,本記事ではそのことに触れない。

ERB は Ruby の標準添付ライブラリー erb で提供されているテンプレートエンジンだ。

テンプレート言語の名前として「eRuby」の代わりに「ERB」もしくは「erb」を使っているのをよく見かける。本来は特定のテンプレートエンジンのクラス名,ライブラリー名のはずだが,識者も使っているし,間違いとまでは言えないのかなと思っている。

それはさておき,標準添付ライブラリーに eRuby テンプレートエンジンが用意されているのに,新たに Erubis が作られたのは,高速化と高機能化が主な動機だろうと思う。
実際,Erubis は速いし,高機能だ。

Erubis は Rails に採用された。Rails 5.0 までは Erubis(を改造したもの)が Rails の eRuby テンプレートエンジンだった。

Erubi はこの Erubis のフォーク(派生)である。さらなる高速化を図るとともに,機能をそぎ落として簡素化した。動機の一つには,Erubis の開発が止まって久しい,ということもあるようだ2

そして,Rails ではバージョン 5.1 から Erubi(の改造したもの)を採用するようになった。

Erubi を知らなくて済む理由

ふつうの Rails プログラマーは Erubi について知る必要があまり無い。eRuby の仕様(正確に言えば Rails における eRuby の仕様)だけ分かっていればよい。

それは,テンプレートエンジンを動かすところを Rails がすべてやってくれるからだ3

ではなぜこんな記事を書いているかというと,Rails のビュー以外にも eRuby の使い道があるからだ。とにかくどんなテキストでもデータに基づいて生成できるんだからね。

Erubi を使ってみる

Rails などとは無関係に Erubi の機能を使ってみよう。
その際,Tilt という gem を援用すると楽ができるのでそうしよう。

まず最初に,テンプレートが外部ファイルになっている場合の Erubi の使い方を述べ,次にテンプレートが String オブジェクトとして用意されている場合の書き方に触れる。

テンプレートに対しては,ローカル変数 lv,インスタンス変数 @iv,メソッド m を渡してやることにする。

Erubi の使い方

gem として erubi と tilt を使うので,Gemfile を以下のようにしておく。

Gemfile
source "https://rubygems.org"

gem "tilt"
gem "erubi"

テンプレートは外部ファイル sample.txt.erb

sample.txt.erb
iv: <%= @iv %>
lv: <%= lv %>
m: <%= m 3 %>

に置く。ファイル名は必ずしも拡張子を .txt.erb のように二重に付ける必要はなく,単に sample.erb で構わない4

このテンプレートに,インスタンス変数,ローカル変数,メソッドを渡してレンダリングさせ,表示させてみよう。以下のコードになる。

require "bundler"
Bundler.require

t = Tilt::ErubiTemplate.new("sample.txt.erb")

scope = Object.new

scope.instance_variable_set "@iv", 1

def scope.m(x)
  "<#{x}>"
end

puts t.render(scope, lv: 2)

これを実行すると

iv: 1
lv: 2
m: <3>

と表示される。

コードを読み解いていこう。
まずは

Tilt::ErubiTemplate.new("a.txt.erb")

の部分。
今の場合,Erubi のテンプレートを扱うので,Tilt::ErubiTemplate というクラスを使う。
new にファイルのパスを渡してやると,ファイルを読み込んでインスタンスを作ってくれる。

次に,いきなり最終行だが,

t.render(scope, lv: 2)

を見てみよう。

render は文字通りレンダリングのためのメソッド。レンダリング結果の文字列が返る。テンプレートに与えるデータを引数として渡してやる。
第一引数は置いておいて,第二引数 lv: 2 に注目しよう。これはハッシュである(キーワード引数ではない)。
第二引数のハッシュは,ローカル変数の名前と値の組を指定するものだ。
今の場合,テンプレート中で,値 2 を持つ lv というローカル変数が定義されることになる5
ローカル変数は好きなだけ指定できる。

第一引数には任意のオブジェクトを渡すことができる。このオブジェクトはテンプレート中で self となる。
render の第一引数に渡すオブジェクトを「スコープ」と呼ぶらしい(なので変数名も scope としておいた)。

テンプレート中の Ruby コードは,このスコープオブジェクトのコンテキストで評価される。
だから,スコープオブジェクトのメソッドを呼び出すことができるし,スコープオブジェクトのインスタンス変数を読み書きすることができる。

上のコード例では,Object クラスのインスタンスを生成して scope に代入し,

scope.instance_variable_set "@iv", 1

のようにしてインスタンス変数を定義したが,もちろん他の手段でインスタンス変数を定義してもよい。

また,メソッドについては,上のコード例では

def scope.m(x)
  "<#{x}>"
end

のようにして特異メソッドを定義しておいたが,他の手段でメソッドを定義してもよい。
例えばメソッド群を定義したモジュールを用意しておき,スコープオブジェクトに extend するのでもよい。
つまり,

module Helper
  # 云々
end

scope.extend Helper

といった具合である。
HTML エスケープのメソッドなど,テンプレート中でよく使うヘルパーメソッドを定義しておくといい。

スコープが不要なら render メソッドの第一変数には nil でも渡しておけばよいだろう。

ファイルでなく String の場合

eRuby テキストがファイルではなく既に String オブジェクトとして存在している場合はどう書くか。
たとえば

erb_text = <<EOT
iv: <%= @iv %>
lv: <%= lv %>
m: <%= m 3 %>
EOT

のように与えられている場合。
Tilt::ErubiTemplate.new に,引数ではなくブロックで与えればよい。つまり,

Tilt::ErubiTemplate.new{ erb_text }

のように。ただこれだけのこと。
「ファイルパスなら引数で与え,テキストならブロックで与える」というのは Nokogiri の使い方に似ている。

Erubi::Engine を改造して埋め込み記号を変更

さいごに,改造に関する話題を。

こんなことがあった。
rails new が雛形ファイル群を生成するのと同じように,自作の Ruby 製コマンドでファイル群を生成したかった。
生成するファイルは,毎回完全に同じなわけではなく,いくつかのパラメーターによって内容が違っていた。
こういう場合に生成ファイルのテンプレートを eRuby 形式で用意するのは良いやり方だろう。

ただ,ちょっとややこしいのが,生成するファイル群の中に eRuby ファイルがあったこと。
これの何が問題かというと,「eRuby テンプレートを生成する eRuby テンプレート」になるので,Ruby コード埋め込みがややこしくなるのだ。

eRuby には,<%% %><% %> となり,<%%= %><%= %> となるルールがあるので,「テンプレートのテンプレート」も記述できる。
とは言え,<% %><%% %> がごちゃまぜになったテンプレートは見づらい。
よしっ,テンプレートのテンプレートでは <% %><%= %> の代わりに [[ ]][[= ]] を使うことにしよう!
(えっ?)

そんなことが,できるんである。

regexp オプションを与える

Erubi のテンプレートエンジンである Erubi::Engine を生成する際,regexp というオプションを与えることができる。
これは,Erubi がテンプレートテキストから Ruby コード部分を抜き出す正規表現だ。
デフォルトでは

/<%(={1,2}|-|\#|%)?(.*?)([-=])?%>([ \t]*\r?\n)?/m

となっている。
これを

/\[\[(={1,2}|-|\#|%)?(.*?)([-=])?\]\]([ \t]*\r?\n)?/m

に変えてやればいい。

Tilt を使う場合,Erubis::Engine.new をしなくてよいぶん,一工夫が必要になる。
まず,regexp オプションの値を変えた Erubi::Engine のサブクラスを作り,Tilt::ErubiTemplate.new のオプションで,エンジンクラスとして指定するのだ。
以下のようなコードになる。

require "bundler"
Bundler.require

class MyErubiEngine < Erubi::Engine
  def initialize(input, properties={})
    properties[:regexp] = /\[\[(={1,2}|-|\#|%)?(.*?)([-=])?\]\]([ \t]*\r?\n)?/m
    super(input, properties)
  end
end

t = Tilt::ErubiTemplate.new(engine_class: MyErubiEngine){ <<~EOT }
iv: [[= @iv ]]
lv: [[= lv ]]
m: [[= m 3]]
[[% 3/0 ]]
EOT

scope = Object.new

scope.instance_variable_set "@iv", 1

def scope.m(x)
  "<#{x}>"
end

puts t.render(scope, lv: 2)

実行すると

iv: 1
lv: 2
m: <3>
<% 3/0 %>

のように表示される。
コメントの形式で書いた [[% 3/0 ]]<% 3/0 %> になるのは興味深い。


  1. ほかにもいくつかあるが,さしあたりこの三つの名前を知っていればいいと思う。 

  2. とはいえ,私の印象では Erubis の完成度は高い。最新版が古いからといって,それだけでダメとは言えないんじゃないか。 

  3. フレームワークというものの存在意義の一つはそういうところにある。 

  4. .erb 以外の拡張子でも構わない。ただ,そうするとテキストエディターでのコードハイライトが eRuby 用にならないし,(本記事では扱わないが)Tilt にテンプレートエンジンを自動的に選ばせる使い方もできない。 

  5. よく「テンプレートにローカル変数を渡す」という言い方をするが,正確に言えば渡しているのではなく,定義させているのだ。しかし,誤解がないかぎり「渡す」と表現してもよいと思う。 

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

[Ruby] Hash で Value を昇順にする時 Key も昇順にする

どういう並べ替えをする方法なの?

value を昇順に並べ替えた上で、 同じ value を持つ key についても昇順にする方法です。

具体的には、

# これを
{3: 10, 2: 20, 1: 10}

# こうする
{1: 10, 3: 10, 2: 20}

方法です。

方法

仕組みはよく分かりませんが、いろいろ試した所出来たので投稿します。

# いったん Array に変換
array = hash.to_a

# key value を同時に昇順に変換
array.sort_by! {|x| [x[1], x[0]]}

# Hash に戻す
hash = array.to_h

参考:Rubyでの安定したソート| 8番目の光

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

【第10章】Railsチュートリアル 5.1(第4版) ユーザーの更新・表示・削除

はじめに

個人的な理解・備忘録を目的としてます。
筆者自身は動画版Railsチュートリアルで進めているため、アプリ作成中コード・ブランチ名などの若干の違いがありますので参考程度に流し見して頂けたら嬉しいです。
理解不足のため、何かありましたらコメント等ご指摘してくださると幸いです(^_^;)

10.0 目標

未実装だったedit、update、index、destroyアクションを加え、RESTアクションを完成させる。

その他 個人的進行
単数形と複数形
モデル(概念的)→単
それ以外→複数(ほぼ全部)

10.1 ユーザーを更新する

10.1.1 編集フォーム

編集フォームのモックアップ
公式より参考)

スクリーンショット 2020-01-17 13.53.22.png

まずはフィーチャーブランチを作成。

$ git checkout -b updating-users

最初はeditアクションを実装する。

app/controllers/users_controller.rb
  # GET /users/:id/edit
 def edit
    @user = User.find(params[:id])
  #=> app/views/users/edit.html.erb
  end
  end

app/views/users/edit.html.erb

<% provide(:title, "Edit user") %>
<h1>Update your profile</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <!--入力formの送信先を指定-->
    <%= form_for(@user) do |f| %>
    <!--エラーメッセージ-->
      <%= render 'shared/error_messages' %>

       <!--入力formを作成-->
      <%= f.label :name %>
      <%= f.text_field :name, class: 'form-control' %>

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

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

      <%= f.label :password_confirmation, "Confirmation" %>
      <%= f.password_field :password_confirmation, class: 'form-control' %>

      <%= f.submit "Save changes", class: "btn btn-primary" %>
    <% end %>

    <!--編集完了ボタンとユーザー画像を表示-->
    <div class="gravatar_edit">
      <%= gravatar_for @user %>
      <a href="http://gravatar.com/emails" target="_blank">change</a>
    </div>
  </div>
</div>

        
ユーザーのeditビュー画面の表示と、saveを押すとupdateアクションに移行しているか(エラー画面)を確認する。

スクリーンショット 2020-01-17 14.50.26.png

スクリーンショット 2020-01-17 14.54.01.png

Webブラウザは通常GETリクエストとPOSTの2つのリクエストのみのため、PATCHリクエストを送信できないので、RailsはPOSTリクエストと隠しinputフィールドを利用してPATCHリクエストを「偽造」している。

edit.heml.erbnew.html.erbはform_for(@user)...と構造は同じだが、
editにはDBに入っている値、newはDBにない新しいインスタンスが入り、これをRailsのActive Recordにあるnew_record?メソッドが判断する。

Ruby on Rails チュートリアル 第10章 ユーザー更新 beforeフィルター フレンドリーフォワーディング adminまで

最後に、サイト内移動用のヘッダーSettingsにユーザー一覧表示用のリンクを追加する。

app/views/layouts/_header.html.erb
<li><%= link_to "Users", users_path %></li>

<li><%= link_to "Settings", edit_user_path(current_user) %></li>

10.1.2 編集の失敗

ユーザー登録に失敗したときと似た方法で、編集に失敗した場合について扱う。updateアクションを追加して失敗時の処理表示を実装する。

app/controllers/users_controller.rb
# PATCH /users/:id
  def update
    @user = User.find(params[:id])
    if @user.update_attributes(user_params)
      # Success
    else
      # Failure
      #=> @user.errors.full_messages()
      render 'edit'
    end
  end

パスワードなしで更新すると、エラーメッセージが出る。

スクリーンショット 2020-01-17 17.24.08.png

10.1.3 編集失敗時のテスト

統合テストを生成

$ rails generate integration_test users_edit

テスト内容を記載する。流れは下記の通り。

  1. まず編集ページにアクセス
  2. editビュー(テンプレート)が描画されるかどうかをチェック
  3. その後、無効な情報を送信
  4. editビューが再描画されるかどうかをチェック

この特徴として、PATCHリクエストを送るためにpatchメソッドを使っているというものがある。patchメソッドはとは、getやpost、deleteメソッドと同じように、HTTPリクエストを送信するためのメソッド。

test/integration/users_edit_test.rb
require 'test_helper'

class UsersEditTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end

  test "unsuccessful edit" do
    get edit_user_path(@user)
    assert_template 'users/edit'
    patch user_path(@user), params: { user: { name:  "",
                                              email: "foo@invalid",
                                              password:              "foo",
                                              password_confirmation: "bar" } }

    assert_template 'users/edit'
  end
end

テストが通過すればok!

10.1.4 TDDで編集を成功させる

TDD

TDDとはテスト駆動開発(Test-Driven Development: TDD)の名称で、プログラム実装前にテストコードを書き(テストファーストと呼ばれる)、動作する必要最低限な実装をとりあえず行った後にコードを改善していく手法である。

基本スタイルは
1. (RED:失敗する)テストコードを書く
2. テストに通る(GREEN:成功する)最低限のコードを書く
3. コードの重複を除去する(リファクタリング)
を繰り返すもので、アジャイル開発等でよく用いられる。
(※本記事では(公式
の理解を目的とするため、REDは一部省略してリファクタリングに移る場合もあります)

この節では編集フォームが動作するようにする。
今回はアプリケーション用のコードを実装する前に統合テストとして受け入れテスト (Acceptance Tests)を行う。
 受け入れテストとは、ある機能の実装が完了し、受け入れ可能な状態になったかどうかを決める(成功したらどうなるか?の)テストとされている。
先ほどのテストをベースとして、

  1. 今回はユーザー情報を更新する有効な情報を送信する
  2. 次に、flashメッセージが空でないかどうか
  3. プロフィールページにリダイレクトされるかどうか
  4. DBのユーザー情報をインスタンスに上書きする(リロード)
  5. データベース内のユーザー情報が正しく変更されたかどうか
test/integration/users_edit_test.rb
test "successful edit" do
    get edit_user_path(@user)
    assert_template 'users/edit'
    name  = "Foo Bar"
    email = "foo@bar.com"
    patch user_path(@user), params: { user: { name:  name,
                                              email: email,
                                              password:              "",
                                              password_confirmation: "" } }
    assert_not flash.empty?
    assert_redirected_to @user
    @user.reload
    assert_equal name,  @user.name
    assert_equal email, @user.email
  end

もちろん成功部を実装していないためテストしてもRED。

updateアクションif文に成功パターンとして、flashと@userでリダイレクト動作を追加する。

app/controllers/users_controller.rb
def update
    @user = User.find(params[:id])
    if @user.update_attributes(user_params)
      # Success
      flash[:success] = "Profile updated"
      redirect_to @user
    else

先ほどのテストでパスワードが空で渡しているためバリデーションで弾かれるが、例外処理としてallow_nil: trueというオプションをvalidatesに追加してテストを通過 & 更新flashの表示を確認。

app/models/user.rb
validates :password, presence: true, length: { minimum: 6 }, allow_nil: true

image.png

10.2 認可

editアクションとupdateアクションの動作導入はできたが、今のままでは誰でも (ログインしていないユーザーでも) ユーザー情報を編集できてしまうので、ユーザーにログインを要求し、かつ自分以外のユーザー情報を勝手に変更できないように制御する。こういったセキュリティ上の制御機構をセキュリティモデルと呼ぶ。
公式より参考)
(実際のところ、本人でもセッションが切れてしまった場合も含む)
この節では、ログインしていないユーザーが保護された(自分の権限のない)ページにアクセスしようとしたらログインを促すよう対処する。

認証と認可

日本語だと似たような印象になるが、
認証(英:Authentication, AuthN)
 → 何者であるかを特定すること。
ex.「〇〇ですか?」と尋ねられる、職務質問で身分証の提示を求められるなど
Railsでは、*** authenticateメソッド***

認可(英:Authorization, AuthZ)
 → 行動やリソースの使用を許可すること。
ex.「△△の資格がありますね。あのカウンターへどうぞ」と権限を認められる。
Railsでは、beforeメソッド`

<参考>
認証と認可

「認証と認可」について調べたので、違いをざっくり整理した

10.2.1 ユーザーにログインを要求する

beforeフィルター

beforeフィルターとは、before_actionメソッドを使って何らかの処理が実行される直前に特定のメソッドを実行する仕組みのこと。

今回はユーザーにログインを要求するためコントローラーに追加する。

before_actionの後にメソッド名をシンボルでlogged_in_userメソッドを定義、その後に:onlyオプション (ハッシュ) で渡されたeditアクション、updateアクションを入れることで、「only以下のアクション(edit、updateアクション)が実行される前に、最初に定義したメソッド(logged_in_user)を実行してね」という内容になる。

app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :logged_in_user, only: [:edit, :update]

省略

# beforeアクション

    # ログイン済みユーザーかどうか確認
    def logged_in_user
      unless logged_in?
        flash[:danger] = "Please log in."
        redirect_to login_url
      end
    end

end

スクリーンショット 2020-01-17 22.45.55.png

この段階ではテストしててもRED。原因としては、editアクションやupdateアクションでログインを要求するようになったため、ログインしていないユーザーだとこれらのテストが失敗するようになったため。対処としては、editアクションやupdateアクションをテストする前にログインしておくよう、log_in_asヘルパーを実装する。

test/integration/users_edit_test.rb
test "unsuccessful edit" do
    log_in_as(@user) #=> Michaelとしてログイン

省略

 test "successful edit" do
    log_in_as(@user) #=> Michaelとしてログイン

テストはGREEN。
しかし、実はまだbeforeフィルターの実装はまだ終わっていない。セキュリティモデルに関する実装を取り外してもテストが通ってしまうか、beforeフィルターをコメントアウトしてテスト確認。

app/controllers/users_controller.rb
class UsersController < ApplicationController
  # before_action :logged_in_user, only: [:edit, :update]
0 failures, 0 errors, 0 skips

通過してしまった。
beforeフィルターは基本的にアクションごとに適用していくので、Usersコントローラのテストもアクションごとに書いていく。手順としては、
1.routesからedit、updateの正しい種類のHTTPリクエスト確認
2. そのリクエストを使ってeditアクションとupdateアクションをそれぞれ実行
3. flashにメッセージが代入されたかどうか
4. ログイン画面にリダイレクトされたかどうか

$ rails routes
           Prefix Verb   URI Pattern                  Controller#Action
     sessions_new GET    /sessions/new(.:format)      sessions#new
             root GET    /                            static_pages#home
static_pages_home GET    /static_pages/home(.:format) static_pages#home
             help GET    /help(.:format)              static_pages#help
            about GET    /about(.:format)             static_pages#about
          contact GET    /contact(.:format)           static_pages#contact
           signup GET    /signup(.:format)            users#new
                  POST   /signup(.:format)            users#create
            login GET    /login(.:format)             sessions#new
                  POST   /login(.:format)             sessions#create
           logout DELETE /logout(.:format)            sessions#destroy
            users GET    /users(.:format)             users#index
                  POST   /users(.:format)             users#create
         new_user GET    /users/new(.:format)         users#new
        edit_user GET    /users/:id/edit(.:format)    users#edit
             user GET    /users/:id(.:format)         users#show
                  PATCH  /users/:id(.:format)         users#update
                  PUT    /users/:id(.:format)         users#update
                  DELETE /users/:id(.:format)         users#destroy

editとupdateアクションの保護に対するテスト追加。
beforeフィルターが入っているかの確認(ユーザー:Michael追加)。具体的には、
1. ログインしてない状況でgetリクエスト→ユーザーの編集ページに
2. flashが出て
3. ログインにリダイレクトされるか

もう一つはpatchリクエスト(ブラウザ以外からもある)にもneforeの確認を行うもの。

test/controllers/users_controller_test.rb
require 'test_helper'

class UsersControllerTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end
  .
  .
  .
  test "should redirect edit when not logged in" do
    get edit_user_path(@user)
    assert_not flash.empty?
    assert_redirected_to login_url
  end

  test "should redirect update when not logged in" do
    patch user_path(@user), params: { user: { name: @user.name,
                                              email: @user.email } }
    assert_not flash.empty?
    assert_redirected_to login_url
  end
end

テストして

2 failures, 0 errors, 0 skips

エラーでなく失敗したのok(コメントアウト解除)

10.2.2 正しいユーザーを要求する

ユーザーが自分の情報だけを編集できるようにしたい。まずはユーザーの情報が互いに編集できないことを確認するために、ユーザー用のfixtureファイル(YAML)に2人目のユーザー(Archer)を追加する。

test/fixtures/users.yml
archer:
  name: Sterling Archer
  email: duchess@example.gov
  password_digest: <%= User.digest('password') %>

次に、log_in_asメソッドを使ってeditアクションとupdateアクションをテスト。このとき、既にログイン済みのユーザーを対象として(①ArcherさんでログインしてMichaelさん入ろうとする、②Archerさんでログインしてpatchを送ろうとする)、ログインページではなくルートURLにリダイレクトしている点に注意。

test/controllers/users_controller_test.rb
def setup
    @user       = users(:michael)
    @other_user = users(:archer) #=> 他ユーザー追加
  end

テストではエラーになるので、beforeアクションに書き足す。

app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :logged_in_user, only: [:edit, :update]
  before_action :correct_user,   only: [:edit, :update] #順番に注意! 上から順番に「ログインしたユーザー」且つ正しいユーザー

# beforeアクション

    # ログイン済みユーザーかどうか確認
    def logged_in_user
      unless logged_in?
        flash[:danger] = "Please log in."
        redirect_to login_url
      end
    end

    # 正しいユーザーかどうか確認
  def correct_user
    # GET   /users/:id/edit
    # PATCH /users/:id
    @user = User.find(params[:id])
    redirect_to(root_url) unless @user == current_user
  end

テストは通過。

リファクタリングとして、一般的な慣習に倣ってcurrent_user?という論理値を返すメソッドを実装。correct_userの中で使えるようにしたいので、Sessionsヘルパーの中にこのメソッドを追加する。

(旧)unless @user == current_user
(新)unless current_user?(@user)
app/controllers/users_controller.rb
redirect_to(root_url) unless current_user?(@user)  #=> @user == current_user
app/helpers/sessions_helper.rb
# 渡されたユーザーがログイン済みユーザーであればtrueを返す
  def current_user?(user)
    user == current_user
  end

テスト通過。

10.2.3 フレンドリーフォワーディング

フレンドリーフォワーディング

フレンドリーフォワーディングとは、ユーザーがログインした後、ログイン直前に閲覧していたページヘとリダイレクトさせる(あると便利な)機能のこと。

フレンドリーフォワーディングのテストは、ログイン手前でログインページへ(ユーザさんにログインしてもらう)
ログインした後に編集ページへアクセスするという順序を逆にするもの。

test/integration/users_edit_test.rb
 test "successful edit with friendly forwarding" do
    get edit_user_path(@user)
    log_in_as(@user)
    assert_redirected_to edit_user_url(@user)
    name  = "Foo Bar"
    email = "foo@bar.com"
    patch user_path(@user), params: { user: { name:  name,
                                              email: email,
                                              password:              "",
                                              password_confirmation: "" } }
    assert_not flash.empty?
    assert_redirected_to @user
    @user.reload
    assert_equal name,  @user.name
    assert_equal email, @user.email
  end

実装してないのでテストして失敗(failure)。

ユーザーを希望のページに転送するには、リクエスト時点のページをどこかに保存しておき、その場所にリダイレクトさせる必要があり、store_locationredirect_back_orの2つのメソッドを使って対応する。

app/helpers/sessions_helper.rb
# 記憶したURL (もしくはデフォルト値) にリダイレクト
  def redirect_back_or(default)
    redirect_to(session[:forwarding_url] || default)
    session.delete(:forwarding_url)
  end

  # アクセスしようとしたURLを覚えておく
  def store_location
    session[:forwarding_url] = request.original_url if request.get?
  end

redirect_back_orメソッド
転送先のURLを保存する場所は(今回は一時的なものなので)DBでなくsessionを使い、もともとユーザーが行きたかった場所を保存しておいてURLがある場合はリダイレクトし、ない場合(sessionが切れたり分からなくなったら)デフォルト値にユーザーのページを表示する。終わったらsessionを消す。
デフォルトのURLは、sessionコントローラのcreateアクションに追加し、サインイン成功後にリダイレクトします

store_locationメソッド
リクエストが送られたURLをsession変数のforwarding_urlキーに格納。ただし、GETリクエストが送られたときのみ(後置if)。

ログインユーザー用beforeフィルターにstore_locationメソッドを追加する。

app/controllers/users_controller.rb
# ログイン済みユーザーかどうか確認
    def logged_in_user
      unless logged_in?
        store_location  #=> アクセスしようとしたURLを覚えておく
        flash[:danger] = "Please log in."
        redirect_to login_url
      end
    end
app/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
      params[:session][:remember_me] == '1' ? remember(user) : forget(user)
      redirect_back_or user #=>  フレンドリーフォワーディングを備える

これでテストは通過する。
Settingsの確認もok.

image.png

10.3 すべてのユーザーを表示する

この節ではすべての(大量の)ユーザーをページごとに一覧表示、
かつsignupしたユーザーのみが閲覧できるindexアクションを実装する。
それに伴い、①DBにサンプルデータを追加する方法、②将来ユーザー数が膨大になってもindexページを問題なく表示できるようにするためのユーザー出力のページネーション (pagination=ページ分割) の方法、を学ぶ。

モックアップ
公式より参考)

スクリーンショット 2020-01-19 13.31.38.png

10.3.1 ユーザーの一覧ページ

indexページを不正なアクセスから守るために、まずはindexアクションが正しくリダイレクトするか検証するテスト。

test/controllers/users_controller_test.rb
#=> 習慣として、indexに関するテストは一番上に書く
  test "should redirect index when not logged in" do
    get users_path #=> user(s)_pathでindexのurl(/users)へgetリクエスト
    assert_redirected_to login_url
  end

beforeフィルターに何もないため失敗するので、beforeフィルターのlogged_in_userindexアクションを追加して、このアクションを保護する。すべてのユーザーを表示するために、User.allを使ってデータベース上の全ユーザーを取得し、ビューで使えるインスタンス変数@usersに代入。

app/controllers/users_controller.rb
before_action :logged_in_user, only: [:index, :edit, :update] #=> 「:index」追加

  def index
    @users = User.all
  end

ユーザーのindexビュー(app/views/users/index.html.erb)を新規に作成。
userはハッシュを受け取らないので、引数に2つ(gravatar_for userとsize: 50)を与えるとエラーが起こる。

<% provide(:title, 'All users') %>
<h1>All users</h1>

<ul class="users">
  <% @users.each do |user| %>
    <li>
      <%= gravatar_for user, size: 50 %>
      <%= link_to user.name, user %>
    </li>
  <% end %>
</ul>

image.png

app/helpers/users_helper.rb
module UsersHelper
  # 引数で与えられたユーザーのGravatar画像を返す
  def gravatar_for(user, options = { size: 80 }) #=> デフォでsize80追加
    gravatar_id = Digest::MD5::hexdigest(user.email.downcase)
    size = options[:size] #=>変数size ,下記で「?s=#{size}」追加
    gravatar_url = "https://secure.gravatar.com/avatar/#{gravatar_id}?s=#{size}"
    image_tag(gravatar_url, alt: user.name, class: "gravatar")
  end
end

SCSSの追記

app/assets/stylesheets/custom.scss
/* Users index */

.users {
  list-style: none;
  margin: 0;
  li {
    overflow: auto;
    padding: 10px 0;
    border-bottom: 1px solid $gray-lighter;
  }
}

ビュー画面ができたので、ヘッダー(app/views/layouts/_header.html.erb)にユーザー一覧ページへのリンクを更新する。

<% if logged_in? %>
  <li><%= link_to "Users", users_path %></li> 

テストして通過。

10.3.2 サンプルのユーザー

indexページに複数のユーザーを表示させてみる。
まずはGemfileFaker gemを追加する。

Gemfile
gem 'bcrypt',         '3.1.12'
gem 'faker',          '1.7.3' #=> 追加 

データベース上にサンプルユーザーを生成するRailsタスク(サンプルユーザーを生成するRubyスクリプト)を追加。
Railsではdb/seeds.rbというファイルを標準とする。
中身としては、
1. まずユーザー(Example User)を作る
2. Fakerの「.name」メソッドからそれっぽいユーザーを99人増やす

db/seeds.rb
User.create!(name:  "Example User",
             email: "example@railstutorial.org",
             password:              "foobar",
             password_confirmation: "foobar")

99.times do |n|
  name  = Faker::Name.name
  email = "example-#{n+1}@railstutorial.org"
  password = "password"
  User.create!(name:  name,
               email: email,
               password:              password,
               password_confirmation: password)
end

そしてbundle install。だが、筆者の場合失敗

エラー対応:GemfileにFaker gemを追加できない

bundle installしようとするとエラー。
サーバを止めてもダメ。

環境
Rails v5.1.6
faker v1.7.3

$ bundle install
The dependency tzinfo-data (>= 0) will be unused by any of the platforms Bundler is installing for. Bundler is installing for ruby but the dependency is only for x86-mingw32, x86-mswin32, x64-mingw32, java. To add those platforms to the bundle, run `bundle lock --add-platform x86-mingw32 x86-mswin32 x64-mingw32 java`.
Fetching gem metadata from https://rubygems.org/............
Fetching gem metadata from https://rubygems.org/.
Resolving dependencies...
Bundler could not find compatible versions for gem "i18n":
  In snapshot (Gemfile.lock):
    i18n (= 1.7.0)

  In Gemfile:
    rails (= 5.1.6) was resolved to 5.1.6, which depends on
      activesupport (= 5.1.6) was resolved to 5.1.6, which depends on
        i18n (>= 0.7, < 2)

    faker (= 1.7.3) was resolved to 1.7.3, which depends on
      i18n (~> 0.5)

Running `bundle update` will rebuild your snapshot from scratch, using only
the gems in your Gemfile, which may resolve the conflict.

対応策
fakerのバージョンを指定しない

Gemfile
#旧 gem 'faker',          '1.7.3'
gem 'faker' #=> バージョン指定なし

再度bundle install実行。

$ bundle install
The dependency tzinfo-data (>= 0) will be unused by any of the platforms Bundler is installing for. Bundler is installing for ruby but the dependency is only for x86-mingw32, x86-mswin32, x64-mingw32, java. To add those platforms to the bundle, run `bundle lock --add-platform x86-mingw32 x86-mswin32 x64-mingw32 java`.
Fetching gem metadata from https://rubygems.org/............
Fetching gem metadata from https://rubygems.org/.
Resolving dependencies...
Using rake 13.0.1
Using concurrent-ruby 1.1.5
Using i18n 1.7.0
Using minitest 5.10.3
Using thread_safe 0.3.6
Using tzinfo 1.2.5
Using activesupport 5.1.6
Using builder 3.2.3
Using erubi 1.9.0
Using mini_portile2 2.4.0
Using nokogiri 1.10.5
Using rails-dom-testing 2.0.3
Using crass 1.0.5
Using loofah 2.3.1
Using rails-html-sanitizer 1.3.0
Using actionview 5.1.6
Using rack 2.0.7
Using rack-test 1.1.0
Using actionpack 5.1.6
Using nio4r 2.5.2
Using websocket-extensions 0.1.4
Using websocket-driver 0.6.5
Using actioncable 5.1.6
Using globalid 0.4.2
Using activejob 5.1.6
Using mini_mime 1.0.2
Using mail 2.7.1
Using actionmailer 5.1.6
Using activemodel 5.1.6
Using arel 8.0.0
Using activerecord 5.1.6
Using ansi 1.5.0
Using execjs 2.7.0
Using autoprefixer-rails 9.7.2
Using bcrypt 3.1.12
Using bindex 0.8.1
Using rb-fsevent 0.10.3
Using ffi 1.11.2
Using rb-inotify 0.10.0
Using sass-listen 4.0.0
Using sass 3.7.4
Using bootstrap-sass 3.3.7
Using bundler 1.17.3
Using byebug 9.0.6
Using coderay 1.1.2
Using coffee-script-source 1.12.2
Using coffee-script 2.4.1
Using method_source 0.9.2
Using thor 0.20.3
Using railties 5.1.6
Using coffee-rails 4.2.2
Fetching faker 2.10.1
Installing faker 2.10.1
Using formatador 0.2.5
Using ruby_dep 1.5.0
Using listen 3.1.5
Using lumberjack 1.0.13
Using nenv 0.3.0
Using shellany 0.0.1
Using notiffany 0.1.3
Using pry 0.12.2
Using guard 2.13.0
Using guard-compat 1.2.1
Using guard-minitest 2.4.4
Using multi_json 1.14.1
Using jbuilder 2.7.0
Using jquery-rails 4.3.1
Using ruby-progressbar 1.10.1
Using minitest-reporters 1.1.14
Using puma 3.9.1
Using sprockets 3.7.2
Using sprockets-rails 3.2.1
Using rails 5.1.6
Using rails-controller-testing 1.0.2
Using tilt 2.0.10
Using sass-rails 5.0.6
Using spring 2.0.2
Using spring-watcher-listen 2.0.1
Using sqlite3 1.3.13
Using turbolinks-source 5.2.0
Using turbolinks 5.0.1
Using uglifier 3.2.0
Using web-console 3.5.1
Bundle complete! 24 Gemfile dependencies, 82 gems now installed.
Gems in the group production were not installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.

無事終了

(とてもありがたかった)ご参考先
Railsチュートリアルでfakerがインストールできない場合の対処法

本編へ戻ります

  
DBリセット(これまでの登録ユーザー初期化)、Railsタスクを実行 (db:seed) 。

$ rails db:migrate:reset
$ rails db:seed

サンプルですが、たくさんのユーザーさん登場。

スクリーンショット 2020-01-19 15.44.25.png

10.3.3 ページネーション

ユーザーが増えたのはいいが、今度は逆に1つのページに大量のユーザーが表示されて(仮に1万人とかになったときに)重くなってしまう。
そこで解決するのが、ページネーション (pagination) **というもの。
ページネーションとは、検索などに使われてるような
「1つのページに一度に〇〇個だけ表示する」**というもの。
今回は1つのページに一度に30人だけ表示するのに、シンプルとされるwill_paginateメソッドを使う。そのためには、Gemfileにwill_paginate gem とbootstrap-will_paginate gemを両方含め、Bootstrapのページネーションスタイルを使ってwill_paginateを構成する。

Gemfile
gem 'faker'
gem 'will_paginate',           '3.1.6'
gem 'bootstrap-will_paginate', '1.0.0'
$ bundle install

新たにpaginateメソッドを追加したため、念のためここでサーバーの再起動を行っておく。
indexページ(app/views/users/index.html.erb)でpaginationを使う

<% provide(:title, 'All users') %>
<h1>All users</h1>

<%= will_paginate %>

<ul class="users">
  <% @users.each do |user| %>
    <li>
      <%= gravatar_for user, size: 50 %>
      <%= link_to user.name, user %>
    </li>
  <% end %>
</ul>

<%= will_paginate %>

will_paginateメソッドは、usersビューのコードの中から@usersオブジェクトを自動的に見つけ出し、それから他のページにアクセスするためのページネーションリンクを作成してくれる。ただし、現在の@users変数にはUser.allの結果が含まれているが 、will_paginateではpaginateメソッドを使った結果が必要となる。

必要となるデータの例は次のとおり
paginateでは、キーが:pageで値がページ番号のハッシュを引数に1を渡すと1~30までのユーザーまで出る
ちなみにpageがnilの場合、 paginateは単に最初のページを返す。

$ rails console
> User.paginate(page: 1)
  User Load (1.0ms)  SELECT  "users".* FROM "users" LIMIT ? OFFSET ?  [["LIMIT", 11], ["OFFSET", 0]]
   (0.1ms)  SELECT COUNT(*) FROM "users"
 => #<ActiveRecord::Relation [#<User id: 1, name: "Example User", email: "example@railstutorial.org", created_at: "2020-01-19 06:42:48", updated_at: "2020-01-19 06:42:48", password_digest: "$2a$10$xDXvcjV4nyrflH.nVpxu2uWGCeBYR5quXeo1ERVKIUE...", remember_digest: nil>, #<User id: 2,... 
省略

paginateを使うことで、このアプリでユーザーのページネーションを行えるようになる。
具体的には、indexアクション内のallをpaginateメソッドに置き換えて、indexアクションでUsersをページネートする

app/controllers/users_controller.rb
  def index
    #旧 @users = User.all
    @users = User.paginate(page: params[:page])
  end

image.png

現在の位置(ページネーションの番号)と下のデータが一致。

image.png

10.3.4 ユーザー一覧のテスト

ユーザーの一覧ページが動くようになったので、ページネーションに対するテストを行う。

今回のテストでは、
1. ログイン
2. indexページにアクセス
3. 最初のページにユーザーがいることを確認
4. ページネーションのリンクがあることを確認
の順でテストを行う。

まずはfixtureにさらに30人のユーザーを追加する。
今後必要になるので、2人の名前付きユーザーも一緒に追加。

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

archer:
  name: Sterling Archer
  email: duchess@example.gov
  password_digest: <%= User.digest('password') %>

lana:
  name: Lana Kane
  email: hands@example.gov
  password_digest: <%= User.digest('password') %>

malory:
  name: Malory Archer
  email: boss@example.gov
  password_digest: <%= User.digest('password') %>

<% 30.times do |n| %>
user_<%= n %>:
  name:  <%= "User #{n}" %>
  email: <%= "user-#{n}@example.com" %>
  password_digest: <%= User.digest('password') %>
<% end %>

統合テストを生成。

$ rails generate integration_test users_index
Running via Spring preloader in process 12447
      invoke  test_unit
      create    test/integration/users_index_test.rb test/integration/users_index_test.rb

ページネーションを含めたUsersIndexのテスト内容を記述。
具体的には、
1. Michael(何かのユーザー)でログイン
2. ユーザーのindexページへ移動(テンプレート)
3. ページネーションクラスがあるか
4. ユーザーの名前(変数user)をクリックするとそのprofileページに行くか

test/integration/users_index_test.rb
require 'test_helper'

class UsersIndexTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end

  test "index including pagination" do
    log_in_as(@user)
    get users_path
    assert_template 'users/index'
    assert_select 'div.pagination'
    User.paginate(page: 1).each do |user|
      assert_select 'a[href=?]', user_path(user), text: user.name
    end
  end
end

テストは通過。
  

10.3.5 パーシャルのリファクタリング

いくつかリファクタリングを行う。

リファクタリングの第一歩は、ユーザーのliをrender呼び出しに置き換える。(app/views/users/index.html.erb)
renderをパーシャル (ファイル名の文字列) に対してではなく、Userクラスのuser変数に対して実行している。これは、renderにモデルのインスタンスオブジェクトを渡したときのデフォルトの挙動。この場合、Railsは自動的に_user.html.erbという名前のパーシャルを探しにいくので、各ユーザーを表示するパーシャルを作成する。

<ul class="users">
  <% @users.each do |user| %>
     <%= render user %> 
     <!-- => app/views/リソース名/_モデル名.html.erb-->
     <!-- => app/views/users/_user.html.erb-->
  <% end %>
</ul>

各ユーザーを表示するパーシャル
app/views/users/_user.html.erb

<li>
  <%= gravatar_for user, size: 50 %>
  <%= link_to user.name, user %>
</li>

今度はrenderを@users変数にして、最終的に下記に。

<ul class="users">
  <%= render @users %>
</ul>

Railsは@usersをUserオブジェクトのリストであると推測する。さらに、ユーザーのコレクションを与えて呼び出すと、Railsは自動的にユーザーのコレクションを列挙し、それぞれのユーザーを_user.html.erbパーシャルで出力するので、each文がなくなりコードは短くなった。

一応テストして通過。

10.4 ユーザーを削除する

destroyの実装。この節では、ユーザーを削除するためのリンクを追加する。もちろん、ユーザーを削除(delete)できるのは管理権限を持ったユーザーのみ。

モックアップは以下の形式。(公式より参考)

image.png

ただしその前に、削除を実行できる権限を持つ管理 (admin) ユーザーのクラスを作成する。

10.4.1 管理ユーザー

特権を持つ管理ユーザーを識別するために、論理値をとるadmin属性をUserモデルに追加する。
こうすると自動的にadmin?メソッド (論理値booleanを返す) も使えるようになるため、これを使って管理ユーザーの状態をテストする。
変更後のデータモデルは以下(公式より参考)

スクリーンショット 2020-01-20 18.02.29.png

まずはマイグレーションを実行してadmin属性を追加(属性の型をbooleanに指定)

$ rails generate migration add_admin_to_users admin:boolean
Running via Spring preloader in process 6078
      invoke  active_record
      create    db/migrate/20200120090448_add_admin_to_users.rb

マイグレーションを実行するとadminカラムがusersテーブルに追加される。デフォルトでは管理者になれないことを示す+nilが入るケースを防ぐため、default: false引数を与える。

db/migrate/[timestamp]_add_admin_to_users.rb
class AddAdminToUsers < ActiveRecord::Migration[5.1]
  def change
    add_column :users, :admin, :boolean, default: false
  end
end

マイグレーションを実行。

$ rails db:migrate

コンソール(sandbox)で動作を確認すると、期待どおりadmin属性が追加されて論理値をとり、さらに疑問符の付いたadmin?メソッドも利用できるようになっている。

$ rails console --sandbox
> user = User.first
> user.admin?
 => false 
> user.toggle!(:admin)
=> true 
> user.admin?
 => true 

ここではtoggle!メソッドを使って admin属性の状態をfalseからtrueに反転している。
toggle!メソッドの「!」は破壊的メソッドで、「書き換えたらもう元には戻らない」ことを示している。

演習用として、最初のユーザーだけをデフォルトで管理者にするよう(admin→true)、サンプルデータを更新しておく。

db/seeds.rb
User.create!(name:  "Example User",
             email: "example@railstutorial.org",
             password:              "foobar",
             password_confirmation: "foobar",
             admin: true)

99.times do |n|
  name  = Faker::Name.name
  email = "example-#{n+1}@railstutorial.org"
  password = "password"
  User.create!(name:  name,
               email: email,
               password:              password,
               password_confirmation: password)
end

データベースをリセットして、サンプルデータを再度生成。

$ rails db:migrate:reset
$ rails db:seed

10.4.2 destroyアクション

まず、destroyアクションへのリンクを追加する。ユーザーindexページの各ユーザーに削除用のリンクを追加+管理ユーザーへのアクセスを制限が目標。

ユーザー削除用リンクの実装 (管理者にのみ表示される)
(app/views/users/user.html.erb)
※admin権限を持っていても、自分自身は消せないように && !current
user? で確認を取っている。

<li>
  <%= gravatar_for user, size: 50 %>
  <%= link_to user.name, user %>
  <% if current_user.admin? && !current_user?(user) %>
    | <%= link_to "delete", user, method: :delete,
                                  data: { confirm: "You sure?" } %>
  <% end %>
</li>

実際にユーザExample Userでログインしてみると、アクションまで(エラー画面で)確認できる。

スクリーンショット 2020-01-20 18.46.33.png

スクリーンショット 2020-01-20 18.46.58.png

実際に動作するdestroyアクションを追加する。このアクションでは、該当するユーザーを見つけてActive Recordのdestroyメソッドを使って削除し、最後にユーザーのindexページにリダイレクトさせる。ユーザーを削除するためにはログインしていなくてはならないため、destroyアクションもlogged_in_userフィルター(before_action)に追加している。

ただしこれでは、コマンドラインでDELETEリクエストを直接発行するという方法でサイトの全ユーザーを削除される可能性があるため、destroyアクションにもadmin_userフィルターを入れてアクセス制御を実装する。

app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :logged_in_user, only: [:index, :edit, :update, :destroy]
  before_action :correct_user,   only: [:edit, :update]
  before_action :admin_user,     only: [:destroy]

省略

  # DELETE /users/:id
  def destroy
    User.find(params[:id]).destroy
    flash[:success] = "User deleted"
    redirect_to users_url
  end

private

# 管理者かどうか確認
    def admin_user
      redirect_to(root_url) unless current_user.admin?
    end
end

10.4.3 ユーザー削除のテスト

fixtureファイルの一番上(Michael)を管理者にする。

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

管理者権限の制御をアクションレベルでテストする。
「ユーザーがログインしてないときにDELETEリクエスト送ったらだめ」
「ログインしていたとしても、adminじゃなかったらやはりだめ」
という内容。

test/controllers/users_controller_test.rb
test "should redirect destroy when not logged in" do
    assert_no_difference 'User.count' do
      delete user_path(@user)
    end
    assert_redirected_to login_url
  end

  test "should redirect destroy when logged in as a non-admin" do
    log_in_as(@other_user)
    assert_no_difference 'User.count' do
      delete user_path(@user)
    end
    assert_redirected_to root_url
  end

テストは通過。

最後に、削除リンクとユーザー削除に対する統合テストとして「ユーザーを削除したらユーザーの総数が1つ消えてるよ?」というテストを付け加える(先のテストを大幅に改造)。
上のテストは、
1. サンプルとしてMichaelさん(admin)、Archerさん(non_admin)のユーザーデータを持ってくる
2. ログイン(ユーザーパスが見えるはず)
3. ページネーション見える
4. ユーザーがadminかどうかチェック(adminならdeleteが見えるはず)
5. 選択すればArcherさん(non_admin)は消えるはず

下のテストは,
1. non_adminとしてログイン
2. deleteリンクは見えないはずなのでcountは0か?

test/integration/users_index_test.rb
require 'test_helper'

class UsersIndexTest < ActionDispatch::IntegrationTest

  def setup
    @admin     = users(:michael)
    @non_admin = users(:archer)
  end

  test "index as admin including pagination and delete links" do
    log_in_as(@admin)
    get users_path
    assert_template 'users/index'
    assert_select 'div.pagination'
    first_page_of_users = User.paginate(page: 1)
    first_page_of_users.each do |user|
      assert_select 'a[href=?]', user_path(user), text: user.name
      unless user == @admin
        assert_select 'a[href=?]', user_path(user), text: 'delete'
      end
    end
    assert_difference 'User.count', -1 do
      delete user_path(@non_admin)
    end
  end

  test "index as non-admin" do
    log_in_as(@non_admin)
    get users_path
    assert_select 'a', text: 'delete', count: 0
  end

end

テストは通過。

最後にherokuへデプロイ。

$ git add -A
$ git commit -m "Finish ch10"
$ git checkout master
$ git merge updating-users
$ git push heroku master

本番環境として
・ DBリセットは危険なので本来あまりやらない
・ 本番環境にrun rails db:seedで擬似データを送る。これもあまりやらない
・ リモートのリンクのfetchをクリック

$ heroku pg:reset DATABASE
     WARNING: Destructive action

     To proceed, type sample-app or re-run this command with --confirm sample-app

> sample-app
$ heroku run rails db:migrate
$ heroku run rails db:seed
$ git remote -v
※リンク確認

スクリーンショット 2020-01-20 19.50.58.png

本番環境でログインしてユーザー削除の確認ができたので終了!

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

【Rails】Pryを使ったメソッドのデバッグ方法

Rails案件で「pry」を使って実装したメソッドをデバッグする機会があったので、使い方の備忘録

pryとは

REPLで対話的にプログラムを実行したり、ActiveRecordを利用したDB操作や、変数の中身を調べることができるコマンドラインツール

※ Laravelで言うところの「tinker」とほぼ同じような使い方ができる、超便利なデバッグツール

サンプル

サンプルとして、以下のメソッドを例に解説

app/lib/utils/test_sample.rb
module Utils
  module TestSample
    class << self
      def sample_method(param)
        data = [1, 2, 3, 4, 5]

        data.each do |r|
          if r == param
            return true
          end
        end

        return false
      end
    end
  end
end

メソッドのデバッグ方法

bundle exec rails cで、コンソールを立ち上げる

$ bundle exec rails c
Loading development environment (Rails 6.0.0)
[1] pry(main)> 

ソースコードの「ブレークポイントを貼りたい箇所」にbinding.pryを差し込む

app/lib/utils/test_sample.rb
module Utils
  module TestSample
    class << self
      def sample_method(param)
        data = [1, 2, 3, 4, 5]        

        data.each do |r|
+         binding.pry
          if r == param
            return true
          end
        end

        return false
      end
    end
  end
end

※ 変更を加えた際の反映は、pryのコンソールにreload!を入力してリロードすること

コンソールから、デバッグしたいメソッド指定して実行する

[1] pry(main)> reload!
Reloading...
=> true
[2] pry(main)> Utils::TestSample.sample_method 3

From: /Users/nakano_shingo/Documents/minnshu-gms/app/lib/utils/test_sample.rb @ line 8 Utils::TestSample.sample_method:

     4: def sample_method(param)
     5:   data = [1, 2, 3, 4, 5]
     6: 
     7:   data.each do |r|
 =>  8:     binding.pry
     9:     if r == param
    10:       return true
    11:     end
    12:   end
    13: 
    14:   return false
    15: end

※ 引数を指定したい場合は、上記のようにハイフンの後に引数を渡す

その時点の変数の値を確認したい場合は、コンソールに変数名を入力する

[1] pry(Utils::TestSample)> param
=> 3
[2] pry(Utils::TestSample)> r
=> 1

pryのコマンドは、以下のようにコンソールからhelpを入力すると使い方を教えてくれる

pry> help <コマンド>

よく使うコマンド

コマンド 説明
next 次の行を実行
step 次の行かメソッド内に入る
continue プログラムの実行をcontinueしてpryを終了
finish 現在のフレームが終わるまで実行
exit/quit 実行中のステップを終了し、次のステップ(binding.pry)まで処理を実行
!!! pryのコンソールを抜ける

※ 参考: Pryコマンドまとめ - Qiita

「next」や「step」などのコマンドが使えない場合は、こちら
を参考にpry-navのgemを追加で導入すればOK


スクリプト言語でも、デバッグ時に手軽にステップ実行ができるのはとても便利です

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

Redis のキューに定期的に複数プロセスから重複なしで値を追加

resque のキューに定期的にタイムスタンプを追加して、cron 的に使いたかったのだが、キューに追加する処理を 1 つのプロセスでやるのは可用性が低いので、(複数のサーバに分散した) 複数のプロセスから重複なしで値を追加したかった。

下記の方法でうまくいきそう。

  1. 最後に値を追加したときのタイムスタンプを保持しておき、追加前にチェックする
  2. 1 だけだとほぼ同時に実行される場合に重複する可能性があるので Redis のトランザクション機構を利用する

下記は 1 秒ごとにタイムスタンプをキューに追加する例。

# cron.rb
require "redis"
require "json"

QUEUE_NAME = "per_sec" # キューの名前
TIMESTAMP_NAME = "per_sec_timestamp" # 最後にキューに値を追加したときのタイムスタンプ

redis = Redis.new

loop do
  t = Time.now.to_i

  redis.watch(TIMESTAMP_NAME) # TIMESTAMP_NAME の値が exec までに変わっていれば multi - exec 間のコマンドを失敗させる
  last = redis.get(TIMESTAMP_NAME).to_i

  if last < t
    redis.multi
    redis.rpush(QUEUE_NAME, t)
    redis.set(TIMESTAMP_NAME, t)
    redis.exec ? puts("set #{t}") : puts("transaction fail")
  end
  sleep 0.1
end

上記のスクリプトを 2 プロセス立ち上げて、while true; do redis-cli lpop per_sec; sleep 0.1s; done でキューの内容を pop しつづけると下記のようになり、重複なしでキューに追加できていることがわかる。

redis.gif

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

深くネストしたルーティングにはshallowを使う

目的

  • Qiitaの記事に大まかな流れをテンプレート化しておき、作業効率を上げる。
  • 今後、railsを学ぶ方に向けての参考に役立てる。

前提条件

  • 実行環境
    • Ruby 2.5.1
    • Rails 5.2.4.1
    • MySQL 5.7.7
(変更前)_config/routes.rb
Rails.application.routes.draw do
  resources :projects do
    resources :queries, only: %i[new create] do
      resources :issue_categories
    end
  end
end

👇 shallowを適用したコード

(変更後)_config/routes.rb
Rails.application.routes.draw do
  resources :projects do
    resources :queries, only: %i[new create], shallow: true do
      resources :issue_categories
    end
  end
end

------------------ # ↓ 上と同じ

Rails.application.routes.draw do
  resources :projects do
    shallow do
      resources :queries, only: %i[new create], shallow: true do
        resources :issue_categories
      end
    end
  end
end

👇※赤文字部分が省略される

Helper HTTP Verb Path Controller#Action
1 変更前 project_query_issue_categories_path GET /projects/:project_id/queries/:query_id/issue_categories(.:format) issue_categories#index
変更後 query_issue_categories_path /queries/:query_id/issue_categories(.:format)
2 変更前 project_query_issue_categories_path POST /projects/:project_id/queries/:query_id/issue_categories(.:format) issue_categories#create
変更後 query_issue_categories_path /queries/:query_id/issue_categories(.:format)
3 変更前 new_project_query_issue_category_path GET /projects/:project_id/queries/:query_id/issue_categories/new(.:format) issue_categories#new
変更後 new_query_issue_category_path /queries/:query_id/issue_categories/new(.:format)
4 変更前 edit_project_query_issue_category_path GET /projects/:project_id/queries/:query_id/issue_categories/:id/edit(.:format) issue_categories#edit
変更後 edit_issue_category_path /issue_categories/:id/edit(.:format)
5 変更前 project_query_issue_category_path GET /projects/:project_id/queries/:query_id/issue_categories/:id(.:format) issue_categories#show
変更後 issue_category_path /issue_categories/:id(.:format)
6 変更前 project_query_issue_category_path PATCH /projects/:project_id/queries/:query_id/issue_categories/:id(.:format) issue_categories#update
変更後 issue_category_path /issue_categories/:id(.:format)
7 変更前 project_query_issue_category_path PUT /projects/:project_id/queries/:query_id/issue_categories/:id(.:format) issue_categories#update
変更後 issue_category_path /issue_categories/:id(.:format)
8 変更前 project_query_issue_category_path DELETE /projects/:project_id/queries/:query_id/issue_categories/:id(.:format) issue_categories#destroy
変更後 issue_category_path /issue_categories/:id(.:format)

関連URL

Rails のルーティング

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

Rubyのencodeとforce_encodingの違い

はじめに

Rubyのencodeメソッドとforce_encodingメソッドの違いをご存じでしょうか?
まずはRubyのリファレンスマニュアルを見てみましょう。

encode

self を指定したエンコーディングに変換した文字列を作成して返します。(以下、省略)

force_encoding

文字列の持つエンコーディング情報を指定された encoding に変えます。
このとき実際のエンコーディングは変換されず、検査もされません。

encodeはその名の通り文字列を変換してくれて、force_encodingはエンコーディング情報(?)を変えるけど、文字列自体は変換されないと。エンコーディング情報が何かは分かりませんが、とりあえず動かしてみましょう。

動作確認

検証用に「テスト」という文字列をiso-2022-jpという文字符号化方式に変換したものを用意します。メールでUTF-8を期待していたところに別の文字符号化方式の文字列が送られてきたイメージです。

puts "テスト".encode('iso-2022-jp')

出力結果

$B%F%9%H(B

以降の内容を試す場合は下記のリンク先からエンコードされた文字列をコピーしてご使用ください。
https://paiza.io/projects/jCRDa1-PFB06JTcOZTGJ-g

この文字列をUTF-8の元の文字列に戻してみましょう。

まずはencodeを試してみます。

puts "$B%F%9%H(B".encode('utf-8')

出力結果

$B%F%9%H(B

あれ、何も変わらない・・・。

次にforce_encoding

puts "$B%F%9%H(B".force_encoding('utf-8')

出力結果

$B%F%9%H(B

こちらも変わらず・・・。

この文字列を元に戻すにはどうすれば良いのでしょうか?
以下に、前提となる知識と変換方法をご紹介します。

エンコーディング情報

force_encodingの説明の中にもありましたが、文字列はエンコーディング情報を持ちます。
文字列のエンコーディング情報を確認するにはencodingメソッドで確認することができます。

puts "テスト".encoding

出力結果

UTF-8

encodingの返り値として、Encodingオブジェクトを返します。Encodingクラスは文字エンコーディング(文字符号化方式)のクラスです。encodeforce_encodingの実行後に確認してみると違いが分かりそうですね。

文字列とバイト

たとえば「あ」という文字列はバイトで表すと3つの数字が返ってきます。

puts "#{"あ".bytes}"

出力結果

[227, 129, 130]

それぞれの数字を16進数に変換すると[e3 81 82]となり、この数字はUTF-8の「あ」と一致します。(参考)
このように文字はそれぞれバイト列が決まっているので、バイト列の数値が変化していれば文字が変わったと判断できそうですね。

対象文字列の確認

さて、これらを踏まえた上で改めて先程の文字列を見てみましょう。

str = "$B%F%9%H(B"
puts "文字列:#{str}"
puts "バイト列:#{str.bytes}"
puts "エンコーディング情報:#{str.encoding}"

出力結果

文字列:$B%F%9%H(B
バイト列:[36, 66, 37, 70, 37, 57, 37, 72, 40, 66]
エンコーディング情報:UTF-8

エンコーディング情報を見るとUTF-8となっているので、encodeUTF-8に変換しようとしても何も変わりません。また、force_encodingを実行しても、元々のエンコーディング情報がUTF-8のためこちらも何も変わりません。これによって最初の動作確認で何も変化しなかった理由が分かりました。では、どうすれば変換できるのでしょうか?

変換方法

以下の2ステップで変換します。
1. force_encodingでエンコーディング情報をiso-2022-jpに変換する
2. encodeで文字列をUTF-8に変換する

str = "$B%F%9%H(B"
puts "オリジナル"
puts str
puts str.encoding
puts "#{str.bytes}"

puts "force_encoding"
force_encode_str = str.force_encoding('iso-2022-jp')
puts force_encode_str
puts force_encode_str.encoding
puts "#{force_encode_str.bytes}"

puts "encode"
encode_str = force_encode_str.encode('utf-8')
puts encode_str
puts encode_str.encoding
puts "#{encode_str.bytes}"

実行結果

オリジナル
$B%F%9%H(B
UTF-8
[27, 36, 66, 37, 70, 37, 57, 37, 72, 27, 40, 66]

force_encoding
$B%F%9%H(B
ISO-2022-JP
[27, 36, 66, 37, 70, 37, 57, 37, 72, 27, 40, 66]

encode
テスト
UTF-8
[227, 131, 134, 227, 130, 185, 227, 131, 136]

force_encodingで文字列に期待するコーディング情報に変換し、その後、encodeで期待する文字符号化方式(今回であればUTF-8)で文字列を変換しました。文字列はもちろんのこと、バイト列も変換されているのが確認できますね。

まとめ

encode

  • 文字列を指定したエンコーディングに変換
  • 文字列がもつエンコーディング情報についても、指定したエンコーディングに変換
  • エンコーディング情報と指定したエンコーディングが同一の場合、変化しない

force_encoding

  • 文字列自体は変化しない
  • 文字列がもつエンコーディング情報を指定したエンコーディングに変換
  • encodeの前処理として使いそう

参考

encoding - What is the difference between #encode and #force_encoding in ruby? - Stack Overflow

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

[Ruby]メールアドレスのドメイン数を調べる

メールアドレスのドメインとそのドメインの総数を配列として多い順で表示する方法です。

コード
emails = []
User.all.each{|u| emails << /@/.match(u.email).post_match}
emails.group_by(&:itself).map{|key, value| [key, value.count]}.to_h.sort{|(k1, v1), (k2, v2)| v2 <=> v1 }
結果
emails
=> [["gmail.com", 1234], ["yahoo.com", 1024], ["vodafone-jp.com", 761], ["hotmail.com", 373],,,,,,]
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails×Ajax】いいね機能の実装で上手く出来ないあなたへの2つの注意喚起 #学習者向け

目的

はじめまして。
今回、Railsでいいね機能をQiita等の記事通りに行っても上手く行かない!という方へ向けた、ちょっとした実装の際のチェック項目を列挙させていただきます。

前提条件

対象となる読者

  • 「いいね機能」の実装において、railsの同期処理では問題なく処理されるが、Ajax通信が上手く行かない

開発環境

  • ruby 2.5.1
  • Rails 5.2.4.1
  • mysql Ver 14.14

実装済み機能

  • 同期処理で「いいね機能」が正しく処理されていること

筆者が参考にした記事

Ajax処理のいいね機能の実装方法のチェック項目

ずばり、先に結論をここで提示させていただきます

1. 部分テンプレートの呼び出しが正しく相対パスで指定されているか

2. インスタンス変数の指定がfavorites_controller.rbで指定されているか

の2点です。では、詳しく見ていきましょう。

1. 部分テンプレートの呼び出しが正しく相対パスで指定されているか

よく陥りがちなミスの一つですね。実際にどのように間違えて実装しどの様なエラー文が出たのでしょうか?

view/items/show.html.haml
.btn-bar
  .btn-box
    = render partial: "favorite_ajax", locals: { item: @item }
-# view/items/_favorite_ajax.html.hamlでいいねボタンを部分テンプレートを作成した

view/items/_favorite_ajax.html.haml
- if user_signed_in? -# ユーザーがログインしているか判断
  - if item.favorited_by?(current_user) -# ログイン中のユーザーがいいねしているかしていないかを判断
    = link_to item_favorites_path(item.id), method: :delete, class: "favorite red", remote: true do -# リクエストをjs形式で送信
      = icon('fas', 'heart')
      いいね!
      = item.favorites.count
  - else
    = link_to item_favorites_path(item.id), method: :post, class: "favorite", remote: true do -# リクエストをjs形式で送信
      = icon('far', 'heart')
      いいね!
      = item.favorites.count
- else
  = link_to new_user_session_path, class: "favorite", remote: false do -# リクエストをjs形式で送信
    = icon('far', 'heart')
    いいね!
    = item.favorites.count
view/favorites/destroy.js.haml(失敗例)
$('.btn-box').html("#{escape_javascript(render partial: "favorite_ajax", locals: { item: @item })}");
-# この記述ではview/favorites/_favorite_ajax.html.hamlを呼び出していることとなる。従って、対応するファイルが無いことからTemplate::Error(Missing partial)が発生
view/favorites/destroy.js.haml(失敗例)
$('.btn-box').html("#{escape_javascript(render partial: "favorite_ajax", locals: { item: @item })}");
-# この記述ではview/favorites/_favorite_ajax.html.hamlを呼び出していることとなる。従って、対応するファイルが無いことからTemplate::Error(Missing partial)が発生

エラー文
スクリーンショット 2020-01-20 10.40.39.png

items_controller.rbのshowアクションのビューでいいね機能の実装をしています。また、いいね機能のDBへの保存・削除はfavorites_controller.rbのcreateアクション・destroyアクションで実装をしています。

今回、いいねボタンを押した際にビューが切り替わる部分をview/items/_favorite_ajax.html.hamlで切り出し部分テンプレートを作成しました。ajaxではview/favorites/destroy.js.haml view/favorites/destroy.js.hamlをそれぞれ用意し、view/items/_favorite_ajax.html.hamlを呼び出したかったのですが、相対パスの指定が誤っていました。以下のように修正するとTemplate::Error(Missing partial)は解消されます。

view/favorites/destroy.js.haml
$('.btn-box').html("#{escape_javascript(render partial: "items/favorite_ajax", locals: { item: @item })}");
-# partial: にitems/ を追加
view/favorites/destroy.js.haml
$('.btn-box').html("#{escape_javascript(render partial: "items/favorite_ajax", locals: { item: @item })}");
-# partial: にitems/ を追加

2. インスタンス変数の指定がfavorites_controller.rbで指定されているか

こちらはまず、どんなエラー文が出たか確認して見ましょう
スクリーンショット 2020-01-20 11.21.25.png
renderの中身のitem.favorited_by?に対して

undefined method `favorited_by?' for nil:NilClass

とエラーが出ています。ここでいうitemとはitems_controller.rbのshowアクションで定義されているインスタンス変数@itemをrenderの中身ではitemとして記述している、という意味です。favorited_by?については、item.rbで事前に定義した「ログイン中のユーザーがいいねしているかしていないかを判断」するメソッドです。

models/item.rb
class Item < ApplicationRecord
# (中略)
  def favorited_by?(user)
    favorites.where(user_id: user.id).exists?
  end
end

このことから、
 render内ではitem.favorited_by?が定義されていない
→ render内ではitemそのものが定義されていない
view/favorites/destroy.js.hamlでは、@itemが定義されていない
favorites_controller.rbでは、@itemが定義されていない!!

ということが判明しました。確認してみると確かにfavorites_controller.rbでは、@itemが定義されていなかったので、以下のように記述を加えたところ、正しくAjax処理が実行されました。
(items_controller.rbでも同様のset_itemメソッドを定義済みです)

favorites_controller.rb
class FavoritesController < ApplicationController
  before_action :authenticate_user!
# 追記==========================================================================
  before_action :set_item 
# ==============================================================================
  def create
    favorite = current_user.favorites.build(item_id: params[:item_id])
    if favorite.save
    else
      flash.now[:alert] = favorite.errors.full_messages
    end
  end

  def destroy
    favorite = Favorite.find_by(item_id: params[:item_id], user_id: current_user.id)
    if favorite.destroy
    else
      flash.now[:alert] = '削除できませんでした。'
    end
  end

  private
# 追記==========================================================================
  def set_item
   @item = Item.find(params[:item_id])
 end
# ==============================================================================
end

まとめ

いかがだったでしょうか。
いいね機能のAjaxは、実装の手順そのものはすごくシンプルです。しかし、いいね機能専用のビューを用意していなかったり、部分テンプレートの保存場所の違いによって記述内容が異なるケースがあります。当たり前のことではあるのですが、記事通り実装してみて上手く出来なかった時、解決の一助となれば幸いです。

※私自身初めてのQiitaの投稿です!
ご指摘等ございましたらコメントにてお待ちしております。

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

【Rails】 DataTables 実装方法

はじめに

RailsアプリケーションでDataTablesを使っている記事を見かけなかったので、まとめることにしました。
やり方がわかっていないと細かい仕様を変更するのに時間がかかってしまいますが、やり方さえ覚えてしまえばとても使いやすい強力なツールです。
Bootstrap や jQuery UI を使うことによって、時間をかけずに多機能&良いレイアウトを提供してくれるため、爆速で開発できるRailsととても相性がいいと個人的には思っています。

DataTables

DataTables は、HTMLのテーブルに、ページ切り替え、ページ当たりの件数設定、ソート、フィルタなどの機能を簡単に追加できるjQuery プラグインのライブラリ

使い方

設定

GemFile
# ページネーションにはkaminariを使用するため
gem 'kaminari'
gem 'jquery-datatables-rails'
$ bundle install
$ rails g jquery:datatables:install
$ rails g jquery:datatables:install bootstrap3
app/assets/javascripts/application.js
//= require dataTables/jquery.dataTables
//= require dataTables/bootstrap/3/jquery.dataTables.bootstrap
app/assets/stylesheets/application.css
*= require dataTables/jquery.dataTables
*= require dataTables/bootstrap/3/jquery.dataTables.bootstrap

※ version3系をダウンロードする。(GitHubにBootstrap4の記載がないため念のため。。)
4系でも使えると知っている方がいればコメントください。。

# 下記のようにファイルを配置する
app/assets/javascripts/bootstrap.min.js
app/assets/stylesheets/bootstrap.min.css

アプリケーション実装

前提条件

Userモデルを scaffold で作成していること。

全体像

厳密には異なるところもありますが、ざっくりと全体像を示すとこんな感じになります。

① id が users の要素を持った table を作成(table#users)し、CoffeeScript にて読み込み
② ajax で CoffeeScript から Rails 側へ、データの算出を要求する
③ コントローラーにて ajax を受け取り、 UserDatatable モデルへデータの算出を要求する
④ 要求を満たすデータを返す
⑤ ④のデータを json ファイルにして返す
⑥ CoffeeScript にて実装したテーブルを HTML にて表示する

image.png

ビュー

slim だと本当にシンプルに書くことができます。

app/views/users/index.html.slim
// 検索項目
button#search_btn type="button"
  |詳細検索
button#search_exec_btn type="button"
  |検索
button#search_clear_btn type="button"
  |クリア
table
  tr
    td = label :user, :id
    td = text_field_tag :id, '', id: "search_id"
    td = label :user, :username
    td = text_field_tag :username, '', id: "search_username"
    td = label :user, :name
    td = text_field_tag :name, '', id: "search_name"

// DataTables のテーブル表示
table#incidents

ルーティング

jsonファイルを dataTables 側に渡すときに使用するルーティング。
のちほど、Javascriptにて使用する。

config/routes.rb
resources :users do
  collection do
    post 'ajax_data'
  end
end

コントローラー

UserDatatableのインスタンスをjsonファイルとして返す。

app/controllers/users_controller.rb
def ajax_data
  respond_to do |format|
    format.html
    format.json {render json: UsersDatatable.new(params) }
  end
end

UserDatatableはパラメーターを受け取って、SQL を実行し JSON 形式に変換するクラス。
テーブルに表示させたい項目や検索結果などをこちらのファイルにて割り出している。

app/datatables/users_datatable.rb
class UsersDatatable
  attr_accessor :params

  def initialize(params)
    @params = params
  end

  # jQuery DataTables へ渡すためのハッシュを作る
  # 補足:コントローラーの render json: で指定したオブジェクトに対して as_json が呼び出される
  def as_json(options = {})
    {
        recordsTotal: User.count, # 取得件数
        recordsFiltered: users.total_count, # フィルター前の全件数
        data: users, # 表データ
    }
  end

  def users
    @users ||= fetch_users
  end

  # 検索条件や件数を指定してデータを取得
  def fetch_users
    User.where(search_sql).page(page).per(per)
  end

  # カラム情報を配列にする
  def columns
    return [] if params["columns"].blank?
    params["columns"].to_unsafe_h.map{|_,v| v["data"]}
  end

  # 検索ワードが指定されたとき
  def search_sql
    search_sql = []
    for column, search_params in params["columns"] do
      search_sql.push("#{search_params["data"]} like '%#{search_params["search"]["value"]}%'") if search_params["search"]["value"].present?
    end
    search_sql.join(" and ")
  end

  # ソート順
  def order_sql
    return "" if params["order"]["0"].blank?
    order_data = params["order"]["0"]
    order_column = columns[order_data["column"].to_i]
    # "id desc" のようにSQLの一部を作る
    "#{order_column} #{order_data["dir"]}"
  end

  # kaminari 向け、ページ数
  def page
    params["start"].to_i / per + 1
  end

  # kaminari 向け、1ページで取得する件数
  def per
    params["length"].to_i > 0 ? params["length"].to_i : 10
  end
end

Javascript

app/assets/javascripts/users.coffee
$ ->
  # DataTables オブジェクト作成(ここではhtmlのid=users)
  user_table = new DataTables($('#users'))
  # ajax にて、 json ファイルを読み込み。作成したルーティングを引数に入れる。
  user_table.setAjax("/users/ajax_data")
  # user_table へカラムを追加する
  user_table.setColumns([
    { data: 'id',         title: 'ユーザID', width: '5%' },
    { data: 'username',   title: 'ユーザ名', width: '25%' },
    { data: 'name',       title: '名前',    width: '30%' },
    { data: 'created_at', title: '登録日時', width: '20%' },
    { data: 'updated_at', title: '更新日時', width: '20%' },
  ])
  user_table.setOption(false, true, true)
  # 表示順番を0番目のカラムを昇順で表示(ここではidを昇順で表示)
  user_table.setOrders([[0,'asc']])
  # #users へ user_table を描写する。
  user_table.drawTable()

  # 各行をクリックすると詳細画面へ遷移するように設定
  $('#users tbody').on 'click', 'tr', ->
    data = $('#users').dataTable().fnGetData(this);
    document.location = "/users/#{data.id}"

  # 詳細検索ボタンを押すと、検索ボタンおよびカラムが表示されるように設定
  $('#search_btn').on 'click', ->
    $('.search-body').slideToggle(200)
    $('#search_exec_btn').slideToggle(200)
    $('#search_clear_btn').slideToggle(200)

  # 検索ボタンを押すと、表示されているテーブルのカラムが検索条件に合致したものだけを表示するように設定。
  $('#search_exec_btn').on 'click', ->
    # dataTable の API を利用する。
    user_table_api = $('#users').dataTable().api()
    # 各検索カラムの値を読み込み
    search_id =       $('#search_id').val()
    search_username = $('#search_username').val()
    search_name =     $('#search_name').val()
    # 検索結果をテーブルに描画する
    user_table_api.columns(0).search(search_id).draw()
    user_table_api.columns(1).search(search_username).draw()
    user_table_api.columns(2).search(search_name).draw()

  $('#search_clear_btn').on 'click', ->
    # 検索欄 入力内容 初期化
    tmp_array = ["id", "username", "name"]
    tmp_array.forEach (key) ->
      $("#search_#{key}").val('').trigger('change')

まとめ

少し複雑な実装でしたので、Keynoteで作った全体像を用いての説明でした。
いかがでしたでしょうか。少しでも理解の助けになれればなと思います。

また、DataTables は高機能なライブラリですので、他にもできることを紹介していきたいと思います。

参考

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

HanamiのアプリケーションをREST APIとして実装する

Hanamiは複数のアプリケーションをモノリシックに構築することができます。

複数のアプリケーションを実装するにあたり、REST APIとして実装するアプリケーションを作成する機会がありましたが、3年以上の前の Build a Web API with Hanami ぐらいしか情報がなく苦労したので、備忘録として残しておきます。

API化するHanamiのアプリケーションの準備

今回は公式のチュートリアルが終わった段階の webアプリケーションに対してPOSTメソッド利用できるAPI化を施そうと思います。

APIに不要なView関連ディリクトリの削除

assets, views, template の各ディリクトリは不要なので削除しちゃってください。

   web
-    ├── assets           
-    │   ├── images
-    │   ├── javascripts
-    │   └── stylesheets
     ├── config
     ├── controllers
-    ├── templates       
-    └── views    

このままでは、Hanamiのサーバが起動するときに読み込みエラーになるので、 apps/web/application.rb の各クラスの読み込み箇所を修正します。
※assetsの設定は起動時に読み込まれないので、削除してもしなくても大丈夫です。

apps/web/application.rb
      # Relative load paths where this application will recursively load the
      # code.
      #
      # When you add new directories, remember to add them here.
      #
-      load_paths << [
-        'controllers',
-        'views'  #削除
+     load_paths << ['controllers']
apps/web/application.rb
      # The relative path to templates
      #
-      templates 'templates'
+     # templates 'templates'

また、今回はメーラーも使用しないのでこのタイミングで削除します。

bookshelf/config/environment.rb
-  mailer do
-    root 'lib/bookshelf/mailers'
-
-    # See https://guides.hanamirb.org/mailers/delivery
-    delivery :test
-  end
+  # mailer do
+  #   root 'lib/bookshelf/mailers'
+  # 
+  #   # See https://guides.hanamirb.org/mailers/delivery
+  #   delivery :test
+  # end

テストの作成

HanamiはTDDを推奨の設計手法としてしているのでテストを先に作成しましょう。
今回はPOSTのAPIを実装するので spec/web/controllers/books/create_spec.rb を作成してテストを書いていきます。

spec/web/controllers/books/create_spec.rb
RSpec.describe Web::Controllers::Books::Create, type: :action do
  include Rack::Test::Methods
  let(:app) { Hanami.app }

  describe "create books" do

    let(:request_body) do
      {
        bools: {
          title: 'hogehoge',
          author: 'foo'
        }
      }
    end

    let(:do_request) { post '/api/books', params: request_body, header: { 'Content-Type' => 'application/json' } }

    it 'is http status 201' do
      do_request
      expect(last_response.status).to eq 201
      expect(last_response.body).to eq request_body
    end

    it 'is expect response body' do
      do_request
      expect(last_response.body).to eq request_body
    end

    it 'is created created books' do
      expect { do_request }.to change {
        BookRepository.new.first.nil?
      }.from(true).to(false)
    end
  end
end

アクションのテストとの違いは直接エンドポイントを叩いているところです。
エンドポイントを叩くためには下記のモジュールをincludeして Hanami.appapp に格納する必要があります

  include Rack::Test::Methods
  let(:app) { Hanami.app }

ルーティング

今回は /api/books というエンドポイントを作成します。
まず config/environment.rb において、下記のように設定することでホスト名の次に来るパスに api を自動で追加できます。

mount Web::Application, at: '/api'

そして、Hanami には rails と同じような RESTful Resource(s) な記法で エンドポイントをルーティングできるのでapi以下のルーティングを下記のように設定します

web/config/route.rb
resources 'books', only: [:create]

これでルーティングの設定は完了です。

今回は :create なのでhttpメソッドは POST になりますが、 :update の場合、httpメソッドは PATCH になります。PUT は存在しないので注意してください。
Hanami側から指定してくれているので PUT か PATCh で惑わずに済んで助かります。

Action(Controller)

エラー周りに改善の余地がありますが、バリデーションと保存処理はアクションから独立させています。
最終的に self.body に入れた値がレスポンスボディとして返されるので、作成したリソースからJSON化したものをここに格納すればJSONのレスポンスボディとして返されます。 self.status も同様です。

apps/web/controllers/books/create.rb
module Web
  module Controllers
    module Books
      include Api::Action
      accept :json

      def call(params)
        validate_result = Form::BooksValidator.new(params).validate
        raise StandardError unless validate_result.success?

        BooksInteractor.new.create(attribute)
        self.body = response_body
        self.status = 201
      rescue => exception
        self.status = 400
        @error = exception
      end

      def response_body
        JSON.dump(BooksRepository.new.last.to_hash)
      end
    end
  end
end

Hanamiのバリデーションは コンポーネント指向な dry-validation を模して実装されています。
今回やっていることは rails で言うところの StrongParamater の処理と同じですが、ガッツリとバリデーションも記述できるので、アクション側からそのあたりの責務を一通り引き取ることができます。

lib/bookshelf/validators/form/books_validator.rb
require 'hanami/validations'

module Form
  class BooksValidator
    include Hanami::Validations

    validations do
      required(:books).schema do
        required(:title) { filled? }
        required(:author) { filled? }
      end
    end
  end
end

保存処理をIteratorにまとめたものです。こちらも保存処理関連の責務をアクションから剥がすことができます。

lib/bookshelf/interactors/books_interactor/create.rb
require 'hanami/interactor'

module BooksInteractor
  class Create
    include Hanami::Interactor

    def initialize(params)
      @attributes = {
          title: params[:books][:title],
          author: params[:books][:author]
      }
    end

    def call
      BooksRepository.new.create(@attributes)
    end
  end
end

これらの実装により、Web アプリケーションをPOSTメソッドを持ったAPIにすることができます。

終わりに

Hanami を触っていて思ったことは Entity と Repository の元となっている ROM(Ruby Object Mapper) の理解や Itarator の使い所をわかっていないと、どうしても rails 感が抜けずに劣化 rails になりがちな点です。まだプロダクトレベルのサンプルも少なく手探りなところもありますが、自分の中で消化できたらそのあたりの記事も書けたらと思います。

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

【これからプログラミング&クラウドを始める人向け】AWS Cloud9 を利用して Ruby の開発環境を作ってみる③ - Ruby のバージョン管理

はじめに

この記事は無料で AWS Cloud9 を利用してプログラム学習を開始する方法いついて入門者向けに記載した内容になっています。

AWS アカウントの作り方は → こちら
AWS Cloud9 の開始方法は → こちら

今回行うこと

複数の案件や学習教材を利用した場合、それぞれ Ruby のバージョンが異なる場合があります。
今回は複数の Ruby のバージョンをインストールして状況に応じてバージョン変更する方法を Cloud9 上で実践してみます。

Ruby のバージョン確認

  • 右下の□ボタンを押すとコマンドコンソールが全画面表示になります。

image.png

  • AWS Cloud9 上で現在インストールされている Ruby のバージョンを確認してみます。
ruby -v

image.png

バージョン管理ソフトウェアの導入

  • 今回は「 rvm (Ruby Version Manager) 」を利用する手順になります。
  • 先程と同様にまずは Cloud 9 上の rvm のバージョンを確認します。
rvm -v
  • 上記のコマンドを入力すると rvm のバージョンが表示されます。

image.png

  • 次に rvm のヘルプ表示を行います。
rvm help

※ rvm とだけ入力してもヘルプは表示されます。

  • 入力するとコマンドとその説明が表示されます。

image.png

  • 次にこの環境に表示されている ruby のバージョンを表示してみます。
rvm list
  • 現在この環境で利用されているバージョンは 2.6.3 であると表示されています。

image.png

  • 次にインストール可能なバージョンを確認します。
rvm list known
  • 実行可能な Ruby のバージョンがずらっと出てきます

image.png

このように実際に環境を構築する際にはヘルプを開き、バージョンを確認しながら進めることになります。

Cloud9上に Ruby 別バージョンのインストール

鍵のインストール

image.png

  • コマンドをターミナル上で実行します。root 権限での実行が必要なため sudo 権限で実行します。
sudo gpg --keyserver hkp://pool.sks-keyservers.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB
  • 実行後の画面はこのようになっています。

image.png

バージョンを指定してインストールする

  • 今回は敢えて 2.3.2 をインストールしてみます。
    • 数分ほど完了までに時間がかかります。
rvm install 2.3.2
  • 完了後の画面はこんな感じです。

image.png

  • ここで改めてインストールされているバージョンを確認します。
rvm list
  • 以下のキャプチャのように言語の使用状況は言語表示の手前の記号で判断ができます。
    • => 現在使用中のバージョン
    • =* 現在使用中かつデフォルト設定バージョン
    • * デフォルト設定されているバージョン

image.png

  • 現在はインストールした 2.3.2 が使用中の設定になっています。

使用するバージョンの変更

  • 以下のコマンドで使用するバージョンを変更することができます。
rvm use 2.6.2(対象バージョン)
  • 実行結果と確認結果は以下のようになっています。

image.png

  • ## デフォルト設定のバージョンの変更
  • デフォルトのバージョンは --default を追加したコマンドで変更ができます。
rvm --default use 2.3.2(対象バージョン)

image.png

インストールした特定バージョンの削除

  • 以下のコマンドで削除することができます。
rvmsudo rvm remove 2.3.2
  • 実行結果

image.png

まとめ

上記のようにバージョン管理のソフトウェアを活用して、複数バージョンの管理を行うことが可能です。行う内容自体は他の言語でも同様のことを進めていくことになります。次回は Git 環境を AWS のサービスを利用して用意していく記事を書こうと思います。お楽しみに!

関連記事

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