エンジニア志望のブログ

AWSにアプリをデプロイする

はじめに
AWSでサーバーを構築してアプリを動かすまでをまとめてみます。
AWSのEC2の作成などはご自身でお願いします。
また、この記事は初心者が備忘録として書いた記事なので参考にする際はお気をつけください。

Ruby : 2.7.3
Rails : 6.1.4
DB : MySQL
WEBサーバー : nginx
Applicationサーバー : puma

EC2インスタンスの作成は省略

この記事ではEC2インスタンス作成後の内容となっています。
EC2はAmazon Linux 2 AMI (HVM) イメージを使用。
EC2インスタンスの作成は他の記事を参照するようお願いします。

Railsアプリ

Railsアプリは作成しGitにプッシュしておいてください。

構築するサーバー

この記事で構築するサーバーは以下のようなイメージです。
 クライアント → nginx → rails(puma) → mysql
nginxはリバースプロキシとして動かします。

サーバーに環境を用意

まずEC2インスタンスSSHで接続してください。

パッケージのインストール

使用していくパッケージのインストールを行います。

$ sudo yum -y install git gcc-c++ glibc-headers openssl-devel readline libyaml-devel readline-devel zlib zlib-devel libffi-devel libxml2 libxslt libxml2-devel libxslt-devel sqlite-devel libcurl-devel mysql mysql-devel

Nodeのインストール

$ curl -sL https://rpm.nodesource.com/setup_16.x | sudo bash -
$ sudo yum -y install nodejs

yarnのインストール

$ curl -sL https://dl.yarnpkg.com/rpm/yarn.repo | sudo tee /etc/yum.repos.d/yarn.repo
$ sudo yum -y install yarn

MySQLのインストール

今回RDSを使わずにMySQLをインストールして使用していきます。
データベースはprivateなネットワークに作成してください。

#mariaDBの削除
$ sudo yum remove mariadb-libs

MySQLリポジトリの追加

$ sudo yum localinstall https://dev.mysql.com/get/mysql80-community-release-el7-3.noarch.rpm

MySQL Serverのインストール

$ sudo yum install mysql-server
$ mysqld --version

MySQL Serverの起動

# サーバーの起動
$ sudo systemctl start mysqld.service
# サーバーの状態の確認
$ sudo systemctl status mysqld.service

サーバーを起動しようとしたらエラーが出た...以下の記事を参考に解決
Centos7でMysqlの起動にハマった話 | Tips of Rubbish


MySQLにログイン

#rootパスワードを確認
$ sudo cat /var/log/mysqld.log | grep password
#mysqlにログイン 確認したパスワードを入力  exitで終了
$ mysql -u root -p
#初期設定
$ sudo mysql_secure_installation
#新しいパスワードでmysqlにログインできるか確認
$ mysql -u root -p

データベースとユーザーの作成

#ログイン後
#ユーザーの作成
mysql > create user 'ユーザー名(test_user)'@'ホスト名(localhost)' identified by 'パスワード(メモしておく)';
#データベースの作成
mysql > create database データベース名(test_app);
#権限の付与
mysql > grant all on *.* to 'ユーザー名(test_user)'@'ホスト名(localhost)';
#権限の変更をデータベースに反映
mysql > flush privileges;
mysql > exit;

Railsアプリのdatabase.ymlのproductionグループのデータベース名とユーザー名を対応しておく

nginxのインストール

ngiinxのインストール

$ sudo amazon-linux-extras install -y nginx1

nginxの起動

$ sudo systemctl start nginx

状態確認

$ systemctl status nginx

nginxの設定ファイル

nginxの設定ファイルは /etc/nginx に置いてある

nginxの設定ファルは以下の順で読み込まれる

 /etc/nginx/nginx.conf
 /etc/nginx/conf.d/*.conf

Nginxとは?インストールや設定ファイルの仕組みを解説【CentOS】 | キツネの惑星

nginxの設定

nginxのインストールが終わったら
nginxとRuby on Railsが連携できるように/etc/nginc/conf.dフォルダ配下に設定ファイルを作成していきます。
今回はrails.confというファイルを作成してその中に設定を記述します。
vimで作業する場合は sudo vim /etc/nginx/conf.d/rails.conf

#リバースプロキシとして扱うための設定
#ここでバックエンドのサーバーを指定する
upstream puma{
        #tmp/sockets/puma.sockでpumaのsocketファイルを指定
        server unix:///var/www/アプリ名/tmp/sockets/puma.sock;
}

server {
        #接続を待ち受けたいポートを指定
        listen 80;
        listen [::]:80;
        server_name puma;

        #assetsファイルにアクセスされたときの設定
        location ~^ /assets/ {
                root /var/www/Test/public;
        }


        location / {
                proxy_read_timeout 300;
                proxy_connect_timeout 300;
                proxy_redirect off;
                proxy_set_header Host $host;
                proxy_set_header X-Forwarded-Proto $http_x_forwarded_protp;
                proxy_set_header X-Forwarded=for $proxy_add_x_forwarded_for;
                proxy_pass http://puma;
        }
}
pumaの設定

NginxとRailsを連携させるための設定をする。
Railsアプリ/config/puma.rb
puma.sockはNginxとソケット通信をする際に必要になるファイル

# port        ENV.fetch("PORT") { 3000 }
bind "unix://#{Rails.root}/tmp/sockets/puma.sock"

UNIXドメインソケット??
「UNIX ドメインソケット」と「ソケット」について比較する - Qiita

ユーザーの作成、アプリを動作させるディレクトリを作成

$ sudo adduser deploy
$ sudo mkdir -p /var/www
$ sudo chown deploy:deploy /var/www

Railsの環境構築

ここからのデプロイ作業はデプロイユーザーに切り替えて行います。
ルートユーザーからユーザーを切り替えるのは権限の関係で

Rbenvのインストール

# インストール
$ git clone https://github.com/rbenv/rbenv.git ~/.rbenv
# パスを通す
$ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile
$ echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
$ source ~/.bash_profile
#プラグインのインストール
$ mkdir -p "$(rbenv root)"/plugins
$ git clone https://github.com/rbenv/ruby-build.git "$(rbenv root)"/plugins/ruby-build
#.bash_profileの反映
$ source ~/.bash_profile
# セットアップ
$ ~/.rbenv/bin/rbenv init
$ curl -fsSL https://github.com/rbenv/rbenv-installer/raw/main/bin/rbenv-doctor | bash

Rubyのインストール

$ rbenv install 2.7.3
$ rbenv global 2.7.3

Railsのインストール

$ gem install rails -v 6.1.4

アプリのダウンロード

GitHubからクローン

$ cd /var/www
$ git clone ご自身のアプリ

※database.ymlのproductionのdatabaseとusernameはMySQLで設定したものを指定

ライブラリを導入

$ cd アプリ名
$ bundle install

MySQLインストール時にエラーが出た
Ruby - bundle install 時に出るエラー Gem::Ext::BuildError: ERROR: Failed to build gem native extension.|teratail
上記記事を参考に解決
シークレットキーの作成

# メモしておく
$ rails secret

アプリの設定

.bash_profileに環境変数を設定していきます。
ここではRailsのシークレットキー、MySQLのパスワードなどを適宜設定します。

$ vim ~/.bash_profile

export SECRET_KEY_BASE=rails secretで作成したシークレットキー
export TEST_DATABASE_PASSWORD=MySQLのユーザー登録で設定したパスワード

#.bash_profileの反映
$ source ~/.bash_profile

テーブルの作成

$ rails db:migrate RAILS_ENV=production

MySQLサーバーに接続できないとエラーが出た
以下の記事を参考に解決
Rails アプリ起動時のMysqlエラー を解消 (Mysql2::Error::ConnectionError ・ Can't connect to local MySQL server through socket '/tmp/mysql.sock') - Qiita

アプリの起動

ルートユーザーで作業

nginxの再起動
$ sudo systemctl restart nginx.service
アプリを起動

deployユーザー

$ cd /var/www/アプリ名
$ rails assets:precompile RAILS_ENV=production
$ rails server -e production

参考にした書籍

www.amazon.co.jp

おわりに

RailsとNginxの連携にかなり手こずってしまいました、、
Nginxの設定など詳しく分からないところが多いので今後触る機会に理解していければと思います。
次回はもっとスムーズにアプリをデプロイできるようになりたいです。

Railsのdatabase.ymlについて

はじめに
Railsのデータベース接続の中身をいまいち理解していないまま使っていたので調べてみました。
ActiveRecordでデータベースに接続するにはconfig/database.ymlに設定を定義する必要があります。
そのdatabase.ymlについて深堀りしていきたいと思います。

環境
Ruby : 2.7.3
Rails : 6.1.4

メモ

デフォルトの設定ファイル

railsアプリを作成したときに用意されたデフォルトのdatabase.ymlです。
この設定ファイルはMySQL仕様になっています。

default: &default
  adapter: mysql2
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password:
  socket: /tmp/mysql.sock

development:
  <<: *default
  database: MysqlTest_development

test:
  <<: *default
  database: MysqlTest_test

production:
  <<: *default
  database: MysqlTest_production
  username: MysqlTest
  password: <%= ENV['MYSQLTEST_DATABASE_PASSWORD'] %>

yamlについて

そもそもyamlを書いて来なかったのでyamlについて。
アンカー
&を利用することで名前を付け、定義した内容を他の場所で参照できるようにする。

&default

エイリアス
*を利用することでアンカーで定義した内容を参照する。

 *default

マージする
<<を利用することでマージをする。

 <<: *default

database.ymlのパラメータについて

  adapter: 接続するデータベースの種類
  encoding: 文字コード
  pool: コネクションプーリングで使用するコネクションの上限
  username: ユーザー名
  password: パスワード
  socket: ソケットファイルのパス

コネクションプールとは??

DBに接続した状態を維持したコネクションをいくつか用意ておき、それを使い回す仕組みのこと
DBとの新規接続を減らすことでパフォーマンスを向上させるらしい、、、
デフォルトでは5つのコネクションを使い回すように設定されているようです。

Railsチュートリアル第14章

はじめに
Ruby on Railsチュートリアル(第6版)のメモ、演習の解答例を記述した記事です。
解答は個人のものなので、誤りがあればご指摘ください。
開発環境  Ruby: 2.7.2 , Rails: 6.1.4

メモ

14章

14章ではフォロー機能を実装する。
フォロー機能を実装するには誰が誰をフォローしているのかという情報を保管して置く必要がある。
そこでRelationshipsモデルを生成する。
Relationshipsモデルはfollower_id(誰が)とfollowed_id(誰を)を持つテーブル
誰が誰をフォーローしているかが分かれば誰が誰にフォローされているのかが分かる。といったことを念頭に実装していく。

Relationshipsモデル

フォローしたユーザー、フォローされたユーザーが関係性を持ったデータモデルを作成していく。

ここでは次のようなモデルを作成していく
テーブル名:Relationship
フォローした人 :follower_id
フォローされた人:followed_id

例えば、
user_idが1のユーザーがuser_idが2のユーザーをフォローした場合
follwer_id = 1、followed_id = 2となる

検索の高速化のためのインデックスを追加、フォロー、フォロワーの組み合わせが一意であることを保証するために複合インデックスも追加する。

User/Relationshipの関連付け

今回作成したRelationshipモデルをUserモデルと関連付ける方法が以前と異なる

1. RelationshipモデルをActiveRelationshipモデルとして扱いたい
2.UserモデルからActiveRelationshipモデルのカラムをfollower_idをキーとして検索できるようにしたい

以上の2つが以前のモデルの関連付けのときと違うのかと思う。

1のRelationshipモデルをActiveRelationshipモデルとして扱うには
:class_nameオプションを用いる
実際のモデルを:class_nameオプションに指定することで実現する
2のUserモデルからActiveRelationshipモデルのカラムを検索するには
:foreign_keyオプションを用いる
Userモデルに外部キーを設定していないため:foreign_keyオプションで設定することで実現する

class User < ApplicationRecord
  has_many :microposts, dependent: :destroy
  has_many :active_relationships, class_name:  "Relationship",
                                  foreign_key: "follower_id",
                                  dependent:   :destroy
  .
  .
  .
end


Userモデルと関連付ける
belongs_toでは1対1のつながりを設定する際にモデル名のシンボルを渡す

以前は以下の書き方でUserモデルとMicropostモデルを関連付けた。
そしてmicropost.user_idで関連付けられた値を取得できた。

class Micropost < ApplicationRecord
  belongs_to :user
  .
  .
  .
end

しかし今回はactive_relationship.follower、active_relationship.followedを使いたい、User_idをfollower_id,follwed_idとして関連付けたいので以下のようなコードになる。
:class_nameオプションで実際のモデル名を指定する。

class Relationship < ApplicationRecord
  belongs_to :follower, class_name: "User"
  belongs_to :followed, class_name: "User"
end


関連付けを行った結果以下のメソッドが利用できるようになる

active_relationship.follower
active_relationship.followed
user.active_relationships.create(followed_id: other_user.id)
user.active_relationships.create!(followed_id: other_user.id)
user.active_relationships.build(followed_id: other_user.id)

参考:Active Record の関連付け - Railsガイド

多対多の関係を持ったモデル

多対多の関係を持たせるには、has_many :through関連付けを行う。
この関連付けをするには2つのモデルの間に第3のモデルが必要になる。

今回UserモデルとUserモデル同士の関連付けを行い、その仲介役としてActiveRelationshipモデルを設ける。

:throughオプションには仲介役のモデルをシンボルで渡す。
今回はactive_relationshipsを渡す。

参考:Active Record の関連付け - Railsガイド

次のコードでは
UserモデルがRelationshipテーブルからfollower_idを検索してフォローしているユーザーを取得できる

has_many :followeds, through: :active_relationships

例えばユーザーID1のユーザーがユーザーID2,3のユーザーをフォローしているして、user.followedでユーザー2,3のオブジェクトを(配列で)取得できる。

英語圏の問題でuser.followedsという名前は不適切なので
user.followingとしてユーザーがフォローしているユーザーを取得できるようにする。上記のコードを変更したコードが下記になる。

  has_many :following, through: :active_relationships, source: :followed

:sourceでは関連付けのもとの名前を指定している。

メンバールーティング

resourcesで生成されたRESTfulな7つのルーティングに対して、必要であればルーティングを追加することができる。

resources :users で生成されたRESTfulなルーティングにメンバールーティングを追加。

  resources :users do
    member do
      get :following, :followers
    end
  end

参考:Rails のルーティング - Railsガイド

Ajax

Ajax...AsynchronousJavaScript And XML、非同期処理のこと
チュートリアルではremote: trueとして設定するように書かれていますがそのとおりに実装するとうまくいきませんでした。

Railsガイドを見るとform_withはAjaxをデフォルトで使えることを前提としているようです。

form_withは:localオプションを指定しない場合Ajaxを使うことになります。
なので対処としては、local: falseとするか、local: true自体を削除するかでAjaxを使えるようになります。

local: trueを消して動かしてみたらAjaxが機能しませんでした。Railsのバージョンが関係してくるのでしょうか。。。

参考:Rails で JavaScript を使用する - Railsガイド

Ajaxのテスト

Ajaxでのテストをするときはxhr :trueオプションを使う

assert_difference '@user.following.count', 1 do
  post relationships_path, params: { followed_id: @other.id }, xhr: true
end

xhr(XmlHttpRequest)オプションをtrueにすることでAjaxでリクエストを発行するようにできる。

mapメソッドの短縮表記

mapメソッド

irb(main):001:0> [1,2,3,4].map{ |i| i.to_s }
=> ["1", "2", "3", "4"]

上記を短縮表記で記述

irb(main):002:0> [1,2,3,4].map(&:to_s)
=> ["1", "2", "3", "4"]

フォローしているユーザーのIDを配列で取り出す

irb(main):003:0> User.first.following.map(&:id)
 .....
=> [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51]

上記のコードは便利なのでActiveRecordeでメソッドが用意されている。

User.first.following_ids
  .....
=> [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51]

このメソッドはhas_many: followingの関連付けをしたときにActiveRecordが自動生成したもの。

取得したIDを文字列として連結

irb(main):005:0> User.first.following_ids.join(', ')
  .....
"3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51"

whereメソッド

ActiveRecordのwhereメソッドでデータベースから条件に合うデータを取得する。

マイクロソフトからフォローしているユーザーの投稿と自分の投稿を取得

Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)

SQLのサブセレクト

データベースの問い合わせを減らして高速化を図る

following_idsメソッドを呼ぶとSQLがデータベースに発行される。
よって以下のコードではfollowing_idsでデータベースに問い合わせたあともう一度Micropost.whereで問い合わせており合計2回の問い合わせをしている。

Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)

そこで

following_idsをSQL文に置き換え、これをサブセレクトとして使う。

following_ids = "SELECT followed_id FROM relationships
                             WHERE  follower_id = :user_id"

そしてこのfollowing_idsを利用してデータベースに問い合わせる

following_ids = "SELECT followed_id FROM relationships
                 WHERE follower_id = :user_id"
Micropost.where("user_id IN (#{following_ids})
                 OR user_id = :user_id", user_id: id)

このように書くことでデータベースへの問い合わせが1回になり高速化、効率化につながる。

演習

14.1.1

演習1

図 14.7のid=1のユーザーに対してuser.following.map(&:id)を実行すると、結果はどのようになるでしょうか? 想像してみてください。ヒント: 4.3.2で紹介したmap(&:method_name)のパターンを思い出してください。例えばuser.following.map(&:id)の場合、idの配列を返します。

id=1のユーザーはidが2、7、8、10のユーザーをフォローしている。
よって、user.following.map(&:id)を実行するとid = 2, 7, 8, 10 の配列を返す。

演習2

図 14.7を参考にして、id=2のユーザーに対してuser.followingを実行すると、結果はどのようになるでしょうか? また、同じユーザーに対してuser.following.map(&:id)を実行すると、結果はどのようになるでしょうか? 想像してみてください。

どのユーザーがid=2のユーザーに対してuser.followingを実行したのかが書かれていないのでid=1のユーザーがid=2のユーザーをフォローしたとする。
id=1のユーザーはid=2のユーザーをすでにフォローしているのでフォローできない。
user.following.map(&:id)を実行するとid=1がフォローしているユーザーid = 2,7,8,10 の配列を取得する。

14.1.2

演習1

コンソールを開き、表 14.1のcreateメソッドを使ってActiveRelationshipを作ってみましょう。データベース上に2人以上のユーザーを用意し、最初のユーザーが2人目のユーザーをフォローしている状態を作ってみてください

rb(main):001:0> user1 = User.first
   (0.8ms)  SELECT sqlite_version(*)
  TRANSACTION (0.1ms)  begin transaction
  User Load (0.6ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> #<User id: 1, name: "Example User", email: "example@railstutorial.or...
irb(main):002:0> user2 = User.second
  User Load (0.4ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? OFFSET ?  [["LIMIT", 1], ["OFFSET", 1]]
=> #<User id: 2, name: "Robin Treutel", email: "example-1@railstutorial...
irb(main):003:0> active_relationship = user1.active_relationships.create!(fo
llowed_id: user2.id)
  TRANSACTION (0.1ms)  SAVEPOINT active_record_1
  User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  User Load (0.0ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
  Relationship Create (1.1ms)  INSERT INTO "relationships" ("follower_id", "followed_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["follower_id", 1], ["followed_id", 2], ["created_at", "2021-07-20 16:32:13.643703"], ["updated_at", "2021-07-20 16:32:13.643703"]]
  TRANSACTION (0.0ms)  RELEASE SAVEPOINT active_record_1
=> #<Relationship id: 1, follower_id: 1, followed_id: 2, created_at: "2...
irb(main):004:0> Relationship.first
  Relationship Load (0.7ms)  SELECT "relationships".* FROM "relationships" ORDER BY "relationships"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> #<Relationship id: 1, follower_id: 1, followed_id: 2, created_at: "2021-07-20 16:32:13.643703000 +0000", updated_at: "2021-07-20 16:32:13.643703000 +0000">
演習2

先ほどの演習を終えたら、active_relationship.followedの値とactive_relationship.followerの値を確認し、それぞれの値が正しいことを確認してみましょう。

irb(main):005:0> active_relationship.followed
=> #<User id: 2, name: "Robin Treutel", email: "example-1@railstutorial.org", created_at: "2021-07-16 10:22:37.201055000 +0000", updated_at: "2021-07-16 10:22:37.201055000 +0000", password_digest: [FILTERED], remember_digest: nil, admin: false, activation_digest: "$2a$12$nlGp6dNoTuBZTTqfYFVKb.VG/meRRlIX8VfYVg2P7FR...", activated: true, activated_at: "2021-07-16 10:22:36.947268000 +0000", reset_digest: nil, reset_sent_at: nil>
irb(main):006:0> active_relationship.follower
=> #<User id: 1, name: "Example User", email: "example@railstutorial.org", created_at: "2021-07-16 10:22:36.621010000 +0000", updated_at: "2021-07-16 10:22:36.621010000 +0000", password_digest: [FILTERED], remember_digest: nil, admin: true, activation_digest: "$2a$12$3UocKEklYfAfEG9ow377meLe8YO9E7pdrWIuscmrghY...", activated: true, activated_at: "2021-07-16 10:22:36.362398000 +0000", reset_digest: nil, reset_sent_at: nil>

14.1.5

演習1
irb(main):001:0> user = User.first
   (0.8ms)  SELECT sqlite_version(*)
  TRANSACTION (0.0ms)  begin transaction
  User Load (0.4ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> #<User id: 1, name: "Example User", email: "example@railstutorial.or...
irb(main):002:1* (2..5).each do |n|
irb(main):003:1*   User.find(n).follow(user)
irb(main):004:0> end
  User Load (0.4ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
  TRANSACTION (0.1ms)  SAVEPOINT active_record_1
  User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
  Relationship Create (0.5ms)  INSERT INTO "relationships" ("follower_id", "followed_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["follower_id", 2], ["followed_id", 1], ["created_at", "2021-07-21 15:02:26.930956"], ["updated_at", "2021-07-21 15:02:26.930956"]]
  TRANSACTION (0.0ms)  RELEASE SAVEPOINT active_record_1
  User Load (0.0ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 3], ["LIMIT", 1]]
  TRANSACTION (0.0ms)  SAVEPOINT active_record_1
  User Load (0.0ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 3], ["LIMIT", 1]]
  Relationship Create (0.8ms)  INSERT INTO "relationships" ("follower_id", "followed_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["follower_id", 3], ["followed_id", 1], ["created_at", "2021-07-21 15:02:26.935993"], ["updated_at", "2021-07-21 15:02:26.935993"]]
  TRANSACTION (0.0ms)  RELEASE SAVEPOINT active_record_1
  User Load (0.0ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 4], ["LIMIT", 1]]
  TRANSACTION (0.0ms)  SAVEPOINT active_record_1
  User Load (0.0ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 4], ["LIMIT", 1]]
  Relationship Create (0.1ms)  INSERT INTO "relationships" ("follower_id", "followed_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["follower_id", 4], ["followed_id", 1], ["created_at", "2021-07-21 15:02:26.939977"], ["updated_at", "2021-07-21 15:02:26.939977"]]
  TRANSACTION (0.0ms)  RELEASE SAVEPOINT active_record_1
  User Load (0.0ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 5], ["LIMIT", 1]]MIT ?  [["id", 5], ["LIMIT", 1]]
  Relationship Create (0.1ms)  INSERT INTO "relationships" ("follower_id", "followed_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["follower_id", 5], ["followed_id", 1], ["created_at", "2021-07-21 15:02:26.944312"], ["updated_at", "2021-07-21 15:02:26.944312"]]
  TRANSACTION (0.0ms)  RELEASE SAVEPOINT active_record_1
=> 2..5
irb(main):005:0> user.followers.map(&:id)
  User Load (0.4ms)  SELECT "users".* FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."follower_id" WHERE "relationships"."followed_id" = ?  [["followed_id", 1]]
=> [2, 3, 4, 5]
演習2

上の演習が終わったら、user.followers.countの実行結果が、先ほどフォローさせたユーザー数と一致していることを確認してみましょう。

irb(main):006:0> user.followers.count
   (0.7ms)  SELECT COUNT(*) FROM "users" INNER JOIN "relationships" ON "users"."id" = "relationships"."follower_id" WHERE "relationships"."followed_id" = ?  [["followed_id", 1]]
=> 4
演習3

user.followers.countを実行した結果、出力されるSQL文はどのような内容になっているでしょうか? また、user.followers.to_a.countの実行結果と違っている箇所はありますか? ヒント: もしuserに100万人のフォロワーがいた場合、どのような違いがあるでしょうか? 考えてみてください。

結果は演習2と同じ4となっている。
user.followers.to_a.countとするとuserをフォローしているユーザーの情報を一度配列にしてメモリ上に保存する。なのでフォローワーがたくさんいると配列を生成する無駄な処理が増え、また無駄なメモリ資源も出てしまう。

irb(main):007:0> user.followers.to_a.count
=> 4

14.2.2

演習3

Homeページに表示されている統計情報に対してテストを書いてみましょう。ヒント: リスト 13.28で示したテストに追加してみてください。同様にして、プロフィールページにもテストを追加してみましょう。

リンクがあるかテスト

  test "layout links when logged in user" do
    log_in_as(@user)
    get root_path
    assert_template "static_pages/home"
    assert_select "a[href=?]", root_path, count: 2
    assert_select "a[href=?]", help_path
    assert_select "a[href=?]", about_path
    assert_select "a[href=?]", contact_path
    assert_select "a[href=?]", users_path
    assert_select "a[href=?]", user_path(@user)
    assert_select "a[href=?]", edit_user_path(@user)
    assert_select "a[href=?]", logout_path
    assert_select "a[href=?]", login_path, count: 0
    #演習
    assert_select "a[href=?]", following_user_path(@user)
    assert_select "a[href=?]", followers_user_path(@user)
  end

統計情報が表示されているかテスト

  test "profile display" do
    get user_path(@user)
    assert_template 'users/show'
    assert_select 'title', full_title(@user.name)
    assert_select 'h1', text: @user.name
    assert_select 'h1>img.gravatar'
    assert_select 'div.stats'
    assert_match @user.microposts.count.to_s, response.body
    assert_select 'div.pagination', count: 1
    @user.microposts.paginate(page: 1).each do |micropost|
      assert_match micropost.content, response.body
    end
  end
end

14.2.6

演習1

リスト 14.36のrespond_toブロック内の各行を順にコメントアウトしていき、テストが正しくエラーを検知できるかどうか確認してみましょう。実際、どのテストケースが落ちたでしょうか?

format.htmlの行をコメントアウトして行ったテストでエラーが出た。
エラーになったテストケース
 test_should_follow_a_user_the_standard_way
 test_should_unfollow_a_user_the_standard_way
format.jsの行をコメントアウトして行ったテストではエラーが出なかった。

演習2

リスト 14.40のxhr: trueがある行のうち、片方のみを削除するとどういった結果になるでしょうか? このとき発生する問題の原因と、なぜ先ほどの演習で確認したテストがこの問題を検知できたのか考えてみてください。

※演習文が理解しづらかったので、自分なりの解釈での解答を記述します。

test_should_follow_a_user_the_standard_wayのxhr: trueがある行を削除してテストを行った。結果はRED(failures:1)だった。

問題について
・演習1でformat.htmlをコメントアウトしてエラーが出た原因
ActionController::UnknownFormatエラーが出ていた
つまり原因としてはアクションに対応するviewファイルがないとのこと

・xhr: trueのある行をを削除したらfailsが出た原因
ブロック内の処理が行われていないから

この2つの問題から起因することを考えてみましたが思いつきませんでした。

14.3.1

演習1

マイクロポストのidが正しく並んでいると仮定して(すなわち若いidの投稿ほど古くなる前提で)、図 14.22のデータセットでuser.feed.map(&:id)を実行すると、どのような結果が表示されるでしょうか? 考えてみてください。ヒント: 13.1.4で実装したdefault_scopeを思い出してください。

user.feedで取り出した投稿のIDを格納した配列が表示される。
またdefault_scopeで投稿日時を降順に取得するようにしているので結果は次のようになる。
[ 10, 9, 7, 5, 4, 2, 1 ]

14.3.2

演習1

リスト 14.44において、現在のユーザー自身の投稿を含めないようにするにはどうすれば良いでしょうか? また、そのような変更を加えると、リスト 14.42のどのテストが失敗するでしょうか?

フォローしているユーザーの投稿を取得

Micropost.where("user_id IN (?)", following_ids )

結果がREDになったテスト

 FAIL["test_feed_should_have_the_right_posts", #<Minitest::Reporters::Suite:0x000000010e2c5970 @name="UserTest">, 0.465544999926351]
 test_feed_should_have_the_right_posts#UserTest (0.47s)
        Expected false to be truthy.
        test/models/user_test.rb:99:in `block (2 levels) in <class:UserTest>'
        test/models/user_test.rb:98:in `block in <class:UserTest>'

 FAIL["test_micropost_interface", #<Minitest::Reporters::Suite:0x000000010bd23398 @name="MicropostsInterfaceTest">, 1.2411800000118092]
 test_micropost_interface#MicropostsInterfaceTest (1.24s)
        Expected exactly 1 element matching "div.pagination", found 0..
        Expected: 1
          Actual: 0
        test/integration/microposts_interface_test.rb:12:in `block in <class:MicropostsInterfaceTest>'
演習2

リスト 14.44において、フォローしているユーザーの投稿を含めないようにするにはどうすれば良いでしょうか? また、そのような変更を加えると、リスト 14.42のどのテストが失敗するでしょうか?

フォローしているユーザー以外の投稿を取得

Micropost.where("user_id IN (?)", id)

結果がREDになったテスト

 FAIL["test_feed_should_have_the_right_posts", #<Minitest::Reporters::Suite:0x000000010d851ca0 @name="UserTest">, 0.41781200002878904]
 test_feed_should_have_the_right_posts#UserTest (0.42s)
        Expected false to be truthy.
        test/models/user_test.rb:99:in `block (2 levels) in <class:UserTest>'
        test/models/user_test.rb:98:in `block in <class:UserTest>'

 FAIL["test_micropost_interface", #<Minitest::Reporters::Suite:0x000000010d017f08 @name="MicropostsInterfaceTest">, 1.1911460000555962]
 test_micropost_interface#MicropostsInterfaceTest (1.19s)
        Expected exactly 1 element matching "div.pagination", found 0..
        Expected: 1
          Actual: 0
        test/integration/microposts_interface_test.rb:12:in `block in <class:MicropostsInterfaceTest>'
演習3

リスト 14.44において、フォローしていないユーザーの投稿を含めるためにはどうすれば良いでしょうか? また、そのような変更を加えると、リスト 14.42のどのテストが失敗するでしょうか? ヒント: 自分自身とフォローしているユーザー、そしてそれ以外という集合は、いったいどういった集合を表すのか考えてみてください。

すべてのユーザーの投稿を取得する。

Micropost.all

結果がREDになったテスト

FAIL["test_feed_should_have_the_right_posts", #<Minitest::Reporters::Suite:0x000000010805ca40 @name="UserTest">, 1.4157729999860749]
 test_feed_should_have_the_right_posts#UserTest (1.42s)
        Expected true to be nil or false
        test/models/user_test.rb:103:in `block (2 levels) in <class:UserTest>'
        test/models/user_test.rb:102:in `block in <class:UserTest>'

14.3.3

演習1

Homeページで表示される1ページ目のフィードに対して、統合テストを書いてみましょう。リスト 14.49はそのテンプレートです。

micropost.contentをエスケープして、マイクロソフトが表示されることを確認

  test "feed on Home page" do
    get root_path
    @user.feed.paginate(page: 1).each do |micropost|
      assert_match CGI.escapeHTML(micropost.content), response.body
    end
  end
演習2

リスト 14.49のコードでは、期待されるHTMLをCGI.escapeHTMLメソッドでエスケープしています(このメソッドは11.2.3で扱ったCGI.escapeと同じ用途です)。このコードでは、なぜHTMLをエスケープさせる必要があったのでしょうか? 考えてみてください。ヒント: 試しにエスケープ処理を外して、得られるHTMLの内容を注意深く調べてください。マイクロポストの内容が何かおかしいはずです。また、ターミナルの検索機能(Cmd-FもしくはCtrl-F)を使って「sorry」を探すと原因の究明に役立つはずです。

エスケープしないと改行文字や記号が特殊文字で出力されるため。

I&#39;m sorry. Your words made sense, but your sarcastic tone did not.\n

おわりに

英語として不適切ということでRailsのデフォルトの値を上書きしたりしたので余計分かりづらさがました気がしました。
わかりづらい文章もあった、、

何はともあれRailsチュートリアル完走しました!

実の所これは3週目で
1週目は1〜8章を流し読み
2週目は1〜14章を速さ重視
3週目は1〜14章を理解重視
で学習しました。

Railsチュートリアルで多くのことを学びました。
しかし、テキスト通りに進めていっただけなので、実際にアプリを作るとなると不安なところが多いです。
きっと役に立つアプリを生み出すことができると信じて開発、勉強をしていきます!

Railsチュートリアル第13章

はじめに
Ruby on Railsチュートリアル(第6版)のメモ、演習の解答例を記述した記事です。
解答は個人のものなので、誤りがあればご指摘ください。
開発環境  Ruby: 2.7.2 , Rails: 6.1.4

メモ

マイクロソフトテーブルのインデックス

マイクロポストテーブルのuser_idとcreated_atにインデックスが追加されている

class CreateMicroposts < ActiveRecord::Migration[6.0]
  def change
    create_table :microposts do |t|
      t.text :content
      t.references :user, foreign_key: true

      t.timestamps
    end
    add_index :microposts, [:user_id, :created_at]
  end
end

user_idに関連づけられたマイクロポストを作成時刻の逆順で取り出しやすくするためにインデックスを追加。
また、user_idとcreated_atをひとつの配列に含めているのは、両方のキーを同時に扱う複合キーインデックスを作成するため。

作られたマイクロポストモデル
外部キーとしてuserのidを指定したため
生成されたマイクロソフトモデルにはbelongs_to :userでユーザーモデルに関連づけられている

class Micropost < ApplicationRecord
  belongs_to :user
end

モデルを関連づける

belongs_to:1対1の関係性を持たせる
has_many :1対多の関係性を持たせる


1対1:ひとつの投稿は1人のユーザーによって投稿される
1対多:一人のユーザーは複数の投稿を行うことができる

モデルを関連づけることで使えるメソッド

micropost.user
user.microposts
user.microposts.create
user.microposts.create!
user.microposts.build #newするのと同様
user.microposts.find_by()

紐付いているユーザーを通してマイクロポストを作成
こうして作られたマイクロポストは外部キーであるuser_idは自動的に生成元のユーザーのIDに設定される

デフォルトスコープ

データベースからデータを取り出す際に絞り込みや順序を指定することができる

これはデフォルトでのSQLを変更するため、使用する際には注意が必要とのこと
参考:Railsのdefault_scopeは悪ではない。 - Qiita

Dependent: destroy

1対多の関係の1側の要素が削除された時に同時に関連づけられた多の要素も削除される

ユーザーを削除した際に関連づけられている全ての投稿も削除される

class User < ApplicationRecord
  has_many :microposts, dependent: :destroy
  .
  .
  .
end

request.referrerメソッド

ひとつ前のURLを返す

/homeで何らかのHTTPリクエストを送信してrequest.referrerが呼び出されるとURL /homeが返される

参考:request | Railsドキュメント

SQLインジェクション

SQL文の組み立てに問題があり脆弱性がある場合、この問題を利用して攻撃を行うこと。

参考:安全なウェブサイトの作り方 - 1.1 SQLインジェクション:IPA 独立行政法人 情報処理推進機構

対策
直接SQL文を書かないようにすることでSQLインジェクションの対策をする

Micropost.where("user_id = ?", id)

画像のアップロード

ActiveStorageを用いて画像のアップロードを行う
ファイルをクラウドストレージへのアップロードや、Active Recordオブジェクトに結びつける機能を提供する。
ActiveStorageは画像ファイル、平文、PDFなどを取り扱える
参考:Active Storage の概要 - Railsガイド

準備

1. ActiveStorage用マイグレーションファイルを生成

$ rails active_storage:install

2. マイグレーションファイルを実行

$ rails db:migrate

3. ファイルをモデルに結びつける
has_one_attachedメソッドでアップロードされたファイルとモデルを結びつける。
imageを1対1でモデルと結びつける

class モデル < ApplicationRecord
  ・・
  has_one_attached :image
  ・・
end

1対多で結びつけたい場合has_many_attachedメソッドを利用

4. ファイルを受けつける

<%= f.file_field :image %>

5. ファイルを受け取る
・ストロングパラメータで:imageを許可する
・ActiveStorageのattachメソッドを利用してファイルを受け取る

micropost.image.attach(params[:micropost][:image])

6. ファイルを表示する

    <%= image_tag micropost.image if micropost.image.attached? %>

micropost.image.attached?ではマイクロポストがimageを持っているかを調べる。

画像のバリデーション

Active Storageにはバリデーション機能がネイティブでサポートされていない。
そこで'active_storage_validations'gemを追加して設定を行う。

画像フォーマットのバリデーション

content_type: { in: %w[image/jpeg image/gif image/png],
                message: "must be a valid image format" }

画像サイズのバリデーション

size: { less_than: 5.megabytes,
        message: "should be less than 5MB" }
画像のリサイズ

表示する画像のサイズを設定する。

1. ImageMagickを開発環境にインストール

brew install imagemagick

2. gemを追加

gem 'image_processing',           '1.9.3'
gem 'mini_magick',                '4.9.5'

3. インストール

$ bundle install

4. 変換済み画像を作成
保存されている画像を加工

image.variant(resize_to_limit: [500, 500])

これをimage_tagで表示

テストでfixutureで定義されたファイルを使う

/test/fixturesにテストで利用したい画像を追加する
fixuture_file_uploadメソッドを利用してfixtureで定義されたファイルをアップロードしてこれをテストで利用する

演習

13.1.4

演習1

Micropost.first.created_atの実行結果と、Micropost.last.created_atの実行結果を比べてみましょう。

作成日時を降順に設定したため
最後の投稿より最初の投稿の方が新しい

irb(main):001:0> Micropost.first.created_at
   (0.7ms)  SELECT sqlite_version(*)
  Micropost Load (1.0ms)  SELECT "microposts".* FROM "microposts" ORDER BY "microposts"."created_at" DESC LIMIT ?  [["LIMIT", 1]]
=> Fri, 16 Jul 2021 00:10:48.658871000 UTC +00:00
irb(main):002:0> Micropost.last.created_at
  Micropost Load (0.5ms)  SELECT "microposts".* FROM "microposts" ORDER BY "microposts"."created_at" ASC LIMIT ?  [["LIMIT", 1]]
=> Thu, 15 Jul 2021 12:28:01.015111000 UTC +00:00
演習2

Micropost.firstを実行したときに発行されるSQL文はどうなっているでしょうか? 同様にして、Micropost.lastの場合はどうなっているでしょうか? ヒント: それぞれをコンソール上で実行したときに表示される文字列が、SQL文になります。

デフォルトスコープで設定したorder byが追加されている
ORDER BY "microposts"."created_at" DESC

irb(main):003:0> Micropost.first
  Micropost Load (0.8ms)  SELECT "microposts".* FROM "microposts" ORDER BY "microposts"."created_at" DESC LIMIT ?  [["LIMIT", 1]]
=> #<Micropost id: 2, content: "Lorem ipsum", user_id: 1, created_at: "2021-07-16 00:10:48.658871000 +0000", updated_at: "2021-07-16 00:10:48.658871000 +0000">
演習3

データベース上の最初のユーザーを変数userに代入してください。そのuserオブジェクトが最初に投稿したマイクロポストのidはいくつでしょうか? 次に、destroyメソッドを使ってそのuserオブジェクトを削除してみてください。削除すると、そのuserに紐付いていたマイクロポストも削除されていることをMicropost.findで確認してみましょう。

ユーザーが削除されたら削除されたユーザーに関連付けられていた投稿も削除されていることを確認

irb(main):001:0> user = User.first
   (0.6ms)  SELECT sqlite_version(*)
  User Load (0.5ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> #<User id: 1, name: "Example User", email: "example@railstutorial.org", created_at: "2021-07-14 08:39:31.194822000 +0000", updated_at: "2021-07-15 05:42:22.6778...
irb(main):002:0> user.microposts.first.id
  Micropost Load (0.8ms)  SELECT "microposts".* FROM "microposts" WHERE "microposts"."user_id" = ? ORDER BY "microposts"."created_at" DESC LIMIT ?  [["user_id", 1], ["LIMIT", 1]]
=> 2
irb(main):003:0> user.destroy
  TRANSACTION (0.2ms)  SAVEPOINT active_record_1
  Micropost Load (0.3ms)  SELECT "microposts".* FROM "microposts" WHERE "microposts"."user_id" = ? ORDER BY "microposts"."created_at" DESC  [["user_id", 1]]
  Micropost Destroy (0.7ms)  DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 2]]
  User Destroy (1.2ms)  DELETE FROM "users" WHERE "users"."id" = ?  [["id", 1]]
  TRANSACTION (0.1ms)  RELEASE SAVEPOINT active_record_1
=> #<User id: 1, name: "Example User", email: "example@railstutorial.org", created_at: "2021-07-14 08:39:31.194822000 +0000", updated_at: "2021-07-15 05:42:22.677832000 +0000", password_digest: [FILTERED], remember_digest: nil, admin: true, activation_digest: "$2a$12$zTggchTpdRrUiMmX/Af1DuLf2Zm8ASlZ079q5wV7WuR...", activated: true, activated_at: "2021-07-14 08:39:30.948680000 +0000", reset_digest: "$2a$12$VdwivaGX9p8cfpggYPhIE.DO4hXpXpnGwRhRM41M0lI...", reset_sent_at: "2021-07-15 05:42:22.677743000 +0000">
irb(main):004:0> Micropost.find(2)
  Micropost Load (0.3ms)  SELECT "microposts".* FROM "microposts" WHERE "microposts"."id" = ? ORDER BY "microposts"."created_at" DESC LIMIT ?  [["id", 2], ["LIMIT", 1]]
Traceback (most recent call last):
        1: from (irb):4
ActiveRecord::RecordNotFound (Couldn't find Micropost with 'id'=2)

13.2.1

演習1

7.3.3で軽く説明したように、今回ヘルパーメソッドとして使ったtime_ago_in_wordsメソッドは、Railsコンソールのhelperオブジェクトから呼び出すことができます。このhelperオブジェクトのtime_ago_in_wordsメソッドを使って、3.weeks.agoや6.months.agoを実行してみましょう。

irb(main):002:0> helper.time_ago_in_words(3.weeks.ago)
=> "21 days"
irb(main):003:0> helper.time_ago_in_words(6.month.ago)
=> "6 months"
演習2

helper.time_ago_in_words(1.year.ago)と実行すると、どういった結果が返ってくるでしょうか?

irb(main):006:0> helper.time_ago_in_words(1.years.ago)
=> "about 1 year"
演習3

micropostsオブジェクトのクラスは何でしょうか? ヒント: リスト 13.23内のコードにあるように、まずはpaginateメソッド(引数はpage: nil)でオブジェクトを取得し、その後classメソッドを呼び出してみましょう。

b(main):009:0> microposts = Micropost.paginate(page: nil)
   (3.9ms)  SELECT sqlite_version(*)
  Micropost Load (0.4ms)  SELECT "microposts".* FROM "microposts" /* loading for inspect */ ORDER BY "microposts"."created_at" DESC LIMIT ? OFFSET ?  [["LIMIT", 11], ["OFFSET", 0]]
=> #<ActiveRecord::Relation []>
irb(main):010:0> microposts.class
=> Micropost::ActiveRecord_Relation

13.2.2

演習1

(1..10).to_a.take(6)というコードの実行結果を推測できますか? 推測した値が合っているかどうか、実際にコンソールを使って確認してみましょう。

irb(main):001:0> (1..10).to_a.take(6)
=> [1, 2, 3, 4, 5, 6]
演習2

先ほどの演習にあったto_aメソッドの部分は本当に必要でしょうか? 確かめてみてください。

irb(main):002:0> (1..10).take(6)
=> [1, 2, 3, 4, 5, 6]
演習3

Fakerはlorem ipsum以外にも、非常に多種多様の事例に対応しています。Fakerのドキュメント(英語)を眺めながら画面に出力する方法を学び、実際に架空の大学名やHipster IpsumやChuck Norris facts(参考: チャック・ノリスの真実)を画面に出力してみましょう。(訳注: もちろん日本語にも対応していて、例えば沖縄らしい用語を出力するfaker-okinawaもあります。ぜひ遊んでみてください。)

ドラゴンボールのFakerがあったので試してみました笑

  content = Faker::JapaneseMedia::DragonBall.character

13.2.3

演習2

リスト 13.28にあるテストを変更して、will_paginateが1度のみ表示されていることをテストしてみましょう。ヒント: 表 5.2を参考にしてください。

    assert_select 'div.pagination', count: 1

13.3.1

演習1

なぜUsersコントローラ内にあるlogged_in_userフィルターを残したままにするとマズイのでしょうか? 考えてみてください。
DRY原則に反するため。

13.3.2

演習1

Homeページをリファクタリングして、if-else文の分岐のそれぞれに対してパーシャルを作ってみましょう。

views/static_pages/home.html.erb

<% if logged_in? %>
  <%= render 'static_pages/logged_in_user' %>
<% else %>
  <%= render 'static_pages/not_logged_in_user' %>
<% end %>

views/static_pages/_logged_in_user.html.erb

<div class="row">
  <aside class="user_info">
    <section class="user_info">
      <%= render 'shared/user_info' %>
    </section>
    <section class="micropost_form">
      <%= render 'shared/micropost_form' %>
    </section>
  </aside>
</div>

views/static_pages/_not_logged_in_user.html.erb

<div class="center jumbotron">
  <h1>Welcome to the Sample App</h1>  
  <h2>
    This is the home page for the
    <a href="https://railstutorial.jp/">Ruby on Rails Tutorial</a>
    sample application.
  </h2> 
  <%= link_to "Sign up now!", signup_path, class: "btn btn-lg btn-primary" %>
</div>

  <%= link_to image_tag("rails.svg", alt: "Rails logo", width: "200px"),
                        "https://rubyonrails.org/" %>
演習2

マイクロポストを投稿した直後に、ブラウザの更新ボタンを押すとエラーが表示されます。なぜエラーが表示されるのでしょうか?その原因を考えてみましょう。

マイクロポストを投稿するとPOSTリクエストが/micropostsに送信される。
micropostsコントローラーでは投稿に失敗した時renderメソッドを用いてhomeページが表示される。しかしrenderでは'static_pages/home'を表示しただけでURLを更新していない。つまり投稿に失敗した場合URLは/micropostsの状態になる。
なので/micropostsの状態でページの更新をすると/micropostsに対してGETリクエストが送信され、現段階ではリクエストに対するルートが指定されていないためルーティングエラーになる。

演習3

もし上記の現象に対応するとしたら、どんな対応方法があるでしょうか?その対応方法を考えてみましょう。(ヒント: 様々な対応方法がありますが、対応方法によっては今後の実装に支障が出ることがあります。ここでは対応方法のアイデア出しに留めておきましょう。)

投稿に失敗した時にURLを更新できれば解決できると思う。
つまり投稿に失敗した時にリダイレクトするように変更すれば対応できると思う。

参考に
qiita.com
POSTした後にrenderするのは良くないとのことです。
後で改良したいと思います。

13.4.1

演習2

リスト 13.64に示すテンプレートを参考に、13.4で実装した画像アップローダーをテストしてください。テストの準備として、まずはサンプル画像をfixtureディレクトリに追加してください(リスト 13.63)。リスト 13.64で追加したテストでは、Homeページにあるファイルアップロードと、投稿に成功した時に画像が表示されているかどうかをチェックしています。なお、テスト内にあるfixture_file_uploadというメソッドは、fixtureで定義されたファイルをアップロードする特別なメソッドです18 。ヒント: image属性が有効かどうかを確かめるときは、11.3.3で紹介したassignsメソッドを使ってください。このメソッドを使うと、投稿に成功した後にcreateアクション内のマイクロポストにアクセスするようになります。

  test "micropost interface" do
    log_in_as(@user)
    get root_path
    assert_select "div.pagination", count: 1
    assert_select "input[type=file]"
    #無効な送信
    assert_no_difference "Micropost.count" do
      post microposts_path, params: { micropost: { content: ""} }
    end
    assert_template 'static_pages/home'
    assert_select 'div#error_explanation'
    assert_select 'a[href=?]', '/?page=2' #正しいページネーションリンク
    #有効な送信
    content = "This micropost really ties the room toghether"
    image = fixture_file_upload('test/fixtures/kitten.jpg', 'image/jpeg')
    assert_difference "Micropost.count", 1 do
      post microposts_path, params: { micropost: { content: content, image: image } }
    end
    assert assigns(:micropost).image.attached?
    assert_redirected_to root_url
    follow_redirect!
    assert_match content, response.body
    #投稿を削除する
    assert_select 'a', text: 'delete' 
    first_micropost = @user.microposts.paginate(page: 1).first
    assert_difference "Micropost.count", -1 do
      delete micropost_path(first_micropost)
    end
    #違うユーザーのプロフィールにアクセス
    get user_path(users(:archer))
    assert_select 'a', text: 'delete', count: 0
  end

おわりに

この章はかなり内容がつまっていたような気がしました。
Railsの力を借りて画像のアップロードまで実装ができて改めてRailsすごいなと思いました。

Railsチュートリアル第12章

はじめに
Ruby on Railsチュートリアル(第6版)のメモ、演習の解答例を記述した記事です。
解答は個人のものなので、誤りがあればご指摘ください。
開発環境  Ruby: 2.7.2 , Rails: 6.1.4

メモ

隠しフィールド

ページ内に情報を表示せずに保持しておきたい時に使う

hidden_field_tag :email, @user.email

送信先のアクションでparams[:email]で受け取れる

f.hidden_field :email, @user.email

送信先のアクションでparams[:user][:email]で受け取れる
※試しにf.hidden_fieldで実行したらNoMethodErrormがでた。

ActiveRecordのオブジェクトにエラーメッセージを追加

ユーザーオブジェクトのエラーメッセージにメッセージを追加

@user.errors.add(:password, :blank)

演習

12.1.1

演習2

表 12.1の名前付きルートでは、_pathではなく_urlを使うように記してあります。なぜでしょうか? 考えてみましょう。ヒント: アカウント有効化で行った演習(11.1.1.1)と同じ理由です。

メール本文の絶対リンクからリクエストを送るため

12.1.2

演習1

リスト 12.4のform_withメソッドで、@password_resetではなく:password_resetを使っている理由を考えてみましょう。

今回のフォームではアクションから@password_resetを受け取っておらずモデルの情報がない
そこでscopeオプションでpassword_resetsのプレフィックス(接頭語)を付けることで送信先のアクションでパラメータを明示的に受け取ることができるようにするため。

ちなみに、scopeオプションを指定しなくても送信先でパラメータを受け取ることは可能

12.3.3

演習1

リスト 12.6にあるcreate_reset_digestメソッドはupdate_attributeを2回呼び出していますが、これは各行で1回ずつデータベースへ問い合わせしていることになります。リスト 12.20に記したテンプレートを使って、update_attributeの呼び出しを1回のupdate_columns呼び出しにまとめてみましょう(これでデータベースへの問い合わせが1回で済むようになります)。また、変更後にテストを実行し、 green になることも確認してください。ちなみにリスト 12.20にあるコードには、前章の演習(リスト 11.39)の解答も含まれています。

2回のデータベースへの問い合わせをupdate_columnsを使って1回に

  def create_reset_digest
    self.reset_token = User.new_token
    update_columns(reset_digest: User.digest(reset_token),reset_sent_at: Time.zone.now)
  end
演習2

リスト 12.21のテンプレートを埋めて、期限切れのパスワード再設定で発生する分岐(リスト 12.16)を統合テストで網羅してみましょう(12.21 のコードにあるresponse.bodyは、そのページのHTML本文をすべて返すメソッドです)。期限切れをテストする方法はいくつかありますが、リスト 12.21でオススメした手法を使えば、レスポンスの本文に「expired」という語があるかどうかでチェックできます(なお、大文字と小文字は区別されません)。

  test "expred token" do
    get new_password_reset_path
    post password_resets_path, params: { password_reset: { email: @user.email } }
    @user = assigns(:user)
    @user.update_attribute(:reset_sent_at, 3.hours.ago)
    patch password_reset_path(@user.reset_token), params: { email: @user.email, user: { password: "foobar", password_confirmation: "foobar" }}
    assert_response :redirect
    follow_redirect!
    assert_match /expired/i, response.body
  end
演習3

2時間経ったらパスワードを再設定できなくする方針は、セキュリティ的に好ましいやり方でしょう。しかし、もっと良くする方法はまだあります。例えば、公共の(または共有された)コンピューターでパスワード再設定が行われた場合を考えてみてください。仮にログアウトして離席したとしても、2時間以内であれば、そのコンピューターの履歴からパスワード再設定フォームを表示させ、パスワードを更新してしまうことができてしまいます(しかもそのままログイン機構まで突破されてしまいます!)。この問題を解決するために、リスト 12.22のコードを追加し、パスワードの再設定に成功したらダイジェストをnilになるように変更してみましょう5 。

  def update
    if params[:user][:password].empty?
      @user.errors.add(:password, :blank)
      render 'edit'
    elsif @user.update(user_params)
      log_in @user
      @user.update_attribute(:reset_digest, nil)
      flash[:success] = "Password has been reset."
      redirect_to @user
    else
      render 'edit'
    end
  end
演習4

リスト 12.18に1行追加し、1つ前の演習課題に対するテストを書いてみましょう。ヒント: リスト 9.25のassert_nilメソッドとリスト 11.33のuser.reloadメソッドを組み合わせて、reset_digest属性を直接テストしてみましょう。

パスワードの更新後にreset_digestがnilに更新されたか確認

    assert_nil @user.reload.reset_digest

おわりに

メールの生成送信は前章と似た内容でした
今回はパスワード再設定のテストを分岐通りにテストしました。
分岐が多いとテストを書くの大変なのがわかりました。

残すはマイクロポストの投稿機能とフォロー機能となりました。
引き続き頑張ります!

Railsチュートリアル第11章

はじめに
Ruby on Railsチュートリアル(第6版)のメモ、演習の解答例を記述した記事です。
解答は個人のものなので、誤りがあればご指摘ください。
開発環境  Ruby: 2.7.2 , Rails: 6.1.4

メモ

メールアドレスを利用したアカウントの有効化

ユーザーの新規登録の際にメールアドレスが登録したユーザーのものなのかを確認できるようにする

before_createコールバック

オブジェクトが作成される前に処理を実行する

before_create :create_activation_digest

上記のコードはメソッド参照と呼ばれるものでオブジェクトを生成する前にcreate_acrivation_digestを実行する

before_createのタイミング

チュートリアル内で

”before_createコールバックの方はユーザーが作成される前に呼び出される”
"User.newで新しいユーザーが定義されると、activation_token属性やactivation_digest属性が得られるようになります"

と書かれており、User.newでオブジェクトを生成した時点でbefore_createコールバックが実行されると思ったが違ったようなので記述しておきます。

createメソッドとは
モデルオブジェクトを生成して保存するという一連の処理のこと

ユーザーを作成する = ユーザーオブジェクトを生成して保存する
とのことだったようです。

before_saveとbefore_create どっちが先に呼び出される??
before_saveの方が先に呼び出される

オブジェクトの作成の際に呼び出される順序は以下のようになります

before_validation
after_validation
before_save
around_save
before_create
around_create
after_create
after_save
after_commit/after_rollback

参考:Active Record コールバック - Railsガイド


”保存”を起点にコールバックが処理される?
参考:ActiveRecordのコールバックの順序・コールバック内のロールバック処理について - Hack Your Design!
上記の記事を参考にコールバックを可視化してみました。

コールバックを可視化するためのコード

class User < ActiveRecord::Base
  before_validation -> { puts "before_validation is called" }
  after_validation -> { puts "after_validation is called" }
  before_save -> { puts "before_save is called" }
  before_update -> { puts "before_update is called" }
  before_create -> { puts "before_create is called" }
  after_create -> { puts "after_create is called" }
  after_update -> { puts "after_update is called" }
  after_save -> { puts "after_save is called" }
  after_commit -> { puts "after_commit is called" }
end

新規レコード作成時
Userモデルをnewしてsave
Userモデルをcreate する

irb(main):003:0>user = User.new(name:"yuy", email: "yuy@yuy.yuy", password: "foobar", pa
ssword_confirmation: "foobar" )
=> #<User id: nil, name: "yuy", email: "yuy@yuy.yuy", created_at: nil, updated_at: n...

irb(main):004:0> user.save
before_validation is called
  TRANSACTION (0.2ms)  SAVEPOINT active_record_1
  User Exists? (0.2ms)  SELECT 1 AS one FROM "users" WHERE "users"."email" = ? LIMIT ?  [["email", "yuy@yuy.yuy"], ["LIMIT", 1]]
after_validation is called
before_save is called
before_create is called
  User Create (1.5ms)  INSERT INTO "users" ("name", "email", "created_at", "updated_at", "password_digest") VALUES (?, ?, ?, ?, ?)  [["name", "yuy"], ["email", "yuy@yuy.yuy"], ["
after_create is called
after_save is called
  TRANSACTION (0.0ms)  RELEASE SAVEPOINT active_record_1
after_commit is called
=> true

irb(main):005:0> User.create(name:"yuyy", email: "yuyy@yuy.yuy", password: "foobar", pass
word_confirmation: "foobar" )
before_validation is called
  TRANSACTION (0.1ms)  SAVEPOINT active_record_1
  User Exists? (0.1ms)  SELECT 1 AS one FROM "users" WHERE "users"."email" = ? LIMIT ?  [["email", "yuyy@yuy.yuy"], ["LIMIT", 1]]
after_validation is called
before_save is called
before_create is called
  User Create (0.1ms)  INSERT INTO "users" ("name", "email", "created_at", "updated_at", "password_digest") VALUES (?, ?, ?, ?, ?)  [["name", "yuyy"], ["email", "yuyy@yuy.yuy"], ["created_at", "2021-07-14 01:09:11.939377"], ["updated_at", "2021-07-14 01:09:11.939377"], ["password_digest", "$2a$12$UVy2HGBnjaIMadalnA.OmuMzdCYLQGJTnkgaFwWZB4b7mOk6uEFoi"]]
after_create is called
after_save is called
  TRANSACTION (0.0ms)  RELEASE SAVEPOINT active_record_1
after_commit is called
=> #<User id: 105, name: "yuyy", email: "yuyy@yuy.yuy", created_at: "2021-07-14 01:09:11.939377000 +0000", updated_at: "2021-07-14 01:09:11.939377000 +0000", password_digest: [FILTERED], remember_digest: nil, admin: false, activation_digest: nil, activated: false, activated_at: nil>

モデルをnewした時点ではコールバックが呼び出されていません。
save、createするとコールバックが呼び出されるようです。
before_save、before_createどちらも呼び出されてますね。
また、before_updateとafter_update呼び出されていません。

レコード更新時
ユーザーモデルをupdateする

irb(main):006:0> user.update(name: "yuuuuy")
before_validation is called
  TRANSACTION (0.1ms)  SAVEPOINT active_record_1
  User Exists? (0.1ms)  SELECT 1 AS one FROM "users" WHERE "users"."email" = ? AND "users"."id" != ? LIMIT ?  [["email", "yuy@yuy.yuy"], ["id", 104], ["LIMIT", 1]]
after_validation is called
before_save is called
before_update is called
  User Update (0.1ms)  UPDATE "users" SET "name" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["name", "yuuuuy"], ["updated_at", "2021-07-14 01:26:17.018216"], ["id", 104]]
after_update is called
after_save is called
  TRANSACTION (0.0ms)  RELEASE SAVEPOINT active_record_1
after_commit is called
=> true

こちらは更新なのでbefore_createとafter_createは呼び出されていません。

Action Mailer

アプリケーションでメールの送受信を行えるようにするライブラリ

メイラーの作成

メイラーを生成する

account_activationメイラーとpassword_resetメイラーを生成

$ rails generate mailer UserMailer account_activation password_reset

メイラーごとにテキスト用のメールテンプレートとHTMLメール用のテンプレートが生成される

Running via Spring preloader in process 84968
      create  app/mailers/user_mailer.rb
      invoke  erb
      create    app/views/user_mailer
      create    app/views/user_mailer/account_activation.text.erb
      create    app/views/user_mailer/account_activation.html.erb
      create    app/views/user_mailer/password_reset.text.erb
      create    app/views/user_mailer/password_reset.html.erb
      invoke  test_unit
      create    test/mailers/user_mailer_test.rb
      create    test/mailers/previews/user_mailer_preview.rb
メールに記述するURLの生成

メールを利用したアカウントの有効化では
メールアドレスと有効化トークンのダイジェストを利用してユーザーが本人であるかデータベースから参照して有効化を行う

そこで送るメールに記載するURLにメールアドレスと有効化トークンのダイジェストを含ませておく

URLの生成

edit_account_activation_url(@user.activation_token, email: @user.email)

上記の名前付きルートから生成されるURLの例
email部分はRailsが自動的にエスケープした文字列を生成

account_activations/q5lt38hQDc_959PVoo6b7A/edit?email=foo%40example.com
送信メールのプレビュー

メールのメッセージのプレビューをその場で見ることができる
config/environments/development.rbを変更する

Rails.application.configure do
  .
  .
  .
  config.action_mailer.raise_delivery_errors = false

  host = 'example.com' # ここをコピペすると失敗します。自分の環境のホストに変えてください。
  # クラウドIDEの場合は以下をお使いください
  config.action_mailer.default_url_options = { host: host, protocol: 'https' }
  # localhostで開発している場合は以下をお使いください
  # config.action_mailer.default_url_options = { host: host, protocol: 'http' }
  .
  .
  .
end

受け取ったパラメータに応じて呼び出すメソッドを切り替える

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

remember部分を変数として扱いたい

self.FOOBAR_digest

変数として扱うことで引数に応じてメソッドを切り替えることができる

メタプログラミング

プログラムでプログラムを作成すること
sendメソッドで実現する

>> user = User.first
>> user.activation_digest
=> "$2a$10$4e6TFzEJAVNyjLv8Q5u22ensMt28qEkx0roaZvtRcp6UZKRM6N9Ae"
>> user.send(:activation_digest)
=> "$2a$10$4e6TFzEJAVNyjLv8Q5u22ensMt28qEkx0roaZvtRcp6UZKRM6N9Ae"
>> user.send("activation_digest")
=> "$2a$10$4e6TFzEJAVNyjLv8Q5u22ensMt28qEkx0roaZvtRcp6UZKRM6N9Ae"
>> attribute = :activation
>> user.send("#{attribute}_digest")
=> "$2a$10$4e6TFzEJAVNyjLv8Q5u22ensMt28qEkx0roaZvtRcp6UZKRM6N9Ae"

演習

11.1

演習2

表 11.2の名前付きルートでは、_pathではなく_urlを使うように記してあります。なぜでしょうか? 考えてみましょう。ヒント: 私達はこれからメールで名前付きルートを使います。

ページ外部のメールからリクエストを送りたいから絶対リンクを使う

11.1.2

演習2

コンソールからUserクラスのインスタンスを生成し、そのオブジェクトからcreate_activation_digestメソッドを呼び出そうとすると(Privateメソッドなので)NoMethodErrorが発生することを確認してみましょう。また、そのUserオブジェクトからダイジェストの値も確認してみましょう。

yuy@yu sample_app % rails c --sandbox
Running via Spring preloader in process 1705
Loading development environment in sandbox (Rails 6.1.4)
Any modifications you make will be rolled back on exit
irb(main):001:0> User.create(name:  "User",email: "user@railstutorial.org", 
password: "foobar", password_confirmation: "foobar", admin: false, activated
: false, activated_at: Time.zone.now)
   (0.5ms)  SELECT sqlite_version(*)
  TRANSACTION (0.0ms)  begin transaction
  TRANSACTION (0.1ms)  SAVEPOINT active_record_1
  User Exists? (0.1ms)  SELECT 1 AS one FROM "users" WHERE "users"."email" = ? LIMIT ?  [["email", "user@railstutorial.org"], ["LIMIT", 1]]
  User Create (0.3ms)  INSERT INTO "users" ("name", "email", "created_at", "updated_at", "password_digest", "activation_digest", "activated_at") VALUES (?, ?, ?, ?, ?, ?, ?)  [["name", "User"], ["email", "user@railstutorial.org"], ["created_at", "2021-07-13 23:49:56.915739"], ["updated_at", "2021-07-13 23:49:56.915739"], ["password_digest", "$2a$12$u9f0aKuqXqZFCQXrWyMTa.U6nXimKEX8JA9HnkIbS4b4GXbVSfaJe"], ["activation_digest", "$2a$12$Tas6a/c9vXYMIf7lbswHruVxuVskADJhPr1NZRNOQJmGio2MT2vIC"], ["activated_at", "2021-07-13 23:49:56
  TRANSACTION (0.0ms)  RELEASE SAVEPOINT active_record_1
=> #<User id: 103, name: "User", email: "user@railstutorial.org", created_at: "2021-07-13 23:49:56.915739000 +0000", updated_at: "2021-07-13 23:49:56.915739000 +0000", password_digest: [FILTERED], remember_digest: nil, admin: false, activation_digest: "$2a$12$Tas6a/c9vXYMIf7lbswHruVxuVskADJhPr1NZRNOQJm...", activated: false, activated_at: "2021-07-13 23:49:56.643115000 +0000">
irb(main):002:0> User.last.create_activation_digestC LIMIT ?  [["LIMIT", 1]]
Traceback (most recent call last):
        1: from (irb):2
NoMethodError (private method `create_activation_digest' called for #<User:0x0000000143b4a688>)
Did you mean?  restore_activation_digest!
irb(main):003:0> User.last.activation_digest
  User Load (0.1ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ?  [["LIMIT", 1]]
=> "$2a$12$Tas6a/c9vXYMIf7lbswHruVxuVskADJhPr1NZRNOQJmGio2MT2vIC"
演習3

リスト 6.35で、メールアドレスの小文字化にはemail.downcase!という(代入せずに済む)メソッドがあることを知りました。このメソッドを使って、リスト 11.3のdowncase_emailメソッドを改良してみてください。また、うまく変更できれば、テストスイートは成功したままになっていることも確認してみてください。

    #メールアドレスを全て小文字にする
    def downcase_email
      self.email.downcase!
    end

11.3.1

演習1

コンソール内で新しいユーザーを作成してみてください。新しいユーザーの記憶トークンと有効化トークンはどのような値になっているでしょうか? また、各トークンに対応するダイジェストの値はどうなっているでしょうか?

記憶トークンと記憶ダイジェストはnil

irb(main):001:0> user = User.create(name:"yuy", email: "yuy@yuy.yuy", passwo
rd: "foobar", password_confirmation: "foobar" )
   (0.7ms)  SELECT sqlite_version(*)
  TRANSACTION (0.0ms)  begin transaction
  TRANSACTION (0.0ms)  SAVEPOINT active_record_1
  User Exists? (0.2ms)  SELECT 1 AS one FROM "users" WHERE "users"."email" = ? LIMIT ?  [["email", "yuy@yuy.yuy"], ["LIMIT", 1]]
  User Create (1.1ms)  INSERT INTO "users" ("name", "email", "created_at", "updated_at", "password_digest", "activation_digest") VALUES (?, ?, ?, ?, ?, ?)  [["name", "yuy"], ["email", "yuy@yuy.yuy"], ["created_at", "2021-07-14 02:20:18.142063"], ["updated_at", "2021-07-14 02:20:18.142063"], ["password_digest", "$2a$12$2VL3d7UILjexu2t7rmJXiuiyE0jxKbcxr65XsBcW1qIL2/NwqmyPi"], ["activation_digest", "$2a$12$BpedetJAqun5id9xd68UKeT8pC/IYscPU2D5Wk3w7STe8pDSkOsFS"]]
  TRANSACTION (0.0ms)  RELEASE SAVEPOINT active_record_1
=> #<User id: 104, name: "yuy", email: "yuy@yuy.yuy", created_at: "2021...

記憶トークンと記憶ダイジェストを更新

irb(main):002:0> user.remember
  TRANSACTION (0.2ms)  SAVEPOINT active_record_1
  User Update (0.1ms)  UPDATE "users" SET "updated_at" = ?, "remember_digest" = ? WHERE "users"."id" = ?  [["updated_at", "2021-07-14 02:23:13.938806"], ["remember_digest", "$2a$12$CtvDghirVjL61HmrQGrLKuDJIiDTkF2Xe3AmhbE5c48spCYMbbzDC"], ["id", 104]]
  TRANSACTION (0.0ms)  RELEASE SAVEPOINT active_record_1
=> true
irb(main):003:0> user.remember_token
=> "6s19ZgORpmwTfsAq1uOU4g"
irb(main):004:0> user.remember_digest
=> "$2a$12$CtvDghirVjL61HmrQGrLKuDJIiDTkF2Xe3AmhbE5c48spCYMbbzDC"
演習2

リスト 11.26で抽象化したauthenticated?メソッドを使って、先ほどの各トークン/ダイジェストの組み合わせで認証が成功することを確認してみましょう。

irb(main):007:0> user.authenticated?(:remember, user.remember_token)
=> true

11.3.3

演習1

リスト 11.35にあるactivateメソッドはupdate_attributeを2回呼び出していますが、これは各行で1回ずつデータベースへ問い合わせしていることになります。リスト 11.39に記したテンプレートを使って、update_attributeの呼び出しを1回のupdate_columns呼び出しにまとめてみましょう。これでデータベースへの問い合わせが1回で済むようになります(注意!update_columnsは、モデルのコールバックやバリデーションが実行されない点がupdate_attributeと異なります)。また、変更後にテストを実行し、 green になることも確認してください。

update_columnsの使い方には注意

  def activate
    update_columns(activated: true, activated_at: Time.zone.now)
  end
演習2

現在は、/usersのユーザーindexページを開くとすべてのユーザーが表示され、/users/:idのようにIDを指定すると個別のユーザーを表示できます。しかし考えてみれば、有効でないユーザーは表示する意味がありません。そこで、リスト 11.40のテンプレートを使って、この動作を変更してみましょう9 。なお、ここで使っているActive Recordのwhereメソッドについては、13.3.3でもう少し詳しく説明します。

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

  def show
    @user = User.find(params[:id])
    redirect_to root_url and return unless @user.activated?
  end
演習3

ここまでの演習課題で変更したコードをテストするために、/users と /users/:id の両方に対する統合テストを作成してみましょう。

有効でないユーザが表示されないか、表示できないかのテスト

  test "should not show non activated user" do
    log_in_as(@admin)
    get users_path
    assert_select "a[href=?]", user_path(@non_activated_user), count: 0
    get user_path(@non_activated_user)
    assert_redirected_to root_url
  end

おわりに

アカウントの有効化を実装しました。
普段サービスを利用する際にユーザー登録をすると届くメールはこんなふうに作られているんだ、と楽しみながら実装ができました。
今回コールバックでは、はてなが浮かんで納得しきれない部分があったのでコールバックの内容が多めです笑

Railsチュートリアル第10章

はじめに
Ruby on Railsチュートリアル(第6版)のメモ、演習の解答例を記述した記事です。
解答は個人のものなので、誤りがあればご指摘ください。
開発環境  Ruby: 2.7.2 , Rails: 6.1.4

メモ

パスワードが空のままでもユーザー情報が編集できるようにする

パスワードのvalidatesにallow_nil: trueを追加する。

ユーザーの新規作成時にはhas_secure_passwordがpasswordとpassword_confirmationの存在性を検証するようになっているため、ユーザーの新規作成時に空のパスワードが有効になることはない。

認可と認証

認証(authentication):サイトのユーザーを識別すること
認可(authorization) :ユーザーが実行可能な操作を管理すること

セキュリティモデル

ログインしたユーザーのみがユーザの情報を変更できるようにする、ログインしたユーザーのみが見ることができるページを設定するような仕組み

Railsではbeforeフィルターを用いて実現する。

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

ログイン成功時に元々行きたかったページに転送させる機能

ログインをせずに保護されたページを開いた場合ログイン画面に移動させられ、
移動させられたログイン画面でユーザーがログインすると保護された開きたかったページに転送する仕組み

実装の流れ

ログインしていないユーザーが保護されたページにGETリクエストを送った場合
session変数を利用して開きたかったページの情報を保存しておく

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

実際にログインした後にリダイレクトさせるには

 redirect_to session[:forwarding_url]

リダイレクトした後に保存しておいた情報を削除することを忘れずに
削除しないと次回ログインした時に保存されたページに転送されてしまう。

session.delete(:forwarding_url)

ページネーション

ひとつのページに決められたユーザー数だけを表示するような仕組み

実装の流れ

gemファイルをインストール
 will_paginate gem
 bootstrap-will_paginate gem
表示させたいオブジェトをページネーションが理解できるオブジェクトに置き換える

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

ビューにwill_paginateメソッドを追加する

サンプルユーザーの生成

データベースにサンプルデータを生成する
db/seeds.rbファイルに記述。

# メインのサンプルユーザーを1人作成する
User.create!(name:  "Example User",
             email: "example@railstutorial.org",
             password:              "foobar",
             password_confirmation: "foobar")

# 追加のユーザーをまとめて生成する
99.times do |n|
  name  = Faker::Name.name
  email = "example-#{n+1}@railstutorial.org"
  password = "password"
  User.create!(name:  name,
               email: email,
               password:              password,
               password_confirmation: password)
end

create!はcreateアクションと同じ動きをするがcreate!では無効なデータを生成する場合例外を発生させる。

サンプルデータを生成

$ rails db:seed

role

あるユーザーだけにある処理を実行できる権限を与えること

DELETEリクエストの偽造

ブラウザはネイティブでDELETEリクエストを送信できない
RailsではJavaScriptを使ってDELETEリクエストを偽造する
よって、JavaScriptがオフになっているとDELETEのリンクが無効になる
このような場合フォームとPOSTリクエストを使ってDELETEリクエストを偽造することができる

演習

10.1.1

演習1

先ほど触れたように、target="_blank"で新しいページを開くときには、セキュリティ上の小さな問題があります。それは、リンク先のサイトがHTMLドキュメントのwindowオブジェクトを扱えてしまう、という点です。具体的には、フィッシング(Phising)サイトのような、悪意のあるコンテンツを導入させられてしまう可能性があります。Gravatarのような著名なサイトではこのような事態は起こらないと思いますが、念のため、このセキュリティ上のリスクも排除しておきましょう。対処方法は、リンク用のaタグのrel(relationship)属性に、"noopener"と設定するだけです。早速、リスト 10.2で使ったGravatarの編集ページへのリンクにこの設定をしてみましょう。

    <div class="gravatar_edit">
      <%= gravatar_for(@user) %>
      <a href="https://gravatar.com/emails" target="_blank", rel="noopener">change</a>
    </div>

10.1.3

演習1
  test "unsuccessful edit" do
    get edit_user_path(@user)
    assert_template 'users/edit'
    patch user_path(@user), params: { user: { name: "", email: "foo@invalid", password: "foo", password_confirmation: "bar" } }
    assert_template 'users/edit'
    assert_select "div.alert-danger", "The form contains 4 errors"
  end

10.2.2

演習1

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

自分以外のユーザーの情報を悪意のあるユーザーに見られる、あるいは書き換えられる危険性があるから。

演習2

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

editアクション
URLのID部の値を書き換えるだけで他のユーザーの編集画面を開くことができるから。

10.2.3

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

10.3.1

演習1
レイアウトにあるすべてのリンクに対して統合テストを書いてみましょう。ログイン済みユーザーとそうでないユーザーのそれぞれに対して、正しい振る舞いを考えてください。ヒント: log_in_asヘルパーを使ってリスト 5.32にテストを追加してみましょう。

  test "layout links when logged in user" do
    log_in_as(@user)
    get root_path
    assert_template "static_pages/home"
    assert_select "a[href=?]", root_path, count: 2
    assert_select "a[href=?]", help_path
    assert_select "a[href=?]", about_path
    assert_select "a[href=?]", contact_path
    assert_select "a[href=?]", users_path
    assert_select "a[href=?]", user_path(@user)
    assert_select "a[href=?]", edit_user_path(@user)
    assert_select "a[href=?]", logout_path
    assert_select "a[href=?]", login_path, count: 0
  end

10.3.4

演習2
先ほどは2つともコメントアウトしましたが、1つだけコメントアウトした場合、テストが green のままであることを確認してみましょう。will_paginateのリンクが2つとも存在していることをテストしたい場合は、どのようなテストを追加すれば良いでしょうか? ヒント: 表 5.2を参考にして、数をカウントするテストを追加してみましょう。

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

10.4.1.2

演習1

Web経由でadmin属性を変更できないことを確認してみましょう。具体的には、リスト 10.56に示したように、PATCHを直接ユーザーのURL(/users/:id)に送信するテストを作成してみてください。テストが正しい振る舞いをしているかどうか確信を得るために、まずはadminをuser_paramsメソッド内の許可されたパラメータ一覧に追加するところから始めてみましょう。最初のテストの結果は red になるはずです。最後の行では、更新済みのユーザー情報をデータベースから読み込めることを確認します( 6.1.5)。

  test "should not allow the admin attribute to be edited via the web" do
    log_in_as(@other_user)
    assert_not @other_user.admin?
    patch user_path(@user), params: { user: { password: "password",
      password_digest: "password", admin: true } }
    assert_not @other_user.reload.admin?
  end

おわりに

この章は前章に比べて理解しやすかったです。
9章が鬼門なのかな、、