20191128のRailsに関する記事は30件です。

Railsの非同期通信でDELETEを行う

はじめに

自作アプリ「CheckMe!」はユーザビリティ向上を目指してDBとのやりとりに非同期通信を多く使っています。
CRUDを非同期でやっていますが、Deleteについて結構つまづくポイントがあったので備忘録を兼ねてまとめておきます。
この記事が同じことをやりたい人の手助けになれば幸いです。

条件

削除対象のidが含まれたリンク(クラスに’delete_link’を持つ)をクリックした時にイベントを中止して
idのアイテムを削除する非同期通信をする。

実装

$('.delete_link').on('click', function (e) {

    // stopPropagationがないとHTMLとJSONの
    // リクエストが同時に送られてしまう
    e.preventDefault();
    e.stopPropagation();

    //リンクに含まれるURLを取得する
    var url = $(this).attr('href');

    $.ajax({
      url: url,
      type: "DELETE",
      dataType: 'json',
      // CSRF tokenを発行する
      beforeSend: function (xhr) { xhr.setRequestHeader("X-CSRF-Token", $('meta[name="csrf-token"]').attr('content')) }
    })
      .done(function (data) {
        成功した時
      })
      .fail(function () {
        失敗した時
      })
})

ポイント

stopPropagationを使うこと

preventDefaultだけだと、なぜかリクエストを送った際にHTMLとJSONのリクエストが
同時に送られてしまう。結果、2回異なるリクエストで非同期通信したことになってしまうので
stopPropagationを使う。

CSRF tokenを発行する

CSRF tokenが無いと認証が通らず、そこで処理が止まってしまうのでtokenを渡すよう
にしてあげます。

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

gmaps4railsのマップタイプを設定する

はじめに

Google Map APIを利用したアプリ開発をしています。
その時に、マップの表示のデフォルトを航空写真にしたかったので実装してみました。
マップの表示の種類のことをマップタイプと呼ぶそうです。

コード

このようになりました。

<script>
  handler = Gmaps.build('Google');
  handler.buildMap({ provider: {}, internal: {id: 'map'}}, function(){
    markers = handler.addMarkers(<%= raw @hash.to_json %>);
    handler.bounds.extendWith(markers);
    handler.fitMapToBounds();

    // マップタイプの追記
    handler.getMap().setMapTypeId("satellite");

  });
</script>

参考

参考にしたのはこちら

gmap4railsの設定の変更に役立った
gmaps4railsの地図の拡大率を設定する

mapTypeIdというものがあることを知った
Google Maps API の使い方・利用方法

Mapクラスのメソッド一覧

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

【Rails】CSVファイルからデータをインポート

簡単なTODOアプリに、CSVファイルからタスクを追加する機能を実装します。
手順は以下の記事で説明されているものとほとんど同じです。めちゃくちゃ参考になりました。
【Ruby on Rails】CSVインポート

実装

タスク追加、編集、削除機能、ログイン機能などを持つTODOアプリにCSVアップロード機能を実装して行きます。
Ruby on Railsで簡単なアプリを作成
【Rails】ログイン機能を実装する

rubyの標準ライブラリcsvを追加

/config/application.rb
require 'csv'

rooというgemを追加

csvファイルを読み込むためのgemrooを追加します。

Gemfile
gem 'roo'
terminal
$ bundle install

タスク一覧画面にcsvアップロード用のフィールドを追加

/app/views/tasks/_logged_in.html.erb
<%= form_tag import_tasks_path, multipart: true do %>
  <%= file_field_tag :file %>
  <%= submit_tag "インポート" %>
<% end %>

コントローラーにアクションを追加

Task.import(params[:file])で使われているimportメソッドは後ほど定義します。

/app/controllers/tasks.controller.rb
def import
  Task.import(params[:file])
  redirect_to root_url
end

ルーティングを設定

collection {post :import}と書き込むことで、resources :tasksで作成されるルーティング以外の、tasksコントローラーのアクションへのルーティングを追加することができます。

/config/route.rb
resources :tasks do
  collection {post :import}
end
terminal
$ rails routes
.
.
import_tasks POST   /tasks/import(.:format)   tasks#import
.
.

import_tasksという名前付きルートが追加されました。

モデルにCSV読み込み、登録処理を実装

/app/models/task.rb
#importメソッド
def self.import(file)
  CSV.foreach(file.path, headers: true) do |row|
    # IDが見つかれば、レコードを呼び出し、見つかれなければ、新しく作成
    task = find_by(id: row["id"]) || new
    # CSVからデータを取得し、設定する
    task.attributes = row.to_hash.slice(*updatable_attributes)
    task.save
  end
end

# 更新を許可するカラムを定義
def self.updatable_attributes
  ["title", "user_id"]
end

動作確認

以下のようなファイルを用意します。

taskForTodoApp.csv
title,user_id
パンを買う,2
筋トレ,2
メルカリの発送,8
ティッシュを交換する,2

実行結果
 2019-11-28 18.43.08.png

ハマったポイント

・CSVファイル内にuser_idを記載しておらず、データベースへの登録時にエラーが発生していたところで少しハマりました。

TODO

・TSVファイルも取り込めるようにする

参考

【Ruby on Rails】CSVインポート

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

さくらVPSでCentOS7 10.Ruby On Railsインストール

はじめに

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

今回は、Ruby On Railsをインストールします。
どういった構成が正解かは判断がつきかねているのですが、今のところこんな感じです・・(^^;

目次

  1. 申し込み
  2. CentOS7インストール
  3. SSH接続
  4. Apache・PHPインストール
  5. MariaDBインストール
  6. FTP接続
  7. sftp接続
  8. phpMyAdminインストール
  9. 環境のバックアップ
  10. Ruby On Railsインストール

10.Ruby On Railsインストール

必要パッケージインストール

Node.jsインストール

Node.jsのバージョンを管理する「n」パッケージをインストールします。
鶏と卵のような話ですが、「n」パッケージインストールにはnpmが必要で、npmにはNode.jsが必要ということで、Node.jsをインストールする為にまずnpmとNode.jsをインストールします。

npmインストール

$ sudo yum install epel-release

node.jsインストール

$ sudo yum install nodejs npm

「n」パッケージインストール

$ sudo npm install -g n

「n」パッケージを使ってNode.jsをインストール

最新版は「latest」、安定版は「stable」でインストールできます。

$ sudo n latest
$ node -v
v11.13.0
$ sudo n stable
$ node -v
v10.15.3

「n」パッケージの使い方

コマンド 内容
n インストールしているバージョンの表示やバージョンを切り替える
n ls すべてのバージョンを表示する
n --latest 最新バージョンを表示する
n --lts 安定バージョンを表示する
n latest 最新バージョンをインストールする
n stable 安定バージョンをインストールする
n [version] 指定したバージョンをインストールする
n rm [version] 指定したバージョンを削除する
n prune すべてのバージョンを削除する

その他もろもろインストール

$ sudo yum -y install gcc make openssl openssl-devel gcc-c++ mysql-devel readline-devel libxml2-devel libxslt-devel git bzip2 zlib-devel sqlite-devel
$ sudo npm install yarn -g

Rubyインストール

rbenvインストール

rbenvは、複数のRubyのバージョンを管理し、プロジェクトごとにRubyのバージョンを指定して使うことを可能としてくれるツールです。
まずこれをインストールします。

$ cd /usr/local
$ sudo git clone https://github.com/rbenv/rbenv.git
$ sudo mkdir rbenv/plugins && cd rbenv/plugins
$ sudo git clone git://github.com/sstephenson/ruby-build.git

ログイン時にrbenvを使えるようにする初期化スクリプトを記述します。

$ sudo vi /etc/profile.d/rbenv.sh

以下を追加

export RBENV_ROOT="/usr/local/rbenv"
export PATH="${RBENV_ROOT}/bin:${PATH}"
eval "$(rbenv init -)"

パーミッションの変更

$ sudo chmod -R 777 /usr/local/rbenv

設定を反映させるために、ここで一度ログアウトして再ログインします。
再ログインしたら、確認。

$ rbenv install -l
2.6.5

Rubyインストール

rbenvを使ってRubyをインストールします。
インストール可能なRubyバージョン一覧を表示します。

$ rbenv install -l

バージョン2.6.5のRubyをインストールします。

$ rbenv install 2.6.5

全体で使用するRubyのバージョンを指定

$ rbenv global 2.6.5

確認

$ ruby --version
ruby 2.6.5p114 (2019-10-01 revision 67812) [x86_64-linux]

Rails環境構築

GemとBundler

GemはRubyGems(Ruby用のパッケージ管理ツール)で管理されるrubyのアプリケーションやライブラリです。
Bundlerは、gem動詞の互換性を保ちながらパッケージの種類やバージョンを管理するツールです。

gem

確認

$ which gem
/usr/local/rbenv/shims/gem

最新版にアップデート

$ gem update --system

バージョン確認

$ gem -v
3.0.6

bundlerインストール

システムのgemにはbundlreのみインストールし、bundler以外のものはプロジェクトのvendor/bundleに格納します。
ので、ここではbundlerのみインストール。

$ gem install bundler

プロジェクトのフォルダ

以前、4. Apache・PHPインストールでApacheをインストールした時に作成した、/var/www/の中にappというフォルダを作成し、その中にプロジェクトを作っていきます。

フォルダ作成

$ mkdir /var/www/app

Webコンテンツに複数のユーザが操作出来るように作成したグループに権限を与えます。

$ sudo chown root:webadmin /var/www/app/
$ sudo chmod 2775 /var/www/app/ -R

「bundle exec」の省略設定

railsコマンドをプロジェクト内のvendor/bundleに格納すると、railsコマンド呼出時に、「bundle exec rails server」のように「bundle exec」を付けて呼び出す必要があります。
このbundle execを省略できるように設定します。
この設定は、各ユーザーごとに必要です。

スクリプトをダウンロード

$ cd
$ curl -L https://github.com/gma/bundler-exec/raw/master/bundler-exec.sh > ~/.bundler-exec.sh

bundler-exec.shを編集

$ vi ~/.bundler-exec.sh

リストにrailsを追記(下の方にあります)

  ・
  ・
 (省略)
  ・
  ・
unicorn
unicorn_rails
wagon
rails
}"

define-bundler-aliases

unset -f define-bundler-aliases

反映させます。

$ source ~/.bashrc

利用可能なRailsのバージョンの確認

$ gem query -ra -n "^rails$"

現時点で最新は6.0.1でしたが、以下のプロジェクトでは5.2.4を使います。

プロジェクト作成

プロジェクト用フォルダ

bundlerを利用する為、「rails new 」する前に、フォルダを作成します。

$ mkdir /var/www/app/HelloWorld
$ cd /var/www/app/HelloWorld

ローカルのrubyのバージョンを指定。

$ rbenv local 2.6.5

確認

$ rbenv version
2.6.5 (set by /var/www/app/HelloWorld/.ruby-version)

Gemfile

Gemfileの雛形作成

$ bundle init
riting new Gemfile to /var/www/app/HelloWorld/Gemfile

Gemfileの編集

$ vi Gemfile

railsのバージョン5.2.4を使用できるように下記のように修正します。

# frozen_string_literal: true

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

# コメントを外し、バージョンを記述
gem "rails", "5.2.4"

gemのインストール

$ bundle install --path vendor/bundle

インストールのメッセージが流れ、無事に終わるかと思ったのですが、こんなメッセージが・・・

HEADS UP! i18n 1.1 changed fallbacks to exclude default locale.
But that may break your application.

Please check your Rails app for 'config.i18n.fallbacks = true'.
If you're using I18n (>= 1.1.0) and Rails (< 5.2.2), this should be
'config.i18n.fallbacks = [I18n.default_locale]'.
If not, fallbacks will be broken in your app by I18n 1.1.x.

For more info see:
https://github.com/svenfuchs/i18n/releases/tag/v1.1.0

「i18nの設定方法が変わったので気を付けなさい」ということみたいです。
だだ、今回はRailsのバージョン5.2.4にしていますので、I18n (>= 1.1.0) and Rails (< 5.2.2)の条件には引っ掛からないので大丈夫だと思います。

新規プロジェクト作成

これでやっとrailsコマンドが使えるようになりましたので、プロジェクトを作成します。
フォルダは作成済みですので、プロジェクト名の指定は不要です。

$ rails new .

.ruby-versionを上書きしますか?と聞いてきますのでYを入力。

Overwrite /var/www/app/HelloWorld/.ruby-version? (enter "h" for help) [Ynaqdhm]Y

Gemfileを上書しますか?と聞いてきますのでYを入力。

Overwrite /var/www/app/HelloWorld/Gemfile? (enter "h" for help) [Ynaqdhm]Y

順調に行くと思いきや、以下のメッセージが出ました。

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 "sprockets":
  In snapshot (Gemfile.lock):
    sprockets (= 4.0.0)

  In Gemfile:
    sass-rails (~> 5.0) was resolved to 5.1.0, which depends on
      sprockets (>= 2.8, < 4.0)

    rails (~> 5.2.4) was resolved to 5.2.4, which depends on
      sprockets-rails (>= 2.0.0) was resolved to 3.2.1, which depends on
        sprockets (>= 3.0.0)

Running `bundle update` will rebuild your snapshot from scratch, using only
the gems in your Gemfile, which may resolve the conflict.
         run  bundle exec spring binstub --all
bundler: command not found: spring
Install missing gem executables with `bundle install`

依存関係に問題があるから、「bundle lock --add-platform x86-mingw32 x86-mswin32 x64-mingw32 java」を実行しろということのようです。
あと、「bundle update」を実行すると、スナップショットを最初から再構築します、と。
なので、やってみます。

$ 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 "sprockets":
  In snapshot (Gemfile.lock):
    sprockets (= 4.0.0)

  In Gemfile:
    sass-rails (~> 5.0) was resolved to 5.1.0, which depends on
      sprockets (>= 2.8, < 4.0)

    rails (~> 5.2.4) was resolved to 5.2.4, which depends on
      sprockets-rails (>= 2.0.0) was resolved to 3.2.1, which depends on
        sprockets (>= 3.0.0)

Running `bundle update` will rebuild your snapshot from scratch, using only
the gems in your Gemfile, which may resolve the conflict.
$ bundle update
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`.
 ・
 ・
(以下略)
 ・
 ・

と、同じようなメッセージが・・・
とりあえず、再度「bundle lock --add-platform x86-mingw32 x86-mswin32 x64-mingw32 java」して、「bundle update」すると、「Bundle updated!」と出たので、とりあえずOKかなと。

サーバー起動

3000のポートを開ける

ファイアウォールを使っている場合

ポートを開ける

$ sudo firewall-cmd --permanent --add-port=3000/tcp
$ sudo firewall-cmd --reload![2019-11-28.png](https://qiita-image-store.s3.ap-northeast-

さくらVPSのパケットフィルタを使っている場合

  1. さくらVPSコントロールパネルにログインし、サーバーを選択
  2. [パケットフィルタ]-[パケットフィルタ設定へ>]を選択
  3. [+任意の解放ポート設定を追加する]ボタンをクリック
  4. [TCP][3000]を設定
  5. [設定]ボタンをクリック


以上でポートが開きます。

サーバー起動

$ 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

無事起動しました。

ブラウザで確認

次回

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

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

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

RailsのDM一覧に最後のメッセージを表示させる方法

はじめに

記事①記事②を参考にDM機能を作らせていただきました。投稿してくださった方々に深く感謝いたします。
以下は記事①の通りにDM機能を作成した前提で進めます。

やりたいこと

TwitterやLINEのようにメッセージの一覧に最後のメッセージを表記させる。

実装

記事①通りに進めるとメッセージ一覧はrooms#indexに表示させることになると思うので、以下のようにすればユーザ間の最後のメッセージを引っ張ってくることができます。

まず、記事②を参考にroomsコントローラ側に@anotherEntriesを定義します。

roomsコントローラー
def index
  @currentEntries = current_user.entries
  myRoomIds = []

  @currentEntries.each do | entry |
    myRoomIds << entry.room.id
  end

  @anotherEntries = Entry.where(room_id: myRoomIds).where('user_id != ?', @user.id)
end

そして、@anotherEntriesにはuser_idが自分のidではない相手の情報が配列として入っているので、以下のようにすればやりとりした最後のメッセージを引っ張ってこられます。

rooms#index
<% @anotherEntries.each do |e| %>
  <%= Message.find_by(id: e.room.message_ids.last).content %>
<% end %>

上はただ表記させただけなので、あとは自分好みにリンクにしたり字数に制限をかけたりしてみてください。
また、ここでは最後のメッセージを表示させましたが、上記のcontentの部分をuser.nameにすれば最後にメッセージを送ったユーザ(自分か相手)の名前を引っ張ってくることもできます。

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

has_many_attachedなActiveStorageに属性を付与する

とあるModelに複数のファイルをAttachさせるときに、
ActiveStoragehas_many_attached を使うことはよくあると思いますが、
このとき、

  • ファイルに属性を付与したい
  • 同じ属性のファイルが既にあるなら削除したい

的な感じの気持ちが溢れ出ると思います。

ので、やります。

class Foo < ApplicationRecord
  has_many_attached :bars
end

このクラスで何かをattachします

class Foo < ApplicationRecord
  has_many_attached :bars
  def attach_bars
    bar = 'something' #何らか適当なファイル的なやつ
    bars.attach(
      io: StringIO.new(bar),
      filename: 'なんらかてきとうなふぁいるめい.txt',
      content_type: 'text/plain',
      metadata: { bar_type: 'something' }
    )
  end
end

metadata: { bar_type: 'something' } の部分が何かいい感じのやつで、これで active_storage_blobs.metadata"bar_type": "something" な感じでデータが保持されるようになります。やったね。

これで属性付与できたので、 bars の中で bar_type == 'something' なattachmentを削除したい場合は、

foo = Foo.find(xxx)
foo.bars.select {|bar| bar.metadata[:bar_type] == 'something' }
  .each {|bar| bar.purge }

みたいな感じでpurgeすることはできるんですが、仮に bars が100件とか200件とかなってきたらウザウザなので、事前にクエリで絞り込む的なことがしたくなりました。

というわけで、しました。

foo.bars.joins(:blob).where(
  "`active_storage_blobs`.`metadata`->>'$.bar_type' = ?", 'something'
).each { |bar| bar.purge }

やったね!

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

コメントの削除も非同期で対応する

コメント投稿の非同期は成功しました。
そこで、コメント削除も非同期にて対応しようと思い。
クロームの検証画面でエレメントコピーして修正して下記コードを追加しました。
削除

コメントして即削除でActiveRecord::RecordNotFound in TweetsController#destroyが出ます。

一度リロードをすれば問題なく削除できているので
非同期部分で引っかかっているのは確認済みです。

どうすればいいのでしょうか?
コメント投稿と削除で別々の
コードがいるのでしょうか?
記載をするべきでしょうか?
何かファイルを増やしてそこに記載がいるのでしょうか?

$(function(){
  function buildHTML(comment){
    var html = `<p>
                  <strong>
                    <a href=/users/${comment.user_id}>${comment.user_name}</a>
                    :
                  </strong>
                  ${comment.text}
                  <a class="comment-delete" rel="nofollow" data-method="delete" href="/tweets/${comment.tweet_id}">削除</a>
                </p>`
    return html;            
  }


  $('#new_comment').on('submit',function(e){
    e.preventDefault();
    console.log(this)
    var formData = new FormData(this);
    var url = $(this).attr('action')
    $.ajax({
      url: url,
      type: "POST",
      data: formData,
      dataType: "json",
      processData: false,
      contentType: false
    })
    .done(function(data){
      var html = buildHTML(data);
      $('.comments').append(html);
      $('.form_message').val('');
      $('.form__submit').prop('disabled', false);
    })
    .fail(function(){
      alert('error');
    })
  })
})
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

新卒が研修を修了したので、Rails本番コーディングする前にインプットしたこと3つ ~ 性能・セキュリティ編 ~

この記事はモチベーションクラウドアドベントカレンダー5日目の記事です。
こんにちは。リンクアンドモチベーション、モチベーションクラウド開発チームの江上です。

つい先日、約3ヶ月のエンジニア研修を卒業した19年新卒のメンバーを開発チームに迎え入れました。弊社では全社研修後、数ヶ月のエンジニア研修の後エンジニアやプロダクトオーナー、カスタマーサポートなどのプロダクトチームに配属されるフローになっていますので、例年この時期に配属になります。

ありがたいことに、現在モチベーションクラウドは大手のお客様にも使っていただけるプロダクトに成長しました。それに伴い、性能やセキュリティにおいて求められる基準も高くなってきています。そこで、既存メンバーの復習も兼ねて、新人であったとしても最低限押さえておくべきことを、具体例を交えて開発開始前にインプットすることにしました。

同じく本番コードを初めて書く新人を迎え入れる方、今から本番コードを書き始めようという方、クリスマス前に初心にかえって、清らかな心を取り戻したい方の参考になれば幸いです。

0. 前提条件

弊社では、以下の事項を習得していることを研修卒業の条件とするプログラムを実施しています。
- SQLの基本習得
- DockerかVMをつかった開発環境の構築習得
- Rails Tutorial最低1周
- rspecの書き方習得
- Gitの使い方習得
- エディタの使い方習得

1. 事前知識

1-1. 読本

あくまで本番コードを書く上で、以下の資料を事前に読むことを決めています。
読むだけではなく、自分なりの感想文(まとめ)を提出してもらっています。
たまに、先輩からのいくつかクイズを出されます。
- リーダブルコード
- 安全なwebアプリケーションの作り方
- モチベーションクラウドのマニュアル

1-2. 開発中の注意事項

1-2-1. 開発に必要な画面を常に開いておくこと

なにか問題があっても、logを見ていなかったり、consoleで試していないと勘違いで進んでしまう可能性があります。
- ググるためのchrome
- 試すためのrails console
- 確認するためのlog
は常に用意しておきましょう。

1-2-2. 作業ブランチは最新にすること

朝来たら、 git pullgit merge 親ブランチ をしましょう。
特に、pushするときになってmergeすると、コンフリクト地獄で死にたくなります。

2. パフォーマンスについて 

全てを網羅できているわけではないですが、抑えておいてほしいところを列挙しました。

2-1. N+1を起こさない

N+1問題とは

ループ処理の中で都度SQLを発行してしまい、大量のSQLが発行されてパフォーマンスが低下してしまう問題のことです。
例えば、1回1000件取るのに300msで終わるのに、1000回1件ずつ取ると、30ms * 1000 = 30000msかかってしまったりします。

前提

class ModelA < ActiveRecord::Base
  has_many model_bs
end
class ModelB < ActiveRecord::Base
  belongs_to model_a
end

解消方法

2-1-1. includesする(実行計画によっては、n+1になる場合もあるので注意。references使ってる場合はjoinされる)
before_fix
ModelA.all.each do |m|
  m.model_bs # ここで毎回クエリが発行される
end
after_fix
ModelA.includes(:model_bs).all.each do |a|
  a.model_bs
end
2-1-2. ループする必要がないならループしない

※ update_allはコールバックが実行されない点は注意

before_fix
model_as = ModelA.where(id: [1,2,3,4,5])
model_as.each do |a|
  a.model_bs.update(some_flg: true)
end
after_fix
model_as = ModelA.where(id: [1,2,3,4,5])
model_bs = ModelB.where(id: model_as.map(&:model_b_id))
model_bs.update_all(some_flg: true)

2-1-3. joinしたかったら、先に1000件とか一括で持ってきて、Hashにしとく

before_fix
model_as = ModelA.where(id: [1,2,3,4,5])
model_as.each do |a|
  b = a.model_bs
end
after_fix
model_as = ModelA.where(id: (1...1000).to_a)
model_bs = ModelB.where(id: model_as.map(&:model_b_id))
model_bs_hash = model_bs.inject({}) do |hash, b|
                  hash[b.id] = b
                  hash
                end
model_as.each do |a|
  b = model_bs_hash[a.model_b_id]
end

2-2. レコード数が多いテーブルはジョインしない

1:nの関係にあったとき、1レコード平均10件の関連レコードがあると、100万レコードのテーブルと関連レコードをjoinしたときに生成される一時テーブルは1000万レコードになります。
1:1の関連の場合など、問題がないケースもありますが、レコード数が多いテーブルを認識し注意を払うことは大切です。

解決方法

だいたいN+1と同じ方法で解決できます

2-3. json, text型を含むテーブルはselect句で必要なカラムに絞って取得する

レコード数が少なくとも、json、text型を含むレコードは1レコードのデータ量が大きくなりがちです。特に1000文字以上の文字列が入ってる場合は注意が必要です。
データ量が多いと、APサーバーとDB間のネットワークやメモリを余分に消費してしまい遅延やエラーの原因になります。

解決方法

2-3-1. select句で取得カラムを絞る
before_fix
ModelA.all
after_fix
ModelA.all.select(:column_1, :column_2)
2-3-2. includesしない

N+1と同様です

2-4 逐次で計算させない

毎回DBのmax関数などで計算させながら表示してるなどしてると、毎回負荷がかかる上に、その計算値を使ったsortをしたい場合は毎回全件取得する必要が出てきます。

2-4-1. どこかのカラムやキャッシュに計算結果を保持する
before_fix
ModelA.select("max(model_bs.id) as max_b_id").
       joins(:model_bs).
       sort {|a| a.max_b_id}.
       first(10)
after_fix
class ModelA < ActiveRecord::Base
  has_one model_a_stat
end
class ModelAStat < ActiveRecord::Base
  belongs_to model_a
end
ModelA.select("max_b_id").
       joins(:model_a_stat).
       order(max_b_id: :asc).
       limit(10)

3. セキュリティについて

セキュリティも奥が深いので、全てを網羅できているわけでは全然ないですが、コードを書く上で最低限抑えておいてほしいところを列挙しました。

3-1. paramsの取り扱い

paramsはブラウザをちょっといじったり、curlなどを使えば簡単に操作できてしまうので、自分が期待してる値でないものがくる可能性を常に考えておく必要があります。

3-1-1 params[:user_id]は使わない

current_userなどのsessionから引っ張ってきたインスタンスがapplication_controllerのbefore_filterあたりにだいたいあるので、理由がない限りそちらを使いましょう

before_fix
ModelA.where(user_id: params[:user_id])
after_fix
def current_user
  User.where(id: session[:id]).first
end

ModelA.where(user_id: current_user.id)

3-1-2 sessionから引っ張ってこれない、params[:xxx_id]

current_userやcurrent_companyなどのsessionから引っ張ってきたインスタンスでの検証も行いましょう

before_fix
ModelA.where(xxx_id: params[:xxx_id])
after_fix
ModelA.where(xxx_id: params[:xxx_id], user_id: current_user.id)

3-1-3 プレースホルダーを使う

where句などにparamsを入れる場合はプレースホルダーを使いましょう。
paramsじゃなくても、常にプレースホルダー使えば考えることは少なくなります。

before_fix
ModelA.where("xxx_id = #{params[:xxx_id]}")
after_fix
ModelA.where("xxx_id = ?", params[:xxx_id])

3-1-4 xxx_typeなどのenumてきなパラメータや、orderのasc, descなどの定型文字が入ることを期待するパラメータ

case文などを使って、規定文字以外の場合は、何かに倒すか、エラーにしましょう。

before_fix
ModelA.order(xxx_id: params[:direction])
after_fix
direction = (params[:direction] == "desc") ? "desc" : "asc"
ModelA.order(xxx_id: direction)

3-2. DB以外の共有リソースの取り扱い

DB以外の共有リソース

  • ファイル
  • キャッシュ
  • S3
  • ディレクトリ
  • グローバル変数
  • クラス変数
  • インスタンス変数

問題

DBと違って、transactionなど、エラー時に巻き戻してくれる仕組みなどは基本的にないです。
よって
- 同時アクセスで書き換えられる
- エラーが起きると残ってしまって、次来た処理で不具合が起きる
などが起きえます。

解決方法

3-2-1. 一時的にしか使わないならTmpfile使う
before_fix
file_path = "some_file_name"
File.open(file_path, "w")
after_fix
Tmpfile.open
3-2-2. ensureでdeleteやcloseする(Tmpfile使えばcloseで削除されるので不要)
before_fix
file_path = "some_file_name"
File.open(file_path, "w") do |f|
  f.puts "xxx"
end
after_fix
file_path = "some_file_name"
begin
  file = File.open(file_path, "w") do |f|
    f.puts "xxx"
  end
ensure
  File.delete file_path
end
3-2-3. ファイルは追記モード(a)では使わない
before_fix
file_path = "some_file_name"
File.open(file_path, "a") do |f|
  f.puts "xxx"
end
before_fix
file_path = "some_file_name"
File.open(file_path, "w") do |f|
  f.puts "xxx"
end

最後に

改めてこの資料をつくったことで、性能やパフォーマンスの意識が少し上がった気がしています。
今後も常に心がけないとすぐに劣化していってしまうため、定期的に啓蒙活動を続けていく所存です。
利用しているお客様のためにも、抑えるべき部分はしっかり抑えてよりよいプロダクト開発を行っていきます。

また、今後こちらをベースにオンボーディング用資料として充実させていこうと思います。
「もっとこうしたほうがいい」「うちではこうしてる」などあればぜひコメントいただければ幸いです。

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

Railsでeach�(ループ)の中にモーダルを入れても変数が適応されない問題

問題

variables.each do |variable|という繰り返しのブロックの中で
それぞれの異なるvariableをmodalに渡す場合、
下記コードだと最初のvariableの値しか表示されない。

<% books.each |book| do %>
  <a href="#book_modal">クリックするとモーダルを表示するリンク</a> 

  <div class="modalList" id="book_modal">
    # ここはモーダル
  </div>
<% end %>

例えば、
variables = [var1, var2, var3, var4]
とした場合、本来期待する動きは
1つめのモーダルには、var1
2つめのモーダルには、var2
3つめのモーダルには、var3
4つめのモーダルには、var4
だが、上記だと4つのモーダルすべてに
variable = var1
が反映され、var2, var3, var4が反映されない。

解決策

モーダルのidが一意となるような実装にしてやればよい。

<% books.each |book| do %>
  <a href="#book_modal-<%= book.id %>">クリックするとモーダルを表示するリンク</a> 

  <div class="modalList" id="book_modal-<%= book.id %>">
    # ここはモーダル
  </div>
<% end %>

最初にあげたコードだと、4つのモーダルのidがすべて book_modalのため、
最初のvariableしか反映されなかった。

なので、Bookオブジェクトに一意に定められている idを使用して、
id = "book_modal-<%= book.id %>"
とした。

すると、

1つめのモーダルのid: book_modal-var1
2つめのモーダルのid: book_modal-var2
3つめのモーダルのid: book_modal-var3
4つめのモーダルのid: book_modal-var4

のように各モーダルのidが変わってくれるので、
eachで回している変数のそれぞれの値を反映させることができた。

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

RailsとLaravelの比較

はじめに

この記事はRailsとLaravelを比較分析してみた記事です.
Railsは以前から勉強していて,新たにLaravelを使ってみました.
Railsの勉強にはRuby on Rails 5 超入門改訂4版 基礎 Ruby on Rails (IMPRESS KISO SERIES)を使いました.
Ruby on Rails 5 超入門についてはチュートリアル形式で一見わかりやすいのですが,誤植などが多いのとレベルが優しすぎるのであまりオススメしません.

Laravelの勉強はPHPフレームワーク Laravel Webアプリケーション開発 バージョン5.5 LTS対応を使いました.買う前に中身を確認しなかった僕が悪いのですが,本の厚さの割に内容が薄いです.また辞書的に使う分には良いのですが,チュートリアル形式で作っていく感じではないので,フレームワークを使ったことのない人などにはオススメ出来ません.

RailsとLaravelの基本情報

*以下,2019/11/28現在の情報

Rails Laravel
言語 Ruby PHP
初リリース 2004年 2011年
github https://github.com/rails/rails https://github.com/laravel/framework
github start数 44.6K 19.5K

githubのstart数ではrailsの方がLarvelの倍くらいの数になっています.Railsはいま流行りと個人的に思っていたりするのですが,リリースからはもう10年以上経過していました.

Googleトレンドで調べてみた

Googleトレンドでは検索のトレンドを調べて,検索キーワードで比較したりできるサービスです.Googleは面白いサービスを作っていますね.
これを使ってRailsとLaravelについて調べてみました.

すべての国

https://trends.google.co.jp/trends/explore?date=all&q=Rails,Laravel

スクリーンショット 2019-11-28 16.02.49.png

日本

https://trends.google.co.jp/trends/explore?date=all&geo=JP&q=Rails,Laravel

スクリーンショット 2019-11-28 16.03.22.png

Laravelはリリースが2011年なので,2012年頃まではほぼ0です.
全ての国の場合ではLaravelがRailsをやや追い越したくらいになっています.日本の場合ではまだRailsを追いかけていますが,このままの状態が続くと追い越しそうです.

個人的分析

ここからはRailsとLaravelを使ってみた感想になります.

本,参考資料などの多さは? 勝者:Rails

Railsの方がネットでググったときなどの参考になる記事などが多いです.それはRailsで使われている言語のRubyの開発者が日本人(まつもとゆきひろ氏)なのでRubyの日本語資料がそもそも多いという点と,Laravelより7年早くリリースしているからかなと思っています.

*IT分野の中で日本発祥の技術はRubyと深層学習のChainerくらいしか知らない(他にあったらコメントください)のでRubyは本当すごいです.

勉強のしやすさは? 勝者:Rails

難易度的にもRailsとLaravelは遜色ないですが,やはり本や参考資料などが多い方が勉強しやすいです.以前Go言語のフレームワークのGinを使う機会があったのですが,公式以外のドキュメントが少なくて開発しにくかったことがあります.

使いやすさは? 同じ

  • railsにはscaffoldという色々必要なものを一発で作ってくれるコマンドがあるのですが,Laravelは標準でサポートしていません(あるけど,githubの更新も止まっているのでバージョンに寄っては今後使えなくなるかも).
  • Laravelは比較的命名規則などが緩いので,ネットで調べても色々な書き方をしているのでわかりにくいです.
  • 逆にRailsは規則が厳しいのですが,厳しいが故に,変数名などを修正するのが億劫になります.
  • Laravelはデフォルトでログイン認証などが入っているので,インストールなどの手間が省けます.

RailsとLaravelのどっちがいいの? 若干Laravelかな

ここまで読んだ人にはRailsの方が良い感じに見えますが,他のエンジニアの人の意見を聞くと

「Railsは比較的開発が速くできるからスタートアップに多いが,保守・運用や難しい機能にはRailsは向いておらず,JavaやPHPのフレームワークの方が開発が容易」

という意見が多かったです(社会人になって1ヶ月くらいなのでこの辺は自分の中で不確定要素).確かに求人とか見てるとPHPとかJavaの募集の方が多いなという印象を持ったのでLaravelとしました.

まとめ

Railsが最強(僕の大学の教授)という人もいれば,Railsはオワコンと言っている人もいますのでどうなんですかね...
僕の周囲ではRailsの方がよく耳にします.
Rails,LarvelはRubyとPHPという言語の違いは多少ありますが,考え方や使い方はほとんど同じです.個人的には勉強のしやすいRailsを最初にやってMVCの基礎を勉強して,Laravelとかに移ればいいかなと思います.

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

【Rails】もっと早く知りたかったデバッグ用gem 'better_errors','binding_of_caller'

はじめに

先日伊藤さんのこちらの動画(↓)を見ました。
プログラミング初心者歓迎!「エラーが出ました。どうすればいいですか?」から卒業するための基本と極意 - YouTube

「いや、なんでもっと早く見なかった!」と思うくらい具体的なデバッグ手法が諸々解説されています。

そこで出てきたgem better_errors&binding_of_callerの導入方法と見方について簡単にまとめます。

今まで

  • puts デバッグ
  • binding.pry
  • rails serverのログ

を使ってエラーと闘っていましたが、この2つのgemはもっと早く知りたかったです:sweat_smile:

この記事が役に立つ方

  • エラーに苦しんでいるRails初心者

この記事のメリット

  • デバッグの効率が上がる

環境

  • macOS Catalina 10.15.1
  • zsh: 5.7.1
  • Ruby: 2.6.5
  • Rails: 5.2.3
  • Docker: 19.03.5

better_errorsとは?

デフォルトのエラー画面をわかりやすく整形してくれるgem。

binding_of_callerとは?

上記better_errorsと一緒に使うことで、ブラウザ上でirbを使えるようになるgem。
※本記事では使用方法について触れていません。

導入方法

Gemfile
group :development do
  gem 'better_errors'
  gem 'binding_of_caller'
end

Gemfileに上記追記し、

bundle install

で完了。簡単!

※Dockerを使用している場合はもうひと手間必要

app/config/environments/development.rb
BetterErrors::Middleware.allow_ip! "0.0.0.0/0"

仮想環境を使っている方は、うまく動作しないようです。
私はDockerを使っていますが、上記コードを追記してサーバー再起動で動作しました。
Gem 『Better errors』が動かないとき | HippoBlog

使用例

ArgumentErrorが出ていた場合

form_withを使おうとしたらエラーが出た場合を例にします。

いつもの赤いエラー画面から表示が変わっています。
スクリーンショット 2019-11-28 14.54.16.png

エラーメッセージが最上部にあるのは変わりませんが、
その下に2つタブがあります。これが超便利。

  • Application Frames
  • All Frames

1. Application Frames

最初はApplication Framesが表示されています。
ここでは、自分の書いたコードを対象にエラーに関係する箇所を明示してくれています。
スクリーンショット 2019-11-28 14.54.16.png

2. All Frames

次に、All Framesをクリックすると、自分が書いた箇所以外(gemやActiveSupportなど)まで踏み込んでエラーに関係する箇所が表示されます。

それぞれクリックしていくとコードが表示されます。

例えば今回は、form_withのソースコードまで表示してくれます。
スクリーンショット 2019-11-28 14.55.22.png

form_withの最初の1行を抜き出すとこんな感じです。

def form_with(model: nil, scope: nil, url: nil, format: nil, **options, &block)

これで指定すべき引数がわかり、今回は

「いけね!そういえばmodel:って書き忘れた!」

と気づくことが出来ます。便利!


その他、NoMethodError~ for nil:NilClassとか出ていたら、ブラウザでそのまま変数の中身を確認したり出来て非常に便利です。
スクリーンショット 2019-11-28 15.57.41.png
※画像最下部がコンソールになっています。

おわりに

最後まで読んで頂きありがとうございました:bow_tone1:

デバッグ手法は学び始めの段階でなるべく多くリストアップしておいたほうが効率がいいと思いますが、better_errorsはもっと早く導入していたかったです:sweat_smile:

伊藤さんのYoutube、非常に勉強になるのでまた他の動画も見させて頂きます。

参考にさせて頂いたサイト(いつもありがとうございます)

プログラミング初心者歓迎!「エラーが出ました。どうすればいいですか?」から卒業するための基本と極意 - YouTube
プログラミング初心者歓迎!「エラーが出ました。どうすればいいですか?」から卒業するための基本と極意(解説動画付き) - Qiita
Gem 『Better errors』が動かないとき | HippoBlog
【Rails】better_errorsとbinding_of_callerで自分でエラーを解決できるようになろう【初心者向け】 - Qiita

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

openssl1.0系がbrewから消えたことでrailsでエラーが発生する時の対処法

openssl1.0系がbrewから消えたことでrailsでエラーが発生する時の対処法

次のようなエラーが発生する場合の対処法です。
Library not loaded: /opt/local/lib/libssl.1.0.0.dylib (LoadError)

openssl1.1系にアップデートされた状態で、postgresqlのバージョンが古いとエラーが発生します。
対処法としては、
brew upgrade postgresqlでpostgresqlをアップデートしたら直りました。

参考までに、openssl1.0系がbrewから消えた原因のHomebrewのコミットはこれです。
https://github.com/Homebrew/homebrew-core/commit/0349a7ca76f483483c6d5c1d4cfe6b458dee2665

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

Ruby on Rails のselectメソッドで生成されるoptionタグにdisabled属性を追加する

はじめに

Ruby on Rails のselectメソッドでされるoptionタグにdisabledを設定したい。

まとめ

第3引数の要素情報を入れている箇所に、ハッシュ形式で追加すれば、設定されました。
他の属性追加についても同様にできました。

<%= f.select :users, User.all.map { |user| [user.name, user.id, {disabled: true}] } %>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

cloud9って何なの?

cloud9の使い方

1.environmentはパソコンのローカル環境のこと

 以下の画像のPythonとかpracticeがenvironmentと呼ばれていて、environmentの一つ一つが仮想的な環境になっている。パソコン一台を仮想空間に持っているみたいなイメージ。environmentを4つ以上作ると有料なので注意してください!

スクリーンショット (2).png

 environmentを起動(OPEN IDEを押す)する度にEC2と呼ばれているAWSのサーバーのインスタンス(仮想サーバーの一部)が生成されて仮想サーバーを使うことが出来ます。逆にログアウトするとサーバーは返却される。environmentを使うときだけサーバーを借りて、使わないときは返却するというイメージ。大規模開発とかでは使う分だけお金を払うのでお得にサーバを利用できます。

 environmentは個別の環境なので、言語のバージョンの設定やMysqlの準備などはenvironmentごとに行う必要があります。アプリごとに環境を設定するのはめんどくさいと思うのでenvironmentごとの容量(1GB)超えないまでは複数のアプリ開発も同じenvironmentでやっちゃった方がいいと思います。

2.ターミナルを使おう!

 緑のプラスボタンを押して"New Terminal"を押すとターミナルを起動でき、仮想的なパソコン(environment)に命令を出すことが出来ます。コマンドはLinuxコマンドになるので、Windowsユーザの方は一通りLinuxコマンド勉強してから使った方がいいかもしれないです。
 スクリーンショット (4).png

3.作成したアプリケーションをプラウザで確認しよう

 2番で起動したターミナルで、作成したアプリケーションをAWSのプラウザで確認できます。以下の画像の左画面で、表示したいアプリケーションのディレクトリに移動してサーバを起動します(railsの場合は"rails s")。その後、上の一覧のPreviewを押して、”Preview running application”を押すと作成したアプリケーションを以下の画像の右側の表示が出て来ます。

 スクリーンショット (6).png

その右上の矢印ボタンを押すとcloud9のプラウザで以下のようにアプリケーションの確認が出来ます。

 スクリーンショット (8).png

※注意
cloud9のプラウザはたまにCSSの適用がされなかったり、明らかに通っているパスが通らなかったりという不具合があるので、そういう時はサーバの再起動をすると大抵直ります。

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

Railsでデータ抽出時、シンボルとクエリのSQL発行文の違い

環境

  • Rails: 6.0.0
  • Mysql: 5.7
  • Ruby: 2.6.3

やりたいこと

where句を使ってのデータ抽出

シンボル

hogehoge_controller.rb
ids = [1, 3]
message = Message.where(id: ids)

#=> SELECT `messages`.* FROM `messages` WHERE `messages`.`id` IN (1, 3) ORDER BY `messages`.`created_at` DESC

SQL文ベタ書き

hogehoge_controller.rb
ids = [1, 3]
message = Message.where(id: ids)

#=> SELECT `messages`.* FROM `messages` WHERE (id = 1,3) ORDER BY `messages`.`created_at` DESC
# "Mysql2::Error: Operand should contain 1 column(s)"というエラー発生

結論

シンボルだとIN句を使ってクエリを発行してくれる。

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

Railsで開発環境のURLをlocalhost:3000ではなくa.example.comとかにする方法

/etc/hosts ファイルをsudoで編集します。Linuxでも同様に動きます。

127.0.1.1      a.example.com
127.0.1.1      b.example.com

これで、アプリを起動させて、ブラウザーをそれらのいずれかに向けると、動作するはずです。次に、Railsアプリのサーバーを起動します。

rails s 
rails s -p 3001

最後に、ブラウザで次を指定します。

foo.example.com:3000
bar.example.com:3001

3000番とかを消したい場合

80番ポートで起動させてください

sudo rails server --port=80

参考

local Rails with domain name? [duplicate]

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

�deviseとvalidationのエラーメッセージを日本語化

【その1】

config/locales下に2つのファイルを新規作成。

ja.yml
devise.ja.yml

【その2】

ja.ymlに以下を貼り付け

ja.yml
---
ja:
  activerecord:
    errors:
      messages:
        record_invalid: 'バリデーションに失敗しました: %{errors}'
        restrict_dependent_destroy:
          has_one: "%{record}が存在しているので削除できません"
          has_many: "%{record}が存在しているので削除できません"
  date:
    abbr_day_names:
    - 
    - 
    - 
    - 
    - 
    - 
    - 
    abbr_month_names:
    - 
    - 1
    - 2
    - 3
    - 4
    - 5
    - 6
    - 7
    - 8
    - 9
    - 10
    - 11
    - 12
    day_names:
    - 日曜日
    - 月曜日
    - 火曜日
    - 水曜日
    - 木曜日
    - 金曜日
    - 土曜日
    formats:
      default: "%Y/%m/%d"
      long: "%Y年%m月%d日(%a)"
      short: "%m/%d"
    month_names:
    - 
    - 1
    - 2
    - 3
    - 4
    - 5
    - 6
    - 7
    - 8
    - 9
    - 10
    - 11
    - 12
    order:
    - :year
    - :month
    - :day
  datetime:
    distance_in_words:
      about_x_hours:
        one: 1時間
        other: %{count}時間
      about_x_months:
        one: 1ヶ月
        other: %{count}ヶ月
      about_x_years:
        one: 1
        other: %{count}
      almost_x_years:
        one: 1年弱
        other: "%{count}年弱"
      half_a_minute: 30秒前後
      less_than_x_seconds:
        one: 1秒以内
        other: "%{count}秒未満"
      less_than_x_minutes:
        one: 1分以内
        other: "%{count}分未満"
      over_x_years:
        one: 1年以上
        other: "%{count}年以上"
      x_seconds:
        one: 1
        other: "%{count}秒"
      x_minutes:
        one: 1
        other: "%{count}分"
      x_days:
        one: 1
        other: "%{count}日"
      x_months:
        one: 1ヶ月
        other: "%{count}ヶ月"
      x_years:
        one: 1
        other: "%{count}年"
    prompts:
      second: 
      minute: 
      hour: 
      day: 
      month: 
      year: 
  errors:
    format: "%{attribute}%{message}"
    messages:
      accepted: を受諾してください
      blank: を入力してください
      confirmation: %{attribute}の入力が一致しません
      empty: を入力してください
      equal_to: %{count}にしてください
      even: は偶数にしてください
      exclusion: は予約されています
      greater_than: %{count}より大きい値にしてください
      greater_than_or_equal_to: %{count}以上の値にしてください
      inclusion: は一覧にありません
      invalid: は不正な値です
      less_than: %{count}より小さい値にしてください
      less_than_or_equal_to: %{count}以下の値にしてください
      model_invalid: 'バリデーションに失敗しました: %{errors}'
      not_a_number: は数値で入力してください
      not_an_integer: は整数で入力してください
      odd: は奇数にしてください
      other_than: %{count}以外の値にしてください
      present: は入力しないでください
      required: を入力してください
      taken: はすでに存在します
      too_long: %{count}文字以内で入力してください
      too_short: %{count}文字以上で入力してください
      wrong_length: %{count}文字で入力してください
    template:
      body: 次の項目を確認してください
      header:
        one: "%{model}にエラーが発生しました"
        other: "%{model}に%{count}個のエラーが発生しました"
  helpers:
    select:
      prompt: 選択してください
    submit:
      create: 登録する
      submit: 保存する
      update: 更新する
  number:
    currency:
      format:
        delimiter: ","
        format: "%n%u"
        precision: 0
        separator: "."
        significant: false
        strip_insignificant_zeros: false
        unit: 
    format:
      delimiter: ","
      precision: 3
      separator: "."
      significant: false
      strip_insignificant_zeros: false
    human:
      decimal_units:
        format: "%n %u"
        units:
          billion: 十億
          million: 百万
          quadrillion: 千兆
          thousand: 
          trillion: 
          unit: ''
      format:
        delimiter: ''
        precision: 3
        significant: true
        strip_insignificant_zeros: true
      storage_units:
        format: "%n%u"
        units:
          byte: バイト
          eb: EB
          gb: GB
          kb: KB
          mb: MB
          pb: PB
          tb: TB
    percentage:
      format:
        delimiter: ''
        format: "%n%"
    precision:
      format:
        delimiter: ''
  support:
    array:
      last_word_connector: "、"
      two_words_connector: "、"
      words_connector: "、"
  time:
    am: 午前
    formats:
      default: "%Y年%m月%d日(%a) %H時%M分%S秒 %z"
      long: "%Y/%m/%d %H:%M"
      short: "%m/%d %H:%M"
    pm: 午後

https://github.com/svenfuchs/rails-i18n/blob/master/rails/locale/ja.yml


【その3】

devise.ja.ymlに以下を貼り付け

devise.ja.yml
ja:
  activerecord:
    attributes:
      user:
        confirmation_sent_at: パスワード確認送信時刻
        confirmation_token: パスワード確認用トークン
        confirmed_at: パスワード確認時刻
        created_at: 作成日
        current_password: 現在のパスワード
        current_sign_in_at: 現在のログイン時刻
        current_sign_in_ip: 現在のログインIPアドレス
        email: Eメール
        encrypted_password: 暗号化パスワード
        failed_attempts: 失敗したログイン試行回数
        last_sign_in_at: 最終ログイン時刻
        last_sign_in_ip: 最終ログインIPアドレス
        locked_at: ロック時刻
        password: パスワード
        password_confirmation: パスワード(確認用)
        remember_created_at: ログイン記憶時刻
        remember_me: ログインを記憶する
        reset_password_sent_at: パスワードリセット送信時刻
        reset_password_token: パスワードリセット用トークン
        sign_in_count: ログイン回数
        unconfirmed_email: 未確認Eメール
        unlock_token: ロック解除用トークン
        updated_at: 更新日
    models:
      user: ユーザ
  devise:
    confirmations:
      confirmed: アカウントを登録しました。
      new:
        resend_confirmation_instructions: アカウント確認メール再送
      send_instructions: アカウントの有効化について数分以内にメールでご連絡します。
      send_paranoid_instructions: メールアドレスが登録済みの場合、本人確認用のメールが数分以内に送信されます。
    failure:
      already_authenticated: すでにログインしています。
      inactive: アカウントが有効化されていません。メールに記載された手順にしたがって、アカウントを有効化してください。
      invalid: "%{authentication_keys}またはパスワードが違います。"
      last_attempt: もう一回誤るとアカウントがロックされます。
      locked: アカウントは凍結されています。
      not_found_in_database: "%{authentication_keys}またはパスワードが違います。"
      timeout: セッションがタイムアウトしました。もう一度ログインしてください。
      unauthenticated: アカウント登録もしくはログインしてください。
      unconfirmed: メールアドレスの本人確認が必要です。
    mailer:
      confirmation_instructions:
        action: メールアドレスの確認
        greeting: "%{recipient}様"
        instruction: 以下のリンクをクリックし、メールアドレスの確認手続を完了させてください。
        subject: メールアドレス確認メール
      email_changed:
        greeting: こんにちは、%{recipient}様。
        message: あなたのメール変更(%{email})のお知らせいたします。
        subject: メール変更完了。
      password_change:
        greeting: "%{recipient}様"
        message: パスワードが再設定されたことを通知します。
        subject: パスワードの変更について
      reset_password_instructions:
        action: パスワード変更
        greeting: "%{recipient}様"
        instruction: パスワード再設定の依頼を受けたため、メールを送信しています。下のリンクからパスワードの再設定ができます。
        instruction_2: パスワード再設定の依頼をしていない場合、このメールを無視してください。
        instruction_3: パスワードの再設定は、上のリンクから新しいパスワードを登録するまで完了しません。
        subject: パスワードの再設定について
      unlock_instructions:
        action: アカウントのロック解除
        greeting: "%{recipient}様"
        instruction: アカウントのロックを解除するには下のリンクをクリックしてください。
        message: ログイン失敗が繰り返されたため、アカウントはロックされています。
        subject: アカウントの凍結解除について
    omniauth_callbacks:
      failure: "%{kind} アカウントによる認証に失敗しました。理由:(%{reason})"
      success: "%{kind} アカウントによる認証に成功しました。"
    passwords:
      edit:
        change_my_password: パスワードを変更する
        change_your_password: パスワードを変更
        confirm_new_password: 確認用新しいパスワード
        new_password: 新しいパスワード
      new:
        forgot_your_password: パスワードを忘れましたか?
        send_me_reset_password_instructions: パスワードの再設定方法を送信する
      no_token: このページにはアクセスできません。パスワード再設定メールのリンクからアクセスされた場合には、URL をご確認ください。
      send_instructions: パスワードの再設定について数分以内にメールでご連絡いたします。
      send_paranoid_instructions: メールアドレスが登録済みの場合、パスワード再設定用のメールが数分以内に送信されます。
      updated: パスワードが正しく変更されました。
      updated_not_active: パスワードが正しく変更されました。
    registrations:
      destroyed: アカウントを削除しました。またのご利用をお待ちしております。
      edit:
        are_you_sure: 本当によろしいですか?
        cancel_my_account: アカウント削除
        currently_waiting_confirmation_for_email: "%{email} の確認待ち"
        leave_blank_if_you_don_t_want_to_change_it: 空欄のままなら変更しません
        title: "%{resource}編集"
        unhappy: 気に入りません
        update: 更新
        we_need_your_current_password_to_confirm_your_changes: 変更を反映するには現在のパスワードを入力してください
      new:
        sign_up: アカウント登録
      signed_up: アカウント登録が完了しました。
      signed_up_but_inactive: ログインするためには、アカウントを有効化してください。
      signed_up_but_locked: アカウントが凍結されているためログインできません。
      signed_up_but_unconfirmed: 本人確認用のメールを送信しました。メール内のリンクからアカウントを有効化させてください。
      update_needs_confirmation: アカウント情報を変更しました。変更されたメールアドレスの本人確認のため、本人確認用メールより確認処理をおこなってください。
      updated: アカウント情報を変更しました。
      updated_but_not_signed_in: あなたのアカウントは正常に更新されましたが、パスワードが変更されたため、再度ログインしてください。
    sessions:
      already_signed_out: 既にログアウト済みです。
      new:
        sign_in: ログイン
      signed_in: ログインしました。
      signed_out: ログアウトしました。
    shared:
      links:
        back: 戻る
        didn_t_receive_confirmation_instructions: アカウント確認のメールを受け取っていませんか?
        didn_t_receive_unlock_instructions: アカウントの凍結解除方法のメールを受け取っていませんか?
        forgot_your_password: パスワードを忘れましたか?
        sign_in: ログイン
        sign_in_with_provider: "%{provider}でログイン"
        sign_up: アカウント登録
      minimum_password_length: "(%{count}字以上)"
    unlocks:
      new:
        resend_unlock_instructions: アカウントの凍結解除方法を再送する
      send_instructions: アカウントの凍結解除方法を数分以内にメールでご連絡します。
      send_paranoid_instructions: アカウントが見つかった場合、アカウントの凍結解除方法を数分以内にメールでご連絡します。
      unlocked: アカウントを凍結解除しました。
  errors:
    messages:
      already_confirmed: は既に登録済みです。ログインしてください。
      confirmation_period_expired: の期限が切れました。%{period} までに確認する必要があります。 新しくリクエストしてください。
      expired: の有効期限が切れました。新しくリクエストしてください。
      not_found: は見つかりませんでした。
      not_locked: は凍結されていません。
      not_saved:
        one: エラーが発生したため %{resource} は保存されませんでした。
        other: "%{count} 件のエラーが発生したため %{resource} は保存されませんでした。"

https://github.com/tigrish/devise-i18n/blob/master/rails/locales/ja.yml

【その4】

config/application.rbに以下の一文を追記

application.rb
module Dansyalist
  class Application < Rails::Application
    config.i18n.default_locale = :ja
    
  end
end



以上4点にお気をつけて、快適な日本語ライフをお楽しみください。



ではまた!

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

バリデーションを設定しよう。

バリデーションとは

フォームから送られてきた値が正常な値であるか検証する機能です。
よく使う2つを見ていきましょう。

presence: true
unique: true


①値が空ではないか

フォームが空の場合は保存できなくします。

モデルに
validates :カラム名, presence: trueを追記

例)tweetsテーブルのtextカラムに入る値を入力必須にしたい↓

tweet.rb
class Tweet < ApplicationRecord
  validates :text, presence: true
end

textを入力しなかった場合にエラーとなり、保存できません。

②値が他と被ってないか

他のレコードに既に同じ値がある場合は保存できなくします。
唯一無二の値のことを、一意な値と言います。

モデルに、
validates :カラム名, unique: trueを追記

例)tweetsテーブルのtextカラムに入る値を一意にしたい↓

tweet.rb
class Tweet < ApplicationRecord
  validates :text, unique: true
end

まったく同じテキストは保存できなくなりました。
おはようとツイートできるのは1人だけ。早いもの勝ちとなります。

エラーメッセージを表示させる

保存できないことを知らせてあげましょう。

new.html.haml
- if @tweet.errors.any?
  %div
    %h2= "#{@tweet.errors.full_messages.count}件のエラーが発生しました。"
    %ul
      - @tweet.errors.full_messages.each do |message|
        %li= message



もしエラーメッセージが表示されない時は以下を疑いましょう。
form_withlocal: trueが抜けてる。

new.html.haml
= form_with model: tweet do |form| #誤り

= form_with model: tweet, local: true do |form| #正しい

僕はエラーメッセージが表示されなくて、2日間悩みました。



ではまた!

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

Railsチュートリアル 第10章 ユーザーの更新・表示・削除 - すべてのユーザーを表示する

何をするか

Usersコントローラーにindexアクションを実装していきます。これにより、「すべてのユーザーの一覧表示」という操作が可能になります。

追加機能として、以下の機能も実装していきます。

  • RDBへのサンプルユーザーの追加
  • ページネーション機能
    • 英語ではpagination=ページ分割
    • ユーザー一覧の出力を複数ページに分割する機能のこと

Railsチュートリアル本文においては、ユーザー一覧ページのモックアップとして、図 10.8が示されています。

ユーザー情報のセキュリティモデル

Railsチュートリアル本文においては、現在開発中のサンプルアプリケーションにおいて、ユーザー情報のセキュリティモデルは以下のように設計することとしています。

  • ユーザーのshowページは、非ログインユーザーも含め、すべての利用者に表示を許可する
  • ユーザーのindexページは、ログイン済のユーザーのみに表示を許可する

ユーザーの一覧ページ

「ユーザーのindexページは、ログイン済のユーザーのみに表示を許可する」という動作に対するテスト

表題内容の対偶をとると、「非ログインユーザーがユーザーのindexページを表示しようとした場合、表示を許可しない」という内容になります。「表示を許可しない」という場合の動作は、「ログインページにリダイレクトする」というのが最も自然ですね。

ということで、「非ログインユーザーがユーザーのindexページを表示しようとした場合」に対応するテストの実装は、以下のようになります。名前は「should redirect index when not logged in」とします。

test "should redirect index when not logged in" do
  get users_path
  assert_redirected_to login_url
end

実装箇所はtest/controllers/users_controller_test.rb内です。

test/controllers/users_controller_test.rb
  require 'test_helper'

  class UsersControllerTest < ActionDispatch::IntegrationTest

    def setup
      @user       = users(:rhakurei)
      @other_user = users(:mkirisame)
    end
+
+   test "should redirect index when not logged in" do
+     get users_path
+     assert_redirected_to login_url
+   end

    ...略
  end

現時点で、このテストは失敗します。

test/controllers/users_controller_test.rb(10行目)
test "should redirect index when not logged in" do
# rails test test/controllers/users_controller_test.rb:10
Running via Spring preloader in process 1376
Started with run options --seed 60339

ERROR["test_should_redirect_index_when_not_logged_in", UsersControllerTest, 0.728221700002905]
 test_should_redirect_index_when_not_logged_in#UsersControllerTest (0.73s)
AbstractController::ActionNotFound:         AbstractController::ActionNotFound: The action 'index' could not be found for UsersController
            test/controllers/users_controller_test.rb:11:in `block in <class:UsersControllerTest>'

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

Finished in 0.73096s
1 tests, 0 assertions, 0 failures, 1 errors, 0 skips

現時点でUsersコントローラーにindexアクションは実装されていないため、「Usersコントローラーにindexアクションが実装されていない」旨のエラーメッセージが出力されます。

Usersコントローラーにindexアクションを実装し、beforeフィルターの対象とする

続いてindexアクションを実装していきます。indexアクションの動作は、現状ではRailsデフォルトの動作とするため、実装は何も記述せずにメソッド定義だけ記述することとします。

「ユーザーのindexページは、ログイン済のユーザーのみに表示を許可する」という動作は、「indexアクションを、beforeフィルターのlogged_in_userに追加し、同メソッドによる保護対象とする」ことにより実現できます。早速app/controllers/users_controller.rbの内容を修正しましょう。

app/controllers/users_controller.rb
  class UsersController < ApplicationController
-   before_action :logged_in_user, only: [:edit, :update]
+   before_action :logged_in_user, only: [:index, :edit, :update]
    before_action :correct_user,   only: [:edit, :update]
+
+   def index
+   end

    ...略
  end

ここまでの実装が完了すると、テスト「should redirect index when not logged in」が成功するようになります。

# rails test test/controllers/users_controller_test.rb:10
Running via Spring preloader in process 1402
Started with run options --seed 22324

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

Finished in 0.54567s
1 tests, 1 assertions, 0 failures, 0 errors, 0 skips

indexビューを実装する

Usersコントローラーでindexアクションの実装が済んだら、今度はindexビューを実装します。中身は「全ユーザーが格納された変数を作成し、順々に表示する」というものになります。

indexビューを実装するために、Usersコントローラーのindexアクションに必要となる新たな実装

以下の動作をUsersコントローラーのindexアクションにに実装する必要があります。

  1. User.allを用い、RDBに保存された全ユーザーの情報を取得する
  2. 取得したユーザー情報を、ビューで使えるユーザー変数@usersに代入する

コードとしては以下のようになります。

@users = User.all

早速Usersコントローラーのindexアクションに反映しましょう。

app/controllers/users_controller.rb
  class UsersController < ApplicationController
    ...略

    def index
+     @users = User.all
    end

    ...略

  end

indexビューを実際に実装する

indexビューのファイル名はapp/views/users/index.html.erbとなります。現時点で当該ファイルは存在しないため、まず当該ファイルを新規に作成するところから始まります。

>>> touch app/views/users/index.html.erb

空のapp/views/users/index.html.erbが作成できたら、埋め込みRubyを記述していきます。

app/views/users/index.html.erb
<% 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>

上述埋め込みRubyコードで、特筆すべき点は以下です。

  • ユーザーを列挙する領域全体はul要素としている
  • eachメソッドでユーザーを列挙している
  • 各ユーザーのプロフィール画像と名前をliタグで囲っている
  • UsersHelper#gravatar_for:sizeオプションに、デフォルト以外の値を与えている
    • デフォルトは80、今回与えた値は50

(参考)UsersHelper#gravatar_forの現状の実装

なお、私の環境では、現状のUsersHelper#gravatar_forの実装は以下のようになっています。

UsersHelper#gravatar_for
def gravatar_for(user, options = { size:80 })
  gravatar_id = Digest::MD5::hexdigest(user.email.downcase)
  size = options[:size]
  gravatar_url = "https://secure.gravatar.com/avatar/#{gravatar_id}?s=#{size}"
  image_tag(gravatar_url, alt: user.name, class: "gravatar")
end

ユーザーのindexページの表示を整えるために、CSSに手を加える

app/assets/stylesheets/custom.scss
  ...略
+
+ .users {
+     list-style: none;
+     margin: 0;
+     li {
+         overflow: auto;
+         padding: 10px 0;
+         border-bottom: 1px solid $gray-lighter;
+     }
+ }

  ...略

ビューのヘッダー部に、ユーザー一覧ページへのリンクを追加する

ビューのヘッダー部には、すでにユーザー一覧ページへのリンクが存在します。リンク先は、現時点まで仮に#としてきましたが、ここまでの実装でユーザー一覧ページが表示できるようになったので、実際のユーザー一覧ページにリンクするようにします。

app/views/layouts/_header.html.erb
  <header class="navbar navbar-fixed-top navbar-inverse">
    <div class="container">
      ...略
      <nav>
        <ul class="nav navbar-nav navbar-right">
          ...略
          <% if logged_in? %>
-           <li><%= link_to "Users", '#' %></li>
+           <li><%= link_to "Users", users_path %></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", edit_user_path(current_user) %></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>

実際に動くユーザーのindex

ここまでの実装が完了すれば、ユーザーのindexは実際に動くようになります。

# rails test
Running via Spring preloader in process 1441
Started with run options --seed 33187

  36/36: [=================================] 100% Time: 00:00:03, Time: 00:00:03

Finished in 3.18539s
36 tests, 96 assertions, 0 failures, 0 errors, 0 skips

新たに実装したテストも含め、すべてのテストが無事通ることが確認できました。

スクリーンショット 2019-11-26 7.40.45.png

上記はログインユーザーでusersページを表示したスクリーンショットです。

演習 - ユーザーの一覧ページ

1. レイアウトにあるすべてのリンクに対して統合テストを書いてみましょう。ログイン済みユーザーとそうでないユーザーのそれぞれに対して、正しい振る舞いを考えてください。

ヒント: log_in_asヘルパーを使ってリスト 5.32にテストを追加してみましょう。

テストを実装するソースファイルはtest/integration/site_layout_test.rbとなります。

ログイン状態の再現に必要な処理

ログイン状態を再現するために、まずはsetupメソッドにより、fixtureから@userに有効なユーザー情報を与える必要があります。対応するコードは以下になります。

def setup
  @user = users(:rhakurei)
end

ログイン済みユーザー・非ログインユーザー共通に必要となるテスト

リクエストの内容は、「当該レイアウトを使うリソースへのGETリクエスト」であれば何でも構いません。今回は「/ へのGETリクエスト」とします。

get root_path

Home・Help・About・Contactへのリンクは、ログイン済みユーザー・非ログインユーザー関係なく共通です。対応するコードは以下になります。

ログイン済みユーザー・非ログインユーザー共通に必要となるテスト
assert_select "a[href=?]", root_path
assert_select "a[href=?]", help_path
assert_select "a[href=?]", about_path
assert_select "a[href=?]", contact_path

非ログインユーザーに必要となるテスト

ログイン済みユーザーでない場合、レイアウトに存在するリンクは以下の要件を満たす必要があります。

  • ログインページへのリンクが存在すること
  • ユーザー一覧ページへのリンクが存在しないこと
  • ユーザー情報ページへのリンクが存在しないこと
  • ユーザー情報編集ページへのリンクが存在しないこと
  • ログアウトページへのリンクが存在しないこと

対応するコードは以下になります。

非ログインユーザーに必要となるテスト
assert_select "a[href=?]", login_path
assert_select "a[href=?]", users_path,            count: 0
assert_select "a[href=?]", user_path(@user),      count: 0
assert_select "a[href=?]", edit_user_path(@user), count: 0
assert_select "a[href=?]", logout_path,           count: 0

ログイン済みユーザーに必要となるテスト

ログイン済みユーザーの場合、レイアウトに存在するリンクは以下の要件を満たす必要があります。

  • ログインページへのリンクが存在しないこと
  • ユーザー一覧ページへのリンクが存在すること
  • 自身のユーザー情報ページへのリンクが存在すること
  • 自身のユーザー情報編集ページへのリンクが存在しないこと
  • ログアウトページへのリンクが存在すること

対応するコードは以下になります。

ログイン済みユーザーに必要となるテスト
assert_select "a[href=?]", login_path, count: 0
assert_select "a[href=?]", users_path
assert_select "a[href=?]", user_path(@user)
assert_select "a[href=?]", edit_user_path(@user)
assert_select "a[href=?]", logout_path

test/integration/site_layout_test.rbの変更内容

test/integration/site_layout_test.rbの変更内容の全体像は以下のようになります。

test/integration/site_layout_test.rb
  require 'test_helper'

  class SiteLayoutTest < ActionDispatch::IntegrationTest
+
+   def setup
+     @user = users(:rhakurei)
+   end

    ...略
+
+   test "layout links without login" do
+     get root_path
+     assert_select "a[href=?]", root_path
+     assert_select "a[href=?]", help_path
+     assert_select "a[href=?]", about_path
+     assert_select "a[href=?]", contact_path
+     assert_select "a[href=?]", login_path
+     assert_select "a[href=?]", users_path,            count: 0
+     assert_select "a[href=?]", user_path(@user),      count: 0
+     assert_select "a[href=?]", edit_user_path(@user), count: 0
+     assert_select "a[href=?]", logout_path,           count: 0
+   end
+
+   test "layout links with login" do
+     log_in_as @user
+     get root_path
+     assert_select "a[href=?]", root_path
+     assert_select "a[href=?]", help_path
+     assert_select "a[href=?]", about_path
+     assert_select "a[href=?]", contact_path
+     assert_select "a[href=?]", login_path, count: 0
+     assert_select "a[href=?]", users_path
+     assert_select "a[href=?]", user_path(@user)
+     assert_select "a[href=?]", edit_user_path(@user)
+     assert_select "a[href=?]", logout_path
+   end
  end

今実装したテストが成功することを確認する

ここまで実装が完了すれば、test/integration/site_layout_test.rb全体を使ったテストを実行できるようになります。ここまでに記述してきた振る舞いは、本当に正しいのでしょうか。実際にやってみましょう。

# rails test test/integration/site_layout_test.rb
Running via Spring preloader in process 1584
Started with run options --seed 33479

  3/3: [===================================] 100% Time: 00:00:02, Time: 00:00:02

Finished in 2.33232s
3 tests, 25 assertions, 0 failures, 0 errors, 0 skips

テストは無事成功しました。ここまでに記述してきた振る舞いは確かに正しいようです。

サンプルのユーザー

Fakerというgem

Fakerはダミーデータを生成するためのgemです。「開発環境などで、開発中のアプリケーションの振る舞いを検証するため、ある程度まとまった数のデータが欲しい」という場合に便利です。

GemfileにFakerを追加する

Gemfile
  source 'https://rubygems.org'

  gem 'rails',        '5.1.6'
  gem 'bcrypt',       '3.1.12'
+ gem 'faker',        '1.7.3'
  ...略

  group :development do
    ...略
  end

  ...略

Faker gemは、通常開発環境でしか使わないため、通常はGemfileの:developmentグループ以下に追加します。しかしながら、Railsチュートリアルのサンプルプログラムにおいては、「本番環境でFakerを使う」という例外的な運用が発生するため、Faker gemをGemfileのグループ外に追加しています。

bundle installでFakerをインストールする

FakerをGemfileに追加したら、例によってbundle installを実行します。

# bundle install
...略
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.6.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 (< 2, >= 0.7)

    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.

うまくいきませんでした。RailsとFakerで、それぞれが別に依存するi18nというgemの要求バージョンが合わないことが原因のようです。

指示通りにbundle updateを実行してみます。

# bundle update
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.....
...略
Fetching faker 1.7.3
Installing faker 1.7.3
...略
Bundle updated!
Gems in the group production were not installed.

今度こそ無事にFaker gemがインストールできたようです。

100人のサンプルユーザーをRDB上に生成する

db/seeds.rbというファイルに、100人のサンプルユーザーを生成するためのコードを追加します。サンプルユーザーの内訳は以下のとおりです。

  • 「Example User」という名前とメールアドレスを持つ1人のユーザー
  • それらしい名前とメールアドレスを持つ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

このコードのポイントは以下です。

  • createではなくcreate!メソッドを用いている
    • ユーザーが無効な場合にnilを返すのではなく、例外を投げる
    • エラーを検知できる可能性を高めることを狙っている

RDBのリセットと、100人のユーザーの生成

RDBをリセットした上で、上記db/seeds.rbの内容に基づき、実際に100人のユーザーをサンプルアプリケーション上に生成してみます。

まずはRDBをリセットします。

# rails db:migrate:reset
Dropped database 'db/development.sqlite3'
Dropped database 'db/test.sqlite3'
Created database 'db/development.sqlite3'
Created database 'db/test.sqlite3'
...略
== [timestamp] AddRememberDigestToUsers: migrating =========================
-- add_column(:users, :remember_digest, :string)
   -> 0.0116s
== [timestamp] AddRememberDigestToUsers: migrated (0.0119s) ================

続いて、実際にdb/seeds.rbの内容をRDBに反映します。コマンドはrails db:seedです。

# rails db:seed

エラーがなければ、シェルには何も表示されずに実行が終わります。

100人のユーザーが生成された!

ログインしてindexページを表示してみましょう。

スクリーンショット 2019-11-26 18.22.26.png

Michael Hartl氏の手により、最初のいくつかのメールアドレスには、デフォルトのGravatar画像以外の写真が関連付けられています。その写真もきちんと表示されていますね。

# rails console --sandbox

>> User.count
   (1.1ms)  SELECT COUNT(*) FROM "users"
=> 100

rails consoleからUser.countを実行した結果も、きちんと100が返ってきました。

演習 - サンプルのユーザー

1. 試しに他人の編集ページにアクセスしてみて、10.2.2で実装したようにリダイレクトされるかどうかを確かめてみましょう。

現在、id=1のユーザーでログインしていることを前提とします。

--- !ruby/object:ActionController::Parameters
parameters: !ruby/hash:ActiveSupport::HashWithIndifferentAccess
  controller: users
  action: show
  id: '1'
permitted: false

/users/1/edit へのGETリクエストに対しては、最終的に「200 OK」のステータスコードが返ってきます。

Started GET "/users/1/edit" ...略
Completed 200 OK in 648ms (Views: 604.0ms | ActiveRecord: 9.6ms)
--- !ruby/object:ActionController::Parameters
parameters: !ruby/hash:ActiveSupport::HashWithIndifferentAccess
  controller: users
  action: edit
  id: '1'
permitted: false

続いて、/users/2/edit にGETリクエストを送出してみます。

Started GET "/users/2/edit" ...略
Redirected to http://localhost:8080/
Filter chain halted as :correct_user rendered or redirected
Completed 302 Found in 14ms (ActiveRecord: 8.1ms)


Started GET "/" for ...略
Completed 200 OK in 378ms (Views: 351.2ms | ActiveRecord: 2.8ms)

「ルートURLにリダイレクトされる」という動作は、確かに実装した通りですね。

ページネーション

ページネーションの必要性

「1つのページに大量のユーザーが表示される」というのは、検索ロボットや自動処理用のスクリプトによるアクセスであればともかく、人間が使う上では非常に使いづらいです。100人でも人間が扱うには多いですし、今後ユーザー数が数千人に増える可能性もあります。1つのページに表示されるユーザーの数は、人間が扱いやすい人数、例えば30人にしたいところです。

そうした要求を実現する機能がページネーション(pagination)です。Railsチュートリアル本文では、will_paginateメソッドによるページネーションを実装する、としています。

ページネーションの実装に必要なgemの追加

前提として、ページネーションの実装は以下の条件で行うものとします。

  • ページネーションにはwill_paginateメソッドを用いる
  • Bootstrapのページネーションスタイルを適用する

上記前提条件の元で新たに必要となるgemは、will_paginatebootstrap-will_paginateの2つになります。早速Gemfileに追加しましょう。

Gemfile
  source 'https://rubygems.org'

  gem 'rails',                   '5.1.6'
  gem 'bcrypt',                  '3.1.12'
  gem 'faker',                   '1.7.3'
+ gem 'will_paginate',           '3.1.6'
+ gem 'bootstrap-will_paginate', '1.0.0'
  ...略

続いてbundle installを実行します。

# bundle install
...略
Fetching gem metadata from https://rubygems.org/............
Fetching gem metadata from https://rubygems.org/.
Resolving dependencies....
...略
Fetching will_paginate 3.1.6
Installing will_paginate 3.1.6
Fetching bootstrap-will_paginate 1.0.0
Installing bootstrap-will_paginate 1.0.0
...略
Bundle complete! 26 Gemfile dependencies, 84 gems now installed.
Gems in the group production were not installed.
Bundled gems are installed into `/usr/local/bundle`

問題なくgemの追加が完了したようですね。

ページネーションを実際に使う

必要となる実装

追加・変更が必要となる実装は以下です。

  • indexビューでページネーションを使うようにする
  • Usersコントローラーのindexアクションの動作を、ページネーションに対応したものに変更する
    • 具体的には、RDBからユーザーを取得する処理を書き換える

indexページでページネーションを使うようにする

ページネーションのリンクを表示するためのメソッドをapp/views/users/index.html.erbに追加すればOKです。今回はwill_paginateメソッドですね。

app/views/users/index.html.erb
  <% 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 %>

Railsチュートリアル本文では、indexビュー内のwill_paginateメソッドの動作について以下のように説明しています。

  1. usersビューのコード中から、@usersオブジェクトを自動的に見つけ出す
  2. 他のページにアクセスするためのページネーションリンクを作成する

will_paginateをページの上下に2つ追加しているのは、「表示されているユーザー一覧の先頭・末尾両方にページネーションのリンクを表示する」ようにするためです。

will_paginateメソッドが必要とする@users変数の内容

will_paginateメソッドを使う場合、@users変数はpaginateメソッドの戻り値である必要があります。paginateメソッドの実行例を以下に示します。

# rails console --sandbox
>> User.paginate(page: 1)
  User Load (14.7ms)  SELECT  "users".* FROM "users" LIMIT ? OFFSET ?  [["LIMIT", 11], ["OFFSET", 0]]
   (0.8ms)  SELECT COUNT(*) FROM "users"
=> #<ActiveRecord::Relation [#<User id: 1,...
  • paginateメソッドは、キーが:pageで値がページ番号のハッシュを引数として取る
  • User.paginateは、:pageパラメーターに基づき、RDBからUserモデルに紐付けされたひとかたまり(デフォルトでは30)のデータを取り出す
    • 1ページ目は1番目から31番目のユーザー
    • 2ページ目は31番目から60番目のユーザー
    • 以下略
    • なお、キー:pageに対する値がnilの場合は、単に最初のページが返ってくる
# rails console --sandbox
>> User.paginate(page: nil)
  User Load (0.2ms)  SELECT  "users".* FROM "users" LIMIT ? OFFSET ?  [["LIMIT", 11], ["OFFSET", 0]]
   (0.3ms)  SELECT COUNT(*) FROM "users"
=> #<ActiveRecord::Relation [#<User id: 1,...

上記はキー:pageに対する値をnilとしてUser.paginateを実行した例です。

Usersコントローラーのindexアクションの動作を、ページネーションに対応したものに変更する

具体的には、indexアクション内のallpaginateに置き換えます。なお、params[:page]は、ビュー側のwill_paginateによって自動的に与えられます。

def index
  @users = User.paginate(page: params[:page])
end

実際にapp/controllers/users_controller.rbに対して行う変更は以下のとおりになります。

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

    def index
-     @users = User.all
+     @users = User.paginate(page: params[:page])
    end

    ...略
  end

ページネーション機能が使えるようになる

ここまでの実装が完了すれば、サンプルアプリケーションにおけるページネーション機能が有効になります。

スクリーンショット 2019-11-27 12.35.46.png

ページネーションのリンクが表示されているのがわかります。

スクリーンショット 2019-11-27 12.38.48.png

2ページ目は上記スクリーンショットのようになります。

演習 - ページネーション

1. Railsコンソールを開き、pageオプションにnilをセットして実行すると、1ページ目のユーザーが取得できることを確認してみましょう。

本文中で例を示したとおりです。

2. 先ほどの演習課題で取得したpaginationオブジェクトは、何クラスでしょうか? また、User.allのクラスとどこが違うでしょうか? 比較してみてください。

>> User.paginate(page: 1).class
=> User::ActiveRecord_Relation
>> User.paginate(page: 1).class.superclass
=> ActiveRecord::Relation
>> User.paginate(page: 1).class.superclass.superclass
=> Object

>> User.all.class                                    
=> User::ActiveRecord_Relation

paginationオブジェクトのクラスはUser::ActiveRecord_Relationです。User.allのクラスと違いはないようです。

ユーザー一覧のテスト

30人超のユーザーをfixtureに追加する

ページネーションに対するテストを行うためには、30人を上回るユーザー情報が必要となります。これだけの人数のユーザー情報を人手で追加するのは面倒です。

しかしながら、fixtureは埋め込みRubyに対応しています。2人のユーザー情報は手動で追加し、さらに30人のそれらしいユーザー情報はイテレータを使って追加することにしましょう。

test/fixtures/users.yml
  rhakurei:
    name: Reimu Hakurei
    email: rhakurei@example.com
    password_digest: <%= User.digest('password') %>

  mkirisame:
    name: Marisa Kirisame
    email: example.example@example.org
    password_digest: <%= User.digest('password') %>
+
+ skomeiji:
+   name: Satori Komeiji
+   email: example_example@example.net
+   password_digest: <%= User.digest('password') %>
+
+ rusami:
+   name: Renko Usami
+   email: example0@example.com
+   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 %>

indexに対する統合テストの生成

続いて、indexに対する統合テストを生成します。

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

indexに対する統合テストを書く

paginationクラスを持ったdivタグをチェックして、最初のページにユーザーがいることを確認する」というテストを書いていきます。

test/integration/users_index_test.rb
require 'test_helper'

class UsersIndexTest < ActionDispatch::IntegrationTest
  def setup
    @user = users(:rhakurei)
  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

テストは無事成功

test/integration/users_index_test.rbを対象としてテストを実行してみます。

# rails test test/integration/users_index_test.rb
Running via Spring preloader in process 12058
Started with run options --seed 56737

  1/1: [===================================] 100% Time: 00:00:03, Time: 00:00:03

Finished in 3.12612s
1 tests, 32 assertions, 0 failures, 0 errors, 0 skips

無事テストが成功しました。

余談 - 「div.paginationが無い」と言われてテストが失敗する場合

私はこのテストが通らずに数十分悩むこととなりました。Railsチュートリアル 第10章 - 「div.paginationが無い」と言われてテストが失敗する場合にて、顛末を記述しています。

演習 - ユーザー一覧のテスト

1. 試しにリスト 10.45にあるページネーションのリンク (will_paginateの部分) を2つともコメントアウトしてみて、リスト 10.48のテストがredに変わるかどうか確かめてみましょう。

app/views/users/index.html.erb
  <% provide(:title, 'All Users') %>
  <h1>All Users</h1>

- <%= will_paginate %>
+ <%# <%= 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 %> %>
# rails test test/integration/users_index_test.rb
Running via Spring preloader in process 12071
Started with run options --seed 63044

 FAIL["test_index_including_pagination", UsersIndexTest, 2.9782001999847125]
 test_index_including_pagination#UsersIndexTest (2.98s)
        Expected at least 1 element matching "div.pagination", found 0..
        Expected 0 to be >= 1.
        test/integration/users_index_test.rb:12:in `block in <class:UsersIndexTest>'

  1/1: [===================================] 100% Time: 00:00:02, Time: 00:00:02

Finished in 2.98834s
1 tests, 2 assertions, 1 failures, 0 errors, 0 skips

Expected at least 1 element matching "div.pagination", found 0..というメッセージが出てテストが失敗しています。paginationというクラスを持つdiv要素がない、という理由によるテストの失敗ですね。

2.1. 先ほどは2つともコメントアウトしましたが、1つだけコメントアウトした場合、テストが greenのままであることを確認してみましょう。

app/views/users/index.html.erb
  <% provide(:title, 'All Users') %>
  <h1>All Users</h1>

- <%= will_paginate %>
+ <%# <%= 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 %>
# rails test test/integration/users_index_test.rb
Running via Spring preloader in process 12084
Started with run options --seed 955

  1/1: [===================================] 100% Time: 00:00:02, Time: 00:00:02

Finished in 2.81957s
1 tests, 32 assertions, 0 failures, 0 errors, 0 skips

確かにテストが成功しますね。「will_paginateのリンクは2つ存在しなければテストが失敗する」という実装を実現するために…というのは次の演習です。

2.2. will_paginateのリンクが2つとも存在していることをテストしたい場合は、どのようなテストを追加すれば良いでしょうか?

ヒント: 表 5.2を参考にして、数をカウントするテストを追加してみましょう。

assert_select 'div.pagination'に、count: 2というオプションを追加します。

test/integration/users_index_test.rb
  require 'test_helper'

  class UsersIndexTest < ActionDispatch::IntegrationTest
    ...略

    test "index including pagination" do
      log_in_as(@user)
      get users_path
      assert_template 'users/index'
-     assert_select 'div.pagination'
+     assert_select 'div.pagination', count: 2
      User.paginate(page: 1).each do |user|
        assert_select 'a[href=?]', user_path(user), text: user.name
      end
    end
  end
# rails test test/integration/users_index_test.rb
Running via Spring preloader in process 12097
Started with run options --seed 32036

 FAIL["test_index_including_pagination", UsersIndexTest, 2.6599518999864813]
 test_index_including_pagination#UsersIndexTest (2.66s)
        Expected exactly 2 elements matching "div.pagination", found 1..
        Expected: 2
          Actual: 1
        test/integration/users_index_test.rb:12:in `block in <class:UsersIndexTest>'

  1/1: [===================================] 100% Time: 00:00:02, Time: 00:00:02

Finished in 2.66190s
1 tests, 2 assertions, 1 failures, 0 errors, 0 skips

Expected exactly 2 elements matching "div.pagination", found 1というメッセージが出てテストが失敗しています。「paginationというクラスを持つdiv要素が2つ必要」と言ってきているので、テスト対象は確かに正しいようです。

app/views/users/index.html.erbのすべてのコメントアウトを外し、正しい実装コードに戻します。

app/views/users/index.html.erb
  <% provide(:title, 'All Users') %>
  <h1>All Users</h1>

- <%# <%= will_paginate %> %>
+ <%= 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 %>
# rails test test/integration/users_index_test.rb
Running via Spring preloader in process 12110
Started with run options --seed 37850

  1/1: [===================================] 100% Time: 00:00:02, Time: 00:00:02

Finished in 2.50007s
1 tests, 32 assertions, 0 failures, 0 errors, 0 skips

今度こそテストは成功しました。

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

動機

indexページに関する機能的実装はここまでで完了し、テストも実装して成功する状態まで持っていくことができました。indexページに関するコードをさらに質の高いコードに書き換えていく準備はここまでで整っている…というのが現状の立ち位置です。

ここまで言及されていませんでしたが、Railsチュートリアル本文によれば、「Railsにはコンパクトなビューを作成するための素晴らしいツールがいくつもある」とのことです。そうしたコードを使ってindexページのコードをリファクタリングしたい、というのがこの節の動機です。

userパーシャルを実装し、renderで個別ユーザー情報のHTMLコードを生成するようにする

app/views/users/index.html.erb
  <% 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>
+     <%= render user %>
    <% end %>
  </ul>

  <%= will_paginate %>

上記コードのポイントは以下です。

  • Userクラスのuser変数を引数としてrenderを呼び出している
  • app/views/users/_user.html.erbというパーシャルが必要になる

まずはapp/views/users/_user.html.erbを作成しましょう。

>>> touch app/views/users/_user.html.erb

続いて、app/views/users/_user.html.erbの内容を記述していきます。

app/views/users/_user.html.erb
<li>
  <%= gravatar_for user, size: 50 %>
  <%= link_to user.name, user %>
</li>

ここまでで、「userパーシャルを実装し、renderで個別ユーザー情報のHTMLコードを生成するようにする」というリファクタリングはひとまず完了しました。

実は、render@usersを直接引数に取ることができる

先ほど、「Userクラスのuser変数を引数としてrenderを呼び出している」と言及しました。実は、@usersを直接引数とし、ブロックを使わない形でrenderを呼び出すというリファクタリングが可能です。

app/views/users/index.html.erb
  <% provide(:title, 'All Users') %>
  <h1>All Users</h1>

  <%= will_paginate %>

  <ul class="users">
-   <% @users.each do |user| %>
-     <%= render user %>
-   <% end %>
+   <%= render @users %>   
  </ul>

  <%= will_paginate %>

上記コードのポイントは以下です。

  • Railsは、@usersをUserオブジェクトのコレクションであると推測する
  • Userオブジェクトのコレクションを引数としてrenderを呼び出した際、renderの動作は以下となる
    • Userオブジェクトのコレクションを列挙する
    • Userオブジェクトの各インスタンスを、_user.html.erbパーシャルで出力する

最後にテストを実行します。

# rails test
Running via Spring preloader in process 12214
Started with run options --seed 3088

  39/39: [=================================] 100% Time: 00:00:05, Time: 00:00:05

Finished in 5.56925s
39 tests, 146 assertions, 0 failures, 0 errors, 0 skips

テストは無事成功しました。

演習 - パーシャルのリファクタリング

1. リスト 10.52にあるrenderの行をコメントアウトし、テストの結果がredに変わることを確認してみましょう。

app/views/users/index.html.erb
  <% provide(:title, 'All Users') %>
  <h1>All Users</h1>

  <%= will_paginate %>

  <ul class="users">
-   <%= render @users %>
+   <%# <%= render @users %> %>
  </ul>

  <%= will_paginate %>
# rails test test/integration/users_index_test.rb
Running via Spring preloader in process 12201
Started with run options --seed 4733

 FAIL["test_index_including_pagination", UsersIndexTest, 2.5006732000038028]
 test_index_including_pagination#UsersIndexTest (2.50s)
        Expected at least 1 element matching "a[href="/users/14035331"]", found 0..
        Expected 0 to be >= 1.
        test/integration/users_index_test.rb:14:in `block (2 levels) in <class:UsersIndexTest>'
        test/integration/users_index_test.rb:13:in `block in <class:UsersIndexTest>'

  1/1: [===================================] 100% Time: 00:00:02, Time: 00:00:02

Finished in 2.51114s
1 tests, 3 assertions, 1 failures, 0 errors, 0 skips

Expected at least 1 element matching "a[href="/users/14035331"]", found 0というのは、「ユーザー情報へのリンクが描画されていない」という趣旨のメッセージですね。

演習が終わったら、app/views/users/index.html.erbのコードを元に戻しておくことも忘れずに。

app/views/users/index.html.erb
  <% provide(:title, 'All Users') %>
  <h1>All Users</h1>

  <%= will_paginate %>

  <ul class="users">
-   <%# <%= render @users %> %>
+   <%= render @users %>
  </ul>

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

ルーティングのネスト

いつ使うの

ツイートにコメントできるようにする時を考えます。
コメントのparamsの中に、どのツイートに対してのコメントなのかわかるようにそのツイートのid情報を含めます。

やり方→ルーティングをネストさせる

実際に例を見たほうが早いです。

routes.rb
Rails.application.routes.draw do
  resources :tweets do
    resources :comments, only: [:create, :destroy]
  end
end

このように記述し、
rails routesを叩いてルーティングを確認すると、、、

ターミナル
tweet_comments POST   /tweets/:tweet_id/comments(.:format)                                                     comments#create
 tweet_comment DELETE /tweets/:tweet_id/comments/:id(.:format)                                                 comments#destroy

真ん中にtweet_idが含まれてるぅ!!!
これでどのツイートに対してのコメントかわかるようになったね!!!

【注意】form_withでインスタンスを2つ渡す

haml
- form_with model: [@tweet, @comment] do |form|
  = form.text_field :text
  = form.submit

@tweetを渡すことでtweet_idを渡せるようになります。
お忘れなく。



ではまた!

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

【Rails】SSLの導入【Rails Tutorial 7章まとめ】

SSL

Secure Sockets Layer(SSL)は、ローカルのサーバーからネットワークに流れるデータを暗号化し、セキュリティを強化する技術である。
SSLを使うとURLがhttpからhttpsに変わる。

SSLの導入

本番環境用の設定ファイルであるproduction.rbのconfig.force_sslをtrueに設定し、本番環境でSSLを使用させる。

config/environments/production.rb
Rails.application.configure do
  .
  .
  .
  # Force all access to the app over SSL, use Strict-Transport-Security,
  # and use secure cookies.
  config.force_ssl = true
  .
  .
  .
end

50行目あたりにある。

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

【Rails】ユーザー登録フォームの作成その3 ユーザー登録の成功【Rails Tutorial 7章まとめ】

ユーザー登録成功時の処理

ユーザー登録に成功すると、なぜかnewページに戻る。
これはcreateアクションでユーザーが作成されたあと、対応するcreateビューに行こうとするが、createビューが存在しないためである。
createビューは必要ないため、ここではユーザーのプロフィールページにリダイレクトすることにする。

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

user_url(@user)の行き先は/user/:idへのGETリクエストであり、showアクションに到達する。
また、リダイレクトの場合名前付きルートは_pathではなく_urlを使用する。
なお、user_url(@user)を単に@userと書いてもよいらしい。

flash変数を使って、ユーザー登録成功時のメッセージを表示している。

ユーザー登録成功時のテスト

ユーザー登録成功時のテストを書く。

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_not flash.empty?
  end
end

ユーザー登録失敗時のテストと基本は同じ。
こちらはassert_differenceを使っている。
これはブロックの前後で第一引数が第二引数分だけ増減したことを確認する。
ユーザー登録の成功により、User.countは1増えているはずである。

リダイレクト先に移動するにはfollow_redirect!を使う。

flashが表示されていることも確認しておく。

flashの表示

flashをレイアウトに追加して表示できるようにする。

app/views/layouts/application.html
  <body>
    <%= render 'layouts/header' %>
    <div class="container">
      <% flash.each do |message_type, message| %>
        <div class="alert alert-<%= message_type %>"><%= message %></div>
      <% end %>
      <%= yield %>
      <%= render 'layouts/footer' %>
      <%= debug(params) if Rails.env.development? %>
    </div>
    .
    .
    .
  </body>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】ユーザー登録フォームの作成その2 登録失敗時のエラー【Rails Tutorial 7章まとめ】

ユーザー登録に失敗した場合の処理

ユーザー登録に失敗した場合は、renderメソッドを使って新規登録ページに戻り、エラーメッセージを表示する。
renderを使うことで、newアクションの@user = User.newは実行されず、以前送信した内容がフォーム内に保持される。

エラーメッセージはパーシャルを使って分けておくことにする。
フォーム内にはエラーメッセージ用パーシャルへのrenderを書く。

app/views/users/new.html
<%= form_for(@user) do |f| %>
  <%= render 'shared/error_messages' %>

  <%= 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 "Create my account", class: "btn btn-primary" %>
<% end %>

各フォームについているform_controlクラスは、bootstrap用である。

エラーメッセージ用のパーシャルは次のようである。
app/views/shared/_error_messages.html.erb

app/views/shared/_error_messages.html
<% if @user.errors.any? %>
  <div id="error_explanation">
    <div class="alert alert-danger">
      The form contains <%= pluralize(@user.errors.count, "error") %>.
    </div>
    <ul>
    <% @user.errors.full_messages.each do |msg| %>
      <li><%= msg %></li>
    <% end %>
    </ul>
  </div>
<% end %>

コントローラにまたがって共通で使うパーシャルはsharedディレクトリに入れておく。

ここで、any?メソッドはempty?メソッドの逆であり、要素が一つでもある場合はtrue、ない場合はfalseを返す。
@user.errors.countは、エラーの個数を返す。

pluralizeメソッドは、第一引数に整数を、第二引数に英単語をとり、整数値に応じて英単語を複数化する。
不規則活用にも使える。

>> helper.pluralize(1, "error")
=> "1 error"
>> helper.pluralize(5, "error")
=> "5 errors"

エラーが起こり、newページに戻されると、エラーメッセージが表示される。
また、Railsによってfield_with_errorsクラスを持つdivタグによってフォームのラベルとフォームそのものを囲んでくれる。
スクリーンショット 2019-11-28 4.22.05.jpg
これによってcssを使ってエラーを起こしたフォームの枠を赤くしたりできる。
スクリーンショット 2019-11-28 4.23.41.jpg
cssは以下のよう。

app/assets/stylesheets/custom.scss
/* forms */
.
.
.
#error_explanation {
  color: red;
  ul {
    color: red;
    margin: 0 0 30px 0;
  }
}

.field_with_errors {
  @extend .has-error;
  .form-control {
    color: $state-danger-text;
  }
}

has_errorはbootstrapのcssクラスである。

登録エラー時のテスト

新規ユーザー登録用の統合テストを生成する。

$ rails generate integration_test users_signup

登録エラー時のテストを書く。

test/integration/users_signup_test.rb
require 'test_helper'

class UsersSignupTest < ActionDispatch::IntegrationTest

  test "invalid signup information" do
    get signup_path
    assert_template 'users/new'
    assert_no_difference 'User.count' do
      post users_path, params: { user: { name:  "",
                                         email: "user@invalid",
                                         password:              "foo",
                                         password_confirmation: "bar" } }
    end
    assert_template 'users/new'
  end
end

①まずsignup_pathにGETリクエストを送信する。行き先はnewアクションのnewビューである。
②newビューが表示されていることを確認する。
③assert_no_differenceを使って、ユーザー登録操作の前後でユーザー数が変わらないことを確認する。ユーザー登録が失敗すればユーザー数は変わらないはずである。
④users_pathにPOSTリクエストを送信する。postメソッドの第二引数にはparamsハッシュを指定する。
(paramsは変数でありハッシュでもあるらしい...よく分からない)
⑤ユーザー登録が失敗し、renderメソッドによってnewページに戻っていることを確認する。

必要であれば
⑥エラーメッセージが表示されていることを確認する。

assert_select 'div#<CSS id for error explanation>'
assert_select 'div.<CSS class for field with error>'

URLの修正

登録エラーでnewページに戻ると、URLが/signupから/usersに変わる。
これはusersリソースのRESTfulなルーティングによるもの。
フォームの送信は/usersのPOSTリクエストに送られて、createアクションに到達する。
その後登録エラーでnewページに戻るが、リダイレクトではなく再読み込みしていないので、URLはPOSTリクエストがされた時のままになる。
(という認識であってるかな?)

これを修正するためにルーティングを変更する。
URLが/signupのPOSTリクエストを作る(行き先はcreateアクション)。

config/routes.rb
Rails.application.routes.draw do
  .
  .
  .
  get  '/signup',  to: 'users#new'
  post '/signup',  to: 'users#create'
  resources :users
end

フォームの送信先もこのルーティングに変更する。

app/views/users/new.html.erb
<%= form_for(@user, url: signup_path) do |f| %>
  .
  .
  .
<% end %>

テストのPOSTリクエスト部分を修正する。また、form_forから生成されるformタグのaction属性(送信先のURLを値にとる)が正しいURLになっていることを確認する。

test/integration/users_signup_test.rb
  test "invalid signup information" do
    get signup_path
    assert_template 'users/new'
    assert_no_difference 'User.count' do
      post signup_path, params: { user: { name:  "",
                                         email: "user@invalid",
                                         password:              "foo",
                                         password_confirmation: "bar" } }
    end
    assert_template 'users/new'
    assert_select 'form[action="/signup"]'
  end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】ユーザー登録フォームの作成その2 登録失敗時のエラー【Rails Tutorial 7章まとめ】

ユーザー登録に失敗した場合の処理

ユーザー登録に失敗した場合は、renderメソッドを使って新規登録ページに戻り、エラーメッセージを表示する。
renderを使うことで、newアクションの@user = User.newは実行されず、以前送信した内容がフォーム内に保持される。

エラーメッセージはパーシャルを使って分けておくことにする。
フォーム内にはエラーメッセージ用パーシャルへのrenderを書く。

app/views/users/new.html
<%= form_for(@user) do |f| %>
  <%= render 'shared/error_messages' %>

  <%= 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 "Create my account", class: "btn btn-primary" %>
<% end %>

各フォームについているform_controlクラスは、bootstrap用である。

エラーメッセージ用のパーシャルは次のようである。
app/views/shared/_error_messages.html.erb

app/views/shared/_error_messages.html
<% if @user.errors.any? %>
  <div id="error_explanation">
    <div class="alert alert-danger">
      The form contains <%= pluralize(@user.errors.count, "error") %>.
    </div>
    <ul>
    <% @user.errors.full_messages.each do |msg| %>
      <li><%= msg %></li>
    <% end %>
    </ul>
  </div>
<% end %>

コントローラにまたがって共通で使うパーシャルはsharedディレクトリに入れておく。

ここで、any?メソッドはempty?メソッドの逆であり、要素が一つでもある場合はtrue、ない場合はfalseを返す。
@user.errors.countは、エラーの個数を返す。

pluralizeメソッドは、第一引数に整数を、第二引数に英単語をとり、整数値に応じて英単語を複数化する。
不規則活用にも使える。

>> helper.pluralize(1, "error")
=> "1 error"
>> helper.pluralize(5, "error")
=> "5 errors"

エラーが起こり、newページに戻されると、エラーメッセージが表示される。
また、Railsによってfield_with_errorsクラスを持つdivタグによってフォームのラベルとフォームそのものを囲んでくれる。
スクリーンショット 2019-11-28 4.22.05.jpg
これによってcssを使ってエラーを起こしたフォームの枠を赤くしたりできる。
スクリーンショット 2019-11-28 4.23.41.jpg
cssは以下のよう。

app/assets/stylesheets/custom.scss
/* forms */
.
.
.
#error_explanation {
  color: red;
  ul {
    color: red;
    margin: 0 0 30px 0;
  }
}

.field_with_errors {
  @extend .has-error;
  .form-control {
    color: $state-danger-text;
  }
}

has_errorはbootstrapのcssクラスである。

登録エラー時のテスト

新規ユーザー登録用の統合テストを生成する。

$ rails generate integration_test users_signup

登録エラー時のテストを書く。

test/integration/users_signup_test.rb
require 'test_helper'

class UsersSignupTest < ActionDispatch::IntegrationTest

  test "invalid signup information" do
    get signup_path
    assert_template 'users/new'
    assert_no_difference 'User.count' do
      post users_path, params: { user: { name:  "",
                                         email: "user@invalid",
                                         password:              "foo",
                                         password_confirmation: "bar" } }
    end
    assert_template 'users/new'
  end
end

①まずsignup_pathにGETリクエストを送信する。行き先はnewアクションのnewビューである。
②newビューが表示されていることを確認する。
③assert_no_differenceを使って、ユーザー登録操作の前後でユーザー数が変わらないことを確認する。ユーザー登録が失敗すればユーザー数は変わらないはずである。
④users_pathにPOSTリクエストを送信する。postメソッドの第二引数にはparamsハッシュを指定する。
(paramsは変数でありハッシュでもあるらしい...よく分からない)
⑤ユーザー登録が失敗し、renderメソッドによってnewページに戻っていることを確認する。

必要であれば
⑥エラーメッセージが表示されていることを確認する。

assert_select 'div#<CSS id for error explanation>'
assert_select 'div.<CSS class for field with error>'

URLの修正

登録エラーでnewページに戻ると、URLが/signupから/usersに変わる。
これはusersリソースのRESTfulなルーティングによるもの。
フォームの送信は/usersのPOSTリクエストに送られて、createアクションに到達する。
その後登録エラーでnewページに戻るが、リダイレクトではなく再読み込みしていないので、URLはPOSTリクエストがされた時のままになる。
(という認識であってるかな?)

これを修正するためにルーティングを変更する。
URLが/signupのPOSTリクエストを作る(行き先はcreateアクション)。

config/routes.rb
Rails.application.routes.draw do
  .
  .
  .
  get  '/signup',  to: 'users#new'
  post '/signup',  to: 'users#create'
  resources :users
end

フォームの送信先もこのルーティングに変更する。

app/views/users/new.html.erb
<%= form_for(@user, url: signup_path) do |f| %>
  .
  .
  .
<% end %>

テストのPOSTリクエスト部分を修正する。また、form_forから生成されるformタグのaction属性(送信先のURLを値にとる)が正しいURLになっていることを確認する。

test/integration/users_signup_test.rb
  test "invalid signup information" do
    get signup_path
    assert_template 'users/new'
    assert_no_difference 'User.count' do
      post signup_path, params: { user: { name:  "",
                                         email: "user@invalid",
                                         password:              "foo",
                                         password_confirmation: "bar" } }
    end
    assert_template 'users/new'
    assert_select 'form[action="/signup"]'
  end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】ユーザー登録フォームの作成【Rails Tutorial 7章まとめ】

newアクションとルーティング

ユーザーを新規登録するため、newアクションで新しいUserオブジェクトを作成し、@user変数に入れておく。

app/controllers/users_controller.rb
class UsersController < ApplicationController

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

  def new
    @user = User.new
  end
end

ルーティングはURLを/signupとして、usersコントローラのnewアクションに紐づけておく。

config/routes.rb
Rails.application.routes.draw do
  .
  .
  .
  get  '/signup', to: 'users#new'
  resources :users
end

ユーザー登録フォーム

form_forヘルパーメソッドを使って、ユーザーの新規登録フォームを作る。
form_forはUserオブジェクトを引数にとり、その属性でフォームを生成する。

ユーザー登録フォームは次のようになる。
app/views/users/new.html.erb

app/views/users/new.html
<% provide(:title, 'Sign up') %>
<h1>Sign up</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_for(@user) do |f| %>
      <%= f.label :name %>
      <%= f.text_field :name %>

      <%= f.label :email %>
      <%= f.email_field :email %>

      <%= f.label :password %>
      <%= f.password_field :password %>

      <%= f.label :password_confirmation, "Confirmation" %>
      <%= f.password_field :password_confirmation %>

      <%= f.submit "Create my account", class: "btn btn-primary" %>
    <% end %>
  </div>
</div>

このコードにより生成されるhtmlは次のようになる。

app/views/users/new.html
<form accept-charset="UTF-8" action="/users" class="new_user"
      id="new_user" method="post">
  <input name="utf8" type="hidden" value="&#x2713;" />
  <input name="authenticity_token" type="hidden"
         value="NNb6+J/j46LcrgYUC60wQ2titMuJQ5lLqyAbnbAUkdo=" />
  <label for="user_name">Name</label>
  <input id="user_name" name="user[name]" type="text" />

  <label for="user_email">Email</label>
  <input id="user_email" name="user[email]" type="email" />

  <label for="user_password">Password</label>
  <input id="user_password" name="user[password]"
         type="password" />

  <label for="user_password_confirmation">Confirmation</label>
  <input id="user_password_confirmation"
         name="user[password_confirmation]" type="password" />

  <input class="btn btn-primary" name="commit" type="submit"
         value="Create my account" />
</form>

form_forを使ったフォームの構造

form_forの役割

form_forは変数fを使ったブロックをとる構造になっており、次のようなhtmlを生成する。

<%= form_for(@user) do |f| %>
  .
  .
  .
<% end %><form accept-charset="UTF-8" action="/users" class="new_user"
      id="new_user" method="post">
  .
  .
  .
</form>

action属性は"/users"、method属性は"post"となっている。
この2つの属性は、/usersに対してHTTPのPOSTリクエストを送信する、といった指示をしている。
すると、Usersリソースが提供するRESTfulなルート(https://qiita.com/kagamiya9/items/48f66b20aee03fe9da1f )に基づいて、createアクションに行き着く。

このような流れが自動でできるのは、次のような理由らしい。
①form_forの引数は@userであり、Railsは@userがUserクラスであることを認識する。
@userはnewアクションで新規作成されているため、Railsはpostメソッドを使ってフォームを構築すべきだと判断する。

各入力フォーム

nameの入力フォームは次のようになる。

<%= f.label :name %>
<%= f.text_field :name %><label for="user_name">Name</label>
<input id="user_name" name="user[name]" type="text" />

emailの入力フォームは次のようになる。

<%= f.label :email %>
<%= f.email_field :email %><label for="user_email">Email</label>
<input id="user_email" name="user[email]" type="email" />

nameのtype属性は"text"だが、emailでは"email"である。
前者はf.text_field、後者はf.email_fieldとすることでtype属性がそれぞれ決まっている。
こうすると、スマホなどではメールアドレス入力用のキーボードが表示されるようになっている。

passwordの入力フォームは次のようになる。

<%= f.label :password %>
<%= f.password_field :password %><label for="user_password">Password</label>
<input id="user_password" name="user[password]" type="password" />

type属性がpasswordの場合、フォームに文字を入力すると黒丸•で表示されるようになる。

password_confiramtionの入力フォームは次のようになる。

<%= f.label :password_confirmation, "Confirmation" %>
<%= f.password_field :password_confirmation %><label for="user_password_confirmation">Confirmation</label>
<input id="user_password_confirmation"
         name="user[password_confirmation]" type="password" />

password_confirmationでは、f.labelの第二引数に文字列"Confirmation"をとっている。
これにより、フォーム上のラベルが指定した文字列になる。
他のフォームは属性ごとに自動で設定されている。
(nameはNameになる。なお、"Confirmation"を除くと、Password confirmationになる)
サブミットボタンも同様で、value属性が任意の文字列になる。

Createアクションの作成

name属性によるハッシュの構成

<input id="user_name" name="user[name]" - - - />
<input id="user_password" name="user[password]" - - - />

inputには特殊なname属性がついている。
Railsはnameの値を使って、初期化したハッシュを (params変数経由で) 構成する。

どういうことかというと、送信された内容はまず次のようなハッシュになり、params変数に代入される。

params = { users: { name: "Foo Bar", email: "foo@invalid", 
           password: "foo", password_confirmation: "bar" } }

createアクションでは、このparams変数を使ってユーザーを新規登録する。

@user = User.new(params[:user])

:userシンボルの値は、入力される属性(nameやemail)とその値からなるハッシュなので、上のコードは次のコードと同じである。

@user = User.new(name: "Foo Bar", email: "foo@invalid",
                 password: "foo", password_confirmation: "bar")

Strong Parameters

上のようなコードはセキュリティ上問題があるらしい。
paramsをそのまま送信すると、管理者用の属性であるadminなどの値を送信して、管理者権限を奪われるからだとか。
この辺は説明が非常に分かりにくいのだが、要は許可された属性以外は送信できないようにする、ということだ。

結局のところ、次のようなuser_paramsメソッドをコントローラ内に作る。
これはStrong Parametersと呼ばれる。
先の問題があるコードは、マスアサイメントと呼ばれる。

app/controllers/users_controller.rb
class UsersController < ApplicationController
  .
  .
  .
  def create
    @user = User.new(user_params)
    if @user.save
      # 保存の成功をここで扱う。
    else
      render 'new'
    end
  end

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

private内にあるメソッドはweb経由で外部に晒されることがない。

ユーザー登録に失敗した場合の処理

ユーザー登録に失敗した場合は、renderメソッドを使って新規登録ページに戻り、エラーメッセージを表示する。
エラー部分は長いので別記事にまとめることにする。

ユーザー登録に成功した場合の処理

成功部分も別記事にまとめることにする。

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

【Rails】ユーザー登録フォームの作成その1【Rails Tutorial 7章まとめ】

newアクションとルーティング

ユーザーを新規登録するため、newアクションで新しいUserオブジェクトを作成し、@user変数に入れておく。

app/controllers/users_controller.rb
class UsersController < ApplicationController

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

  def new
    @user = User.new
  end
end

ルーティングはURLを/signupとして、usersコントローラのnewアクションに紐づけておく。

config/routes.rb
Rails.application.routes.draw do
  .
  .
  .
  get  '/signup', to: 'users#new'
  resources :users
end

ユーザー登録フォーム

form_forヘルパーメソッドを使って、ユーザーの新規登録フォームを作る。
form_forはUserオブジェクトを引数にとり、その属性でフォームを生成する。

ユーザー登録フォームは次のようになる。
app/views/users/new.html.erb

app/views/users/new.html
<% provide(:title, 'Sign up') %>
<h1>Sign up</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_for(@user) do |f| %>
      <%= f.label :name %>
      <%= f.text_field :name %>

      <%= f.label :email %>
      <%= f.email_field :email %>

      <%= f.label :password %>
      <%= f.password_field :password %>

      <%= f.label :password_confirmation, "Confirmation" %>
      <%= f.password_field :password_confirmation %>

      <%= f.submit "Create my account", class: "btn btn-primary" %>
    <% end %>
  </div>
</div>

このコードにより生成されるhtmlは次のようになる。

app/views/users/new.html
<form accept-charset="UTF-8" action="/users" class="new_user"
      id="new_user" method="post">
  <input name="utf8" type="hidden" value="&#x2713;" />
  <input name="authenticity_token" type="hidden"
         value="NNb6+J/j46LcrgYUC60wQ2titMuJQ5lLqyAbnbAUkdo=" />
  <label for="user_name">Name</label>
  <input id="user_name" name="user[name]" type="text" />

  <label for="user_email">Email</label>
  <input id="user_email" name="user[email]" type="email" />

  <label for="user_password">Password</label>
  <input id="user_password" name="user[password]"
         type="password" />

  <label for="user_password_confirmation">Confirmation</label>
  <input id="user_password_confirmation"
         name="user[password_confirmation]" type="password" />

  <input class="btn btn-primary" name="commit" type="submit"
         value="Create my account" />
</form>

form_forを使ったフォームの構造

form_forの役割

form_forは変数fを使ったブロックをとる構造になっており、次のようなhtmlを生成する。

<%= form_for(@user) do |f| %>
  .
  .
  .
<% end %><form accept-charset="UTF-8" action="/users" class="new_user"
      id="new_user" method="post">
  .
  .
  .
</form>

action属性は"/users"、method属性は"post"となっている。
この2つの属性は、/usersに対してHTTPのPOSTリクエストを送信する、といった指示をしている。
すると、Usersリソースが提供するRESTfulなルート(https://qiita.com/kagamiya9/items/48f66b20aee03fe9da1f )に基づいて、createアクションに行き着く。

このような流れが自動でできるのは、次のような理由らしい。
①form_forの引数は@userであり、Railsは@userがUserクラスであることを認識する。
@userはnewアクションで新規作成されているため、Railsはpostメソッドを使ってフォームを構築すべきだと判断する。

各入力フォーム

nameの入力フォームは次のようになる。

<%= f.label :name %>
<%= f.text_field :name %><label for="user_name">Name</label>
<input id="user_name" name="user[name]" type="text" />

emailの入力フォームは次のようになる。

<%= f.label :email %>
<%= f.email_field :email %><label for="user_email">Email</label>
<input id="user_email" name="user[email]" type="email" />

nameのtype属性は"text"だが、emailでは"email"である。
前者はf.text_field、後者はf.email_fieldとすることでtype属性がそれぞれ決まっている。
こうすると、スマホなどではメールアドレス入力用のキーボードが表示されるようになっている。

passwordの入力フォームは次のようになる。

<%= f.label :password %>
<%= f.password_field :password %><label for="user_password">Password</label>
<input id="user_password" name="user[password]" type="password" />

type属性がpasswordの場合、フォームに文字を入力すると黒丸•で表示されるようになる。

password_confiramtionの入力フォームは次のようになる。

<%= f.label :password_confirmation, "Confirmation" %>
<%= f.password_field :password_confirmation %><label for="user_password_confirmation">Confirmation</label>
<input id="user_password_confirmation"
         name="user[password_confirmation]" type="password" />

password_confirmationでは、f.labelの第二引数に文字列"Confirmation"をとっている。
これにより、フォーム上のラベルが指定した文字列になる。
他のフォームは属性ごとに自動で設定されている。
(nameはNameになる。なお、"Confirmation"を除くと、Password confirmationになる)
サブミットボタンも同様で、value属性が任意の文字列になる。

Createアクションの作成

name属性によるハッシュの構成

<input id="user_name" name="user[name]" - - - />
<input id="user_password" name="user[password]" - - - />

inputには特殊なname属性がついている。
Railsはnameの値を使って、初期化したハッシュを (params変数経由で) 構成する。

どういうことかというと、送信された内容はまず次のようなハッシュになり、params変数に代入される。

params = { users: { name: "Foo Bar", email: "foo@invalid", 
           password: "foo", password_confirmation: "bar" } }

createアクションでは、このparams変数を使ってユーザーを新規登録する。

@user = User.new(params[:user])

:userシンボルの値は、入力される属性(nameやemail)とその値からなるハッシュなので、上のコードは次のコードと同じである。

@user = User.new(name: "Foo Bar", email: "foo@invalid",
                 password: "foo", password_confirmation: "bar")

Strong Parameters

上のようなコードはセキュリティ上問題があるらしい。
paramsをそのまま送信すると、管理者用の属性であるadminなどの値を送信して、管理者権限を奪われるからだとか。
この辺は説明が非常に分かりにくいのだが、要は許可された属性以外は送信できないようにする、ということだ。

結局のところ、次のようなuser_paramsメソッドをコントローラ内に作る。
これはStrong Parametersと呼ばれる。
先の問題があるコードは、マスアサイメントと呼ばれる。

app/controllers/users_controller.rb
class UsersController < ApplicationController
  .
  .
  .
  def create
    @user = User.new(user_params)
    if @user.save
      # 保存の成功をここで扱う。
    else
      render 'new'
    end
  end

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

private内にあるメソッドはweb経由で外部に晒されることがない。

ユーザー登録に失敗した場合の処理

ユーザー登録に失敗した場合は、renderメソッドを使って新規登録ページに戻り、エラーメッセージを表示する。
エラー部分は長いので別記事にまとめることにする。

ユーザー登録に成功した場合の処理

成功部分も別記事にまとめることにする。

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

Rails 学習メモ

今日からチュートリアルを開始

プロジェクトの作成

$ rails new project_name

サーバーの起動

$ rails server

これで localhost:3000 でアクセス可能

Controllerの生成

$ rails generate controller home top
=> rails g controllerに変換可能

Laravelの php artisan make:controller 的な感じではなく、自動で views/home/tophtml.erb と、 app/controllers/home_controller.rb とルーティングの config/routes.rb が生成され、ルーティングに自動で追加される。便利だなこれ。
ただ、既存のControllerが存在する場合、 rails generate controller home test はエラーになる。

ルーティング

config/route.rb に記述

route.rb
Rails.application.routes.draw do
  get "top" => "home#top"
  get "about" => "home#about"
end

画像挿入

public配下に画像ファイルを配置し、呼び出し元で /image_name.png などを入力すれば反映される。

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

【Rails】Usersリソース【Rails Tutorial 7章まとめ】

Usersリソース

ユーザー情報をアプリケーション上で表示するために、RESTアーキテクチャに従って、データの作成、表示、更新、削除をリソースとして扱う。
これらに対応する4つの基本操作(POST、GET、PATCH、DELETE)を各アクションに割り当てていく。

何がなんだかよく分からないが...

<id=1のユーザーを表示する場合>
URLは自動的に/users/1となる。
それに対してGETリクエストを送信すると、自動的にshowアクションが呼び出され、showビューが表示される。

みたいなことをRESTとやらが勝手にしてくれる、ということらしい。

細かいことは後で調べることにして、ルーティングに次のように書く。

config/routes.rb
Rails.application.routes.draw do
  root 'static_pages#home'
  get  '/help',    to: 'static_pages#help'
  .
  .
  .
  resources :users
end

すると次のようなHTTPリクエスト、URL、アクション、名前付きルートの関係が自動で構築される。
スクリーンショット 2019-11-28 1.19.03.jpg

ルーティングはこの通りに自動で設定されるので、各アクションやビューを手動で作ればOKである。

ユーザー情報を表示するshowアクションと仮のビューを作る。

app/controllers/users_controller.rb
class UsersController < ApplicationController

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

  def new
  end
end
app/views/users/show.html.erb
<%= @user.name %>, <%= @user.email %>

これにより、/users/1にアクセスすると、params[:id]の:idに1が代入される。
findメソッドはid=1のユーザーをデータベースから探し、あれば@userに代入される。
@userのnameやemailは埋め込みRubyを使うことで、showビューで表示されるようになる。

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

【Rails】Gravatarによるプロフィール画像の表示【Rails Tutorial 7章まとめ】

Gravatar

Gravatar(http://ja.gravatar.com/ )は、メールアドレスとプロフィール画像を関連づけてくれるサービスである。

Gravatarヘルパーメソッド

UserオブジェクトからGravatarに登録された画像を表示できるようにするため、gravatar_forヘルパーメソッドを作成する。

app/helpers/users_helper.rb
module UsersHelper

  # 引数で与えられたユーザーのGravatar画像を返す
  def gravatar_for(user)
    gravatar_id = Digest::MD5::hexdigest(user.email.downcase)
    gravatar_url = "https://secure.gravatar.com/avatar/#{gravatar_id}"
    image_tag(gravatar_url, alt: user.name, class: "gravatar")
  end
end

内容の詳細は無視することにして、これをユーザー表示ビューで使用してみる。

app/views/users/show.html.erb
<% provide(:title, @user.name) %>
<h1>
  <%= gravatar_for @user %>
  <%= @user.name %>
</h1>

/users/1にアクセスして、ちゃんと表示されているか確認する。
スクリーンショット 2019-11-28 2.00.26.jpg
プロフィール画像を設定したメールアドレスの場合
スクリーンショット 2019-11-28 2.02.52.jpg

gravatarの画像は.gravatarというcssクラスが与えられている。

サイズのオプション引数

次のようにすると、gravatar_for user, size: 50のようにサイズを指定して呼び出せる。

app/helpers/users_helper.rb
module UsersHelper

  # 引数で与えられたユーザーのGravatar画像を返す
  def gravatar_for(user, size: 80)
    gravatar_id = Digest::MD5::hexdigest(user.email.downcase)
    gravatar_url = "https://secure.gravatar.com/avatar/#{gravatar_id}?s=#{size}"
    image_tag(gravatar_url, alt: user.name, class: "gravatar")
  end
end

3行目のURLも変更していることに注意する。

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

【Rails】デバッグ情報の表示【Rails Tutorial 7章まとめ】

debugメソッド

アプリケーションの動作を確認するために、debugメソッドを使ってサイトのレイアウトにデバッグ情報を表示する。

app/views/layouts/application.html.erb
  <body>
    <div>
      .
      .
      .
      <%= render 'layouts/footer' %>
      <%= debug(params) if Rails.env.development? %>
    </div>
  </body>
if Rails.env.development?

とすると、開発環境でのみデバッグ情報を表示できる。
debug(params)から生成されたhtmlには、.debug_dumpというbootstrap用のcssクラスが与えられているようである。

byebugジェムとdebugger

byebugジェムと、それによって使用できるようになるdebuggerメソッドを使うと、アプリケーションの任意の位置でその状態を確認できるようになる。

byebugジェムを導入する。

Gemfile.rb
group :development, :test do
  gem 'sqlite3', '1.3.13'
  gem 'byebug',  '9.0.6', platform: :mri
end

debuggerをアプリケーションの任意の位置に挿入する。

app/controllers/users_controller.rb
  def show
    @user = User.find(params[:id])
    debugger
  end

/users/1にアクセスすると、rails serverを立ち上げたコンソールにbyebugプロントが表示される。

(byebug)

Railsコンソールのようにコマンドを呼び出せる。

(byebug) @user.name
"Example User"
(byebug) @user.email
"example@railstutorial.org"
(byebug) params[:id]
"1"

用が済んだらCtrl+Dで終了し、debuggerメソッドを削除しておく。

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