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

#Rails + #rspec で rake を実行する方法を毎回忘れるので書き留めておく ( LoadError: Can't find 対策 )

まとめ

  • 公式なやり方ではなく、あくまで内部メソッドやらをHackして利用するやり方っぽいので煩雑で使いにくいことは心える
  • rake_require で元のrake fileさえloadできたら勝ち
  • rake_require ではタスク名でも階層でもなく、単にファイル名とディレクトリパスを与えているところがポイント
  • 一度rake file をrequireしてしまえば、task invoke できる
  • うまくrequireできない場合は Rake.load_rakefile など内側のメソッドを使って、パスが正しいかどうかひたすらチェックせよ

rake

/app/lib/tasks/foo/bar.rake

namespace :foo do
  task bar: :environment do |task|
    SomeClass.run
  end
end

rspec

require "rails_helper"
require "rake"

describe do
  before(:all) do
    @rake = Rake::Application.new
    Rake.application = @rake

    # This line require file e.g '/app/lib/tasks/foo/bar.rake'
    Rake.application.rake_require('bar', [Rails.root.join('lib', 'tasks', 'foo')])

    Rake::Task.define_task(:environment)
  end

  before(:each) do
    @rake[task].reenable
  end

  describe  do
    # Do not use dot
    # BAD CASE : foo.bar 
    let(:task) { 'foo:bar' }

    it do
      expect(SomeClass).to receive(:run)
      @rake[task].invoke
    end
  end
end

Doc

https://apidock.com/ruby/v1_9_3_392/Rake/Application/rake_require

File lib/rake/application.rb, line 452

def rake_require(file_name, paths=$LOAD_PATH, loaded=$")
  fn = file_name + ".rake"
  return false if loaded.include?(fn)
  paths.each do |path|
    full_path = File.join(path, fn)
    if File.exist?(full_path)
      Rake.load_rakefile(full_path)
      loaded << fn
      return true
    end
  end
  fail LoadError, "Can't find #{file_name}"
end

Ref

Rspecでrake taskをテストする方法  - Qiita
Rakeタスクのテストの仕方 - Qiita
[Ruby on Rails]RSpecによるRakeのテスト | Developers.IO

Original by Github issue

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

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

Ruby on railsの基本箇所についての復習①(フォルダを作る)

はじめに

Ruby on rails(以降rails)を使って、簡単なアプリを作るまでの手順を、なるべく細分化して書いていこうと思います。

実行

railsアプリを作成するのに必ずしなければいけないのはアプリのフォルダを作ることです
これはターミナル上でrailsコマンドを入力できます。

今回は、sampleという名前のアプリを作っていきます(名前はなんでもいいです)。
最初にアプリを作る場所(ディレクトリ)を決めます。どこでもいいのですが、今回はデスクトップ上に作成しようと思います。
まずはターミナルでデスクトップに移動します。

cd desktop

移動したら次はアプリのフォルダを作ります。
アプリのフォルダは、移動したディレクトリでrails _railsバージョン_ new アプリ名 -d mysqlというrailsコマンドを実行すれば良いです。
railsのバージョンは、ターミナル上でrails -vと入力すればわかります。筆者の場合はこの記事を書いている時点でRails 5.2.3となっています。アプリ名はsampleですので、

rails _5.2.3_ new sample -d mysql

と入力すれば狙い通りのフォルダを作れます。

基礎中の基礎の部分ですが、この一連の流れは何かを参考にしながらでもいいので間違いなくできるようになってください。

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

【Rails】flashの使い方

flashを利用して、アクション実行後に簡単なメッセージを表示させることができます。
ログイン周りの処理において特に重宝されます。
(Sessionはモデルを持たない→ActiveRecordがエラーメッセージを吐かないから)

使い方

flash[:<キー>] = <メッセージ>で登録し、flash.each do |message_type, message|...で出力します。

flashflash.now

flashの仲間にflash.nowがあり、
flash→次のアクションまでデータを保持する→redirect_toと一緒に使う
flash.now→次のアクションに移行した時点でデータが消える→renderと一緒に使う
という使い分けが必要です。

TODOアプリにflashを実装する

簡単なTODOアプリにflashを実装していきます。
Ruby on Railsで簡単なアプリを作成
RailsアプリをHerokuにデプロイする手順
【Rails】バリデーションを実装する
【Rails】ログイン機能を実装する

application.html.erbにflash表示領域を確保する

/app/views/layouts/application.html.erb
<!DOCTYPE html>
.
.
  <body>
    <% flash.each do |message_type, message| %>
      <%= message %>
    <% end %>
.
.

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

redirect_toの前にflashを、renderの前にflash.nowをそれぞれ追加します。

/app/controllers/tasks_controller.rb
class TasksController < ApplicationController
  before_action :logged_in_user, only:[:create, :edit, :update, :destroy]
  def index
    @tasks = Task.all
  end

  def new
    @task = Task.new
  end

  def create
    @task = Task.new(task_params)
    if @task.save
      flash[:success] = "タスクを追加しました。"
      redirect_to tasks_url
    else
      flash.now[:danger] = "登録に失敗しました。"
      render 'tasks/new'
    end
  end

  def edit
    @task = Task.find(params[:id])
  end

  def update
    @task = Task.find(params[:id])
    if @task.update(task_params)
      flash[:success] = "タスクを修正しました。"
      redirect_to tasks_url
    else
      flash.now[:danger] = "更新に失敗しました。"
      render 'tasks/edit'
    end
  end

  def destroy
    @task = Task.find(params[:id])
    @task.destroy
    flash[:success] = "タスクを削除しました。"
    redirect_to tasks_url
  end

  private
    def task_params
      params.require(:task).permit(:title)
    end
end

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

tasksコントローラー同様、redirect_toの前にflashを、renderの前にflash.nowを追加します。

/app/controllers/tasks_controller.rb
class SessionsController < ApplicationController
  def new
  end

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      log_in user
      flash[:success] = "ログインしました。"
      redirect_to root_url
    else
      flash.now[:danger] = "ログインに失敗しました。"
      render 'new'
    end
  end

  def destroy
    log_out if logged_in?
    flash[:success] = "ログアウトしました。"
    redirect_to root_url
  end
end

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

Rails/アンチパターン: 一見すると存在しないアクション

何の変哲も無い(?)コントローラー。これはエクスポート処理関連の各種コントローラーの親クラス

controllers/export_controller.rb
class ExportController < ApplicationController

  def some_method
    やりたい処理
  end
end

そして、何もないHogeController。よく見るとExportControllerを継承している。これを見落とすとやばい

controllers/hoge_controller.rb
class HogeController < ExportController

end

最後にrouting。HogeControllerのsome_methodアクションにroutingしている

routes.rb
get '/hoge', to: 'hoge#some_method'

何が問題か

  • アクション=そのcontrollerに定義するという設計スタイルの慣れていると、ExportControllerを継承しているのを見落としガチで、その場合 「some_methodアクションなど定義されていない。routing自体が意味のないものになっている」という判断をしてしまいがち

どうあるべきか

controllers/export_controller.rb
class ExportController < ApplicationController

  def common_some_method
    やりたい処理
  end
end
controllers/hoge_controller.rb
class HogeController < ExportController

  def some_method
    common_some_method
  end
end

あるいはConcernを使う

controllers/concerns/export_concern.rb
module ExportConsern
  extend ActiveSupport::Concern

  module ClassMethods
    def common_some_method
      やりたい処理
    end
  end
end
controllers/hoge_controller.rb
class HogeController < ApplicationController
  include ExportConcern

  def some_method
    common_some_method
  end
end

など。何れにしても重要なのはHogeControllerには明示的にアクションを定義し、親クラスのメソッドが暗黙的にアクションとして呼ばれることがないようにするべき

まとめとか補足とか

  • 実際には気付ける方法は幾つかあるので、そこまで問題かというまあまあなところ
  • とにかく親クラスのメソッドをアクションとして使用するのは良くない
  • と思っていますが、実際どうなんでしょう

宣伝のようなもの

都内でRailsエンジニアでアンチパターンや失敗談を共有する会をやりたいと考えています。
conpassとかで見かけたらよろしくです。

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

学びメモ(11月)

学んだことを残していきたいと思います。参考にさせていただいた記事を書いた方々には心から感謝します。

CSSのposition: absoluteとrelativeとは

基本形
image.png

absolute

青色のbox2に{ position: absolute; top:150px; left:100px } を指定。
absolute を指定した要素は高さがなくなり、浮いたような状態になるため、box3はbox2を無視して位置を詰める。
image.png

relative

box2に、{ position: relative; top:150px; left:100px; } を指定した例を見ていきましょう。
absoluteとは違い、移動させた要素の高さが残るため、box3は位置を詰めずそのままの位置に表示る。
image.png

親要素に「position:relative;」を記述しない場合は子要素が画面左上を起点に子移動してしまう。
image.png

親要素に「position:relative;」を記述した場合
image.png

array/split <=> array/join

a = "abc".split("")
=> ["a", "b", "c"]

a.join
=> "abc"

string/chars

"1234".chars
=> ["1", "2", "3", "4"]

サービスクラスについて

オブジェクト指向について

オブジェクト指向のイロハ

function(e)のeって何?

gitのcherry-pickについて

git cherry-pickを完全マスター!特定コミットのみを取り込む方法

git pullの取り消し

PostgreSQLとRailsでシーケンスを手動で上げる

PostgresSQL使っていてidを指定して作成した場合にはシーケンスが自動でインクリメントしない。

User.create(id: 1, name: "なまぽ1", email: "namapo1@mailaddress.com")

なので次にd指定しないで作成した場合にエラーで怒られてレコードが作成できない。

User.create(name: "なまぽ2", email: "namapo2@mailaddress.com")
PG::UniqueViolation: ERROR:  duplicate key value violates unique constraint "users_pkey"
DETAIL:  Key (id)=(1) already exists.

こんな時には手動でシーケンスを上げてあげる必要がある。

ActiveRecord::Base.connection.execute("SELECT setval('users_id_seq', coalesce((SELECT MAX(id)+1 FROM users), 1), false)")

現在テーブルに格納されているレコードのidの最大値を取得して、それに + 1したものをシーケンスとして保存してくれます。

ActiveRecordで生SQLを使いたいときに便利なメソッド

ActiveRecord::Base.connection.execute()

git rebaseを初めて使った際のまとめ

Rubyの文字列とシンボルの違いをキッチリ説明できる人になりたい

「MVCの勘違い」について、もう一度考えてみる

Rubyの変数スコープ

「Railsは終わった」と言われる理由

JavaScriptでクロージャ入門。関数はすべてクロージャ?

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

[Ruby] 継承可能なクラス属性を定義する

やりたいこと

継承可能なクラス属性を定義したい。

class A
  self.x = :hoge
end

class B < A
end

class C < B
  self.x = :fuga
end

A.x #=> :hoge
B.x #=> :hoge (親の x を継承する。)
C.x #=> :fuga (再定義した場合はその値を使う。)

たったひとつの冴えたやり方

Active Support コア拡張の Class#class_attribute を使う。

require 'active_support/core_ext/class/attribute'

class A
  class_attribute :x

  self.x = :hoge
end

class B < A
end

class C < B
  self.x = :fuga
end

A.x #=> :hoge
B.x #=> :hoge
C.x #=> :fuga

他に考えた方法

:ng: クラス変数を使う (失敗)

クラス変数はサブクラスとも共有され、サブクラスで代入できるためこの用途に使えない。

class A
  @@x = :hoge
end

class B < A
end

class C < B
  @@x = :fuga
end

A.class_variable_get(:@@x) #=> :fuga ?
B.class_variable_get(:@@x) #=> :fuga ?
C.class_variable_get(:@@x) #=> :fuga

:ng: クラスインスタンス変数を使う (失敗)

クラスインスタンス変数はサブクラスとは共有されず、各クラスで独立している。しかし継承もしない。

class A
  @x = :hoge
end

class B < A
end

class C < B
  @x = :fuga
end

A.instance_variable_get(:@x) #=> :hoge
B.instance_variable_get(:@x) #=> nil ?
C.instance_variable_get(:@x) #=> :fuga

:ok: クラスインスタンス変数を使い、継承時に親クラスの値を引き継ぐ (成功)

class A
  def self.inherited(subclass)
    subclass.instance_variable_set(:@x, @x)
  end

  @x = :hoge
end

class B < A
end

class C < B
  @x = :fuga
end

A.instance_variable_get(:@x) #=> :hoge
B.instance_variable_get(:@x) #=> :hoge
C.instance_variable_get(:@x) #=> :fuga

参考

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

【WIP】Rails × CircleCI × ECSのインフラ構築

簡単なRailsデモアプリを本番環境に上げるまでのインフラ構成に関してのまとめ

* あくまで参考に(実務でそのまま利用できるほどしっかり構築しておりません)

image.png

前提知識

ECSとは?クラスターとは?サービスとは?タスクとは?って人は
ECSの概念を理解しよう
などを読んでください。

Railsアプリ作成

まずはローカルでRailsアプリを作成しましょう。
機能は簡単なものでいいので、scaffoldを利用してとりあえずで作成してしまいましょう。
もちろんフルDocker化

AWS上で利用するリソースの作成

コンソール上(or Terraformなど)からあらかじめ作成しておくべきものになります。

IAMロール・ポリシーの作成

ECSで運用するための必要なIAMロール・ポリシーを作成していきます。
ちなみにポリシーとは、ロールに付与される権限情報です。なのでポリシーのないロールは何も権限がない状態なのでまずはポリシーを作成してロールを作成していきましょう。(sandboxアカウントでは既に作成済みのため不要)

ポリシーの作成

作成手順
  1. IAMページに行って、サイドバーの「ポリシー」選択
  2. 「ポリシーの作成」ボタン押下
  3. JSONタブを開いて下記に記載したJSON内容をコピペして、「ポリシーの確認」押下
  4. それぞれのポリシー名を入力する

下記の4つのポリシーを作成する。

  1. AmazonSSMReadAccess
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ssm:GetParameters",
                "secretsmanager:GetSecretValue",
                "kms:Decrypt"
            ],
            "Resource": "*"
        }
    ]
}
  1. AmazonECSTaskExecutionRolePolicy
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ecr:GetAuthorizationToken",
                "ecr:BatchCheckLayerAvailability",
                "ecr:GetDownloadUrlForLayer",
                "ecr:BatchGetImage",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "*"
        }
    ]
}
  1. AmazonEC2ContainerServiceforEC2Role
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ec2:DescribeTags",
                "ecs:CreateCluster",
                "ecs:DeregisterContainerInstance",
                "ecs:DiscoverPollEndpoint",
                "ecs:Poll",
                "ecs:RegisterContainerInstance",
                "ecs:StartTelemetrySession",
                "ecs:UpdateContainerInstancesState",
                "ecs:Submit*",
                "ecr:GetAuthorizationToken",
                "ecr:BatchCheckLayerAvailability",
                "ecr:GetDownloadUrlForLayer",
                "ecr:BatchGetImage",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "*"
        }
    ]
}
  1. AmazonECSServiceRolePolicy
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "ECSTaskManagement",
            "Effect": "Allow",
            "Action": [
                "ec2:AttachNetworkInterface",
                "ec2:CreateNetworkInterface",
                "ec2:CreateNetworkInterfacePermission",
                "ec2:DeleteNetworkInterface",
                "ec2:DeleteNetworkInterfacePermission",
                "ec2:Describe*",
                "ec2:DetachNetworkInterface",
                "elasticloadbalancing:DeregisterInstancesFromLoadBalancer",
                "elasticloadbalancing:DeregisterTargets",
                "elasticloadbalancing:Describe*",
                "elasticloadbalancing:RegisterInstancesWithLoadBalancer",
                "elasticloadbalancing:RegisterTargets",
                "route53:ChangeResourceRecordSets",
                "route53:CreateHealthCheck",
                "route53:DeleteHealthCheck",
                "route53:Get*",
                "route53:List*",
                "route53:UpdateHealthCheck",
                "servicediscovery:DeregisterInstance",
                "servicediscovery:Get*",
                "servicediscovery:List*",
                "servicediscovery:RegisterInstance",
                "servicediscovery:UpdateInstanceCustomHealthStatus"
            ],
            "Resource": "*"
        },
        {
            "Sid": "ECSTagging",
            "Effect": "Allow",
            "Action": [
                "ec2:CreateTags"
            ],
            "Resource": "arn:aws:ec2:*:*:network-interface/*"
        },
        {
            "Sid": "CWLogGroupManagement",
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:DescribeLogGroups",
                "logs:PutRetentionPolicy"
            ],
            "Resource": "arn:aws:logs:*:*:log-group:/aws/ecs/*"
        },
        {
            "Sid": "CWLogStreamManagement",
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogStream",
                "logs:DescribeLogStreams",
                "logs:PutLogEvents"
            ],
            "Resource": "arn:aws:logs:*:*:log-group:/aws/ecs/*:log-stream:*"
        }
    ]
}

ロールの作成

  1. ecsInstanceRole
  2. AWSServiceRoleForECS
  3. ecsTaskExecutionRole

ecsInstanceRoleの場合は一番簡単で、テンプレートがあるので
IAMページに行って、サイドバーの「ロール」→「ロールの作成」より
IAM_Management_Console.png
IAM_Management_Console.png
IAM_Management_Console.png

ecsTaskExecutionRole
Amazon ECS タスク実行 IAM ロールにある

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ecr:GetAuthorizationToken",
        "ecr:BatchCheckLayerAvailability",
        "ecr:GetDownloadUrlForLayer",
        "ecr:BatchGetImage",
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": "*"
    }
  ]
}

をコピペして貼り付けましょう。

ちなみに上記のやつは公式ドキュメントにもあるので確認してください。
例: Amazon ECS タスク実行 IAM ロール

ALBの作成

ECSのサービス作成時にALBを登録しておけば、コンテナに動的にポートマッピングをしてくれるようになるので楽になります。

クラスターの作成

ECSのサイドバーにある「クラスター」から「クラスターの作成」ボタンを押下
「クラスターテンプレートの選択」は「EC2 Linux + ネットワーキング」を選択
1. クラスター名記載
2. EC2インスタンスタイプの選択(お好み)
3. キーペア(お好み。ただし、デバッグ時にSSHできた方がいいので設定しておくことをおすすめ)
4. コンテナインスタンスの IAM ロールに「ecsInstanceRole」を選択

CircleCIの設定

circleci/config.yml
version: 2.1
orbs:
  aws-cli: circleci/aws-cli@0.1.13
executors:
  builder:
    docker:
      - image: circleci/buildpack-deps

commands:
  init:
    steps:
      - checkout
      - aws-cli/install
      - install_ecs-cli
      - setup_remote_docker
  install_ecs-cli:
    steps:
      - run:
          name: Install ECS-CLI
          command: |
            sudo curl -o /usr/local/bin/ecs-cli https://amazon-ecs-cli.s3.amazonaws.com/ecs-cli-linux-amd64-latest
            sudo chmod +x /usr/local/bin/ecs-cli

jobs:
  build:
    executor: builder
    steps:
      - init
      - run:
          name: Build application Docker image
          command: |
            docker build -f build.Dockerfile --rm=false -t rails-sample-app-build:latest .
      - run:
          name: Save image
          command: |
            mkdir -p /tmp/docker
            docker save rails-sample-app-build:latest -o /tmp/docker/image
      - persist_to_workspace:
          root: /tmp/docker
          paths:
            - image
  deploy:
    executor: builder
    steps:
      - init
      - attach_workspace:
          at: /tmp/docker
      - run: docker load -i /tmp/docker/image
      - run:
          name: Assets precompile and Push Docker image
          command: |
            docker build -f assets.Dockerfile --build-arg RAILS_MASTER_KEY=${RAILS_MASTER_KEY} --rm=false -t rails-sample-app-build:latest .
      - run:
          name: Push Docker image
          command: |
            ecs-cli push rails-sample-app-build:latest
      - run:
          name: ECS Config
          command: |
            ecs-cli configure \
            --cluster rails-sample-${CIRCLE_BRANCH} \
            --region ${AWS_DEFAULT_REGION} \
            --config-name rails-sample-${CIRCLE_BRANCH}
      - run:
          name: migrate deploy
          command: |
            ecs-cli compose \
            --file ecs/${CIRCLE_BRANCH}/migrate/docker-compose.yml \
            --ecs-params ecs/${CIRCLE_BRANCH}/migrate/ecs-params.yml \
            --project-name rails-sample-${CIRCLE_BRANCH}-migrate \
            up \
            --launch-type EC2 \
            --create-log-groups \
            --cluster-config rails-sample-${CIRCLE_BRANCH}
      - run:
          name: Unicorn + Nginx deploy
          command: |
            ecs-cli compose \
            --file ecs/${CIRCLE_BRANCH}/app/docker-compose.yml \
            --ecs-params ecs/${CIRCLE_BRANCH}/app/ecs-params.yml \
            --project-name rails-sample-${CIRCLE_BRANCH}-app \
            service up \
            --container-name nginx \
            --container-port 80 \
            --target-group-arn ${TARGET_GROUP_ARN} \
            --timeout 0 \
            --launch-type EC2 \
            --create-log-groups \
            --cluster-config rails-sample-${CIRCLE_BRANCH}

workflows:
  version: 2
  build-deploy:
    jobs:
      - build
      - deploy:
          requires:
            - build
          filters:
            branches:
              only:
                - master

CircleCIに設定する環境変数

CircleCIのプロジェクトの設定ページ(Settings→[アカウント名or組織名]→[プロジェクト名])に行き、下記の画像の箇所から設定する
https://circleci.com/gh/[アカウント名or組織名]/[プロジェクト名]/edit#env-vars

Project_settings_-_key-sn_chiko_-_CircleCI.png

環境変数名
AWS_ACCESS_KEY_ID [AWSのアクセスキーID]
AWS_ACCOUNT_ID [AWSのアカウントID]
AWS_DEFAULT_REGION [AWSのデフォルトリージョン]
AWS_ECR_REPOSITORY_URL [AWSのECRリポジトリURL]
AWS_SECRET_ACCESS_KEY [AWSのシークレットアクセスキー]
RAILS_MASTER_KEY [config/master.keyの値]
TARGET_GROUP_ARN [ターゲットグループのarn]

Task definitionの作成

docker-compose.yml

rails-sample/ecs/production/app/docker-compose.yml
version: "3"

services:
  app:
    image: [ECRのリポジトリURI]
    entrypoint: bundle exec unicorn -c config/unicorn.rb
    env_file:
      - ../env
    working_dir: /projects/rails-sample
    logging:
      driver: "awslogs"
      options:
        awslogs-region: "ap-northeast-1"
        awslogs-group: "rails-sample-production/app"
        awslogs-stream-prefix: "rails-sample-app"
  nginx:
    image: [ECRのリポジトリURI]
    entrypoint: /bin/bash /etc/nginx/start.sh
    ports:
      - 0:80
    links:
      - "app:app"
    env_file:
      - ../env
    working_dir: /projects/rails-sample
    logging:
      driver: "awslogs"
      options:
        awslogs-region: "ap-northeast-1"
        awslogs-group: "rails-sample-production/nginx"
        awslogs-stream-prefix: "rails-sample-nginx"

ecs-params.yml

タスク実行時に実行ロールの指定やコンテナに注入する環境変数をAWS Systems Managerから取得するして設定するためのファイル

rails-sample/ecs/production/app/ecs-params.yml
version: 1
task_definition:
  # タスク実行時のロールを指定
  task_execution_role: ecsTaskExecutionRole
  services:
    起動するコンテナを記載(app, nginx)
    app:
      # 何らかの理由で失敗・停止した際に、タスクに含まれる他のすべてのコンテナを停止するかどうか(デフォルトはtrue)
      essential: true
      # AWS Systems Managerから秘匿情報を取得してコンテナに環境変数を注入
      secrets:
        - value_from: /production/database_username
          name: DATABASE_USERNAME
        - value_from: /production/database_password
          name: DATABASE_PASSWORD
        - value_from: /production/database_host
          name: DATABASE_HOST
    nginx:
      essential: true
# あまりわかってない
run_params:
  network_configuration:
    awsvpc_configuration:
      assign_public_ip: ENABLED

AWS Systems Managerの設定

AWS Systems Managerは、タスク実行時にコンテナに注入する秘匿情報(環境変数)の管理に使えるAWSサービスです。
初めての人は設定の仕方を含め、
ECSでごっつ簡単に機密情報を環境変数に展開できるようになりました!
を見れば大体分かると思います。

AWS Systems Managerの左側メニューから「パラメータストア」→「パラメータの作成」をクリック。パラメータの詳細画面が表示されるので、パラメータのキー名と値を入力します。タイプには「安全な文字列」を選択します。

パラメータのキー名と値一覧

キー名
/production/database_username [RDSに設定したusername]
/production/database_password [RDSに設定したpassword]
/production/database_host [RDSインスタンスのエンドポイント]

RDSインスタンスのエンドポイント(RDS→データベース→[インスタンス名])
RDS_·_AWS_Console.png

コンテナ全体に注入する環境変数の設定

各環境(production, stagingなど)ごとのディレクトリ以下にenvファイルを用意してそこに記載する

# ここのファイルに追加した環境変数は全てのコンテナに展開されます
# Rails
APP_HOST=54.238.241.230
RAILS_ENV=production
RAILS_LOG_TO_STDOUT=1
RAILS_SERVE_STATIC_FILES=1

# RDS
DATABASE_NAME=rails-sample_production
DATABASE_PORT=3306
DATABASE_POOL=10

# Unicorn
UNICORN_PORT=23380
UNICORN_TIMEOUT=180
UNICORN_WORKER_PROSESSES=2

# Nginx専用
NGINX_APP_SERVER_NAME=app
NGINX_APP_SERVER_PORT=23380
NGINX_DOCUMENT_ROOT=/projects/rails-sample/public
NGINX_FRONT_SERVER_NAME=54.238.241.230

構築の際に詰まる可能性のあるポイント

ECSコンテナインスタンスの作成

Defaultクラスター作成しているし、IAMロールにecs:CreateClusterの権限付与されているから自動で作成なんかもしてくれるのかと思ったら作成してくれなかった。
なので、クラスター作成→インスタンス作成の方が良い(ちな、クラスター作成時にインスタンスも作成するようにはできるっぽい)
→カスタマイズされてるAMI利用時のみ初期スクリプトによってDefaultクラスターを作成しているのかもしれない

:hatched_chick: 参考 :hatched_chick:
Amazon ECS コンテナインスタンスの起動 - Amazon Elastic Container Service
Amazon ECS-optimized AMI - Amazon Elastic Container Service

インスタンスタイプについて

Amazon_ECS.png

ある程度余裕持たないとタスク実行するための容量を持たなくて死ぬ
(ほんとは、ローカルや本番環境で動かした時の使用量見てタスク実行に必要なメモリを設定した方が良い)

ecs-cliでのタスク実行

  • ecs-params.yml ファイル内でtask_execution_roleを指定すること
  • task_execution_roleで指定した適切なポリシーを適用したIAM Roleを用意すること(エラーが出なくて、単純に実行されないので気づきにくい)

aws-cliでRDSの作成

AWS CLI を使って外部からアクセス出来る RDS インスタンスを作る方法が公式ドキュメントに無かったのでメモ。
Aws rds コマンドを使って rds を作成するには、DB subnet group を指定する必要があるらしい。さもなければ以下のようなエラーが出る。

    aws rds create-db-instance \
            --db-instance-identifier chiko-db-production \
            --db-instance-class db.t2.micro \
            --db-subnet-group-name ishihara-db-subnet-group \
            --engine mysql \
          --engine-version 5.7.26 \
            --allocated-storage 20 \
            --master-username root \
            --master-user-password password \
            --backup-retention-period 3 \
            --profile sandbox-admin

参考
AWS CLI を使って RDS を作成する (自分用メモ) - Qiita
AWS-CLI Amazon Aurora インスタンス作成 - Qiita

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

インスタンス変数・ゲッターセッター・アクセスメソッド

今回はインスタンス変数、インスタンスメソッド、ゲッターとセッター、attr_accessorについて書きます

今回のコード

class MarsAlien
  #attr_reader :name →ゲッターの役割
  #attr_writer :name →セッターの役割
  attr_accessor :name #ゲッター・セッターの役割

  def initialize(name) #初期化メソッド
    @name = name  #@nameはインスタンス変数
  end

  def greet #インスタンスメソッド
    puts '#{@name} says hello to earth'
  end
end

mars_bob = MarsAlien.new('Bob')
mars_erich = MarsAlien.new('Erich')

mars_bob.greet #=> Bob says hello to earth

mars_bob.name #=> "Bob"
mars_bob.name = "Johnny" #=> "Johnny"

前回の復習

ざっくりと、前回の確認をします。

・クラス:〇〇製造工場
・インスタンス:工場で作られた固有の〇〇(オブジェクト、物体)
・メソッド:クラスやインスタンスが行える「動作・機能」
initializeメソッド:作成されたインスタンスに、初期値(例:名前など)を設定する

インスタンス変数

では、前回の最後で登場した「インスタンス変数」(例:@name)とは何でしょうか?

インスタンス変数とは、個々のインスタンス毎に特有のデータ(例:名前、年齢)を保持するための変数です。
(別の言い方をすると、インスタンス間で共有しないデータのためのものです)

以下のように、@変数名と記述します。

class MarsAlien
  def initialize(name)
    @name = name #インスタンス変数
  end
end

mars_bob = MarsAlien.new('Bob')
mars_erich = MarsAlien.new('Erich')

上記の例を見てみましょう。火星人クラス(MarsAlien)から作成された、火星人ボブ(mars_bob)や火星人エリック(mars_erich)は、それぞれインスタンスです。そこで、彼らが(共有せず)固有に持っている名前を、初期値としてインスタンス変数で設定します。

また、@から始まるインスタンス変数は、同じクラス内であれば、メソッドを超えて参照することができます。その意味を次の節で見てみましょう。

インスタンスメソッド

インスタンスメソッドとは、その名の通り、固有のインスタンスにのみ使用可能なメソッドです。

クラスが〇〇製造工場であることを思い出してみましょう。「挨拶をする」というメソッド(動作)があったさい、その主体者は、工場(クラス)ではなく、個々のオブジェクト(インスタンス)です。インスタンスメソッドは、そういった個々のオブジェクトが出来る動作を記述しています。

そのため、インスタンスメソッドを使用するためには、newメソッドでインスタンスを作成する必要があります。

class MarsAlien
  def initialize(name) #初期化メソッド
    @name = name  #@nameはインスタンス変数
  end

  def greet #インスタンスメソッド
    puts '#{@name} says hello to earth' #@nameを参照している
  end
end

mars_bob = MarsAlien.new('Bob')
mars_erich = MarsAlien.new('Erich')

mars_bob.greet #=> Bob says hello to earth 
mars_erich.greet #=> Erich says hello to earth

最後の2行で、ボブとエリック(火星人クラスのインスタンス)に、インスタンスメソッドであるgreetメソッドを使用して、地球に挨拶させています。
また、@name(インスタンス変数)を利用することで、メソッドをまたいで参照が可能になっています。

ゲッターとは

ちなみに、@で定義したインスタンス変数が知りたい場合は、どうすればよいのでしょうか?実は、作成したインスタンスから直接参照しようとすると、下記のようなエラーになってしまいます。

class MarsAlien
  def initialize(name) 
    @name = name  #@nameはインスタンス変数
  end
end

mars_bob = MarsAlien.new('Bob')
mars_bob.name #=>undefined method `name'エラーとなり、参照できない

これは、インスタンス変数の値が、クラス内の範囲でしか参照できないためです。
そのため、クラス外からインスタンス変数を参照したい場合は、下記のように、ゲッター、つまり、クラス外からインスタンス変数を「ゲットする」ためだけのメソッドを定義する必要があります。

class MarsAlien
  def initialize(name) 
    @name = name  #@nameはインスタンス変数
  end

  def name #ゲッター
    @name
  end
end

mars_bob = MarsAlien.new('Bob')
mars_bob.name #=> "Bob"(ゲッターがあるので、参照できるようになった)

とはいえ、インスタンス変数が複数ある場合などは、↑のようにゲッターをいちいち書いていたら、とても手間ですね。なんとかならないでしょうか?

実はなんとかなるのですが、その前に、ゲッターと並んで紹介されることが多い「セッター」の概念についても確認しておきましょう。

セッターとは

ゲッターが(主に)インスタンス変数参照用であるのに対し、「セッター」はインスタンス変数を書き換える(更新する)場合に使用されます。
例を見てみましょう。

class MarsAlien
  def initialize(name) 
    @name = name  #@nameはインスタンス変数
  end
end

mars_bob = MarsAlien.new('Bob')
mars_bob.name = "Johnny" #=>undefined method `name'エラーとなり、"Bob"から"Johnny"へ更新できない

上記でエラーが出ているのは、インスタンス変数をクラス外から更新することはできないためです。そのため、インスタンス変数書き換え用メソッドのセッターを用意します。

class MarsAlien
  def initialize(name) 
    @name = name  #@nameはインスタンス変数
  end

  def name=(name) #セッター
    @name = name
  end
end

mars_bob = MarsAlien.new('Bob')
mars_bob.name = "Johnny"  #=> "Johnny"(セッターがあるので、"Bob"から"Johnny"へ更新できる)

このセッターくんも、ちょっと面倒ですよね。

それでは、セッター・ゲッターの記述を(体感)10倍ほど簡単にしてくれるのがattr_accessor(アクセスメソッド)を見てみましょう!

attr_accessorとは

 attr_accessorは、3種類あるうちのアクセスメソッドのひとつです。ゲッターとセッターの役割を持ち、インスタンス変数の値の参照・更新をともに可能にしてくれます

attr_accessor以外のアクセスメソッド>

メソッド 動作
attr_reader :変数名 ゲッターと同じ役割。インスタンス変数の値を参照する
attr_writer :変数名 セッターと同じ役割。インスタンス変数の値を更新する
attr_accessor :変数名 ゲッターとセッター、両方の役割を持つ。インスタンス変数の値が参照でき、かつ更新できる

最後に、attr_accessorの定義方法を確認しましょう。
(ちなみに、:変数名(変数名にコロン)は「シンボル」という構文を使用しています。詳細は、シンボルに関するこちらの記事を参照してください)

class MarsAlien
  #アクセスメソッド(@name変数をクラス外から参照・更新するのを可能にしてくれる)
  attr_accessor :name 

  def initialize(name) 
    @name = name  #@nameはインスタンス変数
  end
end

mars_bob = MarsAlien.new('Bob')

mars_bob.name 
#=> "Bob"(attr_accessorがあるので、クラス外から値が参照できる)

mars_bob.name = "Johnny"  
#=> "Johnny"(attr_accessorがあるので、クラス外から値が更新できる)

参考記事

(英語)クラスメソッドとインスタンスメソッドについて
クラス変数とインスタンス変数について
Rubyリファレンスマニュアル(変数と定数)

おつかれさまでした!

お付き合いくださり、ありがとうございました!!

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

【Rails】テストについて

テストの種類

単体テスト モデルやビューヘルパー単体の動作をチェック
機能テスト コントローラ/ビューの呼び出し結果をチェック
統合テスト ユーザーの実際の操作を想定し、複数のコントローラにまたがるアプリの挙動をチェック

単体テスト

Railsで行うテストの中で、最も基本的なテスト
アプリを構成するライブラリ(主にモデル)が、正しく動作するかをチェックする

test/models/user_test.rb
require 'test_helper'

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: 'Example User', email: 'user@example.com'
  end

  test "should be valid" do
    assert @user.valid?
  end
end

assertメソッド: 第1引数がtrueである場合、テストが成功したものとする
setupメソッド : テストが走る前にインスタンス変数の@userを宣言し、 validメソッドで有効性を確認

機能テスト

コントローラの動作や、ビューの出力をチェックするためのテスト
HTTPリクエストを擬似的に作成することで、アクションメソッドを実行し、HTTPステータスやテンプレート変数、最終的な出力の構造までを確認。また、ルーティングもチェック

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
test/integration/users_signup_test.rb
require 'test_helper'
class UsersSignupTest < ActionDispatch::IntegrationTest

  test "invalid signup information" do
    get signup_path
    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
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

#Rails + rails console の pry でエラー情報が少なすぎて辛いのでスタックトレースを表示する様にする設定例

config/initializers/pry.rb

Pry.config.exception_handler = proc do |output, exception, _pry_|
  output.puts "#{exception}"
  output.puts "#{exception.backtrace}"
end

Exceptions · pry/pry Wiki

Original by Github issue

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

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

#Rails + rake でタスク名を標準出力・ログ出力する例 ( how to Log or STDOUT task name on rake with Rails )

namespace :foo do
  task run: :environment do |task|
    puts task.name
    puts Rails.logger.info task.name
  end
end

Original by Github issue

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

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

#Rails + rake で 定数参照できない実行エラー : NameError: uninitialized constant ModelName

environment指定をすること

namespace :foo do
-   task :run do
+   task run: :environment do
    ::ModelName.bar
  end
end

Original by Github issue

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

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

Hamlの基本箇所についての復習④(ネスト)

はじめに

飽きもせずHamlの基礎について書いていきます。
今回はクラスのネストについてです。

実行

例えば、htmlで次の様に書かれているとします。

<div ="wrapper">
 <div ="top">
 </div>
 <div ="down">
 </div>
</div>

wrapperという大きなクラスがあり、その中にtopdownという小さなクラスが二つあります。
このように親要素・子要素があるhtmlファイルだと、ちょっとした書き間違えが起こります。例えば次のようなものです。

<div ="wrapper">
 <div ="top">
 <div ="down">
 </div>
</div>

これは<div ="top">の閉じタグがない状態です。これだとCSSがちゃんと適用されないなどのエラーが起きます。
単純なミスですが、意外と気づきにくく、エラー解決まで時間を要することも少なくないです。

しかし、Hamlを使えばこの問題を多少なり解決することができます。
前述のコードをHamlで書き直すと次のようになります。

.wrapper
  .wrapper__top
  .wrapper__down

htmlよりも短く書けたのが分かると思います。
一番わかり易い違いは閉じタグがないことです。
クラス名を書いた後に、本文を書くだけでオブジェクトが作れます。
これなら閉じタグを書き忘れた事によるエラーが起きえません。

もう一つはクラスの書き方です。
htmlでは、親要素には親要素のクラスのみ、子要素には子要素のクラスのみを書いていましたが、
Hamlでは子要素には親要素を含めたクラスを書きます。
書き方は.親要素のクラス名__子要素のクラス名と書きます。間に_(アンダーバー)を二つ続けて書きます。
上記のコードだと、親要素がwrapper子要素がtop,downなので、

  .wrapper__top
  .wrapper__down

と書きます。
また、子要素は親要素から半角2文字分のインデントをつけます
そうしないとエラーが起きて、ビューが表示されません。
これにさらに子要素を追加する、例えばtopの子要素としてright,leftをつける場合は、

.wrapper
  .wrapper__top
    .wrapper__top__right
    .wrapper__top__left
  .wrapper__down

という風に書けます。
ちなみに、これをhtmlで記述すると

<div ="wrapper">
 <div ="top">
  <div = "right">
  </div>
  <div = "left">
  </div>
 </div>
 <div ="down">
 </div>
</div>

となります。

注意点としまして、例えば次のように途中のクラス名が間違っていると不具合が起きます。

.wrapper
  .wrapper__top
    .wrapper__tap__right
    .wrapper__tap__left
  .wrapper__down

子要素のクラス名で、親要素のクラスがtopにも拘わらずtapと誤って記述されています。
こうなると、親要素とは全く関係ない要素と判断され、CSSやJSなどで不具合が出てきます。
特にJSの場合、関数が上手く発火してくれない原因の多くがクラス名を間違って書いているだったりする事が多いです。

Hamlは便利ですが、エラーが全く起きないわけではないので、記述する時は最新の注意が必要です(これはHamlに限ったことではありませんが)

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

【Rails】we're sorry, but something went wrongでハマった話(2)

こんにちは!スージーです!
ローカル環境では問題なかったのに本番環境で発生したwe're sorry, but something went wrongの解決までを備忘録として

we're sorry, but something went wrong

このエラーは本番環境でちょいちょい見るのでproduction.logでエラーログを確認する

ターミナル
[ec2-user@ip-~~]$ cd /var/www/app-name
[ec2-user@ip-~~]$ cd log
[ec2-user@ip-~~]$ cat production.log
D, [2019-11-18T07:06:44.834244 #5661] DEBUG -- : hogehoge~~
D, [2019-11-18T07:06:44.834597 #5661] DEBUG -- : hogehoge~~
・
・・
・・・

この時はproduction.logにエラーログが無かった

デバックを考える

  • タイポ→× 開発環境でもエラーが再現されているはず
  • no method error→× 開発環境でもエラーが再現されているはず
  • ルーティングアクション,コントローラの記述がおかしい→× 開発環境でもエラーが再現されているはず
  • エラー発生ページshowアクション:idが本番環境にないのではないか?(同期が教えてくれた)

本番環境のMySQLにログインしてみる

同期の助言により、以前に本番環境下でseedファイルのデータが投入できなかった事を思い出す

ターミナル
[ec2-user@ip-~app-name]$  mysql -u root -p
Enter password:
・
・・
mysql > use app-name_production;
・
・・
database changed
mysql > describe meals;
# <エラーが発生するテーブル>
mysql > show * form meals;
# <レコードを確認>

mealsテーブル

Column Type Options
name varchar null: false
image text
food_stuff text
cooking_time integer null: false
cooking_method integer
post_id integer null: false
user_id integer null: false

エラー箇所特定

user_id:1で投稿したはずなのにmealsテーブルuser_idカラムにはuser_id:17で保存されている...
開発環境では問題なくuser_idが保存されているから道理でエラーが再現されないはずだ...

原因

このuser_idはデプロイ後に追加したカラム
開発環境でマイグレーションファイルを作成した後に以下の手順でdb:migrateした

ターミナル
[ec2-user@ip-~app-name]$ RAILS_ENV=production DISABLE_DATABASE_ENVIRONMENT_CHECK=1 bundle exec rake db:drop
#<最新のDBにする為,一度drop>
[ec2-user@ip-~app-name]$ rake db:create RAILS_ENV=production
#<DBをcreateする>
[ec2-user@ip-~app-name]$ rake db:migrate RAILS_ENV=production
#<migrateする>
[ec2-user@ip-~app-name]$ cd current
#<currentディレクトリ下へ>
[ec2-user@ip-~app-name]$ rake db:seed RAILS_ENV=production
・
・・
・・・
Mysql2::Error: Field 'user_id' doesn't have a default value

上記エラーが最初出た時にuser_idカラムをMySQLにログインしてカラムを新規で作成し突っ込んでいた

ターミナル
database changed
mysql > ALTER TABLE meals ADD user_id int;
mysql > exit
[ec2-user@ip-~app-name]$ cd current
[ec2-user@ip-~app-name]$ rake db:seed RAILS_ENV=production
・
・・
・・・
Mysql2::Error: Field 'user_id' doesn't have a default value

またエラーが出ている...アプリを動かして投稿してみると新規投稿はできていたので、その時は一旦保留にしていたが、やはりここが今回のエラーの原因であるのは間違いなさそう

解決方法

なぜ最新のマイグレーションファイルが本番環境に反映されないのか

参考
【Rails】本番環境デプロイでよく使うコマンド集!AWS/unicorn/nginx/Capistrano使用
https://qiita.com/15grmr/items/7ad36caa82a0fa27c4bd

むむむ...db:migrateの手順が間違っていた...無理矢理に本番環境のDB構造を変えて正しくuser_idが保存されていないのが原因であれば正しくマイグレーションすればエラーは解決するはず。そうすればdb:seedでデータが投入されるはずである

ターミナル
[ec2-user@ip-~app-name]$ RAILS_ENV=production DISABLE_DATABASE_ENVIRONMENT_CHECK=1 bundle exec rake db:drop
[ec2-user@ip-~app-name]$ rake db:create RAILS_ENV=production
# ここで一度自動デプロイをかけて、db:migrateに当たる操作を完了
# ローカルにて
app-name $ bundle exec cap production deploy
# 無事作成されたら、再度EC2のcurrentディレクトリでseedを反映させる
[ec2-user@ip-~app-name current]$ rake db:seed RAILS_ENV=production
・
・・
・・・
[ec2-user@ip-~app-name current]$

無事にseedファイルが本番環境に投下されwe're sorry, but something went wrongのエラーは解決

まとめ

今回、個人アプリで初めてデプロイ後のDB構造を変更・追加を行った。EC2内でdb:create→db:migrateまでしてから自動デプロイしても最新のマイグレーションファイルが本番環境へ反映されない事が原因だった。EC2内では古いDBをdropcreateする所までを行い、capistranoを使った自動デプロイで最新マイグレーションをファイルの読み込みmigrateする

今回のエラーはデプロイの手順の間違えで発生したものだったが、MySQLを触っているとRailsではマイグレーションファイルでバージョン管理してくれるし、ORMのお陰で生SQLを触る機会もほとんどないのでDB関連の知識が無くても動くアプリケーションが作れる。非常に便利だが、SQLの勉強は絶対にした方が良いと感じたエラーだった

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

NameError in CommentsController#create undefined local variable or method `comment_params' for #<CommentsController:0x00007fa3b29e8038> Did you mean? commment_params

tweetに対してコメントできる機能の実装をしてて
commentsコントローラの
createアクションに記載したところ
写真のようなエラーが出て
困っています
原因は何でしょうか?

エラー1.png

エラー2.png

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

Rails6 のちょい足しな新機能を試す107(Digest::UUID編)

はじめに

Rails 6 に追加された新機能を試す第107段。 今回は、 Digest::UUID 編です。
Rails 6 では、 Digest::UUID が require なしで使えるようになりました。

Ruby 2.6.5, Rails 6.0.0, Rails 5.2.3 で確認しました。

$ rails --version
Rails 6.0.0

今回は、簡単なスクリプトを作って確認します。

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

$ rails new rails_sandbox
$ cd rails_sandbox

スクリプトを作成する

Digest::UUID を使ったスクリプトを作成します。

scripts/uuid.rb
puts Digest::UUID.uuid_v3(Digest::UUID::DNS_NAMESPACE, 'rubyonrails.org')
puts Digest::UUID.uuid_v4
puts Digest::UUID.uuid_v5(Digest::UUID::DNS_NAMESPACE, 'rubyonrails.org')

スクリプトを実行する

rails runner でスクリプトを実行します。

$ bin/rails runner scripts/uuid.rb
Running via Spring preloader in process 54
a063dfac-9c20-314f-8597-169d162b1e83
bd997739-d65c-4f56-ac56-81fc08775a79
4d446767-11df-526a-a2da-93799c90dee7

Rails 5 では

Rails 5.2.3 では、 LoadError になります。

$ bin/rails runner scripts/uuid.rb
 ...
 1: from scripts/uuid.rb:1:in `<main>'
 /usr/local/lib/ruby/2.6.0/digest.rb:16:in `const_missing': library not found for class Digest::UUID -- digest/uuid (LoadError)`

明示的に require すれば、 LoadError は解消されます。

scripts/uuid.rb
require 'active_support/core_ext/digest/uuid'
puts Digest::UUID.uuid_v3(Digest::UUID::DNS_NAMESPACE, 'rubyonrails.org')
puts Digest::UUID.uuid_v4
puts Digest::UUID.uuid_v5(Digest::UUID::DNS_NAMESPACE, 'rubyonrails.org')

試したソース

試したソースは以下にあります。
https://github.com/suketa/rails_sandbox/tree/try106_digest_uuid

参考情報

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

Bulmaを使うためにRailsのフロント周りについて改めて調べてみた

個人開発で出くわした悩み事について書きます。
フロントエンドにあまりに疎いため、Bulmaを使おうとしたとき、Yarnで追加するのかbulma-rails gemを使うのか迷いました。
それぞれどういう違いがあるのか、改めて調べてみることにしました。

bulma-railsは何をしているのか?

bluma-railsというgemがあります。
READMEにはこう書かれています

Integrates Bulma with the rails asset pipeline.

なるほど。asset pipelineを使うらしい。
asset pipelineという言葉に引っかかりを感じつつも、とりあえずgemの中身を見てみます。

gemspecを見る

こちらがbluma-railsのgemspecです。
SassCというgemに依存していることがわかります。
SassCは、ffiという他言語の関数を呼んだりできるgemを使って、
C++で書かれたlibsassを使ってSassを扱っています。

ffiというgem、初めて知りましたが面白そうなgemですね。

コードを見てみる

app/assets/stylesheetsというディレクトリの下にbulma.sassというファイルがあり、ここでapp/assets/stylesheets/sassをimportしているのがわかります。

@charset "utf-8"
/*! bulma.io v0.8.0 | MIT License | github.com/jgthms/bulma */
@import "sass/utilities/_all"
@import "sass/base/_all"
@import "sass/elements/_all"
@import "sass/form/_all"
@import "sass/components/_all"
@import "sass/grid/_all"
@import "sass/layout/_all"

あれ、これはつまりこれをapp/assets/stylesheets/bulma.sassに、app/assets/stylesheets/sassにこれを置いた、って感じですね。

続いて、libのbulma-rails.rbを見てみます。
こんな感じ。

module Bulma
  class Engine < ::Rails::Engine
  end
end

Railsエンジンが出てきました。

エンジンも詳しくないのでRailsガイドのRailsエンジン入門を読んでみます。

エンジンの中にあるアセットは、通常のアプリケーションで使われるアセットとまったく同じように振る舞います。
エンジンのクラスはRails::Engineを継承しているので、アプリケーションはエンジンのapp/assetsディレクトリとlib/assetsディレクトリを探索対象として認識します。

bulma-railsは、Railsが探索してくれるところにBulma使うのに必要なファイルを置いてくれるgemだということがわかりました。
置いたらあとはasset pipelineがよしなにやってくれるのでしょうね。

asset pipeline

asset pipeline、随分ご無沙汰しており、どんな人なのか記憶にないので、改めてRailsガイドを読んでみます。

アセットパイプラインはsprockets-rails gemによって実装され、デフォルトで有効になっています。

なるほどsprocketsがアセットのコンパイル、圧縮を頑張る仕組みなのですね。

アセットパイプライン導入後は、app/assetsディレクトリがアセットの置き場所として推奨されています。このディレクトリに置かれたファイルはSprocketsミドルウェアによってサポートされます。

bulma-railsはapp/assets/stylesheets以下にscssファイルを置いてたので、sprocketsがサポートしてくれるというわけですね。

しかしファイル置くだけのgemって変だよな、と、調べていたところ以下の記事を見つけました。

新しいRailsフロントエンド開発(1)Asset PipelineからWebpackへ(翻訳)

依存関係についてはどうでしょうか。Asset Pipelineを常に最新に保つのは大仕事です。プロジェクトにJavaScriptライブラリを1つ追加する場合、CDNから読み込んだコードをコピペしてapp/assetsやlib/assetsやvendor/assetsに置くか、誰かがgem化してくれるまでぼんやり待つ方法があります。その間にも、JavaScriptコミュニティは同じことをnpm installコマンド、今ならyarn addコマンド一発で管理しています。アップデートも同様です。YarnはJavaScriptをBundlerのように便利に扱うことができます。

な、なるほど、、、それでgem化して固めてたのか。先人の苦悩が伝わってきました。Yarnありがとう。

webpacker + YarnでBulmaを追加するとどうなるのか?

Rails6ではwebpackerがデフォルトになっているので、webpacker + Yarnを使って同じことをやってみます。
ロゴが猫なのでYarnは好きです。

コードがあるほうがよいかなとリポジトリ作りました。さて、雑にrails newしてみますとこんな出力が得られました。もりもりいろんなものがinstallされていることがわかります。

meowという気になる名前のモジュールがinstallされてますね。なんでしょうか。

├─ meow@3.7.0

CLI app helperと書いてありました。
meowなページで癒やされたので続きをやっていきます。

雑にアプリを作る

Yay! You’re on Rails! の画面が出ましたが、Bulmaの使い所がないので画面を増やすために雑にscaffoldしていきます。

rails generate scaffold cat name:string description:text

migrationしてrails sしてlocalhost:3000/catsを確認するとこんな画面が出ます。
スクリーンショット 2019-11-24 10.16.47.png
殺風景ですね。Bulmaを導入していきます。

Bulmaを使っていく

yarn add bulma

してみると、こんな出力が出ます。
warningが出てますが、webpackerのissueで対策検討されているようなので今回はさらっと流していきます。

yarn add bulmaしてみると、package.json、yarn.lockが更新され、node_modules以下にbulmaがinstallされます。node_modules以下は.gitignoreに書いてあるのでcommitに乗りません。これでBulmaが追加できました。

あとはRailsで使えるようにしていくだけ、なのですが、フロントエンド音痴ゆえここで大変苦戦しました。Rails6でwebpackを使ってbootstrapを導入する記事を見つけたので、これを参考にやっていきます。

  • application.sassを app/javascript/stylesheetsに作って、Bulmaのcssをimport
  • app/javascript/packs/application.jsで上記をimport
  • app/views/layouts/application.html.erb の stylesheet_link_tag を stylesheet_pack_tag に
  • app/views/cats/index.html.erbの適当な要素にclassを追加

スクリーンショット 2019-11-24 21.37.02.png
これでBulmaのスタイルを適用できました。Bulmaが効いていることをチャッと確かめたかったので、New Catのリンクとh1にだけclassを振っております。

まとめ

フロント開発から随分遠ざかっていたのでwebpackerについて調べる良い機会を得られました。しかしチャッとBulma使っていこう、というくらいのつもりだったのにwarningを追いかけてwebpackerのissueまで読むことになるとは思いませんでした。

asset pipelineを使っているbulma-railsは大変簡単に導入できたので、これはこれで良いなと思いました。ただ、長く開発していくアプリで使うならYarnで管理するのが良さそうですね。次はwebpackerではなく素のwebpackに挑戦してみようと思います。
この記事ではwebpackerの説明は省いてしまいました。webpackがどんなものなのかについては、24日に投稿する記事で紹介される予定です。お楽しみに。

明日は、@mochikichi321さんの「投稿画像の彩度低下問題を解決した話」です。明日もよろしくおねがいします!

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

[メモ]Rails超基礎 勉強して個人的に違いが分からなくなったメソッド

redirect_toメソッド

「redirect_to(URL)」とすることで、そのページに転送することができます。
以下だとcreateアクションを実行すると「/posts/index」ページへ転送される。

def create
  redirect_to("/posts/index")
end

renderメソッド

別のアクションを経由せずに、直接ビューを表示することができます。
render("フォルダ名/ファイル名")のように表示したいビューを指定します。
renderメソッドを使うと、redirect_toメソッドを使った場合と違い、そのアクション内で定義した@変数をビューでそのまま使うことができます。
以下だとupdateアクションが実行された時、@postにid(例 id:1)を持つデータをデータベースから取り出す。
取り出したデータのcontentカラムデータだけ取り出し@post.contentに格納する。
最後に「posts/edit」というURLに@post.contentという変数をビューでそのまま使うことができる。

def update
  @post = Post.find_by(id: params[:id])
  @post.content = params[:content]
  render("posts/edit")
end

form_tagメソッド

「form_tag(送信先のURL) do」のように送信先のURLを指定します。
実行時に、指定されたURLにデータが送信されます。

<%= form_tag("/posts/create") do %>
  <textarea></textarea>
  <input type="submit" value="投稿">
<% end %>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】form_with (local: true)について

なんでlocal: true入れるの?

= form_with(model:post.new, local: true) do |f|

local: trueがない場合、Rails5ではAjaxによる送信という意味になる。普通にHTMLとしてフォームを送信する場合にlocal: trueが必要になる

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

【Rails】routes collectionとmemberについて

colletionとmemberを使う目的

どちらもroutingで使うとき、resourcesでは自動で生成されないactionへの設定を使用

route.rb
resources :users do
  member do
    get :follow
      #follow_user GET    /users/:id/follow(.:format)            users#follow
      get :like
        #like_user GET    /users/:id/like(.:format)                users#like
    end
end

違い

生成する
routingに、:idがつくかつくのか
member つく
collection つかない

member

resouces :users do
  member do
    get :follow
  end
end
  follow_user GET /users/:id/follow(.:format)  users#follow

collection

resource :users do
  collection do
    get :slide
  end
end
slide_user GET /users/slide(.:format)  users#slide
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

kaminariでページネーション作ったる

ページネーションとは

今何ページ目にいるかがわかるやつです。
よく画面の下にあります↓
7355fe4673cc916f3090de3d6f313ad7.png

kaminariというgemを使うことで簡単に実装できます。

【その1】gemを追加する

Gemfile
gem 'kaminari'
bundle install



サーバーを再起動します。

rails s

なぜなら、サーバーを起動した際にgemが反映されるからです。

【その2】コントローラーを編集する

tweets_controller.rb
def index
  @tweets = Tweet.all.page(params[:page]).per(5)
end

ページ数がpageというキーになって、パラメータとして送られます。
1ページあたりに表示する件数をper(5)のように記述します。
この場合は5件表示されます。

【その3】ビューファイルを編集する

index.html.erb
<%= paginate(@tweets) %>

好きなところに置いてください。
これでページネーションが完成します。

pagenateではなくpaginateなので注意!!



ではまた!

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

Railsチュートリアル 第10章 ユーザーの更新・表示・削除 - 認可

認証と認可

ウェブアプリケーションの文脈において、「認証(authentication)」と「認可(authorization)」は以下のような意味で用いられます。

  • 認証…サイトのユーザーを識別すること
  • 認可…認証したユーザーに対し、当該ユーザーが実行可能な操作を管理すること

対応する英語の綴りが似通っており(最初4文字と最後5文字が同じ)、大変紛らわしいのですが、ともかくこの2つの概念の違いは重要です。

Railsチュートリアルにおいては、第8章で認証の仕組みを実装しました。認証の仕組みが実装されているので、認可の仕組みを実装する準備も整っています。これから実装していくのは、認可の仕組みです。

このパートで実装する内容

現状

ここまでの実装により、editアクションとupdateアクションが完全に動作するようになりました。しかしながら、現状では、以下のようにセキュリティ上の大きな問題を含む実装になっています。

  • 全てのユーザーが、あらゆるアクションにアクセスすることができる
  • ログインしていないユーザーを含め、誰でもユーザー情報を編集できる

このような実装は明らかにまずいです。実際にそのような実装になっていることが発覚したら、担当者の顔が真っ青になるのは間違いありません。

必要な実装

以下の実装が必要となるので、これから実装していきます。

  • ログインしていないユーザーが保護されたページにアクセスできないようにする
    • そのようなアクセスが発生した場合、分かりやすいメッセージを表示した上でログインページに転送する
  • ログイン済みのユーザーが許可されていないページにアクセスできないようにする
    • そのようなアクセスが発生した場合、ルートURLにリダイレクトする

「ログインしていないユーザーが保護されたページにアクセスしようとした場合に表示される画面のモックアップ」が、Railsチュートリアル本文の図 10.6にて示されています。

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

Usersコントローラーのbeforeフィルター

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

ユーザーにログインを要求する場面におけるbefore_actionメソッドの利用

現在必要としている実装の内容は、「ログイン済みユーザーであるかどうかを確認し、ログイン済みユーザーでなければログインを要求する」というものです。ということで、before_actionメソッドのユースケースは以下のようになります。

  1. logged_in_userメソッドを定義する
  2. before_action:logged_in_userを与える

logged_in_userメソッドの実装

「ユーザーがログインしていなければ、ログインを要求するフラッシュメッセージを出した上で、ログインページにリダイレクトする」という動作になります。コードは以下です。

def logged_in_user
  unless logged_in?
    flash[:danger] = "Please log in."
    redirect_to login_url
  end
end

before_actionの実装

「beforeフィルターにlogged_in_userを追加する。但し、適用されるのは:editアクションと:updateアクションのみ」という実装になります。

before_action :logged_in_user, only: [:edit, :update]

beforeフィルターは、デフォルトではコントローラー内の全てのアクションに適用されます。特定のアクションのみにbeforeフィルターを適用するようにするためには、:onlyオプションの値として対応するアクションを与えればOKです。

Usersコントローラーの変更内容

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

    ...略

    private

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

app/controllers/users_controller.rbには、以下の実装を行っています。

  • クラス定義の直後、最初のメソッド定義の前にbefore_actionを追加する
  • private以降にlogged_in_userメソッドを追加する

ここまでの実装が完了した後、ログインしていないユーザーが保護されたページにアクセスしようとしたらどうなるか

一度ログアウトしたのち、改めて /users/1/edit にアクセスしてみます。結果、Webブラウザに以下のページが表示されました。

スクリーンショット 2019-11-17 22.48.07.png

  • フラッシュメッセージに「Please log in.」と書かれている
  • ログインページにリダイレクトされている

以上の動作が正しく実装されているようです。

ログインを要求するようになったことに伴うテストの修正

現時点でテストは失敗する

# rails test
Running via Spring preloader in process 605
Started with run options --seed 46346

 FAIL["test_successful_edit", UsersEditTest, 2.683779799990589]
 test_successful_edit#UsersEditTest (2.68s)
        expecting <"users/edit"> but rendering with <[]>
        test/integration/users_edit_test.rb:21:in `block in <class:UsersEditTest>'

 FAIL["test_unsuccessful_edit", UsersEditTest, 2.700610100000631]
 test_unsuccessful_edit#UsersEditTest (2.70s)
        expecting <"users/edit"> but rendering with <[]>
        test/integration/users_edit_test.rb:10:in `block in <class:UsersEditTest>'

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

Finished in 2.76846s
31 tests, 77 assertions, 2 failures, 0 errors, 0 skips

現時点では、test/integration/users_edit_test.rbの「unsuccessful edit」および「successful edit」の両テストで失敗する状態になっています。これは、「editアクションやupdateアクションでログインを要求するようになったため、ログインしていないユーザーだとこれらのテストが失敗する」ためです。

editアクションやupdateアクションをテストする前にログインするようにする

この問題を解決するためには、「editアクションやupdateアクションをテストする前にログインするようにする」必要があります。以前似実装したlog_in_asヘルパーを用いることにより、このような動作が実現できます。

test/integration/users_edit_test.rb
  require 'test_helper'

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

    test "unsuccessful edit" do
+     log_in_as @user
      get edit_user_path(@user)
      ...略
    end

    test "successful edit" do
+     log_in_as @user
      get edit_user_path(@user)
      ...略
    end
  end

テストが成功するようになった

ここまでの実装が完了すると、再びテストが成功するようになります。

# rails test
Running via Spring preloader in process 618
Started with run options --seed 41953

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

Finished in 3.53240s
31 tests, 84 assertions, 0 failures, 0 errors, 0 skips

セキュリティモデルに関する実装がなければテストが通らないようにする

現時点では、セキュリティモデルに関する実装がなくてもテストが通ってしまう

試しに、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: [:edit, :update]
    ...略
  end
# rails test
Running via Spring preloader in process 631
Started with run options --seed 49117

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

Finished in 2.96846s
31 tests, 84 assertions, 0 failures, 0 errors, 0 skips

なんということでしょう。テストが成功してしまったではないですか。このような巨大なセキュリティホールが存在する実装は、なんとしてもテストで検出できる(テストが失敗する)ようにしなければなりません。

beforeフィルターに対するテスト

beforeフィルターは、基本的にアクションごとに適用していきます。よって、beforeフィルターに対するテストは、アクションごとに書いていくことになります。

具体的なテストの実装方針は以下のようになります。

  1. 正しい種類のHTTPリクエストを使って、editアクションとupdateアクションをそれぞれ実行させる
  2. フラッシュメッセージが代入されたかを確認する
  3. ログイン画面にリダイレクトされたかを確認する

どのHTTPリクエストをテスト対象とするか

  • editアクションは、/users/:id/edit へのGETリクエストに対応するアクションである
  • updateアクションは、/users/:id へのPATCHリクエストに対応するアクションである

というわけで、テストの対象は以下のソースコードの通りとなります。テストの名前は、それぞれ「should redirect edit when not logged in」「should redirect update when not logged in」とします。

test "should redirect edit when not logged in" do
  get edit_user_path(@user)
  # TODO:フラッシュメッセージが代入されたか、ログイン画面にリダイレクトされたか
end

test "should redirect update when not logged in" do
  patch user_path(@user), params: { user: { name: @user.name,
                                          email: @user.email } }
  # TODO:フラッシュメッセージが代入されたか、ログイン画面にリダイレクトされたか
end

2つ目のテストでは、patchメソッドを使って、user_path(@user)PATCHリクエストを送信しています。Railsのルーティング機能により、このリクエストではUsersコントローラーのupdateアクションがきちんと実行されます。

フラッシュメッセージが代入されたかのテスト

assert_not flash.empty?

フラッシュメッセージが空でなければテストは成功となります。

ログイン画面にリダイレクトされたかのテスト

assert_redirected_to login_url

リダイレクト先がlogin_urlであればテストは成功となります。リダイレクトなので、_pathではなく_urlを使います。

テストコードの全体像

test/controllers/users_controller_test.rbに対する変更の全体像は以下のようになります。

test/controllers/users_controller_test.rb
  require 'test_helper'

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

    test "should get new" do
      get signup_path
      assert_response :success
    end
+
+   test "should redirect edit when not logged in" do
+     get edit_user_path(@user)
+     assert_not flash.empty?
+     assert_redirected_to login_url
+   end
+
+   test "should redirect update when not logged in" do
+     patch user_path(@user), params: { user: { name: @user.name,
+                                             email: @user.email } }
+     assert_not flash.empty?
+     assert_redirected_to login_url
+   end
  end
setupメソッドの実装を書き忘れると

なお、setupメソッドの実装を書き忘れた場合は、以下のようなエラーが発生します。

# rails test
Running via Spring preloader in process 657
Started with run options --seed 25742

ERROR["test_should_redirect_edit_when_not_logged_in", UsersControllerTest, 3.0655816000071354]
 test_should_redirect_edit_when_not_logged_in#UsersControllerTest (3.07s)
ActionController::UrlGenerationError:         ActionController::UrlGenerationError: No route matches {:action=>"edit", :controller=>"users", :id=>nil}, missing required keys: [:id]
            test/controllers/users_controller_test.rb:10:in `block in <class:UsersControllerTest>'

ERROR["test_should_redirect_update_when_not_logged_in", UsersControllerTest, 3.074150300002657]
 test_should_redirect_update_when_not_logged_in#UsersControllerTest (3.07s)
ActionController::UrlGenerationError:         ActionController::UrlGenerationError: No route matches {:action=>"show", :controller=>"users", :id=>nil}, missing required keys: [:id]
            test/controllers/users_controller_test.rb:16:in `block in <class:UsersControllerTest>'

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

UrlGenerationError: No route matches {:action=>"edit", :controller=>"users", :id=>nil}, missing required keys: [:id]というエラーメッセージが出ているのがポイントです。「必要なユーザー情報が与えられていないため、Railsのルーティング機能がリソースへのURLを生成できない」という意味合いのエラーです。

セキュリティモデルに関する実装がないとテストが失敗することを確認する

以上test/controllers/users_controller_test.rbに対する変更を保存した上で、app/controllers/users_controller.rbbefore_actionをコメントアウトした状態で、改めてテストを実行してみましょう。

app/controllers/users_controller.rb
class UsersController < ApplicationController
  # before_action :logged_in_user, only: [:edit, :update]
  # ...略
end
# rails test test/controllers/users_controller_test.rb
Running via Spring preloader in process 722
Started with run options --seed 2388

 FAIL["test_should_redirect_edit_when_not_logged_in", UsersControllerTest, 1.9847615000035148]
 test_should_redirect_edit_when_not_logged_in#UsersControllerTest (1.99s)
        Expected true to be nil or false
        test/controllers/users_controller_test.rb:16:in `block in <class:UsersControllerTest>'

 FAIL["test_should_redirect_update_when_not_logged_in", UsersControllerTest, 2.026437199994689]
 test_should_redirect_update_when_not_logged_in#UsersControllerTest (2.03s)
        Expected response to be a redirect to <http://www.example.com/login> but was a redirect to <http://www.example.com/users/959740715>.
        Expected "http://www.example.com/login" to be === "http://www.example.com/users/959740715".
        test/controllers/users_controller_test.rb:24:in `block in <class:UsersControllerTest>'

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

Finished in 2.03119s
3 tests, 5 assertions, 2 failures, 0 errors, 0 skips

テストが失敗しました。

テスト「should redirect edit when not logged in」の失敗

「ログインしていない状態でユーザー情報の編集ページを開こうとすると、ログイン画面にリダイレクトされる」という動作のテストです。「test/controllers/users_controller_test.rbの16行目で、trueを返さなければならないところ、nilまたはfalseを返したためテストが失敗」とあります。私の環境では、同行には以下の記述があります。

test/controllers/users_controller_test.rb
assert_not flash.empty?

「フラッシュメッセージが空であってはならない」ということですね。

beforeフィルターが正しく実装されていれば、この場面では「"Please log in."というフラッシュメッセージが定義された上で、ログイン画面にリダイレクトされる」という動作をするはずです。しかしながら、beforeフィルターが実装されていないと、そのままユーザー情報の編集ページを開くことができてしまいます。結果、テストが失敗するのです。

テスト「should redirect update when not logged in」の失敗

「ログインしていない状態でユーザー情報の編集を保存しようとすると、ログイン画面にリダイレクトされる」という動作のテストです。「test/controllers/users_controller_test.rbの24行目で、ログイン画面のURLにリダイレクトされなければならないところ、特定のユーザー情報のURLにリダイレクトされたためテストが失敗」とあります。私の環境には、同行には以下の記述があります。

assert_redirected_to login_url

beforeフィルターが正しく実装されていれば、この場面では「"Please log in."というフラッシュメッセージが定義された上で、ログイン画面にリダイレクトされる」という動作をするはずです。しかしながら、beforeフィルターが実装されていないと、(ユーザー情報の編集が保存された上で)当該ユーザー情報のURLにリダイレクトされることになります。結果、テストが失敗するのです。

セキュリティモデルに関する実装があればテストが成功することを確認する

今度は、app/controllers/users_controller.rbbefore_actionのコメントアウトを解除した上で、改めてtest/controllers/users_controller_test.rbのテストを実行してみましょう。

app/controllers/users_controller.rb
  class UsersController < ApplicationController
-   # before_action :logged_in_user, only: [:edit, :update]
+   before_action :logged_in_user, only: [:edit, :update]
    ...略
  end
# rails test test/controllers/users_controller_test.rb
Running via Spring preloader in process 735
Started with run options --seed 41773

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

Finished in 1.95353s
3 tests, 5 assertions, 0 failures, 0 errors, 0 skips

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

演習 - ユーザーにログインを要求する

1. リスト 10.15only:オプションをコメントアウトしてみて、テストスイートがエラーを検知できるかどうか (テストが失敗するかどうか) 確かめてみましょう。

デフォルトのbeforeフィルターは、すべてのアクションに対して制限を加えます。今回のケースだと、ログインページやユーザー登録ページにも制限の範囲が及んでしまうはずです (結果としてテストも失敗するはずです)。

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

app/controllers/users_controller.rbを上記のように変更した状態でテストを実行してみます。

# rails test test/controllers/users_controller_test.rb
Running via Spring preloader in process 748
Started with run options --seed 6443

 FAIL["test_should_get_new", UsersControllerTest, 0.8396128000022145]
 test_should_get_new#UsersControllerTest (0.84s)
        Expected response to be a <2XX: success>, but was a <302: Found> redirect to <http://www.example.com/login>
        Response body: <html><body>You are being <a href="http://www.example.com/login">redirected</a>.</body></html>
        test/controllers/users_controller_test.rb:11:in `block in <class:UsersControllerTest>'

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

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

「should get new」というテストが失敗しています。当該テストの内容は以下のとおりです。

test "should get new" do
  get signup_path
  assert_response :success
end

このテストは、/signup へのGETリクエスト(すなわちUsersコントローラーのnewアクション=ユーザー登録ページの表示)に対して返ってくるHTTPステータスコードは200番台でなければ通りません。しかしながら、beforeフィルターが /signup に適用されると、/signup へのGETリクエストに対して返ってくるHTTPステータスコードが302になってしまいます。そのため、テストが成功しなくなるのです。

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

現状、必要な実装、実装方針

ログインを要求する仕組みは実装できました。しかしながら、未だ「ログイン済みユーザーが他のユーザーの登録情報を編集できてしまう」という問題は残っています。「ユーザーが自分の情報のみを編集できるようにする」という機能を実装することが必要です。

セキュリティが関係する実装は、確実になされていなければなりません。そうでなければ、「セキュリティ上の深刻な欠陥を見逃す実装」がなされてしまうおそれがあります。先ほどの「ユーザーにログインを要求する」の実装で発生しましたね…。

というわけで、「正しいユーザーを要求する」という仕組みを、テスト駆動開発で実装していく…Railsチュートリアル本文はこのような流れで進んでいきます。

fixtureに新たなユーザーを追加する

「異なるユーザーが、互いに情報を編集できないようにする」という実装を追加するために、まずfixtureに新たなユーザーを追加します(もちろん、正しいUserモデルの実体になるように定義します)。

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') %>

間違ったユーザーが編集しようとしたときのテスト

test "should redirect edit when logged in as wrong user" do
  log_in_as(@other_user)
  get edit_user_path(@user)
  assert flash.empty?
  assert_redirected_to root_url
end

上記のコードは、「あるユーザーでログインした後、別のユーザーのユーザー情報編集ページを開こうとした場合」のテストです。「ルートURLにリダイレクトされる。その際、フラッシュメッセージは空である」という挙動であればテストが通ります。

test "should redirect update when logged in as wrong user" do
  log_in_as(@other_user)
  patch user_path(@user), params: { user: { name: @user.name,
                                            email: @user.email } }
  assert flash.empty?
  assert_redirected_to root_url
end

続いて上記のコードは、「あるユーザーでログインした後、別のユーザーの登録情報を更新しようとした場合」のテストです。これまた「ルートURLにリダイレクトされる。その際、フラッシュメッセージは空である」という挙動であればテストが通ります。

現時点ではテストは失敗する

# rails test test/controllers/users_controller_test.rb
Running via Spring preloader in process 865
Started with run options --seed 61640

 FAIL["test_should_redirect_update_when_logged_in_as_wrong_user", UsersControllerTest, 0.7539288999978453]
 test_should_redirect_update_when_logged_in_as_wrong_user#UsersControllerTest (0.76s)
        Expected false to be truthy.
        test/controllers/users_controller_test.rb:39:in `block in <class:UsersControllerTest>'

 FAIL["test_should_redirect_edit_when_logged_in_as_wrong_user", UsersControllerTest, 2.5815383000008296]
 test_should_redirect_edit_when_logged_in_as_wrong_user#UsersControllerTest (2.58s)
        Expected response to be a <3XX: redirect>, but was a <200: OK>
        test/controllers/users_controller_test.rb:32:in `block in <class:UsersControllerTest>'

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

Finished in 2.61645s
5 tests, 8 assertions, 2 failures, 0 errors, 0 skips

テスト「should redirect edit when logged in as wrong user」の失敗理由

テスト「should redirect edit when logged in as wrong user」のほうは、test/controllers/users_controller_test.rbの32行目でテストが失敗しています。具体的には、以下の記述がされた行です。

assert_redirected_to root_url

失敗した理由は、「HTTPリクエストに対するレスポンスがリダイレクトであるべきところが、200 OK を返してきている」というものです。「 / へのリダイレクトが正しく機能していない」ということですね。

テスト「should redirect update when logged in as wrong user」の失敗理由

一方の「should redirect update when logged in as wrong user」のほうは、test/controllers/users_controller_test.rbの39行目でテストが失敗しています。具体的には、以下の記述がされた行です。

assert flash.empty?

失敗した理由は「フラッシュメッセージは空でなければならないのに、空でないフラッシュメッセージが渡されてきた」というものです。「誰でも無条件にユーザー情報の内容を更新できてしまう」という現状の実装では、このときのフラッシュメッセージは「Profile updated」になりますね。具体的には、UsersController#updateの以下の部分です。

UsersController#update
def update
  if @user.update_attributes(user_params)
    flash[:success] = "Profile updated"  # <=この部分
    # ...略
  else
    # ...略
  end
end

正しいユーザーかどうか確認するコードをUsersコントローラーに追加する

def correct_user
  @user = User.find(param[:id])
  redirect_to(root_url) unless @user == current_user
end

上記のコードは、「HTTPリクエストで編集・更新の対象として与えられたユーザーが現在ログイン中のユーザーと同一であることを確認し、同一でなければ / にリダイレクトする」という動作をするコードです。Usersコントローラーのprivateメソッド以降に追加していきます。current_userは、Rails本体で定義された、現在ログイン中のユーザーを返すメソッドです。

before_action :correct_user,   only: [:edit, :update]

上記のコードは、「beforeフィルターにcorrect_userを追加する。但し、適用されるのは:editアクションと:updateアクションのみ」というコードです。

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

    ...略

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

    def update
-     @user = User.find(params[:id])
      if @user.update_attributes(user_params)
        flash[:success] = "Profile updated"
        redirect_to @user
      else
        render 'edit'
      end
    end

    private

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

      # beforeアクション

      # ログイン済みユーザーかどうか確認
      def logged_in_user
        unless logged_in?
          flash[:danger] = "Please log in."
          redirect_to login_url
        end
      end
+
+     # 正しいユーザーかどうか確認
+     def correct_user
+       @user = User.find(params[:id])
+       redirect_to(root_url) unless @user == current_user
+     end
  end

app/controllers/users_controller.rb全体の変更は、上記ソースコードのようになります。

小さなリファクタリング

editおよびupdateの両アクションの前で、常にcorrect_userメソッドが呼び出される」という前提の場合、以下の処理はcorrect_user内で実装されているため、editおよびupdateには不要になります。

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

ゆえに、当該処理のコードはeditおよびupdateから削除しています。「重複する記述を一本化する」というのは、リファクタリングの常套手段ですよね。

テストが成功することを確認する

改めて、test/controllers/users_controller_test.rbに対応するテストを実行してみます。

# rails test test/controllers/users_controller_test.rb
Running via Spring preloader in process 891
Started with run options --seed 52467

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

Finished in 2.17585s
5 tests, 9 assertions, 0 failures, 0 errors, 0 skips

今度こそテストが問題なく成功することが確認できました。

さらなるリファクタリング - current_user?メソッドを実装する

current_user?メソッドの実装に至る前提

UsersController#correct_user内には、以下のコードが存在します。

unless @user == current_user

上記コードでももちろん期待する動作は実現できます。しかしながら、ロジックを実装するコードにこのような書き方が存在するというのは、「Ruby的」とは言い難く、ソースコード全体のエレガントさを損ねるものです。エレガントで「Ruby的」なソースコードは、もっとこう、以下の例のような記述を期待しますよね。

!current_user.nil?
if logged_in?
unless logged_in?
if page_title.empty?
assert flash.empty?

…ならばそのようにしてしまいましょう、というのがRailsチュートリアル本文の流れです。

current_user?メソッドの実装

当該メソッドの名前はcurrent_user?とします。メソッド全体のコードは以下のようになります。

def current_user?(user)
  user == current_user
end

current_user?メソッドは、correct_user内部で使えるようにしたいので、実装箇所はSessionsヘルパーとなります。

app/helpers/sessions_helper.rb
  module SessionsHelper

    ...略
+
+   # 渡されたユーザーがログイン済みユーザーであればtrueを返す
+     def current_user?(user)
+       user == current_user
+     end

    # 現在ログイン中のユーザーを返す(いる場合)
    def current_user
      ...略
      end
    end

   ...略
  end

current_user?メソッドの使用

実際のUsersController#correct_userメソッドでcurrent_user?メソッドを使用するようにコードを変更します。

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

    private

      ...略

      # beforeアクション

      ...略

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

演習

1. 何故editアクションとupdateアクションを両方とも保護する必要があるのでしょうか? 考えてみてください。

editアクションを保護する必要性

editアクションが保護されていないと、「ログインユーザーが他のユーザーのユーザー情報編集ページを開くことができてしまう」という事態が発生します。

確かにupdateアクションが保護されていれば、上記の状況でユーザー情報の編集がRDBに反映されることはありません。しかし、「他のユーザーのユーザー情報編集ページを開くことができてしまう」という事態そのものが、ユーザー体験としてよろしくないのは容易に想像できます。

updateアクションを保護する必要性

現在のページ構成上、Webブラウザによるアクセスであれば、常に「updateアクションはeditアクションの後に呼び出される」という実装になります。

しかしながら、以下のようなフローが発生する可能性は十分に考えられます。

  • editアクションが実行され、ユーザー情報の編集画面が開かれた後に、当該画面を残したまま、同一ブラウザの別のタブでログアウトする
  • editアクションが実行され、ユーザー情報の編集画面が開かれた後に、当該画面を残したまま、同一ブラウザの別のタブで別のユーザーにログインする

また、Webサービスには、Webブラウザ以外によるアクセスも考えられます。「cURLなどのツールにより、POSTPATCHが直接呼び出される」という可能性も想定しておかなければなりません。

上記のような状況でも意図せぬRDBの更新を防ぐ必要があります。そのため、updateアクションは保護される必要があるのです。

2. 上記のアクションのうち、どちらがブラウザで簡単にテストできるアクションでしょうか?

現在のページ構成上、Webブラウザでupdateアクションをテストするには、「ユーザーの編集ページ」を開いて「Save changes」ボタンをクリックする必要があります。そのためには、まずeditアクションが正常に動作しなければなりません。

ゆえに、editアクションのほうがテストは容易です。

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

現状の実装の問題点

現状の実装は、「保護されたページにアクセスしようとすると、問答無用で自分のプロフィールページに移動させられてしまう」というものです。

「保護されたページに非ログイン状態でアクセスし、その後ログインする」というユースケースに対しては、「ログイン後には、非ログイン状態で開こうとしていたページを開く」という動作のほうがよりユーザーフレンドリーな動作といえるでしょう。具体的には、例えば「非ログイン状態でユーザー情報編集ページにアクセスしようとする→ログイン」というユースケースの場合、ログイン後には(ユーザーのプロフィールページではなく)ユーザー情報編集ページが開かれるようにしたい、ということです。

フレンドリーフォワーディングのテスト

実現したい動作は、「編集画面を開いた後にログインすると、編集ページにリダイレクトされる」という動作です。対応するテストは以下のようになります。

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

このテストを書くファイルは、test/integration/users_edit_test.rbとなります。既存のテスト「successful edit」を書き換えていきます。

test/integration/users_edit_test.rb
  require 'test_helper'

  class UsersEditTest < ActionDispatch::IntegrationTest
    ...略

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

なお、リダイレクトによってedit用のテンプレートが描画されなくなったことに対応し、assert_templateを削除しています。

現時点でテストは通らない

当然といえば当然かもしれませんが、現時点で上記のテストは通りません。

# rails test test/integration/users_edit_test.rb
Running via Spring preloader in process 917
Started with run options --seed 41025

 FAIL["test_successful_edit_with_friendly_forwarding", UsersEditTest, 0.5618743999948492]
 test_successful_edit_with_friendly_forwarding#UsersEditTest (0.56s)
        Expected response to be a redirect to <http://www.example.com/users/959740715/edit> but was a redirect to <http://www.example.com/users/959740715>.
        Expected "http://www.example.com/users/959740715/edit" to be === "http://www.example.com/users/959740715".
        test/integration/users_edit_test.rb:23:in `block in <class:UsersEditTest>'

  2/2: [===================================] 100% Time: 00:00:01, Time: 00:00:01

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

「リダイレクト先が編集ページであるべきところ、実際にはプロフィールページになっている」という趣旨のメッセージが見受けられますね。

フレンドリーフォワーディングの実装

フレンドリーフォワーディングの実装に必要な仕組み

ログイン時にユーザーを希望のページに転送させるには、以下の機能を実装する必要があります。

  • リクエストの時点のページを記憶する(メソッド名はstore_location
  • ログイン後、記憶しておいた場所のURLにリダイレクトさせるようにする(メソッド名はredirect_back_or

フレンドリーフォワーディングに必要な仕組みを実際に実装する

リクエストの時点のページを記憶する

def store_location
  session[:forwarding_url] = request.original_url if request.get?
end

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

  • 転送先のURLを記憶する場所は一時cookies
    • sessionメソッドでアクセスする
    • キーの名前はforwarding_urlとする
  • リクエスト先は、request.original_urlというメソッドで取得できる
  • リクエスト内容がGETである場合のみ、転送先のURLを記憶する
    • リクエスト内容がPOSTPATCHDELETEである場合に対する備え
リクエスト内容がGETでない場合と、その対処は

「リクエスト内容がGETではない場合」というのは、例えば「ログインしていないユーザーがフォームから送信する」ようなユースケース、より具体的には「ユーザーが一時cookiesを手動で削除した上でフォームから送信する」などといったユースケースで発生しえます。

POSTPATCHDELETEが期待されているURLに対してGETリクエストが送られるというのは想定外の動作です。場合によってはエラーが発生することにもなりかねません。

このような場合に転送先のURLを記憶しないようにするため、if request.get?という条件文を用い、「リクエスト内容がGETである場合のみ転送先URLを記憶する」というをするように実装しています。

ログイン後、記憶しておいた場所のURLにリダイレクトさせるようにする

def redirect_back_or(default)
  redirect_to(session[:forwarding_url] || default)
  session.delete(:forwarding_url)
end

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

  • redirect_toメソッドの引数内で||演算子を使っている
  • redirect_toメソッドが呼び出された後、記憶されていたリダイレクト先URLが削除されている
  • redirect_toメソッドが呼び出されても、直ちにリダイレクトはされない
  • redirect_toメソッドが呼び出された後、リダイレクトが実行されるためには、さらに以下いずれかの条件が満たされる必要がある
    • 明示的にreturn文が呼び出される
    • redirect_toメソッドが呼び出されたメソッドの最終行が呼び出され、その評価が完了する
redirect_toメソッドの引数

redirect_toメソッドの引数は以下のようになっています。

session[:forwarding_url] || default

上記コードは、Ruby初心者にとってその挙動が分かりづらいコードです。解説の文章がが少々長くなるので、redirect_toメソッドの引数内で || 演算子を用いる。その動作の解説という別項目を立てて解説しています。

記憶されていたリダイレクト先URLを削除する
session.delete(:forwarding_url)

上記コードは「redirect_toメソッドが呼び出された後、記憶されていたリダイレクト先URLを削除する」というコードです。このコードがないと、「ログアウト→再ログインした際、保護されたページに転送される」という動作がブラウザを終了する(=一時cookieが削除される)まで続くことになります。再ログイン時のリダイレクト先は、ユーザーのプロフィールページである方が望ましいですよね。

Usersコントローラーに「ログインしていないとき、一時cookiesにリダイレクト先のURLを保存する」という仕組みを実装する

store_locationメソッドを適切な場所で呼び出せば、必要な実装が完成します。実装箇所は、Usersコントローラーのbeforeフィルターで用いるlogged_in_userメソッド内です。

def logged_in_user
  unless logged_in?
    store_location
    flash[:danger] = "Please log in."
    redirect_to login_url
  end
end

app/controllers/users_controller.rbそのものの変更内容は以下のようになります。

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

      ...略

      private

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

        # beforeアクション

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

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

Sessionsコントローラーでフレンドリーフォワーディングの仕組みを使うようにする

以上でフレンドリーフォワーディング機構の実装を書き終えました。実際に機構を使うために、Sessionsコントローラーのcreateメソッドを書き換える必要があります。Sessionsコントローラーのcreateメソッドの新たなコードは以下です。

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

上述createアクションの新たな実装を踏まえた上で、app/controllers/sessions_controller.rbに変更を反映すると以下のようになります。

app/controllers/sessions_controller.rb
  class SessionsController < ApplicationController
    def new
    end

    def create
      @user = User.find_by(email: params[:session][:email].downcase)
      if @user && @user.authenticate(params[:session][:password])
        log_in @user
        params[:session][:remember_me] == '1' ? remember(@user) : forget(@user)
-       redirect_to @user
+       redirect_back_or user
      else
        ...略
      end
    end

    def destroy
      ...略
    end
  end

改めてテストを実行する

ここまでの実装が完了したところで、改めてtest/integration/users_edit_test.rbに対してテストを実行します。

# rails test test/integration/users_edit_test.rb
Running via Spring preloader in process 996
Started with run options --seed 18863

  2/2: [===================================] 100% Time: 00:00:01, Time: 00:00:01

Finished in 1.84389s
2 tests, 9 assertions, 0 failures, 0 errors, 0 skips

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

演習 - フレンドリーフォワーディング

1. フレンドリーフォワーディングで、渡されたURLに初回のみ転送されていることを、テストを書いて確認してみましょう。次回以降のログインのときには、転送先のURLはデフォルト (プロフィール画面) に戻っている必要があります。

ヒント: リスト 10.29のsession[:forwarding_url]が正しい値かどうか確認するテストを追加してみましょう。

「次回以降のログインのときには、転送先のURLはデフォルト (プロフィール画面) に戻っている」という動作を実現するためには、「ログインが成功した時点で、session[:forwarding_url]nilになっている」必要があります。

対応するテストコードの変更内容は、test/integration/users_edit_test.rb内以下の部分となります。

test/integration/users_edit_test.rb
  class UsersEditTest < ActionDispatch::IntegrationTest
    ...略

    test "successful edit with friendly forwarding" do
      get edit_user_path(@user)
      log_in_as @user
      assert_redirected_to edit_user_url(@user)
      name = "Foo Bar"
      email = "foo@bar.com"
      patch user_path(@user), params: {user: {  name: name,
                                                email: email,
                                                password: "",
                                                password_confirm: ""} }
      assert_not flash.empty?
      assert_redirected_to @user
      @user.reload
+     assert_nil session[:forwarding_url]
      assert_equal name, @user.name
      assert_equal email, @user.email
    end
  end

本当に正しいテストなのか

「ログインが成功した時点で、session[:forwarding_url]nilにする」という動作は、SessionsHelper#redirect_back_orメソッド内の以下のコードになります。

session.delete(:forwarding_url)

当該部分をapp/helpers/sessions_helper.rbからコメントアウトします。

app/helpers/sessions_helper.rb
  module SessionsHelper
    ...略

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

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

# rails test test/integration/users_edit_test.rb
Running via Spring preloader in process 1134
Started with run options --seed 6312

 FAIL["test_successful_edit_with_friendly_forwarding", UsersEditTest, 1.9922731000115164]
 test_successful_edit_with_friendly_forwarding#UsersEditTest (1.99s)
        Expected "http://www.example.com/users/959740715/edit" to be nil.
        test/integration/users_edit_test.rb:33:in `block in <class:UsersEditTest>'

  2/2: [===================================] 100% Time: 00:00:01, Time: 00:00:01

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

テストが失敗し、下記のメッセージが出力されています。

Expected "http://www.example.com/users/959740715/edit" to be nil.
test/integration/users_edit_test.rb:33

私の環境では、test/integration/users_edit_test.rbの33行目は以下の内容です。

test/integration/users_edit_test.rb(33行目)
assert_nil session[:forwarding_url]

まさしくたった今追加したテストですね。テストコードは正しいようです。

実装に問題はないのか

session.delete(:forwarding_url)

app/helpers/sessions_helper.rbから、上記コードのコメントアウトを解除します。

app/helpers/sessions_helper.rb
  module SessionsHelper
    ...略

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

app/helpers/sessions_helper.rbに変更を保存した上で、改めてtest/integration/users_edit_test.rbを対象としてテストを実行します。

# rails test test/integration/users_edit_test.rb
Running via Spring preloader in process 1147
Started with run options --seed 11229

  2/2: [===================================] 100% Time: 00:00:01, Time: 00:00:01

Finished in 2.00460s
2 tests, 10 assertions, 0 failures, 0 errors, 0 skips

テストが成功しました。実装も問題ないといえますね。

1.発展. 「ログインしていない状態でユーザー情報編集ページにアクセスしようとした」という状況で、フレンドリーフォワーディングのリダイレクト先のURLが正しく渡されていることを、テストを書いて確認してみましょう。

Railsチュートリアル 第10章 フレンドリーフォワーディングの追加演習 - フレンドリーフォワーディングのリダイレクト先のURLが正しく渡されていることのテスト(editアクション編)

1.発展. 「ログインしていない状態でRDB上のユーザー情報を更新しようとした」という状況で、フレンドリーフォワーディングのリダイレクト先のURLが渡されていないことを、テストを書いて確認してみましょう。

Railsチュートリアル 第10章 フレンドリーフォワーディングの追加演習 - フレンドリーフォワーディングのリダイレクト先のURLが渡されていないことのテスト(updateアクション編) - Qiita

2. session[:forwarding_url]にリダイレクト先のURLが保存される状況において、値が正しいかどうか確認してみましょう。また、newアクションにアクセスしたときのrequest.get?の値も確認してみましょう)。

具体的には、以下のような手順になります。

  1. 7.1.3で紹介したdebuggerメソッドをSessionsコントローラのnewアクションに置く
  2. その後、ログアウトして /users/1/edit にアクセスする

まずは、app/controllers/sessions_controller.rbに以下の変更を行います。

app/controllers/sessions_controller.rb
  class SessionsController < ApplicationController
    def new
+     debugger
    end

    ...略
  end

その上で、Webブラウザで以下の操作を行います。

  1. すでにサンプルアプリケーションにログインしている場合、ログアウトする
  2. /users/1/edit にアクセスする

この時点で、rails serverのログ画面は以下のようになります。

Started GET "/login" ...略

[1, 10] in /var/www/sample_app/app/controllers/sessions_controller.rb
    1: class SessionsController < ApplicationController
    2:   def new
    3:     debugger
=>  4:   end
    ...略
(byebug) 

session[:forwarding_url]の値が正しいことを確認する

session[:forwarding_url]の内容は以下のようになります。

(byebug) session[:forwarding_url]
"http://localhost:8080/users/1/edit"

「/users/1/edit にアクセスした」というのが前提なので、session[:forwarding_url]の値は確かに正しいですね。

なお、この時点でのflashの内容は以下のようになります。

(byebug) pp(flash)
#<ActionDispatch::Flash::FlashHash:0x00007f4d1935f850
 @discard=#<Set: {"danger"}>,
 @flashes={"danger"=>"Please log in."},
 @now=nil>
...略

newアクションにアクセスしたときのrequest.get?の値

「/users/1/edit にWebブラウザからアクセスした」というのは、「/users/1/edit にGETリクエストを送出した」ともいえます。この時点では、request.get?trueを返します。

(byebug) request.get?
true

演習の後処理

最後はdebuggerメソッドが実行されなくなるようにしましょう。

app/controllers/sessions_controller.rb
  class SessionsController < ApplicationController
    def new
-     debugger
    end

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

【Heroku】デプロイ後にcode=H14 desc="No web processes running"

事象

話の流れとしてはこの記事の続きになります。

Dockerで開発環境構築をしたRailsアプリのコンテナをpushすることができたのですが、その後

$ heroku addons:create heroku-postgresql:hobby-dev

$ heroku container:release "コンテナ名"

$ heroku open

として、サイトを開いてみると、

スクリーンショット 2019-09-24 7.44.05.png

とHerokuのエラーが発生していました。

解決方法

まず、画面に書かれている通り、コンソール上でheroku logs --tailを実行してみたところ

$ heroku logs --tail
2019-09-10T22:53:00.595256+00:00 heroku[router]: at=error code=H14 desc="No web processes running" method=GET path="/" host=safe-basin-05606.herokuapp.com request_id=e797e1d4-8327-4285-b525-986ed50ea467 fwd="160.237.76.19" dyno= connect= service= status=503 bytes= protocol=https
2019-09-10T22:53:01.484857+00:00 heroku[router]: at=error code=H14 desc="No web processes running" method=GET path="/favicon.ico" host=safe-basin-05606.herokuapp.com request_id=f8c9be65-f445-4b0d-a4fa-83b27c76762f fwd="160.237.76.19" dyno= connect= service= status=503 bytes= protocol=https

と出力されました。

ログによるとcode=H14 desc="No web processes running"が発生しているようなので、この文言で検索してみると、こちらの公式ドキュメントを発見。

公式ドキュメントによると、heroku ps:scale web=1を叩けとのことなので、叩いてみると、

$ heroku ps:scale web=1
 ›   Warning: heroku update available from 7.26.2 to 7.29.0.
Scaling dynos... !
 ▸    Couldn't find that process type (web).

と出力され、失敗してしまいました。

改めて、Web上でherokuのBuildlogを見てみると、

スクリーンショット 2019-10-26 13.04.19.png

と表示されていました。

どうやら、heroku.ymlが含まれていないことが原因の模様で、解決するにはheroku.ymlを作成するか、heroku stack:set heroku-18を実行する必要があるとのこと。

そこでまずはheroku.ymlを作成する方針をとってみました。

Buildlogに記載されていた公式ドキュメントを参考に下記のシンプルなheroku.ymlを作成しました。

heroku.yml
build:
  docker:
    web: Dockerfile

その後

$ heroku stack:set container
$ git push heroku master

を実行し、再度heroku ps:scale web=1を実行してみると、

$ heroku ps:scale web=1
Scaling dynos... done, now running web at 1:Free

となり成功しました。

改めてURLにアクセスしてみると、Herokuに関するエラーは消え、Railsにおいて、DBが見つからないエラーが発生していたので、公式サイト
を参考に

$ heroku run rake db:migrate

としてマイグレーションを実行したところ、とうとうお馴染みの画面がお出迎えしてくれました。

スクリーンショット 2019-10-26 13.17.10.png

原因

公式ドキュメントによると、webサイトに対してdynosが割り当てられていないのが原因で、H14が発生してしまう模様で、それを割り当てるためのコマンドがheroku ps:scale web=1です。

ちなみにdynosとは、dynoの複数形で、公式のdyno解説記事によると、Herokuで使用されるコンテナのことをdynoと呼んでいるそうです。

ただ、heroku.ymlを作成するまで、heroku ps:scale web=1が成功しなかった理由は今の所わかっていません。ご存知の方がいれば、ご教示いただきますと幸いです。

また、heroku.ymlを作成せず、Buildlogに記載されていた、heroku stack:set heroku-18を実行したらどうなっていたかというのも気になりますので、同じ症状に出会い試された方がいれば、ご教示いただけますと幸いです。

まとめ

Herokuデプロイ後にcode=H14 desc="No web processes running"が発生したら、
heroku ps:scale web=1を試してみてください。
このコマンドが成功しない場合は、heroku.ymlを作成した後で再度試してみてください。

この記事が少しでも誰かのお役に立てれば幸いです。
最後までお読みいただきありがとうございました。

参考文献

公式ドキュメント

Heroku Error Codes
https://devcenter.heroku.com/articles/error-codes#h14-no-web-dynos-running

Building Docker Images with heroku.yml
https://devcenter.heroku.com/articles/build-docker-images-heroku-yml#getting-started

Getting Started on Heroku with Rails 5.x
https://devcenter.heroku.com/articles/getting-started-with-rails5#migrate-your-database

dyno:Heroku プラットフォームの中核
https://jp.heroku.com/dynos

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

Railsチュートリアル 第10章 フレンドリーフォワーディングの追加演習 - フレンドリーフォワーディングのリダイレクト先のURLが渡されていないことのテスト(updateアクション編)

何についての記事か

「Railsチュートリアル 第10章 演習 - フレンドリーフォワーディング」の発展学習となります。

同演習には、「渡されたURLに初回のみ転送されていることを確認する。具体的には、リダイレクトのURLはデフォルト (プロフィール画面) に戻っていることを確認する。」という内容の出題があります。それであれば、逆に「『ログインしていない状態でRDB上のユーザー情報を更新しようとした』という状況で、フレンドリーフォワーディングのリダイレクト先のURLが渡されていないこと」のテストも必要なはずです。

というわけで、発展学習としてQiita記事を書いてみました。

内容

フレンドリーフォワーディングのリダイレクト先のURLを保存する処理はどこにあるか

editアクションの場合と同じく、Usersコントローラーのlogged_in_userメソッドとなります。

フレンドリーフォワーディングのリダイレクト先のURLを保存する処理に対応するテスト

上述コードに対応するテストコードは、test/controllers/users_controller_test.rb内にあります。updateアクションに対応するテストは"hould redirect update when not logged in"です。

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

当該テストに記述を追加すれば、「『ログインしていない状態でRDB上のユーザー情報を更新しようとした』という状況で、フレンドリーフォワーディングのリダイレクト先のURLが渡されていないこと」がテストできるはずです。

実際に追加する記述は以下のようになります。

test/controllers/users_controller_test.rb(抜粋)
  test "should redirect update when not logged in" do
    patch user_path(@user), params: { user: { name: @user.name,
                                              email: @user.email } }
    assert_not flash.empty?
    assert_redirected_to login_url
+   assert_nil session[:forwarding_url]
  end

前提 - テストコードの特定行に対してテストを実行する

例えば、test/controllers/users_controller_test.rbの22行目に対応するテストを実行するには、rails testコマンドを以下のように呼び出せばOKです。

# rails test test/controllers/users_controller_test.rb:22

PATCHリクエストに対して、一時cookiesにリダイレクト先のURLが保存される場合に対するテスト

PATCHリクエストに対して、一時cookiesにリダイレクト先のURLが保存される場合」を再現する

SessionsHelper#store_locationメソッドの記述を変更します。

app/helpers/sessions_helper.rb
  module SessionsHelper
    ...略

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

request.get?request.patch?にことにより、「リクエストの内容がPATCHである場合、一時cookiesにリダイレクト先のURLを保存する」という動作にしています。

PATCHリクエストに対して、一時cookiesにリダイレクト先のURLが保存される場合」に、updateアクションに対するテストが失敗することを確認する

# rails test test/controllers/users_controller_test.rb:22
Running via Spring preloader in process 1311
Started with run options --seed 7566

 FAIL["test_should_redirect_update_when_not_logged_in", UsersControllerTest, 0.6280409999890253]
 test_should_redirect_update_when_not_logged_in#UsersControllerTest (0.63s)
        Expected "http://www.example.com/users/959740715" to be nil.
        test/controllers/users_controller_test.rb:27:in `block in <class:UsersControllerTest>'

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

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

test/controllers/users_controller_test.rbの27行目でテストが失敗しました。当該行には、以下のコードが記述されています。

test/controllers/users_controller_test.rb(27行目)
assert_nil session[:forwarding_url]

たった今追加したコードですね。想定したテストが正しく行えていることがわかりました。

SessionsHelper#store_locationメソッドを元に戻す

テストが正しく実装できていることがわかったので、SessionsHelper#store_locationメソッドは元に戻しておきましょう。

app/helpers/sessions_helper.rb
  module SessionsHelper
    ...略

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

実装が正しいことの確認

改めて、test/controllers/users_controller_test.rbの22行目に対応するテストを実行します。

# rails test test/controllers/users_controller_test.rb:22
Running via Spring preloader in process 1324
Started with run options --seed 26169

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

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

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

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

Railsチュートリアル 第10章 フレンドリーフォワーディングの追加演習 - フレンドリーフォワーディングのリダイレクト先のURLが正しく渡されていることのテスト(editアクション編)

何についての記事か

「Railsチュートリアル 第10章 演習 - フレンドリーフォワーディング」の発展学習となります。

同演習には、「渡されたURLに初回のみ転送されていることを確認する。具体的には、リダイレクトのURLはデフォルト (プロフィール画面) に戻っていることを確認する。」という内容の出題があります。それであれば、逆に「『ログインしていない状態でユーザー情報編集ページにアクセスしようとした』という状況で、フレンドリーフォワーディングのリダイレクト先のURLが正しく渡されていること」のテストも必要なはずです。

というわけで、発展学習としてQiita記事を書いてみました。

内容

フレンドリーフォワーディングのリダイレクト先のURLを保存する処理はどこにあるか

現在の実装では、「Usersコントローラーのeditアクションが呼び出される前には、同コントローラーのlogged_in_user」メソッドが呼び出されるようになっています。そのコードは以下のとおりです。

UsersController#logged_in_user
def logged_in_user
  unless logged_in?
    store_location
    flash[:danger] = "Please log in."
    redirect_to login_url
  end
end

「ログインしていない状態でUsersコントローラーのeditアクションが呼び出された」という状況においては、以下のコードが実行されます。

store_location
flash[:danger] = "Please log in."
redirect_to login_url

このうち、フレンドリーフォワーディングのリダイレクト先のURLを一時cookiesに保存するのは、store_locationメソッドですね。当該メソッドは、Sessionsヘルパーに実装されています。

store_locationメソッドの実装

SessionsHelper#store_location
def store_location
  session[:forwarding_url] = request.original_url if request.get?
end

「HTTPリクエストがGETである場合のみ、一時cookiesにフレンドリーフォワーディングのリダイレクト先のURLが保存される」という実装です。

フレンドリーフォワーディングのリダイレクト先のURLを保存する処理に対応するテスト

上述コードに対応するテストコードは、test/controllers/users_controller_test.rb内にあります。editアクションに対応するテストは"should redirect edit when not logged in"です。

test/controllers/users_controller_test.rb(抜粋)
test "should redirect edit when not logged in" do
  get edit_user_path(@user)
  assert_not flash.empty?
  assert_redirected_to login_url
end

当該テストに記述を追加すれば、「『ログインしていない状態でユーザー情報編集ページにアクセスしようとした』という状況で、フレンドリーフォワーディングのリダイレクト先のURLが正しく渡されていること」がテストできるはずです。

実際に追加する記述は以下のようになります。

test/controllers/users_controller_test.rb(抜粋)
  test "should redirect edit when not logged in" do
    get edit_user_path(@user)
    assert_not flash.empty?
    assert_redirected_to login_url
+   assert_not_nil session[:forwarding_url]
  end

前提 - テストコードの特定行に対してテストを実行する

例えば、test/controllers/users_controller_test.rbの15行目に対応するテストを実行するには、rails testコマンドを以下のように呼び出せばOKです。

# rails test test/controllers/users_controller_test.rb:15

store_locationメソッドが呼び出されない場合に対するテスト

store_locationメソッドが呼び出されない場合」を再現する

UsersController#logged_in_userメソッドで、store_locationメソッドが実行されないようにすればOKです。すぐにもとに戻すので、今回は「ソースコードからコメントアウト」という方法をとります。

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

    ...略

    private

      ...略

      # beforeアクション

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

      ...略
  end

store_locationメソッドが呼び出されない場合」に、editアクションに対するテストが失敗することを確認する

editアクションに対するテストには、「should redirect edit when not logged in」という名前がついています。私の環境では、テスト「should redirect edit when not logged in」の始まりは、test/controllers/users_controller_test.rbの15行目となっています。

test/controllers/users_controller_test.rb(15行目)
test "should redirect edit when not logged in" do

この行に対応するテストを実施してみます。

# rails test test/controllers/users_controller_test.rb:15
Running via Spring preloader in process 1220
Started with run options --seed 40280

 FAIL["test_should_redirect_edit_when_not_logged_in", UsersControllerTest, 0.5344693000079133]
 test_should_redirect_edit_when_not_logged_in#UsersControllerTest (0.54s)
        Expected nil to not be nil.
        test/controllers/users_controller_test.rb:19:in `block in <class:UsersControllerTest>'

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

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

test/controllers/users_controller_test.rbの19行目でテストが失敗しましたね。当該行には、以下のコードが記述されています。

test/controllers/users_controller_test.rb(19行目)
assert_not_nil session[:forwarding_url]

たった今追加したコードですね。想定したテストが正しく行えていることがわかりました。

UsersController#logged_in_userメソッドを元に戻す

テストが正しく実装できていることがわかったので、UsersController#logged_in_userメソッドは元に戻しておきましょう。

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

    ...略

    private

      ...略

      # beforeアクション

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

      ...略
  end

GETリクエストに対して、一時cookiesにリダイレクト先のURLが保存されない場合に対するテスト

GETリクエストに対して、一時cookiesにリダイレクト先のURLが保存されない場合」を再現する

SessionsHelper#store_locationメソッドの記述を変更します。

app/helpers/sessions_helper.rb
  module SessionsHelper
    ...略

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

request.get?!request.get?に書き換えることにより、「GETリクエスト以外のリクエストに対して、一時cookiesにリダイレクト先のURLを保存する」という動作にしています。

GETリクエストに対して、一時cookiesにリダイレクト先のURLが保存されない場合」に、editアクションに対するテストが失敗することを確認する

# rails test test/controllers/users_controller_test.rb:15
Running via Spring preloader in process 1272
Started with run options --seed 50959

 FAIL["test_should_redirect_edit_when_not_logged_in", UsersControllerTest, 0.5778716999921016]
 test_should_redirect_edit_when_not_logged_in#UsersControllerTest (0.58s)
        Expected nil to not be nil.
        test/controllers/users_controller_test.rb:19:in `block in <class:UsersControllerTest>'

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

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

先ほどと同じく、test/controllers/users_controller_test.rbの19行目でテストが失敗しました。想定したテストが正しく行えているといえます。

SessionsHelper#store_locationメソッドを元に戻す

テストが正しく実装できていることがわかったので、SessionsHelper#store_locationメソッドは元に戻しておきましょう。

app/helpers/sessions_helper.rb
  module SessionsHelper
    ...略

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

実装が正しいことの確認

改めて、test/controllers/users_controller_test.rbの15行目に対応するテストを実行します。

# rails test test/controllers/users_controller_test.rb:15
Running via Spring preloader in process 1298
Started with run options --seed 23019

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

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

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

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

RailsでぐるなびAPIを使って店舗検索する。

アクセスキーの取得

https://api.gnavi.co.jp/api/ でアクセスキーを取得します。

実装する

shops_controllerを作成し、indexでぐるなびapiで店舗データ取得します。
今回は、先ほど取得したアクセスキーをcredentialsで管理しています。

shops_controller
    api_key= Rails.application.credentials.dig(:grunavi, :api_key)
    url='https://api.gnavi.co.jp/RestSearchAPI/v3/?keyid='
    url << api_key  

    if params[:search]
    word=params[:search]
    url << "&name=" << word #名前で検索
    end
    url=URI.encode(url) #エスケープ
    uri = URI.parse(url)
    json = Net::HTTP.get(uri)
    result = JSON.parse(json)
    @rests=result["rest"]

次に、検索フォームと検索結果を表示する画面を作成します。
レイアウトはBootstrapを使っています。

index.html.slim
= form_tag(shops_index_path,:method => 'get') do
  = text_field_tag :search
  = submit_tag 'Search'

- if @rests
  - @rests.each do |rest|
    .card.mb-3 style=("max-width: 1000px;")
      .row.no-gutters
        .col-lg-6
          = image_tag(rest["image_url"]["shop_image1"])
        .col-lg-6
          .card-body
            p = "名前: #{rest["name"]}"
            p = "カテゴリー: #{rest["category"]}"
            p = "住所: #{rest["address"]}"

では、検索フォームにカレーと打ってみてください。
店名にカレーが入っている店が表示されます。
スクリーンショット 2019-11-25 4.05.00.png

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

【jquery】画面スクロールボタンの実装からイベント発火の基本を復習する

目的

jqueryを用いて画面をスクロールさせるボタンを実装します。
基礎の復習も兼ねてかなり初歩的な部分も調べつつまとめています。

まずは完成形から

HTMLにてボタンを配置

<!--スクロールしたい範囲-->
<div class="contents">
</div>

<!--スクロール機能を持たせる要素-->
<div class="scroll_btn">
</div>

jsファイルに処理を記述

$(function(){
  $(".scroll_btn").on("click", function(){
    $(".contents").animate({scrollTop: $(".contents")[0].scrollHeight}, 500, "swing");
  })
})

jsファイルそれぞれの記述について

$(function(){...})

$(function(){
  //処理したい内容
})

この部分は、
「HTMLの読み込みが全て完了したら、以下の処理を実行しますよ」
という宣言のようなものです。

以下の例のように、javascriptファイルはHTMLのhead要素の部分で読み込みが行われます。

<!DOCTYPE html>
<html>
<head>
  <title>タイトル</title>
  <%= stylesheet_link_tag    'application', media: 'all' %>
  <%= javascript_include_tag 'application' %>
  <%= csrf_meta_tags %>
</head>
<body>

<%= yield %>

</body>
</html>

しかし、実際にjavascriptで処理を行いたい部分は
大抵、head要素より下のbody要素の中にあるかと思います。
HTMLは上から順番に読み込むので、順番通りjavascriptファイルが先に読み込まれても、
javascriptファイルにて指定しているHTMLの要素が
まだ認識されていないことになります。

そのため「$(function(){...})」の記述による
「HTMLが全て読み込まれてから」宣言が必要になってくるというわけです。

発火条件の指定(HTML要素を指定し、そこで何が起きるとイベントが発火するか)

//$(function(){
  $(".scroll_btn").on("click", function(){
    //$(".contents").animate({scrollTop: $(".contents")[0].scrollHeight}, 500, "swing");
  })
//})

これは、
「"scroll_btn"というクラスが設定されている要素が"click"されると、
以下function(){}の処理を行います」
という意味になります。

イベントを定義する時の記述として

//$(function(){
  $(".scroll_btn").click(function(){
    //処理
  })
//})

こういう形でも問題なく動きます。
しかし、「.on()」で記述をしておいた方が
複数のイベントタイプを設定できるなどメリットが多いようです。
これについてはまた別の機会にまとめたいと思います。

処理(画面をスムーズにスクロールさせる)

//$(function(){
  //$(".scroll_btn").on("click", function(){
    $(".contents").animate({scrollTop: $(".contents")[0].scrollHeight}, 500, "swing");
  //})
//})
animate()

このメソッドを利用するオブジェクト(今回はcontentsクラスが指定されたdiv要素)が持つ
プロパティなどを徐々に変化させることができます。
今回はscrollTopプロパティを用いて、指定する高さまでスクロールする機能を持たせます。

scrollTop:$(".contents")[0].scrollHeight

contentsが入ったdiv要素のスクロールできる高さ(scrollHeight)を取得(scrollTop)します。

また、animate()の第二、第三引数で、その他スクロールに関するオプションの設定を行っています。

第二引数 500

これは「duration(アニメーションの動作期間)」を設定しています。
初期値は"normal"が設定されており、"fast"、"slow"と指定できます。
また、完了までの時間をミリ秒単位で指定することもでき、
今回の「500」であれば「0.5秒で完了する」ように設定していることになります。

第三引数 "swing"

これは「easing(値の変化の緩急)」を設定しています。
プラグインを入れずに使える値は”linear”と”swing”だけで
"linear"は一定の変化、”swing”は若干の緩急がつきます。
初期値としては”swing”が設定されているようなので、
今回の場合このオプションは必要ないですね。

終わりに

以上、スクロールボタンの実装を軸にした
jquery基本の復習でした。
もし何か誤っていることなどあれば
ご指摘いただけるとありがたいです。

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