- 投稿日:2019-11-25T22:39:02+09:00
#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 endrspec
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 endDoc
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}" endRef
Rspecでrake taskをテストする方法 - Qiita
Rakeタスクのテストの仕方 - Qiita
[Ruby on Rails]RSpecによるRakeのテスト | Developers.IOOriginal by Github issue
- 投稿日:2019-11-25T22:16:18+09:00
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
と入力すれば狙い通りのフォルダを作れます。
基礎中の基礎の部分ですが、この一連の流れは何かを参考にしながらでもいいので間違いなくできるようになってください。
- 投稿日:2019-11-25T20:57:07+09:00
【Rails】flashの使い方
flashを利用して、アクション実行後に簡単なメッセージを表示させることができます。
ログイン周りの処理において特に重宝されます。
(Sessionはモデルを持たない→ActiveRecordがエラーメッセージを吐かないから)使い方
flash[:<キー>] = <メッセージ>
で登録し、flash.each do |message_type, message|...
で出力します。
flash
とflash.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.rbclass 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 endsessionsコントローラーを修正する
tasksコントローラー同様、
redirect_to
の前にflash
を、render
の前にflash.now
を追加します。/app/controllers/tasks_controller.rbclass 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
- 投稿日:2019-11-25T20:12:34+09:00
Rails/アンチパターン: 一見すると存在しないアクション
何の変哲も無い(?)コントローラー。これはエクスポート処理関連の各種コントローラーの親クラス
controllers/export_controller.rbclass ExportController < ApplicationController def some_method やりたい処理 end endそして、何もないHogeController。よく見るとExportControllerを継承している。これを見落とすとやばい
controllers/hoge_controller.rbclass HogeController < ExportController end最後にrouting。HogeControllerのsome_methodアクションにroutingしている
routes.rbget '/hoge', to: 'hoge#some_method'何が問題か
- アクション=そのcontrollerに定義するという設計スタイルの慣れていると、ExportControllerを継承しているのを見落としガチで、その場合 「some_methodアクションなど定義されていない。routing自体が意味のないものになっている」という判断をしてしまいがち
どうあるべきか
controllers/export_controller.rbclass ExportController < ApplicationController def common_some_method やりたい処理 end endcontrollers/hoge_controller.rbclass HogeController < ExportController def some_method common_some_method end end
あるいはConcernを使う
controllers/concerns/export_concern.rbmodule ExportConsern extend ActiveSupport::Concern module ClassMethods def common_some_method やりたい処理 end end endcontrollers/hoge_controller.rbclass HogeController < ApplicationController include ExportConcern def some_method common_some_method end endなど。何れにしても重要なのはHogeControllerには明示的にアクションを定義し、親クラスのメソッドが暗黙的にアクションとして呼ばれることがないようにするべき
まとめとか補足とか
- 実際には気付ける方法は幾つかあるので、そこまで問題かというまあまあなところ
- とにかく親クラスのメソッドをアクションとして使用するのは良くない
- と思っていますが、実際どうなんでしょう
宣伝のようなもの
都内でRailsエンジニアでアンチパターンや失敗談を共有する会をやりたいと考えています。
conpassとかで見かけたらよろしくです。
- 投稿日:2019-11-25T18:54:01+09:00
学びメモ(11月)
学んだことを残していきたいと思います。参考にさせていただいた記事を書いた方々には心から感謝します。
CSSのposition: absoluteとrelativeとは
absolute
青色のbox2に{ position: absolute; top:150px; left:100px } を指定。
absolute を指定した要素は高さがなくなり、浮いたような状態になるため、box3はbox2を無視して位置を詰める。
relative
box2に、{ position: relative; top:150px; left:100px; } を指定した例を見ていきましょう。
absoluteとは違い、移動させた要素の高さが残るため、box3は位置を詰めずそのままの位置に表示る。
親要素に「position:relative;」を記述しない場合は子要素が画面左上を起点に子移動してしまう。
親要素に「position:relative;」を記述した場合
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でクロージャ入門。関数はすべてクロージャ?
- 投稿日:2019-11-25T18:49:55+09:00
[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他に考えた方法
クラス変数を使う (失敗)
クラス変数はサブクラスとも共有され、サブクラスで代入できるためこの用途に使えない。
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クラスインスタンス変数を使う (失敗)
クラスインスタンス変数はサブクラスとは共有されず、各クラスで独立している。しかし継承もしない。
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クラスインスタンス変数を使い、継承時に親クラスの値を引き継ぐ (成功)
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参考
- 投稿日:2019-11-25T18:45:26+09:00
【WIP】Rails × CircleCI × ECSのインフラ構築
簡単なRailsデモアプリを本番環境に上げるまでのインフラ構成に関してのまとめ
* あくまで参考に(実務でそのまま利用できるほどしっかり構築しておりません)
前提知識
ECSとは?クラスターとは?サービスとは?タスクとは?って人は
ECSの概念を理解しよう
などを読んでください。Railsアプリ作成
まずはローカルでRailsアプリを作成しましょう。
機能は簡単なものでいいので、scaffoldを利用してとりあえずで作成してしまいましょう。
もちろんフルDocker化AWS上で利用するリソースの作成
コンソール上(or Terraformなど)からあらかじめ作成しておくべきものになります。
IAMロール・ポリシーの作成
ECSで運用するための必要なIAMロール・ポリシーを作成していきます。
ちなみにポリシーとは、ロールに付与される権限情報です。なのでポリシーのないロールは何も権限がない状態なのでまずはポリシーを作成してロールを作成していきましょう。(sandboxアカウントでは既に作成済みのため不要)ポリシーの作成
作成手順
- IAMページに行って、サイドバーの「ポリシー」選択
- 「ポリシーの作成」ボタン押下
- JSONタブを開いて下記に記載したJSON内容をコピペして、「ポリシーの確認」押下
- それぞれのポリシー名を入力する
下記の4つのポリシーを作成する。
- AmazonSSMReadAccess
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "ssm:GetParameters", "secretsmanager:GetSecretValue", "kms:Decrypt" ], "Resource": "*" } ] }
- AmazonECSTaskExecutionRolePolicy
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "ecr:GetAuthorizationToken", "ecr:BatchCheckLayerAvailability", "ecr:GetDownloadUrlForLayer", "ecr:BatchGetImage", "logs:CreateLogStream", "logs:PutLogEvents" ], "Resource": "*" } ] }
- 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": "*" } ] }
- 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:*" } ] }ロールの作成
- ecsInstanceRole
- AWSServiceRoleForECS
- ecsTaskExecutionRole
ecsInstanceRoleの場合は一番簡単で、テンプレートがあるので
IAMページに行って、サイドバーの「ロール」→「ロールの作成」より
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.ymlversion: 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: - masterCircleCIに設定する環境変数
CircleCIのプロジェクトの設定ページ(Settings→[アカウント名or組織名]→[プロジェクト名])に行き、下記の画像の箇所から設定する
https://circleci.com/gh/[アカウント名or組織名]/[プロジェクト名]/edit#env-vars
環境変数名 値 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.ymlversion: "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.ymlversion: 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: ENABLEDAWS 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→データベース→[インスタンス名])
コンテナ全体に注入する環境変数の設定
各環境(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コンテナインスタンスの作成
- インスタンスへのIAMロールを付与すること
- ecs-agentのインストール ( Amazon ECS コンテナエージェントのインストール - Amazon Elastic Container Service )
- EC2インスタンスの
/etc/ecs/ecs.config
にCLUSTER_NAME=クラスター名
の登録- 所属するクラスターを変更する場合、
/var/lib/ecs/data/ecs_agent_data.json
を削除してからecs-agentを再起動するDefaultクラスター作成しているし、IAMロールにecs:CreateClusterの権限付与されているから自動で作成なんかもしてくれるのかと思ったら作成してくれなかった。
なので、クラスター作成→インスタンス作成の方が良い(ちな、クラスター作成時にインスタンスも作成するようにはできるっぽい)
→カスタマイズされてるAMI利用時のみ初期スクリプトによってDefaultクラスターを作成しているのかもしれない参考
Amazon ECS コンテナインスタンスの起動 - Amazon Elastic Container Service
Amazon ECS-optimized AMI - Amazon Elastic Container Serviceインスタンスタイプについて
ある程度余裕持たないとタスク実行するための容量を持たなくて死ぬ
(ほんとは、ローカルや本番環境で動かした時の使用量見てタスク実行に必要なメモリを設定した方が良い)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
- 投稿日:2019-11-25T18:39:42+09:00
インスタンス変数・ゲッターセッター・アクセスメソッド
今回はインスタンス変数、インスタンスメソッド、ゲッターとセッター、
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リファレンスマニュアル(変数と定数)おつかれさまでした!
お付き合いくださり、ありがとうございました!!
- 投稿日:2019-11-25T17:15:04+09:00
【Rails】テストについて
テストの種類
単体テスト モデルやビューヘルパー単体の動作をチェック
機能テスト コントローラ/ビューの呼び出し結果をチェック
統合テスト ユーザーの実際の操作を想定し、複数のコントローラにまたがるアプリの挙動をチェック単体テスト
Railsで行うテストの中で、最も基本的なテスト
アプリを構成するライブラリ(主にモデル)が、正しく動作するかをチェックするtest/models/user_test.rbrequire '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 endassertメソッド: 第1引数がtrueである場合、テストが成功したものとする
setupメソッド : テストが走る前にインスタンス変数の@user
を宣言し、 validメソッドで有効性を確認機能テスト
コントローラの動作や、ビューの出力をチェックするためのテスト
HTTPリクエストを擬似的に作成することで、アクションメソッドを実行し、HTTPステータスやテンプレート変数、最終的な出力の構造までを確認。また、ルーティングもチェックapp/controllers/users_controller.rbclass 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 endtest/integration/users_signup_test.rbrequire '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
- 投稿日:2019-11-25T17:08:32+09:00
#Rails + rails console の pry でエラー情報が少なすぎて辛いのでスタックトレースを表示する様にする設定例
- 投稿日:2019-11-25T16:25:20+09:00
#Rails + rake でタスク名を標準出力・ログ出力する例 ( how to Log or STDOUT task name on rake with Rails )
- 投稿日:2019-11-25T16:08:35+09:00
#Rails + rake で 定数参照できない実行エラー : NameError: uninitialized constant ModelName
- 投稿日:2019-11-25T16:05:25+09:00
Hamlの基本箇所についての復習④(ネスト)
はじめに
飽きもせずHamlの基礎について書いていきます。
今回はクラスのネストについてです。実行
例えば、htmlで次の様に書かれているとします。
<div ="wrapper"> <div ="top"> </div> <div ="down"> </div> </div>
wrapper
という大きなクラスがあり、その中にtop
とdown
という小さなクラスが二つあります。
このように親要素・子要素があるhtmlファイルだと、ちょっとした書き間違えが起こります。例えば次のようなものです。<div ="wrapper"> <div ="top"> <div ="down"> </div> </div>これは
<div ="top">
の閉じタグがない状態です。これだとCSSがちゃんと適用されないなどのエラーが起きます。
単純なミスですが、意外と気づきにくく、エラー解決まで時間を要することも少なくないです。しかし、Hamlを使えばこの問題を多少なり解決することができます。
前述のコードをHamlで書き直すと次のようになります。.wrapper .wrapper__top .wrapper__downhtmlよりも短く書けたのが分かると思います。
一番わかり易い違いは閉じタグがないことです。
クラス名を書いた後に、本文を書くだけでオブジェクトが作れます。
これなら閉じタグを書き忘れた事によるエラーが起きえません。もう一つはクラスの書き方です。
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に限ったことではありませんが)
- 投稿日:2019-11-25T15:09:24+09:00
【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をdrop
しcreate
する所までを行い、capistranoを使った自動デプロイで最新マイグレーションをファイルの読み込みmigrate
する今回のエラーはデプロイの手順の間違えで発生したものだったが、MySQLを触っているとRailsではマイグレーションファイルでバージョン管理してくれるし、ORMのお陰で生SQLを触る機会もほとんどないのでDB関連の知識が無くても動くアプリケーションが作れる。非常に便利だが、SQLの勉強は絶対にした方が良いと感じたエラーだった
- 投稿日:2019-11-25T15:01:22+09:00
NameError in CommentsController#create undefined local variable or method `comment_params' for #<CommentsController:0x00007fa3b29e8038> Did you mean? commment_params
- 投稿日:2019-11-25T12:26:10+09:00
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.rbputs 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-93799c90dee7Rails 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.rbrequire '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参考情報
- 投稿日:2019-11-25T12:25:46+09:00
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 endRailsエンジンが出てきました。
エンジンも詳しくないので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.0CLI app helperと書いてありました。
meowなページで癒やされたので続きをやっていきます。雑にアプリを作る
Yay! You’re on Rails! の画面が出ましたが、Bulmaの使い所がないので画面を増やすために雑にscaffoldしていきます。
rails generate scaffold cat name:string description:textmigrationしてrails sしてlocalhost:3000/catsを確認するとこんな画面が出ます。
殺風景ですね。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を追加
これでBulmaのスタイルを適用できました。Bulmaが効いていることをチャッと確かめたかったので、New Catのリンクとh1にだけclassを振っております。まとめ
フロント開発から随分遠ざかっていたのでwebpackerについて調べる良い機会を得られました。しかしチャッとBulma使っていこう、というくらいのつもりだったのにwarningを追いかけてwebpackerのissueまで読むことになるとは思いませんでした。
asset pipelineを使っているbulma-railsは大変簡単に導入できたので、これはこれで良いなと思いました。ただ、長く開発していくアプリで使うならYarnで管理するのが良さそうですね。次はwebpackerではなく素のwebpackに挑戦してみようと思います。
この記事ではwebpackerの説明は省いてしまいました。webpackがどんなものなのかについては、24日に投稿する記事で紹介される予定です。お楽しみに。明日は、@mochikichi321さんの「投稿画像の彩度低下問題を解決した話」です。明日もよろしくおねがいします!
- 投稿日:2019-11-25T11:44:24+09:00
[メモ]Rails超基礎 勉強して個人的に違いが分からなくなったメソッド
redirect_toメソッド
「redirect_to(URL)」とすることで、そのページに転送することができます。
以下だとcreateアクションを実行すると「/posts/index」ページへ転送される。def create redirect_to("/posts/index") endrenderメソッド
別のアクションを経由せずに、直接ビューを表示することができます。
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") endform_tagメソッド
「form_tag(送信先のURL) do」のように送信先のURLを指定します。
実行時に、指定されたURLにデータが送信されます。<%= form_tag("/posts/create") do %> <textarea></textarea> <input type="submit" value="投稿"> <% end %>
- 投稿日:2019-11-25T10:49:50+09:00
【Rails】form_with (local: true)について
- 投稿日:2019-11-25T10:40:14+09:00
【Rails】routes collectionとmemberについて
colletionとmemberを使う目的
どちらもroutingで使うとき、resourcesでは自動で生成されないactionへの設定を使用
route.rbresources :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 endfollow_user GET /users/:id/follow(.:format) users#followcollection
resource :users do collection do get :slide end endslide_user GET /users/slide(.:format) users#slide
- 投稿日:2019-11-25T08:31:57+09:00
kaminariでページネーション作ったる
ページネーションとは
今何ページ目にいるかがわかるやつです。
よく画面の下にあります↓
kaminariというgemを使うことで簡単に実装できます。
【その1】gemを追加する
Gemfilegem 'kaminari'bundle install
サーバーを再起動します。rails s
なぜなら、サーバーを起動した際にgemが反映されるからです。
【その2】コントローラーを編集する
tweets_controller.rbdef index @tweets = Tweet.all.page(params[:page]).per(5) endページ数がpageというキーになって、パラメータとして送られます。
1ページあたりに表示する件数をper(5)のように記述します。
この場合は5件表示されます。【その3】ビューファイルを編集する
index.html.erb<%= paginate(@tweets) %>好きなところに置いてください。
これでページネーションが完成します。pagenateではなくpaginateなので注意!!
ではまた!
- 投稿日:2019-11-25T08:20:00+09:00
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
メソッドのユースケースは以下のようになります。
logged_in_user
メソッドを定義する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.rbclass 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ブラウザに以下のページが表示されました。
- フラッシュメッセージに「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.rbrequire '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.rbclass 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フィルターに対するテストは、アクションごとに書いていくことになります。
具体的なテストの実装方針は以下のようになります。
- 正しい種類のHTTPリクエストを使って、
edit
アクションとupdate
アクションをそれぞれ実行させる- フラッシュメッセージが代入されたかを確認する
- ログイン画面にリダイレクトされたかを確認する
どの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:フラッシュメッセージが代入されたか、ログイン画面にリダイレクトされたか end2つ目のテストでは、
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.rbrequire '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.rb
のbefore_action
をコメントアウトした状態で、改めてテストを実行してみましょう。app/controllers/users_controller.rbclass 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.rbassert_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_urlbeforeフィルターが正しく実装されていれば、この場面では「"Please log in."というフラッシュメッセージが定義された上で、ログイン画面にリダイレクトされる」という動作をするはずです。しかしながら、beforeフィルターが実装されていないと、(ユーザー情報の編集が保存された上で)当該ユーザー情報のURLにリダイレクトされることになります。結果、テストが失敗するのです。
セキュリティモデルに関する実装があればテストが成功することを確認する
今度は、
app/controllers/users_controller.rb
でbefore_action
のコメントアウトを解除した上で、改めてtest/controllers/users_controller_test.rb
のテストを実行してみましょう。app/controllers/users_controller.rbclass 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.15の
only:
オプションをコメントアウトしてみて、テストスイートがエラーを検知できるかどうか (テストが失敗するかどうか) 確かめてみましょう。デフォルトのbeforeフィルターは、すべてのアクションに対して制限を加えます。今回のケースだと、ログインページやユーザー登録ページにも制限の範囲が及んでしまうはずです (結果としてテストも失敗するはずです)。
app/controllers/users_controller.rbclass 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.ymlrhakurei: 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#updatedef 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.rbclass 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.rbmodule 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.rbclass 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などのツールにより、
POST
やPATCH
が直接呼び出される」という可能性も想定しておかなければなりません。上記のような状況でも意図せぬ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.rbrequire '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を記憶する
- リクエスト内容が
POST
、PATCH
、DELETE
である場合に対する備えリクエスト内容が
GET
でない場合と、その対処は「リクエスト内容が
GET
ではない場合」というのは、例えば「ログインしていないユーザーがフォームから送信する」ようなユースケース、より具体的には「ユーザーが一時cookiesを手動で削除した上でフォームから送信する」などといったユースケースで発生しえます。
POST
、PATCH
、DELETE
が期待されている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.rbclass 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.rbclass 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.rbclass 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.rbmodule 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.rbmodule 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が渡されていないことを、テストを書いて確認してみましょう。
2.
session[:forwarding_url]
にリダイレクト先のURLが保存される状況において、値が正しいかどうか確認してみましょう。また、new
アクションにアクセスしたときのrequest.get?
の値も確認してみましょう)。具体的には、以下のような手順になります。
- 7.1.3で紹介した
debugger
メソッドをSessionsコントローラのnew
アクションに置く- その後、ログアウトして /users/1/edit にアクセスする
まずは、
app/controllers/sessions_controller.rb
に以下の変更を行います。app/controllers/sessions_controller.rbclass SessionsController < ApplicationController def new + debugger end ...略 end
その上で、Webブラウザで以下の操作を行います。
- すでにサンプルアプリケーションにログインしている場合、ログアウトする
- /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.rbclass SessionsController < ApplicationController def new - debugger end ...略 end
- 投稿日:2019-11-25T08:18:55+09:00
【Heroku】デプロイ後にcode=H14 desc="No web processes running"
事象
話の流れとしてはこの記事の続きになります。
Dockerで開発環境構築をしたRailsアプリのコンテナをpushすることができたのですが、その後
$ heroku addons:create heroku-postgresql:hobby-dev $ heroku container:release "コンテナ名" $ heroku openとして、サイトを開いてみると、
と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を見てみると、
と表示されていました。
どうやら、heroku.ymlが含まれていないことが原因の模様で、解決するにはheroku.ymlを作成するか、
heroku stack:set heroku-18
を実行する必要があるとのこと。そこでまずはheroku.ymlを作成する方針をとってみました。
Buildlogに記載されていた公式ドキュメントを参考に下記のシンプルなheroku.ymlを作成しました。
heroku.ymlbuild: 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
としてマイグレーションを実行したところ、とうとうお馴染みの画面がお出迎えしてくれました。
原因
公式ドキュメントによると、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-runningBuilding Docker Images with heroku.yml
https://devcenter.heroku.com/articles/build-docker-images-heroku-yml#getting-startedGetting Started on Heroku with Rails 5.x
https://devcenter.heroku.com/articles/getting-started-with-rails5#migrate-your-databasedyno:Heroku プラットフォームの中核
https://jp.heroku.com/dynos
- 投稿日:2019-11-25T07:59:15+09:00
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.rbmodule 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.rbmodule 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テストは無事成功しました。
- 投稿日:2019-11-25T07:36:59+09:00
Railsチュートリアル 第10章 フレンドリーフォワーディングの追加演習 - フレンドリーフォワーディングのリダイレクト先のURLが正しく渡されていることのテスト(editアクション編)
何についての記事か
「Railsチュートリアル 第10章 演習 - フレンドリーフォワーディング」の発展学習となります。
同演習には、「渡されたURLに初回のみ転送されていることを確認する。具体的には、リダイレクトのURLはデフォルト (プロフィール画面) に戻っていることを確認する。」という内容の出題があります。それであれば、逆に「『ログインしていない状態でユーザー情報編集ページにアクセスしようとした』という状況で、フレンドリーフォワーディングのリダイレクト先のURLが正しく渡されていること」のテストも必要なはずです。
というわけで、発展学習としてQiita記事を書いてみました。
内容
フレンドリーフォワーディングのリダイレクト先のURLを保存する処理はどこにあるか
現在の実装では、「Usersコントローラーの
edit
アクションが呼び出される前には、同コントローラーのlogged_in_user
」メソッドが呼び出されるようになっています。そのコードは以下のとおりです。UsersController#logged_in_userdef 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_locationdef 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.rbclass 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.rbclass 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.rbmodule 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.rbmodule 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テストは無事成功しました。
- 投稿日:2019-11-25T04:11:49+09:00
RailsでぐるなびAPIを使って店舗検索する。
アクセスキーの取得
https://api.gnavi.co.jp/api/ でアクセスキーを取得します。
実装する
shops_controllerを作成し、indexでぐるなびapiで店舗データ取得します。
今回は、先ほど取得したアクセスキーをcredentialsで管理しています。shops_controllerapi_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-25T00:39:17+09:00
【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基本の復習でした。
もし何か誤っていることなどあれば
ご指摘いただけるとありがたいです。