- 投稿日:2021-03-02T23:42:39+09:00
デバックツール pry-beybugの導入
はじめに
変数を確認したり、メソッドを呼び出す為の、デバックツールの導入方法のメモになります。
Gemfileに記入する
Gemfile 記入前
group :development do # Access an IRB console on exception pages or by using <%= console %> anywhere in the code. gem 'web-console' gem 'listen', '~> 3.0.5' # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring gem 'spring' gem 'spring-watcher-listen', '~> 2.0.0' endGemfile 記入後
group :development do # Access an IRB console on exception pages or by using <%= console %> anywhere in the code. gem 'web-console' gem 'listen', '~> 3.0.5' # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring gem 'spring' gem 'spring-watcher-listen', '~> 2.0.0' gem 'pry-byebug' enddocker-compose buildを行う
docker-compose up -d
docker-compose psでコンテナを確認する
Name Command State Ports ----------------------------------------------------------------------------- rails_db_1 docker-entrypoint.sh mysqld Up 3306/tcp, 33060/tcp rails_web_1 bundle exec rails s -p 300 ... Up 0.0.0.0:3000->3000/tcprailsサーバーにアタッチを行う
docker attach rails_web_1
あとはデバックをしたい部分にbindng.pryを書くだけ。
デバッカーから抜けたい場合はexitかcontinueで抜ける。
アタッチを外したい場合はCtrlキーを押しながら、pキー、qキーの順に押す。
- 投稿日:2021-03-02T23:24:42+09:00
webpackとは?
webpackとは
- Webアプリケーションを作成する際に必要な、
様々なJavaScriptをひとまとめに管理する
為のツール- モダンなWebアプリケーション開発では、
様々なJavaScriptライブラリを用いる為、その依存関係を管理
してくれる以前取り上げた機械語と人間語とも関連しています。
webpackの公式ドキュメントこちらも参考として添付します。
webpackが行うこと
基本要素 読み 役割 Entry エントリー 依存関係を解決するために、
どのファイルを基準(エントリーポイント)とするかを決めるOutput アウトプット エントリーポイントにされ、
webpackによってまとめられたファイルを、
どこへどのような名前で出力(アウトプット)するのか指定するLoaders ローダー JavaScript以外のCSSやHTMLなどのファイルを
モジュールに変換する方法を読み込み(ロード)、指定した処理を行うPlugins プラグイン 圧縮などの、ファイルをまとめる以外で
ローダーが実行できないタスクを導入し、拡張(プラグイン)するwebpackを用いることで、JavaScriptのライブラリとJavaScript以外の
さまざまな言語を、変換・圧縮した上で、好きな場所に配置することが可能
WebpackerというGem
Railsにも
webpack
を導入してコマンドによる操作が可能だが、
設定ファイルの記述がやや難しくなっている
そのため、設定を簡易化してくれるWebpackerというGem
を使用Webpacker
webpack
をRails仕様にし、専用の設定ファイルやヘルパーメソッドを用意してくれるGemRailsバージョン6系以降からは、デフォルトで
Webpacker
が導入される近年のフロントエンド技術の台頭により、
(主には、JavaScriptのライブラリが充実してきたこと)
Sprocketsからwebpack
を利用する方針へ転換された
Webpacker
によって、
Sprocketsのアセットパイプラインと同じような静的ファイルのプリコンパイルに加え、
JavaScriptのパッケージが利用できるようになる
- 投稿日:2021-03-02T23:07:50+09:00
[Rails] 変数の中身が空か確認するメソッド
Railsで
view
model
controller
でも変数の中身が空か確認したいときに以下のメソッドを使います。nilメソッド
nil?
オブジェクトがnilの場合のみtrue
を返し、それ以外はfalse
を返します。空文字や配列の器、ハッシュの器だけでも全てfalseを返します。emptyメソッド
empty?
String(文字列)
、Array(配列)
、Hash(連想配列)
定義されているメソッドです。空の文字列や空の配列の場合にtrueを返します。nilや真偽値オブジェクトに対して呼び出すとNoMethodError
が発生するので、気をつけましょう。blankメソッド
blank?
blankメソッドを使用したオブジェクトが空白の場合はtrue
を返し、空白ではない場合はfalse
を返すメソッドです。〜が存在しないとき
の条件分岐でよく使います。presentメソッド
present?
blankメソッドの逆(!blank?
)で、オブジェクトが空白ではない場合にtrue
を返し、空白の場合はfalse
を返します。〜が存在するとき
の条件分岐でよく使います。
if hoge.present?
とunless hoge.blank?
は同じ意味になりますが、前者を使った方がわかりやすいです。
- 投稿日:2021-03-02T23:01:17+09:00
dockerでgemを追加したときにbundle installではなくbuildする
備忘録的な感じです
題名にある通りDockerでrails開発をしていてgemを追加した後
$ docker-compose run --rm (コンテナ名) bundle installを試したらgemが更新されずエラーになる
$ docker-compose buildでエラーも出ず解決した。
色々調べていたらdockerfileの
DokerfileFROM ruby:2.6.6 RUN apt-get update -qq && apt-get install -y nodejs postgresql-client RUN mkdir /rails-qanda WORKDIR /rails-qanda COPY Gemfile /rails-qanda/Gemfile COPY Gemfile.lock /rails-qanda/Gemfile.lock RUN bundle install COPY . /rails-qandaのCOPY Gemfile /rails-qanda/Gemfileが
[COPY] ローカルのファイルをコンテナへコピー
という意味らしいのでgemを追加したらここの部分やり直すためにbuildし直すということなのかな。また理解が深まり次第追記予定
docker難しい、、
- 投稿日:2021-03-02T22:42:18+09:00
あとからカラム属性にインデックスを追加する
インデックス(索引)
インデックスとは検索やソートを高速化するためのデータ構造の事です。
むやみにインデックスを貼るのはデータベースリソースを余分に使うため、性能が向上するかどうかを見極めてから作成する必要があります。モデル作成とadd_indexを同時に試みるも、
PG::UndefinedTable: ERROR: relation "care_recipitents" does not existエラーを吐かれてしまったので先にモデルを作成しました。
migrationファイルを作成します。
rails g migration add_index_to_carerexipidentsclass AddIndexToCarerexipidents < ActiveRecord::Migration[6.0] def change add_index :care_recipitents, [ :family_name_kana, :given_name_kana ] end end複合インデックスを作成しています。
rails db:migrateschema.rb
t.index ["family_name_kana", "given_name_kana"], name: "index_care_recipitents_on_family_name_kana_and_given_name_kana"ちゃんと貼られています。
- 投稿日:2021-03-02T22:12:35+09:00
【ShouldaMatchers】複数カラムへの重複チェックで発生したエラーの原因と対応方法
事象
RspecでShouldaMatchersを使用した複数カラムの重複チェック実行時、以下のようなDBのエラーが発生しました。
1) Like is expected to validate that :user_id is case-sensitively unique within the scope of :post_id Failure/Error: it { is_expected.to validate_uniqueness_of(:user_id).scoped_to(:post_id) } Shoulda::Matchers::ActiveRecord::ValidateUniquenessOfMatcher::ExistingRecordInvalid: validate_uniqueness_of works by matching a new record against an existing record. If there is no existing record, it will create one using the record you provide. While doing this, the following error was raised: Mysql2::Error: Field 'user_id' doesn't have a default value The best way to fix this is to provide the matcher with a record where any required attributes are filled in with valid values beforehand. # ./spec/models/like_spec.rb:8:in `block (2 levels) in <main>' # -e:1:in `<main>' # ------------------ # --- Caused by: --- # Mysql2::Error: # Field 'user_id' doesn't have a default value # ./spec/models/like_spec.rb:8:in `block (2 levels) in <main>'※投稿に対してのいいね機能のテストとして、
User
とPost
を保持するLike
モデルを作成していました。spec/models/like_spec.rbRSpec.describe Like, type: :model do it { is_expected.to validate_presence_of(:user_id) } it { should belong_to(:user) } it { is_expected.to validate_uniqueness_of(:user_id).scoped_to(:post_id) } # エラー発生 it { is_expected.to validate_presence_of(:post_id) } it { should belong_to(:post) } endapp/models/like.rbclass Like < ApplicationRecord belongs_to :user belongs_to :post validates :user_id, presence: true, uniqueness: { scope: :post_id } validates :post_id, presence: true end原因
uniqueness
に対してテストをするvalidate_uniqueness_of
は、インスタンスが存在しない場合は作成してテスト実行するため、その他のカラムのDBの制約によってはDBエラーが発生するようです。
今回の場合だと、user_idに対してnullを許容しないケースとなっていたため、nullのデータが作成されてしまい、エラーが発生しました。対応
spec/models/like_spec.rb
に、インスタンスを事前に作成するような記述を追加することで、テストが問題なく完了しました。
ShouldaMathcersを使用したモデルのテストではインスタンス等不要だと思っていましたが、作成が必要なケースがあることも認識しておく必要があります。spec/models/like_spec.rb(修正後)RSpec.describe Like, type: :model do subject { FactoryBot.build(:like) } # 追加 it { is_expected.to validate_presence_of(:user_id) } it { should belong_to(:user) } it { is_expected.to validate_uniqueness_of(:user_id).scoped_to(:post_id) } it { is_expected.to validate_presence_of(:post_id) } it { should belong_to(:post) } end参考
・モジュール:Shoulda :: Matchers :: ActiveRecord — YARD0.8.7.3によるドキュメント
- 投稿日:2021-03-02T21:58:01+09:00
railsのフォロー機能コードベタ貼りメモ
かなり自分用。アウトプットしないと忘れそうなので記録。分かりやすくはない。
db/migrate/relationships.rbdef change create_table :relationships do |t| t.integer :follower_id t.integer :followed_id t.timestamps endmodel/relationships.rbbelongs_to :follower, class_name: "User" belongs_to :followed, class_name: "User"model/user.rb#userテーブル→relationテーブル has_many :relationships, foreign_key: "follower_id", dependent: :destroy #relationテーブル→followedテーブル、followed_idを持って来る has_many :followings, through: :relationships, source: :followed #followerテーブル(本当はuser)→re_relationテーブル(本当relation)へfollowed_idを送る(参照) has_many :reverse_of_relationships, class_name: "Relationship", foreign_key: "followed_id", dependent: :destroy #re_relationテーブル→userテーブルへfollower_idを持って来る has_many :followers, through: :reverse_of_relationships, source: :follower def follow(user_id) relationships.create(followed_id: user_id) end def unfollow(user_id) relationships.find_by(followed_id: user_id).destroy end def following?(user) followings.include?(user) endroutes.rbresources :users, only: [:show,:index,:edit,:update] do resources :relationships, only: [:create, :destroy] do member do get 'following' => 'relationships#followings', as: 'followings' get 'followers' => 'relationships#followers', as: 'followers' end end end #menberよりcollectionのほうがいかもrelationships_controller.rbdef create current_user.follow(params[:user_id]) redirect_to request.referer end def destroy current_user.unfollow(params[:user_id]) redirect_to request.referer endview/users/どこか<% users.each do |user| %> <tr> <td> フォロー数:<%= user.followings.count %> </td> <td> フォロワー数:<%= user.followers.count %> </td> <td> <% unless user == current_user %> <% if current_user.following?(user) %> <%= link_to 'unfollow', user_relationship_path(user.id, current_user.id), method: :delete %> <% else %> <%= link_to 'follow', user_relationships_path(user.id), method: :post %> <% end %> <% end %> </td> </tr> <% end %> </tbody>説明
1行目のrelationshipsはフォローした時にそのカラムを新規保存するときに使う。
relationship = relationships.new(followed_id: user_id) relationship.saveイメージはこんな感じ
2行目のfollowingsは@user = User.find(params[:id])
で特定のuser_idを抽としたときに@user.followings
で@userのフォロワー一覧が出せる。
1、2行目はusersテーブル→relationshipsテーブル→usersテーブルのフォローする人の一連の流れを表している
3、4行目はusersテーブル→relationshipsテーブル→usersテーブルのフォローされる人の一連の流れを表している1行目はUserテーブルからrelationshipsテーブルに値を受け渡すための文。
例えばUser.find(1).relationshipsの記載だとUserid1なのでrelationshipsのfollower_idが1のrelationshipsのidが返される。2行目がカラム→Viewにカラムを渡す時の文。
例えばUserのid1の人のフォロー『されてる』人を参照したいときはUser.followingsとやれば「follower_idが1」のfollowed_idを全部持ってきて!となり、User_id1の人のフォローされてる人をviewに持ってこれる自分メモ
followers, through: :reverse_of_relationships, source: :followed
は
followersからreverse_of_relationshipsを通ってfollowedを持って来るという意味。sourceは持って来る側だから定義しているものの逆を書くイメージ。私は逆で認識してた。あとuserに対してfollowingsとやると沢山のfollowed_idが現れること。多対多ということを忘れず!!
- 投稿日:2021-03-02T21:37:37+09:00
【Railsチュートリアル】第11章 アカウントの有効化②
11.3 アカウントを有効化する
AccountActivationsコントローラのeditアクションを書いていく。アクションへのテストを書き、しっかりとテストできていることが確認できたら、AccountActivationsコントローラからUserモデルにコードを移していく作業(リファクタリング)にも取り掛かかる。
11.3.1 authenticated?メソッドの抽象化
sendメソッドを使うと共通化できる。
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" # 式展開を使って渡す演習 1
コンソール内で新しいユーザーを作成してみてください。新しいユーザーの記憶トークンと有効化トークンはどのような値になっているでしょうか? また、各トークンに対応するダイジェストの値はどうなっているでしょうか?
>> user = User.create(name: "sample", email: "sample@email.com", password: "sample", password_confirmation: "sample") (4.2ms) SELECT sqlite_version(*) (0.1ms) begin transaction User Exists? (1.8ms) SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) LIMIT ? [["email", "sample@email.com"], ["LIMIT", 1]] User Create (8.4ms) INSERT INTO "users" ("name", "email", "created_at", "updated_at", "password_digest", "activation_digest") VALUES (?, ?, ?, ?, ?, ?) [["name", "sample"], ["email", "sample@email.com"], ["created_at", "2021-03-02 04:11:18.445221"], ["updated_at", "2021-03-02 04:11:18.445221"], ["password_digest", "$2a$12$.NT19iae7r0WhFR2OcikiemF5WD3QvT0aA4/LJUSZ9UHYOt4XB0T."], ["activation_digest", "$2a$12$jKN4cC3pqjMUIKs4/EvhIOOza0GueGUAKzsGgaMLixzoGHv/Moo5W"]] (5.5ms) commit transaction => #<User id: 102, name: "sample", email: "sample@email.com", created_at: "2021-03-02 04:11:18", updated_at: "2021-03-02 04:11:18", password_digest: [FILTERED], remember_digest: nil, admin: nil, activation_digest: "$2a$12$jKN4cC3pqjMUIKs4/EvhIOOza0GueGUAKzsGgaMLixz...", activated: false, activated_at: nil>演習 2
リスト 11.26で抽象化したauthenticated?メソッドを使って、先ほどの各トークン/ダイジェストの組み合わせで認証が成功することを確認してみましょう。
>> user.remember_token => nil >> user.remember_digest => nil >> user.activation_token => "DQKQr9KfehJzT6PzsjrEdQ" >> user.activation_digest => "$2a$12$jKN4cC3pqjMUIKs4/EvhIOOza0GueGUAKzsGgaMLixzoGHv/Moo5W"11.3.2 editアクションで有効化
editアクションを書いていく。
# GET /account_acrivations/:id/edit user = User.find_by(email: params[:email]) # emailを元にuserを探して変数userに代入 if user && !user.activated? && user.authenticated? (:activation, params[:id]) # 左: nilかどうかを確認 # 中: userがactivatedされていないかを確認 # 右: :activation, params[:id]の2つで認証する user.update_attribute(:activated, true) user.update_attribute(:activated_at, Time.zone.now) log_in user # ログイン flash[:success] = "Account activated!" redirect_to user # プロフィールページにアクセス else flash[:danger] = "Invalid activation link" redirect_to root_url end end演習 1
コンソールから、11.2.4で生成したメールに含まれているURLを調べてみてください。URL内のどこに有効化トークンが含まれているでしょうか?
----==_mimepart_603dc222a8f1c_14022ad7ea9149d8402a2 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 7bit Hi test, Welcome to the Sample App! Click on the link below to activate your account: http://localhost:3000/account_activations/exKxGElXRHOQ6VyC0ia4MA/edit?email=test%40example.comこの部分がトークン:
account_activations/exKxGElXRHOQ6VyC0ia4MA/
演習 2
先ほど見つけたURLをブラウザに貼り付けて、そのユーザーの認証に成功し、有効化できることを確認してみましょう。また、有効化ステータスがtrueになっていることをコンソールから確認してみてください。
>> user = User.find_by(name: "test") User Load (3.3ms) SELECT "users".* FROM "users" WHERE "users"."name" = ? LIMIT ? [["name", "test"], ["LIMIT", 1]] => #<User id: 103, name: "test", email: "test@example.com", created_at: "2021-03-02 04:42:10", updated_at: "2021-03-02 04:45:05", password_digest: [FILTERED], remember_digest: nil, admin: nil, activation_digest: "$2a$12$MmJY.TpGghnO.LdaF6GSPO8xQUWDIPzzl0L3ISc5FYU...", activated: true, activated_at: "2021-03-02 04:45:05"> >> user.activated => true11.3.3 有効化のテストとリファクタリング
アカウント有効化の統合テストを追加する。
test/integration/users_signup_test.rbtest "valid signup information with account activation" do get signup_path # signup_pathにアクセスする assert_difference 'User.count', 1 do # ユーザー数が1つ増えているか確認。増えたユーザーの情報は以下。 post users_path, params: { user: { name: "Example User", email: "user@example.com", password: "password", password_confirmation: "password" } } end assert_equal 1, ActionMailer::Base.deliveries.size # メールを1つ送っているか確認 user = assigns(:user) # バリデーションされていない@userにアクセスする assert_not user.activated? # userが有効化されていないか確認 log_in_as(user) # 有効化していない状態でログインしてみる assert_not is_logged_in? # ログインできなかったらtrue get edit_account_activation_path("invalid token", email: user.email) # edit_account_activation_pathにトークンとメールアドレスを渡してアクセス assert_not is_logged_in? # ログインできなかったらtrue get edit_account_activation_path(user.activation_token, email: 'wrong') # トークンは正しいが、メールアドレスが無効の場合でアクセス assert_not is_logged_in? # ログインできなかったらtrue get edit_account_activation_path(user.activation_token, email: user.email) # トークンもメールアドレスも有効の場合でアクセス assert user.reload.activated? # ユーザーが更新されたらtrue follow_redirect! # リダイレクトされる assert_template 'users/show' # users/showを描画しているか確認 assert is_logged_in? # ログインできていたらtrue end演習 1
リスト 11.35にあるactivateメソッドはupdate_attributeを2回呼び出していますが、これは各行で1回ずつデータベースへ問い合わせしていることになります。リスト 11.39に記したテンプレートを使って、update_attributeの呼び出しを1回のupdate_columns呼び出しにまとめてみましょう。これでデータベースへの問い合わせが1回で済むようになります(注意!update_columnsは、モデルのコールバックやバリデーションが実行されない点がupdate_attributeと異なります)。また、変更後にテストを実行し、 green になることも確認してください。
app/models/user.rb# アカウントを有効にする 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でもう少し詳しく説明します。
app/controllers/users_controller.rbdef 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? end11.4 本番環境でのメール送信
サンプルアプリケーションの設定を変更し、production環境で実際にメールを送信できるようにする。
演習 1
演習 2
$ heroku push
時にPrecompiling assets failed.
が出て、解決できなかったので後に回します。エラーメモ
たぶんここで引っかかってるけど、調べてもよくわからない。
$ git push heroku . . . remote: SyntaxError: /tmp/build_bb915e0f/config/environments/production.rb:84: syntax error, unexpected '\n', expecting => . . . remote: ! remote: ! Precompiling assets failed. remote: ! remote: ! Push rejected, failed to compile Ruby app. remote: remote: ! Push failed . . .さいごに
メール認証難しかったです!
- 投稿日:2021-03-02T21:19:31+09:00
Railsチュートリアル 第6版 振り返り 第5章
はじめに
このまとめ記事はRailsチュートリアルを1周終えた私が1周目で分からなかった所や記憶に残したい箇所のみをピックアップして記述しています。完全解説記事ではないので注意して下さい。
私と同じく2周目の方、たまに復習したいなと振り返りを行う方等におすすめです。
ナビゲーション(5.1.1)
この章ではレイアウトを作成していく。
その第一段階として、
application.html.erb
にHTML構造を追加して、レイアウトファイルの更新を行う。app/views/layouts/application.html.erb<!DOCTYPE html> <html> <head> <title><%= full_title(yield(:title)) %></title> <%= csrf_meta_tags %> <%= csp_meta_tag %> <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %> <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %> <!--[if lt IE 9]> <script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/r29/html5.min.js"> </script> <![endif]--> </head> <body> <header class="navbar navbar-fixed-top navbar-inverse"> <div class="container"> <%= link_to "sample app", '#', id: "logo" %> <nav> <ul class="nav navbar-nav navbar-right"> <li><%= link_to "Home", '#' %></li> <li><%= link_to "Help", '#' %></li> <li><%= link_to "Log in", '#' %></li> </ul> </nav> </div> </header> <div class="container"> <%= yield %> </div> </body> </html>このコードの中から重要なコードについて順番に確認していく。
<!--[if lt IE 9]> <script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/r29/html5.min.js"> </script> <![endif]-->このコードは、HTML5のサポートが不完全である、Microsoft Internet Explorer(IE)のバージョンが9より小さい場合に実行されるコード。
これによってバージョンの低いIEでもサポートされる様になる。このコードはJavaScriptのコードであり、他のブラウザに影響を与えずにIEにのみ実行することが出来るため便利。
link to
<nav> <ul class="nav navbar-nav navbar-right"> <li><%= link_to "Home", '#' %></li> <li><%= link_to "Help", '#' %></li> <li><%= link_to "Log in", '#' %></li> </ul> </nav>Railsヘルパーの
link to
は、リンクを生成する。第一引数に
リンクテキスト
、第二引数にURL
を指定する。
なお第三引数としてオプションハッシュ(CSSのidなど)
の指定も可能。そして
link to
は、ブラウザからソースを確認するとRailsが埋め込みRubyを評価して、コードは以下の様に置き換わる。<nav> <ul class="nav navbar-nav navbar-right"> <li><a href="#">Home</a></li> <li><a href="#">Help</a></li> <li><a href="#">Log in</a></li> </ul> </nav>homeページのレイアウト
<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!", '#', class: "btn btn-lg btn-primary" %> </div> <%= link_to image_tag("rails.svg", alt: "Rails logo", width: "200px"), "https://rubyonrails.org/" %>homeページのHTMLも書き換え。
link to
が2回出てくる。特に2回目のlink to
に注目。<%= link_to image_tag("rails.svg", alt: "Rails logo", width: "200px"), "https://rubyonrails.org/" %>この
link to
にはimage_tag
ヘルパーが使用されている。
image tag
は画像ファイルのパスとオプションハッシュを引数に指定できる。このヘルパーのおかげで、Railsは該当の画像ファイルをアセットパイプラインを通して
app/assets/images/
ディレクトリの中から探してくれる。
image_tag
はブラウザから生成されたHTMLでは以下の様に表示される。<img alt="Rails logo" width="200px" src="/assets/rails-<long string>.svg">
<long string>
は、例えば画像ファイルを新しい画像に更新したときに、ブラウザ内に保存されたキャッシュに意図的にヒットさせないようにするための仕組み。また、
src
属性にはimage
ディレクトリがない。これは、高速化のための仕組み。Railsは
assets
ディレクトリ直下の画像をapp/assets/images
ディレクトリにある画像と紐付けている。これにより、ブラウザから見るとすべてのファイルが同じディレクトリにあるように見えるようになる。
このようなフラットなディレクトリ構成を採っていると、ファイルをより高速にブラウザに渡すことができるようになる。
BootstrapとカスタムCSS(5.1.2)
ここまでで多くのHTML要素にCSSクラスを関連付けた。
このクラスはBootstrap特有のクラス。Bootstrapの注目すべき点は、Bootstrapを使うことでアプリケーションをレスポンシブデザインにできるということ。
カスタムCSSルールとBootstrapを組み合わせて使用する。
rubygem 'bootstrap-sass', '3.4.1'Gemfileに追加してインストール。
rails g
コマンドを実行することでコントローラーごとに分けられたCSSファイルが自動的に生成されるが、これらのファイルを正しい順序で読み込ませるのは至難の技らしいので、すべてのCSSを1つにまとめる。$ touch app/assets/stylesheets/custom.scss上記のようにカスタムCSSの用のファイルを作成したら、@importでBootstrapを読み込む。
app/assets/stylesheets/custom.scss@import "bootstrap-sprockets"; @import "bootstrap"; #Bootstrapのフレームワークこの時点で結構良いデザインが出来上がってる。
すごいね〜。しかもこれでレスポンシブデザイン対応とか神すぎる。
んでこの後さまざまなスタイルを追加。
app/assets/stylesheets/custom.scss/* universal 共通のデザイン*/ body { padding-top: 60px; } section { overflow: auto; } textarea { resize: vertical; } .center { text-align: center; } .center h1 { margin-bottom: 10px; } /* typography 共通の文字設定など*/ h1, h2, h3, h4, h5, h6 { line-height: 1; } h1 { font-size: 3em; letter-spacing: -2px; margin-bottom: 30px; text-align: center; } h2 { font-size: 1.2em; letter-spacing: -1px; margin-bottom: 30px; text-align: center; font-weight: normal; color: #777; } p { font-size: 1.1em; line-height: 1.7em; } /* header ロゴ編集*/ #logo { float: left; margin-right: 10px; font-size: 1.7em; color: #fff; text-transform: uppercase; letter-spacing: -1px; padding-top: 9px; font-weight: bold; } #logo:hover { color: #fff; text-decoration: none; }パーシャル(5.1.3)
ここまでで大幅にレイアウトを変更することができたが、まだ少々散らかっているらしい。
例えば、IE特有の風変わりな文法のHTML shimだけで3行も占有しているし、HTMLヘッダーは論理的な単位として分けられるため、一箇所にまとめた方が便利。
Railsではパーシャル(partial)という機能でこの問題を解決することが出来る。
app/views/layouts/application.html.erb<!DOCTYPE html> <html> <head> <title><%= full_title(yield(:title)) %></title> <%= csrf_meta_tags %> <%= csp_meta_tag %> <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %> <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %> <%= render 'layouts/shim' %> </head> <body> <%= render 'layouts/header' %> <div class="container"> <%= yield %> </div> </body> </html>
render
と呼ばれるRailsヘルパー呼び出しだけを使って、HTML shimのスタイルシート行を置換している。パーシャルは、ファイル名の先頭にアンダースコアをつけて命名するというルールがある。
このルールに従って、パーシャルを作成していく。
作成場所はapp/views/layouts/
ディレクトリ内。アセットパイプライン(5.2.1)
今まで多くの場面で出てきたこの用語。ようやく詳しく説明してくれます。
Rails開発者視点ではアセットパイプラインの
アセットディレクトリ
マニフェストファイル
プリプロセッサエンジン
以上の3つの主要な機能が理解の対象となるみたい。
1単語ずつ詳しく確認。
アセットディレクトリ
Railsのアセットパイプラインでは、静的ファイルを目的別に分類する、標準的な3つのディレクトリが使用されている。
app/assets
: 現在のアプリケーション固有のアセット。
lib/assets
: 自らの開発チームによって作成されたライブラリ用のアセット。
vendor/assets
: サードパーティのアセット(デフォルトでは存在していない)これらのディレクトリにはアセットクラス用のサブディレクトリがある。
例えば
app/assets
の場合は$ ls app/assets/ config images stylesheetsこの3つのディレクトリがある。
以前作ったカスタムcss
custom.scss
がapp/assets/stylesheets
に配置されたのは、サンプルアプリケーション固有のアセットだったから。マニフェストファイル
アセットをアセットディレクトリで紹介した場所にそれぞれ配置すると、マニフェストファイルを使って、それらをどのように1つのファイルにまとめるのかをRailsに指示することができる。
実際にアセットをまとめる処理を行うのはSprocketsというgem。
また、マニフェストファイルはCSSとJavaScriptには適用されますが、画像ファイルには適用されない。
今回は具体例として、アプリケーションのCSS用マニフェストファイルを確認する。
app/assets/stylesheets/application.css/* * This is a manifest file that'll be compiled into application.css, which will * include all the files listed below. * * Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any * plugin's vendor/assets/stylesheets directory can be referenced here using a * relative path. * * You're free to add application-wide styles to this file and they'll appear at * the bottom of the compiled file so the styles you add here take precedence * over styles defined in any other CSS/SCSS files in this directory. Styles in * this file should be added after the last require_* statement. * It is generally better to create a new file per style scope. * *= require_tree . *= require_self */ここで重要なのが最後の方の数行。
*= require_tree . *= require_self */この部分は、Sprocketsが適切なファイルを読み込むために使われる。
そして
*= require_tree .
は、app/assets/stylesheets
ディレクトリ(サブディレクトリを含む)中のすべてのCSSファイルが、アプリケーションCSSに含まれるようにしている。次に
*= require_self
は、CSSの読み込みシーケンスの中で、application.css自身もその対象に含めている。プリプロセッサエンジン
必要なアセットをディレクトリに配置してまとめた後、Railsはさまざまなプリプロセッサエンジンを介してそれらを実行し、ブラウザに配信できるようにそれらをマニフェストファイルを用いて結合し、サイトテンプレート用に準備する。
Railsはどのプリプロセッサを使うのかを、ファイル名の拡張子を使って判断している。
Sass用の.
scss
CoffeeScript用の
.coffee
埋め込みRuby(ERb)用の
.erb
この3つが最も一般的な拡張子。
また、プリプロセッサエンジンは、繋げて実行する(chain)ことができる。
foobar.js.erb.coffee上の拡張子の場合は、CoffeeScriptとERbの両方で実行される。
コードは、右から左へと実行されるため、この例ではCoffeeScriptが最初に実行される。本番環境での効率性
アセットパイプラインの最大のメリットの1つは、本番のアプリケーションで効率的になるように最適化されたアセットも自動的に生成されること。
従来は、CSSとJavaScriptを整理するために、機能を個別のファイルに分割し、読みやすいフォーマットに整えていた。
この行為はプログラマにとっては便利な方法だが、、最小化されていないCSSファイルを多数に分割すると、ページの読み込み時間が著しく遅くなってしまう問題が起きる。
しかし、アセットパイプラインを使うと開発環境ではプログラマにとって読みやすいように整理しておき、本番環境ではAsset Pipelineを使ってファイルを最小化すればこの問題は解決できる。
具体的には、Asset Pipelineがすべてのスタイルシートを1つのCSSファイル(application.css)にまとめてくれた。
実は、それらのファイルすべてに対して 不要な空白やインデントを取り除く処理を行い、ファイルサイズを最小化してくれてもいたのだ。
結果として、開発環境と本番環境という、2つの異なった状況に対してAsset Pipelineはそれぞれ最高の環境を提供してくれる。
素晴らしい構文を備えたスタイルシート(5.2.2)
Sassは、スタイルシートを記述するための言語であり、CSSに比べて多くの点が強化されている。
SassはSCSSというフォーマットに対応している。
しかし、SCSSはCSSに新しい機能を追加しただけで、全く新しい構文を定義したようなものではない。ネスト
.center { text-align: center; } .center h1 { margin-bottom: 10px; }通常のCSSでは、セレクタを指定しなければCSSを適用できないが、
.center { text-align: center; h1 { margin-bottom: 10px; } }このようにネスト構造にして書き換えることができる。
もう1つ属性を使った例も確認。
#logo { float: left; margin-right: 10px; font-size: 1.7em; color: #fff; text-transform: uppercase; letter-spacing: -1px; padding-top: 9px; font-weight: bold; } #logo:hover { color: #fff; text-decoration: none; }cssでは
#logo
を2回使用しなければいけないが、#logo { float: left; margin-right: 10px; font-size: 1.7em; color: #fff; text-transform: uppercase; letter-spacing: -1px; padding-top: 9px; font-weight: bold; &:hover { color: #fff; text-decoration: none; } }Scssでは
&
アンパーサンドで表現ができる。なお、Sassは、SCSSをCSSに変換する際に、
&:hover
を#logo:hover
に置き換えてくれる。変数
Sassでは、重複する箇所を変数を定義して書くことも可能。
(重複してなくても使用可能。)$light-gray: #777;この様に色を定義した値の変数は、
h1 { color: $light-gray; }のように書くことができる。
レイアウトのリンク(5.3)
サイトのレイアウトが仕上がったので今度はリンクを書き換えてみる。
RailsのERbテンプレートには素のHTMLを直接書くことができる。
<a href="/static_pages/about">About</a>しかしこれはRails流ではない。
<%= link_to "About", about_path %>こっちがRails流。
about_path
の様に名前付きルートを指定するのが一般的である。RailsのルートURL(5.3.2)
では実際に名前付きルートをサンプルアプリケーションの静的ページで使うために、ルーティングを編集する。
今まで
root
メソッドを使用して、ルートURL"/"
をコントローラーのアクションに紐付けてきた。このルートURLのようなルーティングを定義することの効果は、ブラウザからアクセスしやすくすることだけではない。
生のURLではなく、名前付きルートを使ってURLを参照することができるようになるのだ。
例えばルートURLを定義すると、
root_path -> '/' #pathはルートURL以下の文字列を返す。 root_url -> 'https://www.example.com/' #urlは完全なURLの文字列を返す。このように、
root_path
やroot_url
メソッドでURLを参照することが出来る。一般的な規約によると、基本的には
_path
、リダイレクトの場合のみ_url
書式を使う。
(HTTPの標準として、リダイレクトのときに完全なURLが要求されるため)名前付きルートを理解したところで、実際に他のルーティングも変更してみる。
get 'static_pages/help'このコードを
get '/help', to: 'static_pages#help'こんな感じで書き換える。
これで
GETリクエスト
が/help
に送信されたときにStaticPagesコントローラー
のhelpアクション
を呼び出してくれるようになる。(この時、
/help
を書き替えてURLを変更することもできる。
例/help
を/abc
にすると/abc
にURLを変更できる。)またこれによって
_path
と_url
といった名前付きルートも使用可能。help_path -> '/help' help_url -> 'https://www.example.com/help'さらに名前付きルートの名前も変更することが出来る。
get '/help', to: 'static_pages#help', as: 'helf'
as:
オプションを使用すると変更可能。名前変更したことによって、
helf_path -> '/help' helf_url -> 'https://www.example.com/help'名前を変更して
_path
と_url
といった名前付きルートも使用可能に。では、次に全てのルーティングを変更!
変更したら今度はテストがエラーになる。
テストを書き換えてみる。
test/controllers/static_pages_controller_test.rbrequire 'test_helper' class StaticPagesControllerTest < ActionDispatch::IntegrationTest test "should get home" do get root_path assert_response :success assert_select "title", "Ruby on Rails Tutorial Sample App" end test "should get help" do get help_path assert_response :success assert_select "title", "Help | Ruby on Rails Tutorial Sample App" end test "should get about" do get about_path assert_response :success assert_select "title", "About | Ruby on Rails Tutorial Sample App" end test "should get contact" do get contact_path assert_response :success assert_select "title", "Contact | Ruby on Rails Tutorial Sample App" end endこのように名前付きパスを使ってテストを書き換えよう。
リンクのテスト(5.3.4)
省略したがこの前の節では、レイアウト内のいくつかのリンクを埋めた。
これらが正しく動いているかを確認するテストを書いていく。
今回は、アプリケーションの動作を端から端まで(end-to-end)シミュレートしてテストすることができる統合テスト(Integration Test)を使う。
rails generate integration_test site_layout #テストテンプレートを作成。今回の目的は、アプリケーションのHTML構造を調べて、レイアウトの各リンクが正しく動くかどうかチェックすること。
①ルートURL(Homeページ)にGETリクエストを送る.
②正しいページテンプレートが描画されているかどうか確かめる.
③Home、Help、About、Contactの各ページへのリンクが正しく動くか確かめる.
Railsの統合テストでは、上のステップをコードに落とし込んでいくことになる。
まずは、
assert_templateメソッド
を使って、Homeページが正しいビューを描画しているかどうか確かめる。test/integration/site_layout_test.rbrequire 'test_helper' class SiteLayoutTest < ActionDispatch::IntegrationTest test "layout links" do 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 end endこんな感じ。これもまた細かく分けて確認していく。
assert_select "a[href=?]", about_path今回のケースでは、特定のリンクが存在するかどうかを、
aタグ
とhref属性
をオプションで指定して調べている。Railsは自動的に
?
の部分をabout_path
に置換している。よってこのコードのは、
<a href="/about">...</a>この様なHTMLがあるかどうかをテストしている。
次はこれ。
assert_select "a[href=?]", root_path, count: 2
homeページ
は、ルートURLへのリンクは2つある。(1つはロゴ、もう1つはナビゲーションバー)このような時は、
count: x
でxに指定した個数、リンクがあるかをテストすることができる。そして最後に結合テストの実行。
rails test:integrationこのコードでテストを起動することができる。
演習
今回の演習は結構大事。
Applicationヘルパー
で使っているfull_titleヘルパー
を、test環境でも使えるようにする問題。test/test_helper.rbclass ActiveSupport::TestCase fixtures :all include ApplicationHelper #テスト環境にアプリケーションヘルパーを読み込み。 . . . endまずはテスト環境に
Applicationヘルパー
を読み込む。test/integration/site_layout_test.rbget contact_path assert_select "title", full_title("Contact")そして先程の結合テストで
full_titleヘルパー
を使ってみる。しかし問題点として、
full_titleヘルパー
の定義自身にミスがあった場合このままでは発見できない。極端な話、
Ruby on Rails Tutorial Sample App
がRby on Rrails Tuutorial Sample Appe
になっていてもこの結合テストはパスしてしまう。そこで
full_titleヘルパー
のテストを書く。test/helpers/application_helper_test.rbrequire 'test_helper' class ApplicationHelperTest < ActionView::TestCase test "full title helper" do assert_equal full_title, "Ruby on Rails Tutorial Sample App" assert_equal full_title("Help"), "Help | Ruby on Rails Tutorial Sample App" end endこれで完璧。
ちなみに
assert_equal
ではfull_title
が"..."
""内の文字列と同じかを調べている。
assert_select
assert_select
には、色々な指定の仕方がある。
Code マッチするHTML assert_select "div" <div>foobar</div> assert_select "div", "foobar" <div>foobar</div> assert_select "div.nav" <div class="nav">foobar</div> assert_select "div#profile" <div id="profile">foobar</div> assert_select "div[name=yo]" <div name="yo">hey</div> assert_select "a[href=?]", '/', count: 1 <a href="/">foo</a> assert_select "a[href=?]", '/', text: "foo" <a href="/">foo</a> 引用:Railsチュートリアル5.3.4表5.2
なるほどね〜。
用語一覧
- レスポンシブデザイン
「レスポンシブWebデザイン」とは、PC、タブレット、スマートフォンなど、複数の異なる画面サイズをWebサイト表示の判断基準にし、ページのレイアウト・デザインを柔軟に調整することを指す。
現在はPCやスマートフォンなど、デバイス毎に各HTMLファイルを複数用意し最適化することが、一般的な制作方法。
引用: 必読!5分でわかるレスポンシブWebデザインまとめ | 株式会社LIG
- タイポグラフィ typography
文字表現のデザイン処理のこという。情報伝達に適合したスペース、書体、大きさ、字間、字数、行間、行数等の選定だけでわなく、注目度、可読性、美しさなどへの配慮が重要である。
引用:Weblio辞書
- シーケンス
ITの分野では、順番に並んだ一続きのデータや手順のことや、並んだ順番にデータや手順を取り扱う処理方式などのことを意味する場合が多い。派生語として、逐次的な、連続的な、といった様子を表す「シーケンシャル」(sequential)や、順序のあるデータや手順を取り扱う機器やソフトウェアを意味する「シーケンサー」(sequencer)もよく用いられる。
引用:IT用語辞典 e-Words
- プリプロセッサ
コンパイラはプログラミング言語で記述されたソースコードを解釈してコンピュータが解釈・実行できるネイティブコードに変換するが、プリプロセッサはその下準備となるソースコードの追加や変換などの処理を担当する。プリプロセッサへの指示はプログラミング言語本体とは別に規定された特殊な記法(プリプロセッサディレクティブ)を用いて行われ、処理後のコードからは削除され残らないようになっている。
引用:IT用語辞典 e-Words
- スタブ
テスト用に用意した、まだ完成していない機能の代わりとなる部品であり、テスト対象から呼び出される部品の代わりとなるもの。
引用:「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典
- 投稿日:2021-03-02T18:44:04+09:00
【個人学習振り返り】Ruby勉強 #2
■この記事の目的
自分の振り返り用としての投稿です。
Sierから卒業しWebエンジニアになるため、Rubyを1から習得中です。■勉強に利用させていただいた動画
大変勉強になりました。投稿者様に感謝です。
本ページはこちらを参考に手でやった記録になります。
1.環境構築
自分はWindows環境で構築。
ruby・コマンドプロンプト・vscodeはあらかじめ入っていたため、
「SQLite」をインストールします。ちなみに以下すべての記載で入力しているコマンドは、VScode経由での入力です。
1.1 SQlite
1.2 Railsのインストール
そもそも「gem」が何かが不明でしたが以下のページで説明されており、
rubyのパッケージ管理ツールのコマンドのようです。
linuxでいうところのyumのような存在でしょうか。gem install rails -v "5.2.3"
1.3 パッケージ管理ツールのインストール
gem install bundlerGemfileというファイルを作るためのコマンドを実行。
この時点でこのファイルの意義はまだ未理解です・・・bundle initこのコマンドの後、Gemfileというファイルが実行フォルダの直下に作成されるので、
このファイルをテキストエディタで開き、以下を追記します。gem "rails"1.4.Railsのソースコードインストール
一つ上の手順でGemfileに"rails"という文字と定義したことで、
以下でrailsのソースコードがインストールできます。bundle install --path vendor/bundle2.Railsでアプリケーション作成
以下のコマンドでRailsのアプリケーションの基礎を作成します。
途中でオーバーライドするかと質問されたら、「A」を押して、オーバーライドします。
rails new .2.1Railsサーバー起動
rails shttp://localhost:3000/
Yay! You’re on Rails!
と表示されます。
ここまでのはまりポイント。すんなり、"rails s"ができた人はこの項目は飛ばしてください。
→rails s実行で返ってきた結果を抜粋。
サーバーが起動したとは思えないメッセージです。rails\rails_tutorial> rails s
Usage:
rails new APP_PATH [options]...同じ経験をされていた方の記事を発見し、こちらで対応してみました。
gitを入れていないことが原因のようです。
https://qiita.com/Leone/items/dc7f8ef2d5329d297e72gitをインストールvscodeを閉じて開きなおして、再度以下のコマンドでアプリケーションの作成を行いました。
rails new .一回目のgitなしのアプリケーション作成と比べ、かなりファイルが増えました。
しかしまだ"rails s"コマンドは動きません。
bin/rails:3:in
require_relative': cannot load such file -- C:/Users/****/Desktop/rails/rails_tutorial/config/boot (LoadError)
'
from bin/rails:3:inこれは調べてみたら、インストールしていたRubyのバージョンが原因で、boot.rbファイルが作られておりませんでした。
私の環境のRubyは以下のバージョンでした。ruby 3.0.0p0 (2020-12-25 revision 95aff21468) [x64-mingw32]
以下のダウンロードページから、再度2.7台のRubyを入れなおしました。
https://rubyinstaller.org/downloads/
ruby 2.7.2p137 (2020-10-01 revision 5445e04352) [x64-mingw32]
2.2Railsサーバー停止
ターミナルをctrl+cで閉じるとサーバーが停止され、このページに接続できなくなります。
3.TOPページ作成
Railsはファイルの命名規則が非常に厳しく、
コントローラーのファイル名「top_controller.rb」とした場合、
クラス名は必ず「TopController」にしないとエラーとなります。例)ファイル名:小文字アンダーバーで繋ぐ
クラス名:ファイル名のアンダーバーを消す。
「最初の文字」と「アンダーバーの後のアルファベット」を大文字にする。3.1 ルーティング設定
ルーティング設定とは、URLをサーバーが受け取ったときにコントローラーどういう処理をさせるかという意味らしいです。
(コントローラーというのはMVCモデルの中のコントローラーを指します)
Railsのルーターは受け取ったURLを認識し、適切なコントローラ内アクションやRackアプリケーションに割り当てます。ルーターは、ビューでこれらのパスやURLを直接ハードコードすることを避けるためにパスやURLを生成することもできます。
これまでのディレクトリの中に、「config」フォルダがあり、その中のrouter.rbを開きます。
このファイルのdoとendの間に以下を追記します。get '/', to: 'top#index'これの意味としては。
1要素目:getはHTTPメソッドの「get」
2要素目:トップページということで「/」
3要素目:コントローラーとアクションを定義する。この書き方だとTOPコントローラーのindexアクションとなる。3.2 コントローラー設定
これまでのディレクトリの中に、「app」→「controller」がありので、そこにファイルを作ります。
作るファイルは「top_controller.rb」とします。内容は以下の通りにします。
この「ActionController:Base」を継承しているのはコントローラーのルールのようで、絶対に書かないといけないもののようです。class TopController < ActionController::Base def index end end3.3 ビュー設定
これまでのディレクトリの中に、「app」→「views」がありので、そこにフォルダを作ります。
作成するフォルダ名は、コントローラーを「TopController」というクラスで定義したため、「top」となるようです。topの下にファイルを作ります。
ファイル名は、先のコントローラー設定の中のdefで「index」の名前で定義したため、
index.html.erb
とします。
erbという拡張子を始めた見ましたが、railsでは拡張子を「html.erb」とするようです。index.html.erbの中身自体は何でもよいので、動画を参考にこんな感じにしました。
<h1>Hello world</h1>
現状でき上ったものまとめ
config\routes.rbRails.application.routes.draw do # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html get '/', to: 'top#index' endapp\controllers\top_controller.rb
class TopController < ActionController::Base def index end endapp\views\top\index.html.erb
<h1>Hello world</h1>
サーバーを起動してどう動くか見てみます。
rails shttp://localhost:3000/にアクセスすると、
Hello world
が表示されました。
ただ初回実行時のエラーにありました。
以下のエラーが発生しました。raise LoadError, "Unable to autoload constant
コントローラーが読み込めないというエラーのようです。
しかしファイル名の違いなどは何度確認してもなく困惑しました。改善した方法としては、
top_controller.rbにおいて「ActionController::Base」の箇所を
「ApplicationController」に変えたところ改善しました。
この二つの差異が今のところ不明です。ただ試した限り、以下の動作後にいずれのパターンでも動作し、原因は不明のままです。
1.ApplicationControllerに変更
2.Hello world表示可能
3.rails停止
4.ActionController::Base:切り替え
5.rails起動
6.Hello world表示
4.DB接続
config\database.ymlが設定に使用するファイルとなるが、現時点での変更は不要です。
4.1 modelファイル作成
下記のコマンドを実行することでmodelが、app\models\user.rbとして作成する。
意味としては、 rails g model 以降が「DB名」 「列名:データ型」になるrails g model User name:string age:integer以下のようなファイルが作成されました。
invoke active_record
create db/migrate/20210302043737_create_users.rb
create app/models/user.rb
invoke test_unit
create test/models/user_test.rb
create test/fixtures/users.yml4.2 DB作成
rails db:create4.3 ユーザーテーブルの作成
4.1で作成した中のmigrateファイル内のデータを使ってテーブルが作成されます。
migrateファイルというのはDBの定義が書いてあるファイルと覚えればよいです。rails db:migrate4.4 データベースへの接続
今回sqlite3を用いているため、以下のコマンドで接続する。
sqlite3 db/development.sqlite3中身を見るにはこのコマンド。
.schema usersただ、「rails g model User name:string age:integer」というコマンドでつくったので、userが正しいと思っていたが、
db/migrate/20210302043737_create_users.rbファイル内のcreate文を見ると確かに、
「create_table "users」の記載があり、この辺りはまだ理解が及びません。。4.5データの挿入
まずrailsのコンソールを以下のコマンドで立ち上げます。
rails環境でrubyを使用できる環境となる。rails c・insert文
User.create!(name:"Aさん",age:19) User.create!(name:"Bさん",age:20)・select文
このコマンドは内部的には「SELECT "users".* FROM "users" LIMIT ?」と同じになるようです。User.all・select結果の変数格納
この場合、id:1ののユーザーに対するwhere文となり、その結果が変数格納されます。
「SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? 」user=User.find(1)この状態で変数「user」にはid1のユーザーの情報が格納されます。
変数.nameとして名前を見ることや、変数.ageとして年齢を見ることができます。
またそれを指定して内容を更新することができます。user.name="改名A"・update文
現在、変数userの内容を更新していて、id1のユーザーの名前が"改名A"となっています。
ただこの時点では変数しか書き換えていないため、以下のコマンドでDBに反映させます。
内部的には「UPDATE "users" SET "name" = ?, "updated_at" = ? WHERE "users"."id" = ?」になっているようです。user.save・select+where文
条件付きのselect文には、下記のコマンドがあります。
User.where(条件)ageを条件とする場合は、以下となります。
User.where("age>=19")5 新しいページの追加
新しいページを追加します。
今回の目的はユーザーの情報をDBから取得し、画面に出力させます。流れは、「ルーティングの追記」⇒「コントローラーの追加」⇒「ビュー追加」となります。
5.1ルーティングファイルの更新
例により最初にルーティングのファイルを書き換えます。
「config」フォルダのrouter.rbを開きます。それぞれの意味として、
「/users/:id」は、users/1やusers/2として、変数で受け取れるようになります。
「to: 'users#show'」は、usersコントローラーのshowメソッドを追加しています。
「as:"user"」はルーティングに対して、名前を付ける機能です。後でユーザー一覧作成の際に使用します。get '/users/:id', to: 'users#show', as: "user"5.2コントローラーファイルの追加
「app」→「controller」があるので、そこに新しいファイルを作ります。
作るファイルは「users_controller.rb」とします。
内容はTOPページと同じ要領で、
1)ファイル名の大文字&アンダーバー削除
2)メソッドはルーティングで追加した「show」を入れる。ここから新しい要素で、 @user=user.find(params["id"])があります。
これは
user.find()は、モデルからデータを抜き出す処理
params["id"]は、ルーティングから受け取るID
最後の変数に結果を格納しています。これに「@」がついているのは、ビューにファイルを渡せるようにするため。class UsersController < ActionController::Base def show @user=user.find(params["id"]) end end5.3ビューファイルの追加
「app」→「views」がありので、そこに新しいフォルダを作ります。
作成するフォルダ名は、コントローラーを「UsersController」というクラスで定義したため、「users」となるようです。「users」フォルダの配下に、show.html.erbを作成します。
(メソッド名+html.erb)ファイルの内容は以下です。
この<%~~~ %>タグの中がRubyのコードを書く領域になります。<%=と<%の違いは以下の通りです。
<%=:画面表示あり
<%:画面表示なしif文を書く時には、画面表示しないrubyを使いたいので、そういうときは<%を使うように使い分けができる。
<h1>ユーザー詳細</h1> <%= @user.name %> <br> <%= @user.age %> 才再び「rails s」でサーバーを起動させ、以下URLにアクセスします。
http://localhost:3000/users/1画面上で以下の結果が出力されました。
ユーザー詳細
改名A
19 才5.4ユーザー一覧を作成する。
同じ流れで、「ルーティングの追記」⇒「コントローラーの追加」⇒「ビュー追加」となります。
「config」フォルダのrouter.rbに以下を追記。get '/users', to: 'users#index'「app」→「controller」の「users_controller.rb」に追加します。
User.allはすべてのユーザーを取得するコマンドでした。def index @users=User.all end「users」フォルダの配下に、index.html.erbを作成します。
(メソッド名+html.erb)@usersを受け取って、eachで回しています。
2行目のlink_toはhtmlでいうタグにあたる内容です。
user_path(id: user.id)は、タグ内のhrefにあたり、id:user.idへのリンクを自動生成します。
(まだしっかり理解できていません...)<h1>ユーザーすべて</h1> <% @users.each do|user| %> <%= link_to user.name,user_path(id: user.id) %> <br> <% end %>この状態でいかにアクセスすると
http://localhost:3000/users/画面上以下が表示されました。
ユーザーすべて
改名A
Aさん
Bさん
Bさんソースコードは以下のようになっていました。
<a href="/users/1">改名A</a> <br> <a href="/users/2">Aさん</a> <br> <a href="/users/3">Bさん</a> <br> <a href="/users/4">Bさん</a> <br>5.5 追加機能で、「戻る」ボタンを作ってみます。
ユーザーの詳細画面からへのボタン追加のため、ルーティング設定を行います。
行う設定はas名前の追加です。「config」フォルダのrouter.rbを以下に変更。
get '/users', to: 'users#index' ⇒ get '/users', to: 'users#index', as: "users"show.html.erb へ以下を追記。
link_to でタグ化し、asで定義したusersという名前を書くことで、その画面へ遷移することができます。
<%= link_to "戻る" ,users_path %>■また次回に続きます。
- 投稿日:2021-03-02T18:33:22+09:00
【超かんたん】ransackを使って検索機能を実装しよう!
ransackを利用して検索機能を実装します。
今回も初心者向けにレシピ投稿アプリを例に作成していきます。また、検索機能の実装にActive Hashの値も利用するのでActive Hashがわからない方は前回記事にしておりますので、そちらをご覧頂けたらと思います。
【超かんたん】Active Hashで投稿ページにプルダウンメニューを作成しよう!完成イメージ
それでは、実装していきましょう!
ransackとは
ransackはRails用の検索機能を実装するためのGem。公式ドキュメント
導入方法
Gemfileに下記を記述しbundle installします。
Gemfilegem 'ransack'ターミナルbundle installデモデータを投入しよう!
seed.rbにデモデータ生成する記述をしていきます。
category_idとtime_required_idはActive Hashの値と紐付いているカラムになります。また、time_required_idのFakerがわからない方は前回記事にしておりますので、そちらをご覧頂けたらと思います。
【超かんたん】Fakerを使ってダミーデータを作成しよう!db/seed.rb5.times do |n| Recipe.create!( title: "すし・魚料理#{n}", text: "作り方", category_id: 1, time_required_id: Faker::Number.within(range: 2..6) ) end #中略 5.times do |n| Recipe.create!( title: "お菓子・スイーツ#{n}", text: "作り方", category_id: 10, time_required_id: Faker::Number.within(range: 2..6) ) endターミナルrails db:seed検索機能の実装
ルーティングの設定
まずは検索ページ(search)のルーティングと検索結果を表示するページ(result)のルーティングの設定をしましょう。
7つのアクション以外のルーティングを設定するので、recipesにネストさせます。
今回は、URLにidがつかないのでcollectionを利用します。config/routes.rbRails.application.routes.draw do root to: 'home#index' devise_for :users resources :users resources :recipes do collection do get :search get :result end end endコントローラーの編集
以下の記述は公式ドキュメントを参考にしております。
app/controllers/recipes_controller.rbclass RecipesController < ApplicationController before_action :search_recipes, only: [:search, :result] def index @recipes = Recipe.all end #中略 def search end def result @results = @q.result end private def search_recipes @q = Recipe.ransack(params[:q]) end endparams[:q]のキー「:q」でrecipesテーブルからレシピ情報を探し「@q」に格納します。
この@qに対して「.result」とすることで検索結果を取得します。
次にビューファイルを作成していきましょう。
ビューの作成
ビューファイルの作成
ターミナルtouch app/views/recipes/{search.html.erb,result.html.erb}ビューファイルの編集
今回はCSSの説明は省きます。
検索フォームにはsearch_form_forというメソッドを使います。
form_withのransack版というイメージです。
form_withでは「text_field」ですが、search_form_forだと「search_field」になります。また、「:カラム名_マッチャ」とすることで条件にあった検索を行います。
「_cont」だと「入力された値が含まれている」という意味になり、「_eq」は「入力された値と等しい」という意味のマッチャになります。
その他のマッチャについては公式ドキュメントを参照してください。collection_selectメソッドについては前回の記事とRailsドキュメントを参照してください。
以下のように編集していきます。
app/views/recipes/search.html.erb<div class="recipe-form"> <h1 class="text-center">検索する</h1> <%= search_form_for @q, url: result_recipes_path do |f| %> <div class="form-group"> <label class="text-secondary">料理名</label><br /> <%= f.search_field :title_cont, class: "form-control"%> </div> <div class="form-group"> <label class="text-secondary">カテゴリー</label><br /> <%= f.collection_select(:category_id_eq, Category.where.not(id: 0), :id, :name, include_blank: '指定なし') %> </div> <div class="form-group"> <label class="text-secondary">所要時間</label><br /> <%= f.collection_select(:time_required_id_eq, TimeRequired.where.not(id: 0), :id, :name, include_blank: '指定なし') %> </div> <div class="form-group"> <label class="text-secondary">フリーワード</label><br /> <%= f.search_field :text_cont, class: "form-control"%> </div> <div class="actions"> <%= f.submit '検索', class: "btn btn-primary" %> </div> <% end %> </div>次に検索結果ページを作成していきましょう。
以下のように編集していきます。app/views/recipes/result.html.erb<div class="recipes-index text-center"> <h1 class="result-index">検索結果</h1> <% if @results.length != 0 %> <% @results.each do |recipe| %> <div class="recipe"> <div class="recipe-title"> <%= recipe.title %> </div> <div class="recipe-content"> カテゴリー: <span class="recipe-category"><%= recipe.category.name %></span> 所要時間: <span class="recipe-time"><%= recipe.time_required.name %></span> </div> </div> <% end %> <% else %> 該当するレシピはありません <% end %> </div>if文で該当する検索結果がない場合は「該当するレシピはありません」と表示させるようにしています。
それでは実際に検索してみましょう。該当する検索結果がある場合
該当する検索結果がない場合
以上で完成です。
- 投稿日:2021-03-02T18:14:45+09:00
【簡単】railsでbootstrapを用いたエラーメッセージを表示する
記事投稿の背景
オリジナルサービスの投稿フォームを作成していたところ、投稿内容が空白だった場合にエラー表示をさせる方法が分からなかったが、調べてみると簡単に実装できたので共有させていただきます。
環境
ruby 2.7.2
rails 6.1.2
bootstrap導入済み実装
views/posts/new.html.erb<%= form_for @post do |f| %> <div class="field"> <h3>新規投稿</h3> <p>投稿内容:</p> <%= f.text_area :body %> </div> <%= f.submit "投稿する" %> <% end %>上のような投稿フォーム上で、投稿内容が空だった場合エラーメッセージを表示させます。
models/post.rbbelongs_to :user #ここに追記 validates :body, presence: true
validates :body, presence: true
とすることにより、空の投稿内容が認められないように設定します。controllers/posts_controllers.rbdef create @post = Post.new(post_params) @post.user_id = current_user.id if @post.save redirect_to :action => "index" #ここに追記。エラー時に行われる処理 else render action: :new end #終了 end投稿内容が保存されなかった場合(投稿内容が空だった場合)、postsのnewでアクションが行われるように設定します。
views/posts/new.html.erb<%= form_for @post do |f| %> # ここに追記 <% if @post.errors.any? %> <div class="alert alert-danger" role="alert">投稿内容を入力してください</div> <% end %> # 終了 <div class="field"> <h3>新規投稿</h3> <p>投稿内容:</p> <%= f.text_area :body %> </div> <%= f.submit "投稿する" %> <% end %>フォーム内に、
@post.errors.any?
設置することにより、エラーの時のみ中身が発火するようにします。
<div class="alert alert-danger" role="alert">
はbootstrapで用意されたアラートのデザインです。他の色もあるので、何か処理が成功したときのアラート等にも使えます。以上で、エラー表示を簡単に実装することが出来ました!!!
参考記事
- 投稿日:2021-03-02T17:18:08+09:00
RailsチュートリアルでPostgreSQLを導入する
はじめに
RailsチュートリアルでPostgreSQLを導入する方法について説明されている記事が見当たらなかったので書いてみました。エンジニアの方など、間違いを見つけられましたら、私を含むRailsチュートリアルからステップアップしたいエンジニア志望者にとって大変ありがたいので指摘していただけると嬉しいです。
参考
Railsにpostgresqlを導入する
初心者のMac + PostgreSQL インストール
PostgreSQLの環境構築 (macOS/Postgres 11)
PostgreSQLの基本的なコマンドPostgreSQLをインストールする
Homebrewがインストールされていることを前提とする。
ターミナル$ brew install postgresql最新のPostgreSQLがインストールできていることを確認してみよう。
ターミナル$ postgres --version >> postgres (PostgreSQL) 13.2問題なさそう。
データベースの初期化
ターミナル$ initdb /usr/local/var/postgres -E utf8 The files belonging to this database system will be owned by user "username". This user must also own the server process. The database cluster will be initialized with locale "ja_JP.UTF-8". initdb: could not find suitable text search configuration for locale "ja_JP.UTF-8" The default text search configuration will be set to "simple". Data page checksums are disabled. initdb: directory "/usr/local/var/postgres" exists but is not empty If you want to create a new database system, either remove or empty the directory "/usr/local/var/postgres" or run initdb with an argument other than "/usr/local/var/postgres".DBが無事に使えるかの確認
※筆者は不安なので確認していますが、今回の目的とは直接関係ありません
postgresサーバが無事に起動するかを確認してみる。いろいろ出てきた後に「server started」が表示されればOK。停止するには「pg_ctl stop」をターミナルに入力する。ただし、以降の操作は起動したまま行うこと。
(startを付けることでバックグラウンド起動してくれる。付けなくても起動してくれるけど起動中はそのターミナルは使えないから新しいウィンドウで操作する。)ターミナル#バックグラウンド起動 $ postgres -D /usr/local/var/postgres startPostgreSQLが起動している状態で、psqlコマンドが使えるかを確認する。-lはデータベースを一覧表示してくれるオプションなので、データベースが3つぐらい出てきたら問題ない。
ターミナル$ psql -l環境変数にPATHを通す
アプリケーションがDBを探せるようにpostgresの場所を指定する。シェルがbashの人は、zshrcをbash_profileに変更して実行。
ターミナル$ echo 'export PGDATA=/usr/local/var/postgres' >> ~/.zshrc $ source ~/.zshrcRailsで作るアプリケーションにPostgreSQLを導入する
ターミナル$ cd (アプリの場所) $ rails new sample_app(アプリ名) -d postgresql # データベースにPostgreSQLを指定 $ cd sample_app(アプリ名) $ rails db:create #データベース構築 $ rails shttp://localhost:3000/ にアクセスしておなじみの画面が出ればOK
postgresサーバを停止してセッティングは終了
ターミナル$ pg_ctl stopRailsチュートリアルをPostgreSQLで進めるにあたっての注意点
6.1. ローカル環境
ローカルで確認しながら開発するときは、「Rails server」と同様にpostgresサーバも起動しておく。
6.2. Gemfile(3章)
3章のGemfileをインストールする際には、開発環境とテスト環境に「sqlite3」ではなく「pg」を記述する。
Gemfile# 変更前 group :development, :test do gem 'sqlite3', '1.4.1' gem 'byebug', '11.0.1', platforms: [:mri, :mingw, :x64_mingw] end # 変更後 group :development, :test do gem 'pg' #バージョンは本番環境と同じにすることを推奨 gem 'byebug', '11.0.1', platforms: [:mri, :mingw, :x64_mingw] end6.3. Userモデル(6章)
6章でUserモデルを作った後は、データベースを確認したくなると思うのでその方法を残しておく。
PostgreSQLを起動した状態で、アプリケーションがある場所まで移動する。
ターミナル$ postgres -D /usr/local/var/postgres start $ cd sample_app(アプリケーション名)「rails console」を使ってデータの追加、削除はSQLiteと同じように可能。
ターミナル$ rails console irb> user = User.new(name: "EXAMPLE") irb> user.saveまた、「rails dbconsole」からデータベースの一覧を表示したり、実際のテーブルを確認することができる。
ターミナル$ rails dbconsole sample_app_development=> \l #データベース一覧を表示 sample_app_development=> \dt; #テーブル一覧を表示 sample_app_development=> \d users; #Userテーブルの構造を表示 sample_app_development=> select * from users; #Userテーブルのすべてのデータを表示お疲れ様でした。
- 投稿日:2021-03-02T16:56:34+09:00
【まとめ】Railsアプリ AWS化に役立った記事たち
目的
第1回 RailsアプリAWS化計画を成功に導いてくれた有難き記事たちをまとめています。(自分のために)
大まかな流れに活躍してくれたで賞
初心者向け:AWS(EC2)にRailsのWebアプリをデプロイする方法 目次 (以下手順)
この流れにそって実行すれば、基本的にアプリをデプロイすることが可能です。
ただし、最新更新が2017年なのでバージョンによる違い、AWSのUIの違いなどが多数見受けられました。しかし、手順は簡潔にまとまっており、非常に参考になりました。【画像付きで丁寧に解説】AWS(EC2)にRailsアプリをイチから上げる方法【その3〜サーバー設定とRailsアプリの配置編〜】
ゼロから行うのであれば、こちらの方が良いかも知れません。私は前者の方で始めたので、途中で参考記事を変えるのもややこしくなると考えたので移行しませんでした。インストールの諸問題を解決をしてくれたで賞
【AWS EC2】Amazon Linux2にnginxをインストールする方法
手順に沿って進めていて、最初にぶつかる問題は、おそらくAmazon Linux AMIが無く、Amazon Linux2 AMIしかないことでしょう。この手順で2を選択し、Nignxをインストールしようとすると、エラーが発生します。Amazon Linux2にはnginxのyumが無いことが原因で、その解決方法を案内してくれます。
Rails6 開発時につまづきそうな webpacker, yarn 関係のエラーと解決方法
Rails6でwebpackerが標準になったことにより、Railsアプリの開発環境にyarnのインストールが必要になりました。
AmazonLinuxにyarnをインストールする
タイトルそのまんまです。権限によって許可されていない操作(パーミッションエラー)を解決してくれたで賞
AmazonLinuxで新しいユーザを作成してec2-userを削除する
EC2にssh接続する際、デフォルトではec2-userになっています。セキュリティを高めるため、別ユーザーを作成してそちらを使用します。しかし、私の場合だと手順ではパーミッションエラーになってしまいます。そこでこちらのサイトが非常に参考になりました。なお、こちらでは最後にec2-userを削除しますが、手順には影響がないので大丈夫です。
Linuxの権限確認と変更(chmod)(超初心者向け)
そもそもパーミッションてなんやねん!って方は、この記事が参考になります。EC2からRDSにMySQLでログインできない問題を解決してくれたで賞
EC2からMySQLでRDSに接続するが、「Access denied for user」を突き返される
私が書いた記事ですが、悪いのは100自分です。Access denied for userと返されたら、ユーザーネーム、パスワード、セキュリティグループを疑ってみましょう。AWS RDSで設定を変更したらAccess denied (using password: YES)でmysql ログインできなくなった
RDSの設定を変更した時に見落としがちな部分。メモリ不足を解決してくれたで賞
[Rails] CapistranoでEC2へデプロイ:EC2仮想メモリ不足トラブルシュート
EC2の無料枠で使えるt2.microのメモリサイズは1GiBとかなり少なめです。そのため、ある程度負荷のある処理をすると、メモリ不足で処理が完了しないエラーが発生します。
virtual memory exhausted: Cannot allocate memory
というエラーです。
手順でいうと、私の場合は$ bundle install --path vendor/bundle
で発生しました。
調べてみるとbundle install系でこのエラーが発生するのは割と頻繁ぽい(?)。
この記事ではCapistranoを使っているときのようですが、手順での場合でも利用できます。LinuxでRAMメモリのキャッシュやスワップをクリア・解放する方法
同時に、メモリの解放などもさらっておくと良いと思います。DBの設定を環境変数化する手助けをしてくれたで賞
【画像付きで丁寧に解説】AWS(EC2)にRailsアプリをイチから上げる方法【その3〜サーバー設定とRailsアプリの配置編〜】
先程紹介したページの一部です。手順では、データベースのユーザー名やパスワードをdatabase.ymlに直接記述します。しかし、DBへアクセスする情報をdatabase.ymlへ直接記入するとセキュリティ的に問題があります。そこで、これらの秘密情報は環境変数へ移行します。ロードバランサーとHTTPS化を手助けしてくれたで賞
【初心者向け】AWSのサービスを使ってWebサーバーをHTTPS化する
手順でのロードバランサーは以前のバージョンのものです。HTTPS化するのであれば、手順よりこちらにまるまる沿って行ってしまった方がやりやすいです。デプロイ後のエラーの解決を手助けをしてくれた賞
[初学者] AWS デプロイ時のエラー解決に役立ちそうなコマンドや知識
バックグラウンドが黒でちょっと見にくいのですが、エラーに困ったらその糸口を見つけ出すことができると思います。EC2のRuby/Rails環境構築中のwe're sorry, but something went wrongでハマった話
AWSにデプロイして、We're sorryが出たら、とりあえずログを確認してみろよってこと。
ログからエラーの糸口を見つけ出すことができる力があれば、これだけでOK。デプロイしたアプリを更新する手助けをしてくれたで賞
【AWS】 EC2にデプロイしたRailsアプリを更新する方法
一度アプリをデプロイした後に更新することも往々にしてあると思います。そういう時はこちら。本番環境でgit pullした時に起こったエラー【error: Your local changes to the following files would be overwritten by merge: composer.lock Please, commit your changes or stash them before you can merge. 】
上記の更新で、$ git pull origin master
を行うとたまにコンフリクトのエラーが発生します。原因は様々だと思いますが、もし発生した場合はこの方法を試してみてください。Unicornに関する問題を解決してくれたで賞
unicorn使おうと思ったらいろいろ詰まった
基本更新などでgit pullをしたときなどに書き換えられることはないと思うのですが、私の場合コンフリクトが発生してstashで解決したあとに、unicornが起動できなくなりました。原因はGemfileのunicornが消えてなくなっていただけだったのですが。AWSでだけPayjpが動作してくれない問題を解決してくれたで賞
環境変数の代わりに .env ファイルを使用する (dotenv)
私はアプリにPayjpを使った決済機能を導入していたのですが、ローカルやHerokuでは動作していたのに、AWSでだけ動作しない問題にぶち当たりました。私の場合は、この方法を導入するだけで解決しました。Rails 本番環境(EC2)でPAY.JPのAPI keyを読み込めないエラー
こちらの様にすると解決するパターンもあると思います。上記2つの違いとしては、Javascriptファイルから.envファイルを参照するのか、Rubyファイル(コントローラなど)から参照するのかだと思います。私の場合は、コントローラからは後者の記事のように
Rails.application.credentials.payjp[:PAYJP_PRIVATE_KEY]
とせずとも、ENV['鍵名']
で読み取れたので、かならずしもそう記述しなければならないというよりは、.envファイルの設定の問題なのではないかと思います。代わりに、JSファイルから.envファイルを読み取れず、前者の記事通りに実装したところ上手くいったという次第です。EC2ボリュームに関する問題を解決してくれたで賞
【AWS】EC2サーバーにボリュームをアタッチしたら起動しない!
EC2のボリュームは、アタッチ・デタッチができます。インスタンスが正常に起動できない場合などに、ボリュームをデタッチ、別のインスタンスにアタッチすることで中身を確認できたりします。アタッチする際に、ルートデバイスの指定があるのですが、これが何故か表示されているように記入しても上手くいかないケースがあります。その他
セキュリティグループにマイIPを設定してssh接続できない
設定したIPアドレスと今現在自分が使っているIPアドレスに差異がないか確認します。
あなたが現在インターネットに接続しているグローバルIPアドレス確認RailsアプリにFontAwesomeを使用している場合
この場合は、FontAwesomeをローカルでインストールしたときと同じ様にインストールします。Rails6の場合なので、5以前の場合は検証していません。
【Rails】Rails6でFontAwesomeを導入・表示させるための手順を初心者向けに解説ClockWorkを使ったバッチ処理
EC2上でClockWorkの処理を実装する場合です。
clockworkを本番環境で動かす場合いつでも強い味方
さいごに
インターネット上でこうやって探すだけで実装していけるなんて、ありがたいなあと改めて実感しました。もちろん、一筋縄ではありませんし、人によって解決できる方法も違うので、自分に当てはまる解決策を見つけるために費やした時間を振り返ると、まだまだ情報収集能力が足りないなあとひしひしと感じています。
公式ドキュメントはさらっとリンクを載せただけですが、何か実装にひっかかったらやはり公式ドキュメントがこころ強いですよね。
このまとめ記事もだれか1人にでもお役に立てれば嬉しいです。
- 投稿日:2021-03-02T16:25:00+09:00
[Rails]slickを用いて投稿された画像をランダムに取得してスライドショーを実装する
スライドショーの実装
題名通りjQueryのslickを用いて投稿された画像をランダムにスライドショーで実装する方法の紹介です。
初投稿なので誤字や脱字など至らない点はあると思いますが、よろしくお願いいたします。完成形
前提
Rails 6.0.3.5
slickはjQueryのプラグインなのでjQueryの導入をお願いいたします。slickの導入
slick公式サイト
CDNを利用して導入します。
application.html.erb内の<head>
にapprication.erb<link rel="stylesheet" type="text/css" href="//cdn.jsdelivr.net/npm/slick-carousel@1.8.1/slick/slick.css"/> <link rel="stylesheet" type="text/css" href="//cdn.jsdelivr.net/npm/slick-carousel@1.8.1/slick/slick-theme.css"/> <script type="text/javascript" src="//cdn.jsdelivr.net/npm/slick-carousel@1.8.1/slick/slick.min.js"></script>を付け加えましょう。
導入に関してはこれだけです。slick.jsの作成
javascript下にslick.jsファイルを作りましょう。
コードの記載は下の通りです。slick.js$(function() { $('.slider').slick(); });slickの読み込み
slickを読み込むためにapplication.jsに
application.jsrequire('slick')の記載を追加しましょう。
投稿を取得してスライドショーにしよう
スライドショーを実地するための記載をしましょう。
今回の場合は@posts
に画像つきの投稿の全てを取得させています。html.erb<div class="slider"> <% @posts.each do |post| %> <%= link_to image_tag(post.image), post_path(post.id), method: :get, class: :slick_image %> <% end %> </div>これで投稿を取得して画像をスライドショーで表示できるようになったと思います。
取得する投稿をランダムにしよう
↑の場合だと投稿全てを取得して表示してしまうので、
コントローラー内で新たに@randams
を定義してランダムな投稿を取得しましょう。controller.rb@randams = Post.order("RAND()").limit(5) //limit(5)で5件を取得させている。
@randams
にランダムな投稿5件を取得させています。ビューファイルも書き換えましょう。
.html.erb<div class="slider"> <% @randams.each do |randam| %> <%= link_to image_tag(randam.image), post_path(randam.id), method: :get %> <% end %> </div>
@randams
にランダムな投稿5件を取得させてeachで表示させています。スライドを自動にしてみよう
現状のスライドだと寂しいので色々オプションを付け加えてみましょう。
slick.js$(function() { $('.slider').slick({ centerMode: true, //スライド画面に次のスライドが表示される centerPadding: '10%', //次のスライドの幅 dots: true, //スライドの下にドットのナビゲーションを表示 autoplay: true, //自動再生オン autoplaySpeed: 2000, //再生スピード infinite: true //スライドが終了したら最初に戻る }); });他にもさまざまなオプションがあるので気になった方は公式リファレンスをチェック!
最後に
初投稿と言うこともあり、色々不十分な点もあると思いますが、拙い文章にお付き合いいただきありがとうございました。
- 投稿日:2021-03-02T14:15:02+09:00
【Rails開発】基本の基本コマンドチートシート
コラム: エラーメッセージを日本語にする
STEP①Githubのrails-i18nの日本語辞書リポジトリをDL
$ wget https://raw.githubusercontent.com/svenfuchs/rails-i18n/master/rails/locale/ja.yml -P config/localeswget は引数に記載したURLからファイルをカレントディレクトリにDLするコマンド。(Command not foundと出た場合の対処法)
-P オプションでDLするディレクトリを指定します。
これで、翻訳ファイルconfig/locales/ja.yml が作成される。STEP②config/initializers/locale.rbファイルを作成
vim config/initializers/locale.rbSTEP③以下を記述して保存(:wq コマンドで保存できる)
config/initializers/locale.rbRails.application.config.i18n.default_locale = :jaモデルの雛形作り
$rails g model モデル名 属性名:データ型 name:string description:textモデルが動作するには、データベースとモデルクラスの両方が必須。
データベースにテーブルを追加する為に、マイグレーションを実行する。
※Railsで用意されているマイグレーションという仕組みは、スキーマの歴史を進めるだけでなく、戻す機能も備えている。$rails db:migrateコントローラの雛形作り
$rails g controller tasks index show new editconfig.routes.rbから余計なものを削除し、resourcesにまとめる
config.routes.rbroot to: 'tasks#index' resources :tasks一覧画面(index)から新規登録ボタンを作ってリンクを飛ばす
app/views/〜/index.html.slim= link_to '新規登録', new_task_path, class: 'btn btn-primary'翻訳処理
config/locales/ja.ymlerror: 〜 〜 model: task: タスク attributes: id: ID name: 名称 description: 詳しい説明 created_at: 登録日時 updated_at: 更新日時 ここら辺を任意で記載。新規登録画面の為のアクションを実装
app/controllers/tasks_controller.rbdef new @task = Task.new end※アクションからビューに受け渡しをしたいデータをインスタンス変数に入れるのが、アクションの基本役割の一つ。
新規登録画面のビューを作成
app/views/tasks/new.html.slimform_withを使ってぽちぽち
登録アクションの実装
app/controllers/tasks_controller.rbdef create task = Task.new(task_params) task.save! redirect_to tasks_url, notice: "タスク 「#{task.name}」を登録しました」" end private def task_params params.require(:task).permit(:name, :description) end end※なぜrenderでなく、redirect_toなのかは➡︎RenderとRedirectの違い
このままだとnoticeが表示されないので、application.htmlを直す
application.html.slim.container - if flash.notice.present? .alert.alert-success = flash.notice一覧表示機能の実装
コントローラ
tasks_controller.rbdef index @tasks = Task.all endビューの処理
index.html.slim=link_to '新規登録'〜 .mb-3 table.table.table-hover thread.thread-default tr th= Task.human_attribute_name(:name) th= Task.human_attribute_name(:created_at) tbody - @tasks.each do |task| tr td= task.name td= task.created_at詳細表示機能の実装
さっき書いた一覧にリンクをつける
index.html.slimtd = link_to task.name, task_path(task)showアクションを実装
tasks_contoroller.rbdef show @task = Task.find(params[:id]) endshowのビュー実装
views/tasks/show.html.slim編集機能の実装
・一覧と詳細画面に編集リンク追加
・newをコピーして、edit.htmlを作成
・editアクション実装パーシャルでDRYに
・
- 投稿日:2021-03-02T13:56:10+09:00
テストコードについて(Ruby on Rails)
今回は、Ruby on Railsのテストコードについての記事を書きたいと思います。
バージョン
・Ruby 2.6.5
・Rails 6.0.0テストコードとは?
・テストコードとは?
テストコードとは、アプリケーションの動作確認をする際にかくコードです。
アプリケーションを作成したら、新規登録やログインができるか手動で確認する
と思います。
これを、コードを書いて自動で実行できるのがテストコードです。■なぜ、テストコードは必要?
①クオリティの担保ができる
もし、手動で挙動を確認するとデメリットが多い。
・人為的ミスが起きる
・仕様が変わった時に、もう一度やらなくてはいけない
・記録が残らない
こういったデメリットを回避できてクオリティを担保できる。②仕様を見極めることができる
テストコードをかける人は、そのアプリケーションの仕様を理解している。テストコードで大事なのは「何を確認したいのか意識する」
テストコードのパターンと種類
■パターン
・正常系
「ユーザーが開発者の意図する挙動を行った時の挙動」を確認するテストコード。
つまりは、「ログインできる時」など上手くいった時の確認。・異常系
「ユーザーが開発者の意図しない操作を行った時の挙動」を確認するテストコード。
例えば、新規登録の際に「フォーム空欄だと登録できない」などを確認する。※その他にも準正常系など様々な分類があります。
■種類
・単体テストコード
モデルやコントローラーなど機能ごとに問題がないかを確かめる。
モデルの場合バリデーションの確認などを行う。
例えば、「パスワードを英数混合」のバリデーションを設定していたらその挙動を
確認する。Gemの導入
railsでテストコードを行うには、RspecのGemの導入が必要。
Gemfile
group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] gem 'rspec-rails', '~> 4.0.0' endGemfileのgroup :development, :test doの中に記述を行う。
% bundle install% rails g rspec:installGemのインストールを行った後に、rails gでrspecをインストールを行う。
create .rspec create spec create spec/spec_helper.rb create spec/rails_helper.rbすると上記のファイルが作成される。
.rspec
--format documentation作成され.rspecファイルに上記を記述する。
これは、テストコードの結果をターミナル上に可視化するための記述。これで、rspecの導入は完了したので、あとはファイルを作成してコードを書くだけです。
ちなみにテストファイルの生成の仕方は、
% rails g rspec:model userターミナルより上記のコマンドを実行するとファイルが生成でsきます。
これは、userモデルの単体テストコードのファイルを作成したものです。ちなみにこれは、単体テストコードの場合で、次に結合テストの場合について。
結合テストの場合についての準備
結合テストの場合は、System Specという技術を使用します。
これは、CapybaraというGemを用い流のですが、このGemはデフォルトで
導入されています。Gemfile
group :test do # Adds support for Capybara system testing and selenium driver gem 'capybara', '>= 2.15' gem 'selenium-webdriver' # Easy installation and use of web drivers to run system tests with browsers gem 'webdrivers' endGemfileを見ると導入されていることが確認できます。
% rails g rspec:system usersターミナルよりファイルを作成。
これは、userの結合テストコードのファイルです。
つまり、「新規登録」や「ログイン」機能のテストを行うものです。ここまでが、テストコードの説明と導入手順の流れです。
効率よくテストコードを書くために
テストコードを効率よく書くために、導入するGemが
FactoryBotとFakerです。・FactoryBot
インスタンスをまとめることができるGemです。他のファイルであらかじめ各クラスのインスタンスに定める値を設定しておき、各テストコードで使用します。RSpec.describe User, type: :model do describe 'ユーザー新規登録' do it 'nicknameが空では登録できない' do user = User.new(nickname: '', email: 'test@example', password: '000000', password_confirmation: '000000') user.valid? expect(user.errors.full_messages).to include("Nickname can't be blank") end end endこれが、FactoryBotを使わずに書いたテストコードです。
テストを行うたびにインスタンスを生成しなくてはいけません。
別のファイルにあらかじめインスタンスを作成し、再度利用するのがFactoryBotの役目です。Gemfile
group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] gem 'rspec-rails', '~> 4.0.0' gem 'factory_bot_rails' gem 'faker' endGemfileのgroup :development, :test doの中に記述します(Fakerも記述しています。)
記述を行ったら、% bundle installこれで、導入完了です。
「factories」というフォルダができるので、その中にファイルを作成します。
factoriesというフォルダができていない場合は手動で作成してください。spec/factories/users.rb
FactoryBot.define do factory :user do nickname {"test"} email {"test@test"} password {'test1234'} password_confirmation {password} profile {"よろしくお願いします"}このようにそれぞれのインスタンスをあらかじめ作成しておきます。
RSpec.describe User, type: :model do before do @user = FactoryBot.build(:user) end describe "新規登録" do context "登録できる時" do it "プロフィール写真以外の情報が入力されていれば登録できる" do expect(@user).to be_valid end it "プロフィール写真がなくても登録できる" do @user.image = nil expect(@user).to be_valid end end context "登録できない時" do it "emailがないと登録できない" do @user.email = nil @user.valid? expect(@user.errors.full_messages).to include("Email can't be blank") endこのように、beforeであらかじめセットアップしておくと、各項目で使えるようになります。
例えば、"登録できるとき"の"プロフィール写真以外の情報が入力されていれば登録できる"
はあらかじめ、@userの中にFactoryBotの情報が入っているため、これだけの記述で済みます。・Faker
ランダムな値を生成するGemです。メールアドレス、人名、パスワードなど、さまざまな意図に応じたランダムな値を生成してくれます。Fakerを使用すると、名前やemailなどをランダムに生成してくれます。
同じ情報が登録できないようなバリデーションを組んでいる時なんかに使います。
導入の仕方は、先ほどと同じで導入が済んだら、spec/factories/users.rb
FactoryBot.define do factory :user do nickname {Faker::Name} email {Faker::Internet.free_email} password {'test1234'} password_confirmation {password} profile {"よろしくお願いします"} end endFactoryBotのファイルにFakerを使用したい部分に記述します。
今回だと、emailとnicknameを自動生成しました。他にもいろんなものが生成できるので、興味ある方はこちらを参考してください。
Fakerの公式GitHubここまでが、テストコードに関しての説明です。
オリジナルアプリに、テストコードを書くことでより理解が深まりました。初心者のため、記事に不備があるかもしれません。
その際は、コメントしていただければ幸いです。
- 投稿日:2021-03-02T13:33:31+09:00
【Ruby on Rails】デバッグツール(pry-rails)
エラーの時にはデバッグツールを使ってバグを発見したり、処理を止めて動作を確認したりします。
私はpry-rails
を利用しているのでその使い方をまとめます。pry-rails
Railsにおけるデバッグ用のGemです。
私の主な使い方はコントローラー内で記述し、その処理を止めることでどんなデータが渡っているかなどを調べるのに使っています。
テストコードを書く際などにもエラー文を調べるために利用したりします。準備
Gemfileの一番下に
Gemfilegem 'pry-rails'と記述します。
ターミナルで該当のプロジェクトのディレクトリで以下のコマンドを実行します。
ターミナルbundle install以上でGemの導入とGemfileの更新ができました。
使い方
①送られているデータの確認方法
例えばcreateアクションでどのデータが送られているか確認したい場合、コントローラー内の処理を確認したいところに
binding.pry
を記述します。コントローラーdef create binding.pry Sample.create(sample_params) end記述できたらローカルのサーバーから実際にcreateアクションの動作を行ってみます。
(例えばformからの投稿など)この状態でターミナルを見てみるとコンソールが起動していると思います。
下記のような入力街の状態です。ターミナルpry(#<SampleController>)>ここに
params
と入力すると送られているデータが配列で確認できます。
うまく保存ができない時に試すと原因が分かることがあります。②テストコードでの使い方
※テストコードの書き方の詳細は省きます。
以下のように異常系のテストコードを書く際にbinding.pry
で処理を止め、エラーメッセージがどういうものか確認します。context '新規登録がうまくいかない時' do it "名前(name)が空だと登録できない" do @user.name = '' @user.valid? binding.pry end end上記の記述をしてテストコードを実行するとターミナルでコンソールが起動するので
ターミナルuser.errors.full_messagesと入力するとエラーメッセージを出力してくれます。
以上です。
- 投稿日:2021-03-02T13:23:19+09:00
【Railsチュートリアル】第11章 アカウントの有効化①
はじめに
アカウントを有効化するステップを新規登録の途中に差し込み、本当にそのメールアドレスの持ち主なのかどうかを確認できるようにする。
11.1 AccountActivationsリソース
セッション機能(8.1)を使って、アカウントの有効化という作業を「リソース」としてモデル化する。
11.1.1 AccountActivationsコントローラ
resources :account_activations, only: [:edit]URL「/account_activation/トークン/edit」にGETがリクエストされたらeditアクションを呼び出す。
演習 1
現時点でテストスイートを実行すると green になることを確認してみましょう。
確認のみなので省略。演習 2
表 11.2の名前付きルートでは、_pathではなく_urlを使うように記してあります。なぜでしょうか? 考えてみましょう。ヒント: 私達はこれからメールで名前付きルートを使います。
メール本文のURLからアクセスするから。11.1.2 AccountActivationのデータモデル
仮想的な属性を使ってハッシュ化した文字列をデータベースに保存するようにする。
演習 1
本項での変更を加えた後、テストスイートが green のままになっていることを確認してみましょう。
GREEN
演習 2
コンソールからUserクラスのインスタンスを生成し、そのオブジェクトからcreate_activation_digestメソッドを呼び出そうとすると(Privateメソッドなので)NoMethodErrorが発生することを確認してみましょう。また、そのUserオブジェクトからダイジェストの値も確認してみましょう。
>> user = User.new (4.5ms) SELECT sqlite_version(*) => #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil, password_digest: nil, remember_digest: nil, admin: nil, activation_digest: nil, activated: false, activated_at: nil> >> user.create_activation_digest Traceback (most recent call last): 1: from (irb):2 NoMethodError (private method `create_activation_digest' called for #<User:0x00007f5eb1cd6088>) Did you mean? restore_activation_digest! >> user.activation_digest => nil演習 3
リスト 6.35で、メールアドレスの小文字化にはemail.downcase!という(代入せずに済む)メソッドがあることを知りました。このメソッドを使って、リスト 11.3のdowncase_emailメソッドを改良してみてください。また、うまく変更できれば、テストスイートは成功したままになっていることも確認してみてください。
app/models/user.rbdef downcase_email self.email.downcase! endGREEN
11.2 アカウント有効化のメール送信
Action Mailerライブラリを使ってUserのメイラーを追加する。
11.2.1 送信メールのテンプレート
Userメイラーの生成$ rails generate mailer UserMailer account_activation password_reset$ rails generate [メイラー名][アクション名][アクション名]
app/views/user_mailer/account_activation.text.erb
app/views/user_mailer/account_activation.html.erb
ブラウザと違って、メールボックスによってHTMLを描画できないものもあるので、textも用意している。app/mailers/application_mailer.rbclass ApplicationMailer < ActionMailer::Base default from: 'from@example.com' # どこから送るか layout 'mailer' # デフォルトではどんなレイアウトを使うのか。 end
application_mailer.rb
ではメイラー全体の設定をする。app/mailers/user_mailer.rbclass UserMailer < ApplicationMailer # Subject can be set in your I18n file at config/locales/en.yml # with the following lookup: # # en.user_mailer.account_activation.subject # def account_activation @greeting = "Hi" # インスタンス変数を展開 mail to: "to@example.org" end # Subject can be set in your I18n file at config/locales/en.yml # with the following lookup: # # en.user_mailer.password_reset.subject # def password_reset @greeting = "Hi" # インスタンス変数を展開 mail to: "to@example.org" end end
user_mailer.rb
ではメイラーでは何をするのかを設定する。演習 1
コンソールを開き、CGIモジュールのescapeメソッド(リスト 11.15)でメールアドレスの文字列をエスケープできることを確認してみましょう。このメソッドで"Don't panic!"をエスケープすると、どんな結果になりますか?
>> CGI.escape("Don't panic!") => "Don%27t+panic%21"エスケープ:使えない文字列を使える文字列に変換する。
11.2.2 送信メールのプレビュー
メールのメッセージをその場でプレビューすることができるメールプレビューを設定する。
development.rb: 開発環境用 / test.rb: テスト環境用 / production.rb : 本番環境用
host = 'localhost:3000' # ローカル環境用 config.action_mailer.default_url_options = { host: host, protocol: 'http' }cloud9を使っていないので、上記を選択。
host〜をコピペすると、自分が開発しているhost(ドメイン名)にGETリクエストが送れないためにエラーが起きます。演習 1
Railsのプレビュー機能を使って、ブラウザから先ほどのメールを表示してみてください。「Date」の欄にはどんな内容が表示されているでしょうか?
アクセスした日時が表示される。
11.2.3 送信メールのテスト
メールプレビューのテストも作成して、プレビューをダブルチェックできるようする。
リスト 11.21: テストのドメインホストを設定する
test/mailers/user_mailer_test.rbrequire 'test_helper' class UserMailerTest < ActionMailer::TestCase test "account_activation" do user = users(:Michael) # users(:Michael)をuserに代入 user.activation_token = User.new_token # トークン情報を生成してuser.activation_tokenに代入 mail = UserMailer.account_activation(user) # UserMailer.account_activation(user)をmail変数に代入 # ここから確認のテスト assert_equal "Account activation", mail.subject # mail.subject(件名)を確認 assert_equal [user.email], mail.to # mail.to(送り先)を確認 assert_equal ["noreply@example.com"], mail.from # mail.from(送り元)を確認 assert_match user.name, mail.body.encoded # mail.body(本文)にuser.name(名前)が入っているか確認 assert_match user.activation_token, mail.body.encoded # mail.body(本文)にuser.activation_token(トークン)が入っているか確認 assert_match CGI.escape(user.email), mail.body.encoded # mail.body(本文)にuser.emailの「@」が # CGI.escape(エスケープ処理)されているものが入っているか確認 end end演習 1
この時点で、テストスイートが green になっていることを確認してみましょう。
動作確認のみなので省略。演習 2
リスト 11.20で使ったCGI.escapeの部分を削除すると、テストが red に変わることを確認してみましょう。
動作確認のみなので省略。11.2.4 ユーザーのcreateアクションを更新
ユーザー登録を行うcreateアクションにコードを追加し、メイラーをアプリケーションで実際に使えるようにする。
signup直後のログインの廃止(本人確認前にログインできないようにする)して、メールチェックしてもらえるように促す。app/controllers/users_controller.rbclass UsersController < ApplicationController . . . def create @user = User.new(user_params) # app/models/user.rb # attr_accessor :remember_token, :activation_token # before_save :downcase_email # def create_activation_digest # self.activation_token = User.new_token # self.activation_digest = User.digest(activation_token) # end if @user.save # app/models/user.rb # before_create :create_activation_digest UserMailer.account_activation(@user).deliver_now flash[:info] = "Please check your email to activate your account." redirect_to root_url else render 'new' end end . . . end演習 1
新しいユーザーを登録したとき、リダイレクト先が適切なURLに変わったことを確認してみましょう。その後、Railsサーバーのログから送信メールの内容を確認してみてください。有効化トークンの値はどうなっていますか?
適切なURL: root_url
----==_mimepart_603da2dfbacd4_11ed2af75249ad6c294cd Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 7bit Hi moutoon, Welcome to the Sample App! Click on the link below to activate your account: http://localhost:3000/account_activations/iXav_kpcTA4krSc-3LR3DA/edit?email=moutoonm342%40gamil.com演習 2
コンソールを開き、データベース上にユーザーが作成されたことを確認してみましょう。また、このユーザーはデータベース上にはいますが、有効化のステータスがfalseのままになっていることを確認してください。
>> user = User.find_by(name: "moutoon") (0.1ms) begin transaction User Load (5.8ms) SELECT "users".* FROM "users" WHERE "users"."name" = ? LIMIT ? [["name", "moutoon"], ["LIMIT", 1]] => #<User id: 101, name: "moutoon", email: "moutoonm342@gamil.com", created_at: "2021-03-02 02:28:47", updated_at: "2021-03-02 02:28:47", password_digest: [FILTERED], remember_digest: nil, admin: nil, activation_digest: "$2a$12$9y5elJ64pu1EC1COuJfdU.95jcOd0TdqpDyHz7W5m1M...", activated: false, activated_at: nil>activated: falseになっている。
- 投稿日:2021-03-02T13:09:33+09:00
Railsポートフォリオ作成 #5 基本機能バックエンド開発
こんにちは
今回は基本機能のバックエンド開発を行いました。
(前回記事(#4 herokuデプロイ))私は、前職(ホテルの料飲部)における、コミュニケーションの課題を解決するアプリを作っているのですが、今回は、
基本機能のバックエンド開発を行いました
具体的には、「レストランで使うものの数を管理する機能」です。
しかし、肝心の数を管理するやり方が正直いまいちいいのが出てこなくて、一旦プルダウンの選択式しました。
ただこれはUXとしてはいまいちなのでは?
という気がするので、最終的には何かもっといい方法でできるといいんだけど、、、と思っています。感じたこと
Railsでバックエンドやるのは少し慣れてきた。
楽しみながらできるようになってきたし、基本部分は1日で終わらせることができました。
少し成長を感じました。(やっぱり前回のAWSはキツかった、、、)プログラミングはつくづく巨大な岩を小さなトンカチで少しずつ少しずつ削ってるみたいだってこと。
やらないといけないこと、実装しないといけないこと、勉強しないといけないことがありすぎて、押しつぶされそうになって正直諦めそうになります、、、
でもそれを一個一個少しずつ解決していくしかないわけで。
水の呼吸で一刀両断とかできたらいいのに、、、
もくもく会とか参加しながら自分をやらないといけない状況において、仕組みを使って解決していこうと思います。最後に
モデルの単体テストまで書き終わったので、次はフロントエンドをやっていこうと思います。
フロントエンドは経験値が少ないので、復習しながら時間をかけてやっていくことになると思います。
- 投稿日:2021-03-02T12:24:52+09:00
【Nuxt Vue Rails】axiosのContent-Typeがappliation/jsonにならない
- 投稿日:2021-03-02T12:12:16+09:00
Ajaxでお気に入り機能を実装する
自己紹介
9月から独学でプログラミング学習を開始し、
11月からスクールを使って学習をしています。
現在はポートフォリオの作成し転職活動中です。
知識を定着させるために、学びをアウトプットしています。
また、これから学び始める方の参考になることを願っています。
開発環境
- Ruby 3.0
- Ruby on Rails 6.0.3.4
- jQuery 3.5.1
Ajaxでお気に入り機能を実装する
前回、Railsでお気に入り機能を実装するという記事を投稿しました。
今回は、この記事の非同期化を行います。
同時にお気に入り数のカウントも行ってみましょう!1.マイグレーションファイルを作成する
まずはターミナルで下記のコマンドを実行して下さい。
rails g migration AddLikersCountToMoives
そして、生成されたマイグレーションファイルを編集します。class AddLikersCountToMovies < ActiveRecord::Migration[6.0] def change add_column :movies, :likers_count, :integer, default: 0 end endこれで
likees_count
というメソッドが使用できるようになりました。公式のGithubも合わせて参照下さい。
☆ポイント
ここでrails db:migrate
を実行しますが、
その前に、もしお気に入りの登録を行っている場合は全て解除して下さい。
default: 0
としているため、現在の状態が0になってしまいます。
このまま、お気に入り解除をすると-1のような表示になってしまいます。2.ビューファイルを追加する
まずは、
views/movie
配下に_favorite.html.erb
というビューファイルを新しく追加します。<% if current_user.likes?(movie) %> お気に入り解除 <% else %> お気に入り登録 <% end %> <%= movie.likers_count %>これは以前に作成したビューファイルを切り出したものに、
likers_count
を追加したものになります。
それでは、以前に作成したビューファイルを編集しましょう3.ビューファイルの編集をする
<%= link_to favorite_movie_path(movie), remote: true do %> <div id="favorite-movie"> <%= render partial: "movies/favorite", locals: {movie: movie} </div> <% end %>まず、作成した
_favorite.html.erb
をrender partial: "movies/favorite"
で読み込んでいます。
locals: {movie: movie}
はfavorite.html.erb
内のmovieにビューで使っているmovieを渡しています。renderについてはRailsガイドを参照ください。
link_to
のオプションにremote: true
を指定しています。
これによりviews/movies/favorite.js.erb
が呼び出されるようになります。
指定したid="favorite-movie
はfavorite.js.erb
で使用します。4.JavaScriptファイルを作成する。
views/movies
配下にfavorite.js.erb
を作成して下さい。
内部は下記の1行だけです。$("#favorite-movie").html("<%= j(render partial: "movies/favorite", locals: {movie: @movie}) %>");先程のidを指定し、内部のhtmlを
_favorite.html.erb
に書き換えています。
今回、jQueryを使用していますが、導入については、省略致します。
以上で機能の実装は終了です。
前回記事、Railsでお気に入り機能を実装すると合わせてご覧下さい。
また、至らない点があれば、お手数ですがご指摘下さい。
補足①
非同期でお気に入りを行った際に、お気に入り数のカウントが、遅れてしまう可能性があります。
そのような場合、controllers/movies_controller.rb
に下記を追加して下さい。def favorite @movie = Movie.find(params[:id]) current_user.toggle_like!(@movie) + @movie.reload #この1行を追加
reload
でお気に入り処理後に再読み込みを行っています。補足②
お気に入り処理を
each
など繰り返し処理内で行いたいこともあると思います。
その場合は、指定したid="favorite-movie
を下記のように変更して下さい。<%= @movies.each do |movie| %> <div id="favorite-movie-<%= movie.id %>"> <%= render partial: "movie/favorite", locals: {movie: movie} %> </div> <% end %>
views/movies/favorite.js.erb
$("#favorite-movie-<%= @movie.id %>").html("<%= j(render partial: "movies/favorite", locals: {movie: @movie}) %>");id属性にid値を指定することで、繰り返し処理内でもお気に入りの処理を行うことができます。
- 投稿日:2021-03-02T11:46:57+09:00
Railsチュートリアル 12章まとめ
Railsチュートリアル 12章まとめ
この章でやること
- よくあるパスワードを忘れた際にパスワードの再設定をできるようにする
- パスワードリセットコントローラーを作成する
- メールを送信できるようにする
- パスワードを再設定できるようにする
12.1 PasswordResetsリソース
PasswordResetsリソースのモデリングから
必要なデータ(再設定用のダイジェストなど)をUserモデルに追加していくPasswordResetsもリソースとして扱っていきたいので、まずは標準的なRESTfulなURLを用意。
12.1.1 PasswordResetsコントローラ
コントローラーを生成 newとeditのメソッドも追加
$ rails generate controller PasswordResets new edit --no-test-framework
no-test-framework
はテストを生成しないオプション
今回は単体テストをせず統合テストでカバーするためリスト 12.1: パスワード再設定用リソースを追加する
config/routes.rbRails.application.routes.draw do root 'static_pages#home' get '/help', to: 'static_pages#help' get '/about', to: 'static_pages#about' get '/contact', to: 'static_pages#contact' get '/signup', to: 'users#new' get '/login', to: 'sessions#new' post '/login', to: 'sessions#create' delete '/logout', to: 'sessions#destroy' resources :users resources :account_activations, only: [:edit] resources :password_resets, only: [:new, :create, :edit, :update] #対応するビューを作るため endRESTfulのルーティング↓
HTTPリクエスト URL Action 名前付きルート GET password_resets/new new new_password_reset_path POST password_resets create password_resets_path GET password_resets/トークン/edit edit edit_password_reset_url(token) PATCH password_resets/トークン update password_reset_url(token) パスワード再設定画面へのリンクを追加する
app/views/sessions/new.html.erb<% provide(:title, "Log in") %> <h1>Log in</h1> <div class="row"> <div class="col-md-6 col-md-offset-3"> <%= form_with(url: login_path, scope: :session, local: true) do |f| %> <%= f.label :email %> <%= f.email_field :email, class: 'form-control' %> <%= f.label :password %> <%= link_to "(forgot password)", new_password_reset_path %> #パスワード忘れた時のリンク作成→createアクションへ <%= f.password_field :password, class: 'form-control' %> <%= f.label :remember_me, class: "checkbox inline" do %> <%= f.check_box :remember_me %> <span>Remember me on this computer</span> <% end %> <%= f.submit "Log in", class: "btn btn-primary" %> <% end %> <p>New user? <%= link_to "Sign up now!", signup_path %></p> </div> </div>12.1.2 新しいパスワードの設定
Userモデルに2つのカラムを追加していく
・reset_digest属性 ハッシュ化した認証トークンを保存
・reset_sent_at属性 再設定のリンクに有効期限をつけるためにリンク送信時間を記憶migrationファイルを作成
$ rails generate migration add_reset_to_users reset_digest:string \ reset_sent_at:datetime$ rails db:migrateパスワード再設定用のメールアドレスを入力するビュー
password_resets/new.html.erb
を
sessions/new.html.erb
を参考に作成新しいパスワード再設定画面ビュー
app/views/password_resets/new.html.erb<% provide(:title, "Forgot password") %> <h1>Forgot password</h1> <div class="row"> <div class="col-md-6 col-md-offset-3"> <%= form_with(url: password_resets_path, scope: :password_reset, #createアクションへpath local: true) do |f| %> <%= f.label :email %> <%= f.email_field :email, class: 'form-control' %> <%= f.submit "Submit", class: "btn btn-primary" %> <% end %> </div> </div>12.1.3 createアクションでパスワード再設定
以下機能を持ったcreateアクションを定義する
- フォームから送信を行った後、メールアドレスをキーとしてユーザーをデータベースから見つける
- パスワード再設定用トークンと送信時のタイムスタンプでデータベースの属性を更新する
- ルートURLにリダイレクトし、フラッシュメッセージをユーザーに表示
- 送信が無効の場合は、ログイン同様にnewページを出力してflash.nowメッセージを表示
パスワード再設定用のcreateアクション
app/controllers/password_resets_controller.rbclass PasswordResetsController < ApplicationController def create @user = User.find_by(email: params[:password_reset][:email].downcase) #formで送信された小文字化したemailから@userを取得 if @user #もしuserが存在すれば @user.create_reset_digest #@userのcreate_reset..メソッドを発動→パスワード再設定の属性を設定 @user.send_password_reset_email #パスワード再設定のメールを送信する flash[:info] = "Email sent with password reset instructions" redirect_to root_url #root_urlに飛ばす else flash.now[:danger] = "Email address not found" #もしuserがなければ、メール見つからないとメッセージ表示 render 'new' end endUserモデルにパスワード再設定用メソッドを追加する
app/models/user.rbclass User < ApplicationRecord attr_accessor :remember_token, :activation_token, :reset_token #reset_token属性追加 before_save :downcase_email before_create :create_activation_digest . . . # アカウントを有効にする def activate update_attribute(:activated, true) update_attribute(:activated_at, Time.zone.now) end # 有効化用のメールを送信する def send_activation_email UserMailer.account_activation(self).deliver_now end # パスワード再設定の属性を設定する def create_reset_digest self.reset_token = User.new_token #トークンを作成しreset_token属性に代入 update_attribute(:reset_digest, User.digest(reset_token)) #Userモデルのreset_digestにresetトークンを入れてupdateする update_attribute(:reset_sent_at, Time.zone.now) #Userモデルのreset_sent_atに現在時刻を入れてupdate end # パスワード再設定のメールを送信する def send_password_reset_email UserMailer.password_reset(self).deliver_now #今すぐにメールを送信する end private # メールアドレスをすべて小文字にする def downcase_email self.email = email.downcase end # 有効化トークンとダイジェストを作成および代入する def create_activation_digest self.activation_token = User.new_token self.activation_digest = User.digest(activation_token) end end12.2 パスワード再設定のメール送信
・メールを送信できるようにする
12.2.1 パスワード再設定のメールとテンプレート
UserMailer.password_reset(self).deliver_nowこのコードを実装するためにUserメイラーにpassword_resetメソッドを作成し、テキストメールのテンプレートを変えていく
パスワード再設定のリンクをメール送信する
app/mailers/user_mailer.rbclass UserMailer < ApplicationMailer def password_reset(user) @user = user mail to: user.email, subject: "Password reset" end endパスワード再設定のテンプレート(テキスト)
app/views/user_mailer/password_reset.text.erbTo reset your password click the link below: <%= edit_password_reset_url(@user.reset_token, email: @user.email) %> This link will expire in two hours. If you did not request your password to be reset, please ignore this email and your password will stay as it is.パスワード再設定のテンプレート(HTML)
app/views/user_mailer/password_reset.html.erb<h1>Password reset</h1> <p>To reset your password click the link below:</p> <%= link_to "Reset password", edit_password_reset_url(@user.reset_token, email: @user.email) %> <p>This link will expire in two hours.</p> <p> If you did not request your password to be reset, please ignore this email and your password will stay as it is. </p>パスワード再設定のプレビューメソッド(完成)
test/mailers/previews/user_mailer_preview.rb# Preview all emails at http://localhost:3000/rails/mailers/user_mailer class UserMailerPreview < ActionMailer::Preview # Preview this email at # http://localhost:3000/rails/mailers/user_mailer/account_activation def account_activation user = User.first user.activation_token = User.new_token UserMailer.account_activation(user) end # Preview this email at # http://localhost:3000/rails/mailers/user_mailer/password_reset def password_reset #password_resetメソッド定義 user = User.first user.reset_token = User.new_token UserMailer.password_reset(user) end endこれでhttp://localhost:3000/rails/mailers/user_mailer/password_resetのURL
(localhost:3000の部分はAWSではサーバー名に書き換える.....com/rails/mailers/...)12.2.2 送信メールのテスト
メイラーメソッドのテストを書いていく
パスワード再設定用メイラーメソッドのテストを追加する
test/mailers/user_mailer_test.rbrequire 'test_helper' test "password_reset" do user = users(:michael) user.reset_token = User.new_token #userにreset_tokenを代入する mail = UserMailer.password_reset(user) #mail変数にメールを送る assert_equal "Password reset", mail.subject #メールのタイトルは"Password reset"になっているか assert_equal [user.email], mail.to #メールの宛先はuser.emailになっているか assert_equal ["noreply@example.com"], mail.from #差出人はexample.comになっているか assert_match user.reset_token, mail.body.encoded #reset_tokenが入っているか assert_match CGI.escape(user.email), mail.body.encoded #userの.emailはescapeされているか@が%になっているか end end12.3 パスワードを再設定する
次はPasswordResetsコントローラのeditアクションの実装
12.3.1 editアクションで再設定
先ほど定義したパスワード再設定の送信メールには、次のようなリンクが含まれている
https://example.com/password_resets/3BdBrXeQZSWqFIDRN8cxHA/edit?email=fu%40bar.comこのリンクを機能させるには、リンク先のパスワード再設定フォームを表示するビューを作る
注意点
メアドをキーにuserを検索するにはeditアクションとupdateアクションの両方でメアドが必要になる
フォームを一度送信してしまうと、メアドの情報は消えてしまう。
今回はこのメールアドレスを保持するため、隠しフィールドとしてページ内に保存する手法をとる
これにより、フォームから送信したときに、他の情報と一緒にメールアドレスが送信されるようになるパスワード再設定のフォーム
app/views/password_resets/edit.html.erb<% provide(:title, 'Reset password') %> <h1>Reset password</h1> <div class="row"> <div class="col-md-6 col-md-offset-3"> <%= form_with(model: @user, url: password_reset_path(params[:id]), local: true) do |f| %> <%= render 'shared/error_messages' %> <%= hidden_field_tag :email, @user.email %> #hidden_field_tagを追加しメールも送るようにする <%= f.label :password %> #password入力フォーム <%= f.password_field :password, class: 'form-control' %> <%= f.label :password_confirmation, "Confirmation" %> #確認用password入力フォーム <%= f.password_field :password_confirmation, class: 'form-control' %> <%= f.submit "Update password", class: "btn btn-primary" %> <% end %> </div> </div>
hidden_field_tag :email, @user.email
はメールアドレスがparams[:email]に保存される
f.hidden_field :email, @user.email
はparams[:user][:email]に保存される次はeditアクションにparams[:email]が入った@userインスタンス変数を定義
authenticated?メソッドを使って認証するパスワード再設定のeditアクション
app/controllers/password_resets_controller.rbclass PasswordResetsController < ApplicationController before_action :get_user, only: [:edit, :update] before_action :valid_user, only: [:edit, :update]#正しいユーザーだけ . private def get_user @user = User.find_by(email: params[:email]) #@userインスタンス変数=リンクで受け取ったメアドでuserを探す end # 正しいユーザーかどうか確認する def valid_user unless (@user && @user.activated? && #userが存在し、activated?がtrueかつ @user.authenticated?(:reset, params[:id])) #reset_digest(テーブルのデータ)がリンクのURLと一致しているか redirect_to root_url end end end抽象化したauthenticated?はsample_appで以下のように使われた
authenticated?(:reset, params[:id]) authenticated?(:remember, cookies[:remember_token]) authenticated?(:activation, params[:id])12.3.2 パスワードを更新する
AccountActivationsコントローラのeditアクションでは、ユーザーの有効化ステータスをfalseからtrueに変更したが、
今回の場合はフォームから新しいパスワードを送信するようになる。
したがって、フォームからの送信に対応するupdateアクションが必要
このupdateアクションでは、次の4つのケースを考慮する必要がある。
- パスワード再設定の有効期限が切れていないか
- 無効なパスワードであれば失敗させる(失敗した理由も表示する)
- 新しいパスワードが空文字列になっていないか(ユーザー情報の編集ではOKだった)
- 新しいパスワードが正しければ、更新する
(1)と(2)と(4)はこれまでの知識で対応できそうだが、(3)はどのように対応すれば良いのかあまり明確ではない
(1)「パスワード再設定の有効期限が切れていないか」については、editとupdateアクションに次のようなメソッドとbeforeフィルターを用意することで対応できそう
before_action :check_expiration, only: [:edit, :update] # (1)への対応案このcheck_expirationメソッドは、有効期限をチェックするPrivateメソッドとして定義
# 期限切れかどうかを確認する def check_expiration if @user.password_reset_expired?#有効期限が切れていたら flash[:danger] = "Password reset has expired." redirect_to new_password_reset_url end end期限切れかどうかを確認するインスタンスメソッド「password_reset_expired?」を使っている。
beforeフィルターで保護したupdateアクションを使うことで、(2)「無効なパスワードであれば失敗させる(失敗した理由も表示する)」と(4)「新しいパスワードが正しければ、更新する」のケースに対応することができそう
例えば(2)については、更新が失敗したときにeditのビューが再描画され、パーシャルにエラーメッセージが表示されるようにすれば解決できる。
(4)については、更新が成功したときにパスワードを再設定し、あとはログインに成功したときと同様の処理を進めていけば問題なさそう。ちょっと厄介なのが、パスワードが空文字だった場合の処理。
以前Userモデルを作っていたときに、パスワードが空でも良いallow_nilを実装をしたから
したがって、このケースについては明示的にキャッチするコードを追加する必要がある
これが考慮すべき点の(3)「 新しいパスワードが空文字列になっていないか(ユーザー情報の編集ではOKだった)」に当たる。
解決する方法として、今回は@userオブジェクトにエラーメッセージを追加する方法をとってみる次のように
errors.add
を使ってエラーメッセージを追加する。@user.errors.add(:password, :blank)このように書くと、パスワードが空だった時に空の文字列に対するデフォルトのメッセージを表示してくれるようになる
以上の結果をまとめると、(1)のpassword_reset_expired?の実装を除き、すべてのケースに対応したupdateアクションが完成
app/controllers/password_resets_controller.rbclass PasswordResetsController < ApplicationController before_action :get_user, only: [:edit, :update] before_action :valid_user, only: [:edit, :update] before_action :check_expiration, only: [:edit, :update] # (1)への対応 def new end def create @user = User.find_by(email: params[:password_reset][:email].downcase) if @user @user.create_reset_digest @user.send_password_reset_email flash[:info] = "Email sent with password reset instructions" redirect_to root_url else flash.now[:danger] = "Email address not found" render 'new' end end def edit end def update if params[:user][:password].empty? # (3)への対応 @user.errors.add(:password, :blank) render 'edit' elsif @user.update(user_params) # (4)への対応 log_in @user flash[:success] = "Password has been reset." redirect_to @user else render 'edit' # (2)への対応 end end private def user_params params.require(:user).permit(:password, :password_confirmation) end # beforeフィルタ def get_user @user = User.find_by(email: params[:email]) end # 有効なユーザーかどうか確認する def valid_user unless (@user && @user.activated? && @user.authenticated?(:reset, params[:id])) redirect_to root_url end end # トークンが期限切れかどうか確認する def check_expiration if @user.password_reset_expired? flash[:danger] = "Password reset has expired." redirect_to new_password_reset_url end end endあとは、残しておいたpassword_reset_expired?の実装だけ
@user.password_reset_expired?上のコードを動作させるために、password_reset_expired?メソッドをUserモデルで定義していく
このメソッドではパスワード再設定の期限を設定して、2時間以上パスワードが再設定されなかった場合は期限切れとする処理を行うreset_sent_at < 2.hours.ago上の
<
記号を「〜より少ない」と読んでしまうと、「パスワード再設定メール送信時から経過した時間が、2時間より少ない場合」となってしまうので、「少ない」ではなく「〜より早い時刻」と読む。
こうすると「パスワード再設定メールの送信時刻が、現在時刻より2時間以上前(早い)の場合」となり、 期待どおりの条件となるapp/models/user.rbclass User < ApplicationRecord # パスワード再設定の期限が切れている場合はtrueを返す def password_reset_expired? reset_sent_at < 2.hours.ago endこれでpassword_resets_controller.rbのcreateアクションが動く
12.3.3 パスワードの再設定をテストする
送信に成功した場合と失敗した場合の統合テストを作成する
まずは統合テストを作成
$ rails generate integration_test password_resets invoke test_unit create test/integration/password_resets_test.rbパスワード再設定をテストする手順は、
- 最初に「forgot password」フォームを表示して無効なメールアドレスを送信
- 次はそのフォームで有効なメールアドレスを送信
- パスワード再設定用トークンが作成され、再設定用メールが送信される。
- メールのリンクを開いて無効な情報を送信
- そのリンクから有効な情報を送信して、それぞれが期待どおりに動作することを確認
test/integration/password_resets_test.rbrequire 'test_helper' class PasswordResetsTest < ActionDispatch::IntegrationTest def setup ActionMailer::Base.deliveries.clear @user = users(:michael) end test "password resets" do get new_password_reset_path#forgot passのリンク取得 assert_template 'password_resets/new'#passリセット画面出るか assert_select 'input[name=?]', 'password_reset[email]'#input[name]がpassword_resetにあるか # メールアドレスが無効 post password_resets_path, params: { password_reset: { email: "" } } assert_not flash.empty?#flashが空でないか assert_template 'password_resets/new'#newの画面を再描写するか # メールアドレスが有効 post password_resets_path, params: { password_reset: { email: @user.email } } assert_not_equal @user.reset_digest, @user.reload.reset_digest#reset_digestカラムがreload後変化ないか assert_equal 1, ActionMailer::Base.deliveries.size #mailは1通だけ送ったか assert_not flash.empty?#flashが空でないか assert_redirected_to root_url#rootへリダイレクトしたか # パスワード再設定フォームのテスト user = assigns(:user) # メールアドレスが無効 get edit_password_reset_path(user.reset_token, email: "") assert_redirected_to root_url # 無効なユーザー user.toggle!(:activated)#activatedカラムをtrueかfalseに反転(この場合false) get edit_password_reset_path(user.reset_token, email: user.email) assert_redirected_to root_url#失敗することを確認 user.toggle!(:activated)#trueに戻す # メールアドレスが有効で、トークンが無効 get edit_password_reset_path('wrong token', email: user.email) assert_redirected_to root_url # メールアドレスもトークンも有効 get edit_password_reset_path(user.reset_token, email: user.email) assert_template 'password_resets/edit'#editページが描写されるか assert_select "input[name=email][type=hidden][value=?]", user.email#inputタグに正しい名前、type="hidden"、メールアドレスがあるかどうかを確認 # 無効なパスワードとパスワード確認 patch password_reset_path(user.reset_token),#createアクションへ送信 params: { email: user.email, user: { password: "foobaz", password_confirmation: "barquux" } } assert_select 'div#error_explanation'#errorのdivタグ出るか確認 # パスワードが空 patch password_reset_path(user.reset_token), params: { email: user.email, user: { password: "", password_confirmation: "" } } assert_select 'div#error_explanation'#errorのdivタグ出るか確認 # 有効なパスワードとパスワード確認 patch password_reset_path(user.reset_token), params: { email: user.email, user: { password: "foobaz", password_confirmation: "foobaz" } } assert is_logged_in?#ログインされたか assert_not flash.empty?#flashは空でないか assert_redirected_to user#@userページへ飛ぶか end end今回の新しい要素はinputタグぐらい
assert_select "input[name=email][type=hidden][value=?]", user.email上のコードは、inputタグに正しい名前、type="hidden"、メールアドレスがあるかどうかを確認している
<input id="email" name="email" type="hidden" value="michael@example.com" />テストコードは green になるはず
演習
1リスト 12.6にあるcreate_reset_digestメソッドはupdate_attributeを2回呼び出していますが、これは各行で1回ずつデータベースへ問い合わせしていることになります。リスト 12.20に記したテンプレートを使って、update_attributeの呼び出しを1回のupdate_columns呼び出しにまとめてみましょう(これでデータベースへの問い合わせが1回で済むようになります)。また、変更後にテストを実行し、 green になることも確認してください。ちなみにリスト 12.20にあるコードには、前章の演習(リスト 11.39)の解答も含まれています。
user.rb# パスワード再設定の属性を設定する def create_reset_digest self.reset_token = User.new_token#ランダムな文字列をいれる update_columns(reset_digest: User.digest(reset_token), reset_sent_at: Time.zone.now ) end2リスト 12.21のテンプレートを埋めて、期限切れのパスワード再設定で発生する分岐(リスト 12.16)を統合テストで網羅してみましょう(12.21 のコードにあるresponse.bodyは、そのページのHTML本文をすべて返すメソッドです)。期限切れをテストする方法はいくつかありますが、リスト 12.21でオススメした手法を使えば、レスポンスの本文に「expired」という語があるかどうかでチェックできます(なお、大文字と小文字は区別されません)。
password_reset_test.rbtest "expired token" do get new_password_reset_path#reset_pathを取得 post password_resets_path,#mailを送る params: { password_reset: { email: @user.email } } @user = assigns(:user) @user.update_attribute(:reset_sent_at, 3.hours.ago)#メールを送った時間を3時間前にセット patch password_reset_path(@user.reset_token),#passwordの再設定をする params: { email: @user.email, user: { password: "foobar", password_confirmation: "foobar" } } assert_response :redirect follow_redirect! assert_match /expired/i, response.body#Html bodyタグにexpiredがあるか確認 end3 2時間経ったらパスワードを再設定できなくする方針は、セキュリティ的に好ましいやり方でしょう。しかし、もっと良くする方法はまだあります。例えば、公共の(または共有された)コンピューターでパスワード再設定が行われた場合を考えてみてください。仮にログアウトして離席したとしても、2時間以内であれば、そのコンピューターの履歴からパスワード再設定フォームを表示させ、パスワードを更新してしまうことができてしまいます(しかもそのままログイン機構まで突破されてしまいます!)。この問題を解決するために、リスト 12.22のコードを追加し、パスワードの再設定に成功したらダイジェストをnilになるように変更してみましょう 。
app/controllers/password_resets_controller.rbdef 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 end4 リスト 12.18に1行追加し、1つ前の演習課題に対するテストを書いてみましょう。ヒント: リスト 9.25のassert_nilメソッドとリスト 11.33のuser.reloadメソッドを組み合わせて、reset_digest属性を直接テストしてみましょう。
test "password resets" do .... assert_nil user.reload.reset_digest # 追加12.4 本番環境でのメール送信(再掲)
これでパスワード再設定の実装も終わった。
あとはproduction環境でも動くようにするだけ前章でやっているので、細かな設定はスキップ
以下スキップOK
本番環境からメール送信するために、「Mailgun」というHerokuアドオンを利用してアカウントを検証します
本チュートリアルでは、「starter tier」というサービスを使うアプリケーションでMailgunアドオンを使うには、production環境のSMTPに情報を記入する
本番Webサイトのアドレスをhost変数に定義する必要がある。を自分のHerokuのURLに設定してください。その他の設定はこのまま使えるconfig/environments/production.rbRails.application.configure do . . . config.action_mailer.raise_delivery_errors = true config.action_mailer.delivery_method = :smtp host = '<your heroku app>.herokuapp.com' config.action_mailer.default_url_options = { host: host } ActionMailer::Base.smtp_settings = { :port => ENV['MAILGUN_SMTP_PORT'], :address => ENV['MAILGUN_SMTP_SERVER'], :user_name => ENV['MAILGUN_SMTP_LOGIN'], :password => ENV['MAILGUN_SMTP_PASSWORD'], :domain => host, :authentication => :plain, } . . . endこの時点で、Gitのトピックブランチをmasterにマージしておく
`
$ rails test
$ git add -A
$ git commit -m "Add password reset"
$ git checkout master
$ git merge password-reset
続いてリモートリポジトリにプッシュし、Herokuにデプロイします。
$ rails test $ git push && git push heroku $ heroku run rails db:migrateMailgunのHerokuアドオンをまだ追加していなければ、次のコマンドを実行
$ heroku addons:create mailgun:starter注: herokuコマンドのバージョンが古いとここで失敗するかも。その場合はHeroku Toolbeltを使って最新版に更新するか、次の古い文法のコマンドを試す
$ heroku addons:add mailgun:starterメール設定にはMailgunアカウントのuser_nameとpassword設定を記入する行もありますが、そこには記入せず、必ず環境変数「ENV」に設定するよう注意。
本番運用するアプリケーションでは、暗号化されていないIDやパスワードのような重要なセキュリティ情報は「絶対に」ソースコードに直接書き込まない。
そのような情報は環境変数に記述し、そこからアプリケーションに読み込む。
今回の場合、そうした変数はMailgunアドオンが自動的に設定してくれる最後に、受信メールの認証を行います。以下のコマンドを打つと、Mailgun ダッシュボードのURLが表示されるのでブラウザで開きます。
$ heroku addons:open mailgunMailGun公式ドキュメントに従い、受信するメールアドレスを認証。
画面左側の「Sending」→「Domains」のリストにある「sandbox」で始まるサンドボックスドメインを選択。
画面右側の「Authorized Recipients」から受信メールアドレスを認証し、本番環境でのメール送信準備は完了。Herokuへのデプロイが完了したら、ログインページの[forgot password]リンクをクリックして、production環境でパスワードの再設定を行ってみる。
フォームから送信すると、メールが送信されてくるはず。
記載されているリンクをクリックし、無効なパスワードと有効なパスワードをそれぞれ試してみましょう。演習
production環境でユーザー登録を試してみましょう。ユーザー登録時に入力したメールアドレスにメールは届きましたか?
メールを受信できたら、実際にメールをクリックしてアカウントを有効化してみましょう。また、Heroku上のログを調べてみて、有効化に関するログがどうなっているのか調べてみてください。ヒント: ターミナルからheroku logsコマンドを実行してみましょう。
アカウントを有効化できたら、今度はパスワードの再設定を試してみましょう。正しくパスワードの再設定ができたでしょうか?herokuエラー中なのでスキップ
- 投稿日:2021-03-02T11:08:02+09:00
railsアプリのデプロイエラーの原因が、gemのuglifierのバージョンに依るものだった話
railsアプリのデプロイがこけたので、その原因を追求してみた。
デプロイ時のエラーメッセージを読むと、以下の様なメッセージが頻出していた。
Caused by: V8::Error: SyntaxError: Unexpected token: name (jsの変数名) at js_error (<eval>:3623:12167) at croak (<eval>:3623:22038) at token_error (<eval>:3623:22175) at unexpected (<eval>:3623:22263) at semicolon (<eval>:3623:22781) at simple_statement (<eval>:3623:25959) at <eval>:3623:23747 at <eval>:3623:22954 at block_ (<eval>:3623:28083) at ctor.body (<eval>:3623:27686) at function_ (<eval>:3623:27782) at <eval>:3623:24469 at <eval>:3623:22954 at block_ (<eval>:3623:28083) at <eval>:3623:23857 at <eval>:3623:22954 at <eval>:3624:3759 at parse (<eval>:3624:3999) at parse (<eval>:3958:22) at uglifier (<eval>:4003:13)下記の記事で、jsで
let
を使用している場合var
に変更すると良い、というアドバイスがあったので、変更してみた結果、デプロイ成功。
どうやら、gemのuglifierのバージョンが足りず、ES6をサポートしていなかったため、letという新しい書き方を許容していなかったため、エラーが起きたらしい。
https://stackoverflow.com/questions/39221152/rails-5-heroku-deploy-error-execjsprogramerror-syntaxerror-unexpected-token尚、ES6の書き方を許容するのはUglifier 3.2.0以降からだそう。
- 投稿日:2021-03-02T10:37:26+09:00
マジックナンバーとrailsにおける定数管理gem "config"
この記事の目的
開発中に「マジックナンバー」と言う聞き慣れない(恥ずかしながら)言葉を耳にし、その意味と問題点を調べたのでまとめました。また、Railsにおける「マジックナンバー」への対応策の一つを紹介したいと思います。
この記事で以下のことが分かると思います。
- マジックナンバーとは何か
- 開発におけるマジックナンバーの問題点
- その解決策の一つであるrailsのgem "config"の扱い方
1. マジックナンバーとは
マジックナンバーとは、ハードコーディングされた数値のことです。
ハードコーディングとは、固有名詞・固有値をソースコード内に直接埋め込んだコードのことです。
ソースコードとは、開発者がコーディングしているファイルの内容のことです(index.html, stylesheet.cssなど大体そうかと)。ハードコードの中でも数値のことを、プログラミングにおいて「マジックナンバー」と呼びます。
index.html.erb# ハードコードの例 <li> <p>山田太郎<p> <p><span>23</span>歳です</p> # 23がマジックナンバー </li> <li> <p>田中花子</p> <p><span>73</span>歳です</p> # 73がマジックナンバー </li> :上記はSNSにおけるユーザー一覧の例です。恐らく、こんな直接的な書き方はしないと思います。
一般にマジックナンバーはNGとされています(そもそもハードコードがNGなのかな...)。
その理由を次に説明します。2. マジックナンバーが何故いけないのか
一般にマジックナンバーは、コーディングの際に避けるべきだ、と言われています。その主な理由は3つだと思われます。
- 数字だけ見ても何を意味しているか分からない[可読性が低い]
- 変更する場合は全てのマジックナンバーが利用されている場所を変更する必要がある [保守性が低い]
- セキュリティの安全性が下がる[脆弱性を生む]
3つの理由の説明はその他のサイトでも説明されていると思います。ここでは、少し具体的なマジックナンバーを使ってしまいそうな例を提示してみます(実体験)。
products_controller.rbdef show @product = Spree::Product.find(params[:id]) @related_products = @product.related_products(4) # マジックナンバー endこの
4
という数値が何を意味しているか、このコードを見ただけでは分かりません。
これは下の画像における表示個数の個数を表します。このように、実際の機能に対してコードの意味が伝わりにくくなってしまうのがマジックナンバーの良くない点です。
これ以降では、Railsにおけるマジックナンバーに関する対応策であるconfig
と言うgemの扱いを簡単に紹介したいと思います。[画像挿入]
3. Railsにおける定数管理のgem "config"
以下で具体的な使い方は説明しますが、このgemのいいところは以下の2つだと思います
- 定数の意味をソースコード内で表現できる
- 一箇所で定数を管理できる
これがリソースです。
rubyconfig/configgem "config"は、定数管理を指定のYAMLファイルを用いて一箇所で行う為のgemです。場合によっては環境毎に(development, production, test)定数の値を変更することもできます。
ここでは、簡単な方法を一つ紹介し、先程の例を解決したいと思います。4. gem "config"の使い方
ここでは最も簡単だと考えられる、一つのファイルで定数を管理する方法を示します。
その場合config/settings.yml
のみ書き込むことで定数管理を実現します。(1) gem "config"の準備
Gemfilegem "config"Terminal$ bundle install $ rails g config:install create config/initializers/config.rb create config/settings.yml create config/settings.local.yml create config/settings create config/settings/development.yml create config/settings/production.yml create config/settings/test.yml append .gitignore以下のようにファイルが追加されます(自動的に.gitignoreに追加されていました)。
(2) 定数を管理する
config
では、木構造のように定数を管理します。
今回は「商品の個数」の中で、「上限の数値」「テストの際の数値」といった定数を定義します。(このように記載することで、先程の例を解決します)config/settings.ymlproducts_count: max_count: 4 test_count: 10このファイルにて定数の管理を、一括で扱います。また、定数への意味付けも可能になります
[可読性上がる]。(3) - 1 定数を取り出す
定数は以下のように記載することで取り出すことができます。
Terminal$ rails c [1] pry(main)> Settings.products_count.max_count => 4 [2] pry(main)> Settings.products_count.test_count => 10設定に応じて
Setteings. ~ .~
の~部分を置き換えることで、定数を取り出すことが可能になります。(3) - 2 定数を取り出す(ファイル内で)
以上を参考に先程の例を、このように解決することができます。
products_controller.rbdef show @product = Spree::Product.find(params[:id]) @related_products = @product.related_products(Settings.products_count[:max_count]) endマジックナンバーを比べていた時に比べ、可読性が少しは上がります(恐らく)。
以上です。この記事が少しでも参考になるといいな、と思います。
余談
マジックナンバーには、エンジニアの方にコードレビューして頂いた際に出会いました。「実装要件を満たした」と思った自分に対して、予想のできない指摘が飛んできました。実務の世界では当たり前のことができていない、本当にまだまだなのだな、という実感を沸いた経験でした。
リソース
以下を参考にさせて頂きました
https://wa3.i-3-i.info/word12868.html
https://qiita.com/RyoheiHashimoto/items/38ec132bd2852238295e
https://it-biz.online/it-skills/hard-coding/
また、gem "config"に関しましてはその他多くの説明が存在しますので、詳細はそちらを参考にすると実装が捗るかと思います。
- 投稿日:2021-03-02T08:52:54+09:00
Rubyのincludeを理解したい①
書くこと
Rubyのincludeについて、メソッド探索について学んだ結果見えた事実
こちらの記事を参考にさせていただきました。ありがとうございました。【ruby】 メソッド探索から見る、モジュール・特異メソッド・特異クラス
自分なりに、こちらの記事を設計図と車で喩え直してみました。
(こちらの記事は、2部構成になっているうちの、第一部です。)
次の記事はこちら→クラスとインスタンス
本題は、Rubyのincludeについてですが、この記事では後に理解がしやすくなるようにクラスとインスタンス、それに伴う変数とメソッドについて掘り下げていきます。
Rubyには、クラスとインスタンスと言う概念があります。
車を作る時で考えてみましょう。- クラスは、設計図です。
- インスタンスは、その設計図をもとに作られた実際の車です。
もう少し理解してみましょう。クラスとインスタンスには、それぞれメソッドなるものが存在します。
- クラスメソッドは、設計図全体に適応される共通オプションです。
手引書のルールみたいなもんですね
- インスタンスメソッドは、それぞれの車の特徴・動作などをそれぞれに付け加えます。
車は「走る」、あの車は「赤い」などと言ったことを表します。
example.rbclass Car #クラス def color #インスタンスメソッド @color = red #@colorはインスタンス変数と呼ばれます。@colorには、redが代入されています。 end end model_car = Car.new #Carクラスのインスタンスを生成し、model_carに代入しています。 model_car.color #Carクラスで定義されているcolorと言うインスタンスメソッドを、直前で生成したmodel_carインスタンスにかけています。はい、ここまでは、多少Rubyについて勉強している方であれば、理解いただけたと思います。
わからない人も、ふーんと思いながら聞き進めてください。本当に設計図と車の関係?
さて、ここから少しずつ、僕がなるほどなるほどと納得し直した部分に入っていきます。
まずは、先ほどのクラスとメソッドの関係性について
おさらいするとクラスは、設計図です。
インスタンスは、その設計図をもとに作られた実際の車です。
と端的に書きました。
設計図と車の関係であれば
「設計図通りの色や大きさ、エンジンなどの特徴・部品で車が作られる」
つまり、設計図に書いてある内容を、それぞれの車が持つことになります。しかし、クラスとインスタンスの関係は、少し違うようなのです。
どのように違うのでしょうか?実は常に「参照」している
結論から述べると
特徴や動作を「持つ」のではなく、一回一回、設計図を「参照しながら」車は、動いているのです。ちょっとイメージが湧きにくいかと思いますので、先ほどのコードをもとに考えてみましょう。
example.rbclass Car #Carクラスという設計図には、 def color #colorでredを呼び出せるインスタンスメソッドが存在します。 @color = red end end model_car = Car.new #1 model_car.color #ここで、colorを呼び出しています。 #2ここからが、最も大切な部分ですので、注意して読んでみてください。
まず、ここで何が起きているか順を追って説明します。
- model_car変数に、Carクラスのインスタンスが代入されます
- model_carインスタンスに、colorインスタンスメソッドを呼び出します。その結果、model_carオブジェクトはインスタンス変数の@colorを持つことになります。
今の説明から、model_carと言う箱の中に、@colorがあるということをイメージできるでしょうか。
しかし、その箱の中には、colorというインスタンスメソッドは存在しません。つまり、オブジェクトは@colorと言うインスタンス変数を持つことができるけど、colorというメソッドを持つことができない。という結論になります。
そうすると、クラスがインスタンスメソッドを持つことになり、インスタンスはクラスのメソッドを常に参照することになりそうです。
まとめると
オブジェクトは インスタンス変数 と クラスへの参照 を持つ
クラスは インスタンスメソッド を持つと言うことができます。
No Method Errorが起きてしまうわけ
もう少しだけ話を膨らませます。
もし、このクラス(設計図)に、言及がないメソッドが呼び出されたらどうでしょうか?example.rbclass Car #Carクラスには、wheelメソッドは存在しません。 def color @color = red end end model_car = Car.new model_car.wheel #wheelを参照しようとしています #→No Method Errorこのような状態で、No Method Errorが発生します。
このエラーが意味するところは「そんな特徴は、設計図に書いてねーよ!」
ということです。次につながる話ですので、もう少し補足すると「この設計図には書いてないから、この設計図の元になったものを追ってみよう。」
と判断させることもできます。このことについては、次の記事で触れたいと思います。
最後に
最後まで読んでいただき、ありがとうございます。
ソースコード、記事の書き方について「もっとこうしたほうがいいよ!」というご意見、「そこどうなっているの?」というご質問など、お待ちしております。参考文献
- 投稿日:2021-03-02T08:34:37+09:00
Railsチュートリアル11章まとめ
この章でやること
本当にそのメールアドレスの持ち主なのかどうかを確認できるようにする。
SMS認証的なやつ
(1)有効化トークンやダイジェストを関連付けておいた状態で、
(2)有効化トークンを含めたリンクをユーザーにメールで送信し、
(3)ユーザーがそのリンクをクリックすると有効化できるようにするアカウントを有効化する基本的な手順は
1. ユーザーの初期状態は「有効化されていない」(unactivated)にしておく。
2. ユーザー登録が行われたときに、有効化トークンと、それに対応する有効化ダイジェストを生成する。
3. 有効化ダイジェストはデータベースに保存しておき、有効化トークンはメールアドレスと一緒に、ユーザーに送信する有効化用メールのリンクに仕込んでおく
4. ユーザーがメールのリンクをクリックしたら、アプリケーションはメールアドレスをキーにしてユーザーを探し、データベース内に保存しておいた有効化ダイジェストと比較することでトークンを認証する。
5. ユーザーを認証できたら、ユーザーのステータスを「有効化されていない」から「有効化済み」(activated)に変更する。今回実装するアカウント有効化や次章でのパスワード再設定の仕組みはよく似た点が多いので、多くのアイデアを使い回すことができる
それぞれの仕組みの似ている点をまとめる
検索キー string digest authentication password password_digest authenticate(password) id remember_token remember_digest authenticated?(:remember, token) activation_token activation_digest authenticated?(:activation, token) reset_token reset_digest authenticated?(:reset, token) 今時点意味分からなくてもOK
11.1 AccountActivationsリソース
セッション機能を使って、アカウントの有効化という作業を「リソース」としてモデル化する
なお、アカウント有効化もリソースとして扱うが、少し使い方が異なる。
例えば、有効化用のメールのリンクにアクセスして有効化のステータスを変更する場合、RESTのルールに従うと通常はPATCHリクエストとupdateアクションになる。しかし、有効化リンクはメールでユーザーに送られるので、ユーザーがこのリンクをクリックすれば、それはブラウザで普通にクリックしたときと同じであり、その場合ブラウザから発行されるのは(updateアクションで使うPATCHリクエストではなく)GETリクエストになってしまう。
このため、ユーザーからのGETリクエストを受けるために、(本来であればupdateのところを)editアクションに変更して使ってく作業始める前にGitで新機能用のトピックブランチを作成
$ git checkout -b account-activation11.1.1 AccountActivationsコントローラ
AccountActivationsリソースを作るために、まずはAccountActivationsコントローラを生成
$ rails generate controller AccountActivations後ほど詳しく説明があるが、有効化のメールには次のURLを含めることで認証する
edit_account_activation_url(activation_token, ...)
つまり、editアクションへの名前付きルートが必要になるので、
ルーティングにアカウント有効化用のresources行を追加するconfig/routes.rbresources :users resources :account_activations, only: [:edit] #リソースを追加
HTTPリクエスト URL Action 名前付きルート GET /account_activation/トークン/edit edit edit_account_activation_url(token) 先にアカウント有効化用のデータモデルとメイラーを作成し、終わったらeditアクションを編集する
演習
現時点でテストスイートを実行すると green になることを確認してみましょう。
→パスする
表 11.2の名前付きルートでは、_pathではなく_urlを使うように記してあります。なぜでしょうか? 考えてみましょう。ヒント: 私達はこれからメールで名前付きルートを使います。
→pathは相対パス、urlは絶対パス メールからリンクしてもらうので、絶対パスを使う11.1.2 AccountActivationのデータモデル
有効化のメールには一意の有効化トークンが必要になる。
そのため、「送信メールとデータベースに同じ文字列を置いておき、比較して認証する」というのが思いつくが、この方法では万が一データベースの内容が漏れたとき、危ないなので仮想的な属性を使ってハッシュ化した文字列をデータベースに保存するようにして比べる
仮想的な属性は
user.activation_token
で
このようなメソッドでユーザーを認証していく
user.authenticated?(:activation, token)
(まだ使えない)activated属性をusersテーブルに追加して論理値を取れるようにもする。↓
if user.activated? ...
本チュートリアルで使うことはないが、ユーザーを有効にしたときの日時も念のために記録
次のマイグレーションをコマンドラインで実行してのデータモデルを追加
$ rails generate migration add_activation_to_users \ activation_digest:string activated:boolean activated_at:datetime次に、activated属性のデフォルトの論理値をfalseにしておく
db/migrate/[timestamp]_add_activation_to_users.rbclass AddActivationToUsers < ActiveRecord::Migration[6.0] def change add_column :users, :activation_digest, :string add_column :users, :activated, :boolean, default: false add_column :users, :activated_at, :datetime end end$ rails db:migrateActivationトークンのコールバック
createでユーザーを新規登録する前に、有効化トークンや有効化ダイジェストは作成し、メールを送る必要がある
前もメールアドレスをデータベースに保存する前に、メールアドレスを全部小文字に変換するため、before_save
コールバックにdowncaseメソッドをバインドした。
before_save
コールバックを用意しておくと、オブジェクトが1保存される直前、2オブジェクトの作成時や3更新時にそのコールバックが呼び出されるようになる。
しかし今回は、オブジェクトが作成されたときだけコールバックを呼び出し、それ以外のときには呼び出したくない
そこでbefore_create
コールバックが必要となる
before_create :create_activation_digest
上のコードはメソッド参照と呼ばれ、こうするとRailsはcreate_activation_digestというメソッドを探し、ユーザーを作成する前に実行するようになる
create_activation_digestメソッド
自体はUserモデル内でしか使わないので、外部に公開する必要はない
なのでprivateキーワードを指定して、このメソッドをRuby流に隠蔽するprivate def create_activation_digest # 有効化トークンとダイジェストを作成および代入する endクラス内でprivateキーワードより下に記述したメソッドは自動的に非公開になる。
コンソールで確認すると..$ rails console >> User.first.create_activation_digest NoMethodError: private method `create_activation_digest' called for #<User>privete下のメソッドは呼び出せなくなる
今回のbefore_createコールバックを使う目的は、トークンとそれに対応するダイジェストを割り当てるため
self.activation_token = User.new_token #User.new_tokenはランダムな文字列を作るメソッドとして定義ずみ self.activation_digest = User.digest(activation_token)このコードでは、記憶トークンや記憶ダイジェストのために作ったメソッドを使いまわしている
9章で作成したrememberメソッドと比べると...# 永続セッションのためにユーザーをデータベースに記憶する def remember self.remember_token = User.new_token update_attribute(:remember_digest, User.digest(remember_token)) end主な違いは、後者のupdate_attributeの使い方にある。
記憶トークンやダイジェストは既にデータベースにいるユーザーのために作成されるのに対し、
before_createコールバックの方はユーザーが作成される前に呼び出される点が異なる。
このコールバックがあることで、User.newで新しいユーザーが定義されると、activation_token属性やactivation_digest属性が得られるようになる
後者のactivation_digest属性は既にデータベースのカラムとの関連付けができあがっているので、ユーザーが保存されるときに一緒に保存される。実装のコードはこうなる
以前に実装したメールアドレスを小文字にするメソッドもメソッド参照に切り替えている点に注意app/models/user.rbclass User < ApplicationRecord attr_accessor :remember_token, :activation_token #追加 before_save :downcase_email #メソッド参照 before_create :create_activation_digest . private # メールアドレスをすべて小文字にする def downcase_email self.email = email.downcase end # 有効化トークンとダイジェストを作成および代入する def create_activation_digest self.activation_token = User.new_token self.activation_digest = User.digest(activation_token) end endサンプルユーザーの生成とテスト
先に進む前に、サンプルデータとfixtureも更新し、テスト時のサンプルとユーザーを事前に有効化しておく
Time.zone.now
はRailsの組み込みヘルパーで、サーバーのタイムゾーンに応じたタイムスタンプを返す。db/seeds.rb# メインのサンプルユーザーを1人作成する User.create!(name: "Example User", email: "example@railstutorial.org", password: "foobar", password_confirmation: "foobar", admin: true, activated: true,#有効化しておく activated_at: Time.zone.now) # 追加のユーザーをまとめて生成する 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, activated: true,#有効化しておく activated_at: Time.zone.now) endtest/fixtures/users.ymlmichael: name: Michael Example email: michael@example.com password_digest: <%= User.digest('password') %> admin: true activated: true activated_at: <%= Time.zone.now %> archer: name: Sterling Archer email: duchess@example.gov password_digest: <%= User.digest('password') %> activated: true activated_at: <%= Time.zone.now %> lana: name: Lana Kane email: hands@example.gov password_digest: <%= User.digest('password') %> activated: true activated_at: <%= Time.zone.now %> malory: name: Malory Archer email: boss@example.gov password_digest: <%= User.digest('password') %> activated: true activated_at: <%= Time.zone.now %> <% 30.times do |n| %> user_<%= n %>: name: <%= "User #{n}" %> email: <%= "user-#{n}@example.com" %> password_digest: <%= User.digest('password') %> activated: true activated_at: <%= Time.zone.now %> <% end %>データベースを初期化して、サンプルデータを再度生成し直す
$ rails db:migrate:reset $ rails db:seed演習
本項での変更を加えた後、テストスイートが green のままになっていることを確認してみましょう。
コンソールからUserクラスのインスタンスを生成し、そのオブジェクトからcreate_activation_digestメソッドを呼び出そうとすると(Privateメソッドなので)NoMethodErrorが発生することを確認してみましょう。また、そのUserオブジェクトからダイジェストの値も確認してみましょう。>> user = User.new >> user.create_activation_digest NoMethodError: private method `create_activation_digest' called for #<User:0x000000045483b0> Did you mean? restore_activation_digest! from (irb):2 >> user.digest NoMethodError: undefined method `digest' for #<User:0x000000045483b0> from (irb):3 >> user.activation_digest => nilアクセスできず、nilになる
リスト 6.35で、メールアドレスの小文字化にはemail.downcase!という(代入せずに済む)メソッドがあることを知りました。このメソッドを使って、リスト 11.3のdowncase_emailメソッドを改良してみてください。また、うまく変更できれば、テストスイートは成功したままになっていることも確認してみてください。
user.rbdef downcase_email email.downcase! #破壊的メソッドを使えばOK end11.2 アカウント有効化のメール送信
データのモデル化が終わった
- 今度は有効化メールの送信に必要なコードを追加していく。
- Action Mailerライブラリを使ってUserのメイラーを追加する
- メイラーはUsersコントローラのcreateアクションで有効化リンクをメール送信するために使う
- メイラーの構成はコントローラのアクションとよく似ており、メールのテンプレートをビューと同じ要領で定義できる
- メールのテンプレートの中に有効化トークンとメールアドレス(= 有効にするアカウントのアドレス)のリンクを含め、使っていく
11.2.1 送信メールのテンプレート
メイラーは、モデルやコントローラと同様にrails generateで生成
$ rails generate mailer UserMailer account_activation password_resetこれで今回必要となる
account_activation
メソッドと、次章で使うpassword_reset
メソッドが生成されたさらに生成したメイラーごとに、ビューのテンプレートが2つずつ生成されている。
1つはテキストメール用のテンプレート、1つはHTMLメール用のテンプレートで以下のコードが自動で記載される(後で変更する)app/views/user_mailer/account_activation.text.erbUserMailer#account_activation <%= @greeting %>, find me in app/views/user_mailer/account_activation.text.erbapp/views/user_mailer/account_activation.html.erb<h1>UserMailer#account_activation</h1> <p> <%= @greeting %>, find me in app/views/user_mailer/account_activation.html.erb </p>app/mailers/application_mailer.rbclass ApplicationMailer < ActionMailer::Base default from: "from@example.com" layout 'mailer' endapp/mailers/user_mailer.rbclass UserMailer < ApplicationMailer # Subject can be set in your I18n file at config/locales/en.yml # with the following lookup: # # en.user_mailer.account_activation.subject # def account_activation @greeting = "Hi" mail to: "to@example.org" end # Subject can be set in your I18n file at config/locales/en.yml # with the following lookup: # # en.user_mailer.password_reset.subject # def password_reset @greeting = "Hi" mail to: "to@example.org" end end生成されたメイラーの動作を簡単に追ってみる
application_mailer.rb
には、デフォルトのfromアドレスがある
さらにメールのフォーマットに対応するメイラーレイアウトも使われている
user_mailer.rb
の各メソッドには宛先メールアドレスもある
Railsチュートリアルでは関係ないが、生成されるHTMLメイラーのレイアウトやテキストメイラーのレイアウトはapp/views/layoutsで確認可能。生成されたコードにはインスタンス変数@greetingも含まれてるでは最初に、生成されたテンプレートをカスタマイズして、実際に有効化メールで使えるようにしていく。
次に、ユーザーを含むインスタンス変数を作成してビューで使えるようにし、user.emailにメール送信
mailにsubjectキーを引数として渡しているが、この値は、メールの件名にあたるapp/mailers/application_mailer.rbclass ApplicationMailer < ActionMailer::Base default from: "noreply@example.com"#メアド変更 layout 'mailer' endapp/mailers/user_mailer.rbclass UserMailer < ApplicationMailer def account_activation(user)#userの引数を渡す @user = user mail to: user.email, subject: "Account activation"#タイトル付きのメールを送る end def password_reset#今は関係ない @greeting = "Hi" mail to: "to@example.org" end endこのテストは現時点では red (account_activationに引数を与えたため)
これからレイアウトするテンプレートビューは、通常のビューと同様ERBで自由にカスタマイズ可能
ここでは挨拶文にユーザー名を含め、カスタムの有効化リンクを追加していく
この後、Railsサーバーでユーザーをメールアドレスで検索して有効化トークンを認証できるようにしたいので、リンクにはメールアドレスとトークンを両方含めておく必要があるAccountActivationsリソースで有効化をモデル化したので、トークン自体は定義した名前付きルートの引数で使われる
edit_account_activation_url(@user.activation_token, ...)例えば
edit_user_url(user)上のメソッドは、絶対パスのuser_urlでurlを生成し、引数のユーザーの編集ページにアクセスする
https://www.example.com/account_activations/q5lt38hQDc_959PVoo6b7A/edit上の「q5lt38hQDc_959PVoo6b7A」という部分はnew_tokenメソッドで生成されたもの。
URLで使えるようにBase64でエンコードされている。ちょうど/users/1/editの「1」のようなユーザーIDと同じ役割を果たす
このトークンは、AccountActivationsコントローラのeditアクションではparamsハッシュでparams[:id]として参照可能になるクエリパラメータを使って、このURLにメールアドレスも組み込んでみる
※クエリパラメータとは、URLの末尾で疑問符「?」に続けてキーと値のペアを記述したものaccount_activations/q5lt38hQDc_959PVoo6b7A/edit?email=foo%40example.comこのとき、メールアドレスの「@」記号がURLでは「%40」となっている。これは「エスケープ」と呼ばれる手法で、通常URLでは扱えない文字を扱えるようにするために変換されている
※@はURLで使えないRailsでクエリパラメータを設定するには、名前付きルートに対して次のようなハッシュを追加
edit_account_activation_url(@user.activation_token, email: @user.email)このようにして名前付きルートでクエリパラメータを定義すると、Railsが特殊な文字を自動的にエスケープしてくれる
取り出し時もコントローラでparams[:email]からメールアドレスを取り出すときには、自動的にエスケープを解除してくれるここまでできれば、user_mailerで定義した@userインスタンス変数、editへの名前付きルート、ERBを組み合わせて、必要なリンクを作成できる。
アカウント有効化のHTMLテンプレートでは、正しいリンクを組み立てるためにlink_toメソッドを使われている。
app/views/user_mailer/account_activation.text.erbHi <%= @user.name %>, Welcome to the Sample App! Click on the link below to activate your account: <%= edit_account_activation_url(@user.activation_token, email: @user.email) %> #リンクをセット(名前付きルートに引数を渡してURL生成)app/views/user_mailer/account_activation.html.erb<h1>Sample App</h1> <p>Hi <%= @user.name %>,</p> <p> Welcome to the Sample App! Click on the link below to activate your account: </p> <%= link_to "Activate", edit_account_activation_url(@user.activation_token, email: @user.email) %> #リンクをセット(名前付きルートに引数を渡してURL生成)演習
コンソールを開き、CGIモジュールのescapeメソッド(リスト 11.15)でメールアドレスの文字列をエスケープできることを確認してみましょう。このメソッドで"Don't panic!"をエスケープすると、どんな結果になりますか?
irb(main):005:0> CGI.escape("Don't panic!") => "Don%27t+panic%21"11.2.2 送信メールのプレビュー
メールプレビューを使って、今ほど定義したテンプレートの実際の表示を確認する
Railsでは、特殊なURLにアクセスするとメールのメッセージをその場でプレビューすることができる
利用するには、アプリケーションのdevelopment環境の設定に手を加える必要があるconfig/environments/development.rbRails.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ホスト名 'example.com' の部分は、各自のdevelopment環境に合わせて変更
例えば、クラウドIDEでは,自分のブラウザのURLをhostへ代入するhost = '<hex string>.vfs.cloud9.us-east-2.amazonaws.com' # クラウドIDE config.action_mailer.default_url_options = { host: host, protocol: 'https' }ローカル環境で開発している場合は、次のように変える
host = 'localhost:3000' # ローカル環境 config.action_mailer.default_url_options = { host: host, protocol: 'http' }httpsが暗号化なしのhttpに変わっていることに注意
developmentサーバーを再起動して,Userメイラーのプレビューファイルの更新をする
test/mailers/previews/user_mailer_preview.rb 自動生成# Preview all emails at http://localhost:3000/rails/mailers/user_mailer class UserMailerPreview < ActionMailer::Preview # Preview this email at # http://localhost:3000/rails/mailers/user_mailer/account_activation def account_activation UserMailer.account_activation end # Preview this email at # http://localhost:3000/rails/mailers/user_mailer/password_reset def password_reset UserMailer.password_reset end end定義したaccount_activationの引数には有効なUserオブジェクトを渡す必要があるため、上のコードはこのままでは動かない。
これを回避するために、user変数が開発用データベースの最初のユーザーになるように定義し
UserMailer.account_activationの引数として渡すこのとき
user.activation_token
の値にも代入している点に注目
メールのテンプレートでは、アカウント有効化のトークンが必要なので、代入は省略不可。
なお、activation_token
は仮の属性でしかないので、データベースのユーザーはこの値を実際には持っていない。test/mailers/previews/user_mailer_preview.rb# Preview all emails at http://localhost:3000/rails/mailers/user_mailer class UserMailerPreview < ActionMailer::Preview # Preview this email at # http://localhost:3000/rails/mailers/user_mailer/account_activation #このURLに接続すると、メールのプレビューがみれる def account_activation user = User.first user.activation_token = User.new_token UserMailer.account_activation(user) end # Preview this email at # http://localhost:3000/rails/mailers/user_mailer/password_reset def password_reset UserMailer.password_reset end end演習
Railsのプレビュー機能を使って、ブラウザから先ほどのメールを表示してみてください。「Date」の欄にはどんな内容が表示されているでしょうか?
→今日の日付が表示されている11.2.3 送信メールのテスト
最後に、このメールプレビューのテストも作成
自動生成されたTest
```
test/mailers/user_mailer_test.rb
require 'test_helper'class UserMailerTest < ActionMailer::TestCase
test "account_activation" do
mail = UserMailer.account_activation
assert_equal "Account activation", mail.subject
assert_equal ["to@example.org"], mail.to
assert_equal ["from@example.com"], mail.from
assert_match "Hi", mail.body.encoded#解説 Hiがメール本文にあるか
endtest "password_reset" do
mail = UserMailer.password_reset
assert_equal "Password reset", mail.subject
assert_equal ["to@example.org"], mail.to
assert_equal ["from@example.com"], mail.from
assert_match "Hi", mail.body.encoded #
end
end
```
assert_match
は正規表現で文字列をテストできる。assert_match 'foo', 'foobar' # true #fooがある assert_match 'baz', 'foobar' # false #bazない assert_match /\w+/, 'foobar' # true #文字ある assert_match /\w+/, '$#!*+@' # false #文字ないこれから書くテストでは
assert_match
メソッドを使って名前、有効化トークン、エスケープ済みメールアドレスがメール本文に含まれているかどうかをテストする
最後にもう1つ小技を紹介CGI.escape(user.email)上のメソッドを使うと、テスト用のユーザーのメールアドレスをエスケープすることもできる
※@がなくてもテストできるtest/mailers/user_mailer_test.rbrequire 'test_helper' class UserMailerTest < ActionMailer::TestCase test "account_activation" do user = users(:michael) user.activation_token = User.new_token mail = UserMailer.account_activation(user) assert_equal "Account activation", mail.subject assert_equal [user.email], mail.to assert_equal ["noreply@example.com"], mail.from assert_match user.name, mail.body.encoded assert_match user.activation_token, mail.body.encoded #account_activation.html.erbでPタグが長いと失敗するので注意 assert_match CGI.escape(user.email), mail.body.encoded end endなお、このテストではまだ失敗する。
上記テストコードでは、fixtureユーザーに有効化トークンを追加している点に注目。
(user.activation_token = のところ)
追加しない場合は、空白になる。なお、生成されたパスワード設定のテストも削除しているが、のちに戻す。
このテストをパスさせるには、テストファイル内のドメイン名を正しく設定する必要がある。
config/environments/test.rbRails.application.configure do . config.action_mailer.delivery_method = :test config.action_mailer.default_url_options = { host: 'example.com' } #ここは変に変えなくてOK . endこれでテストはパスする
演習
この時点で、テストスイートが green になっていることを確認してみましょう。
→パスしなかったので、テストのコメント注意記載assert_match user.activation_token, mail.body.encoded #account_activation.html.erbでPタグが長いと失敗するので注意日本語で「こちらのリンクをクリックしてください」とか長いとエラーになる
リスト 11.20で使ったCGI.escapeの部分を削除すると、テストが red に変わることを確認してみましょう。
→@マークが邪魔してエラーになる11.2.4 ユーザーのcreateアクションを更新
あとはユーザー登録を行うcreateアクションに数行追加するだけで、メイラーをアプリケーションで実際に使うことができる
以下のコードでは、登録時のリダイレクトの挙動が変更されている点に注意。
変更前は、ユーザーのプロフィールページにリダイレクト=アカウント有効化を実装するうえでは無意味な動作だったので リダイレクト先をルートURLに変更app/controllers/users_controller.rbclass UsersController < ApplicationController . def create @user = User.new(user_params) if @user.save UserMailer.account_activation(@user).deliver_now flash[:info] = "Please check your email to activate your account." redirect_to root_url else render 'new' end end . endcreateアクションの挙動を変えたので、テストが失敗する
失敗が発生するテストの行をひとまずコメントアウトするtest/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' assert_select 'div#error_explanation' assert_select 'div.field_with_errors' end test "valid signup information" do get signup_path assert_difference 'User.count', 1 do post users_path, params: { user: { name: "Example User", email: "user@example.com", password: "password", password_confirmation: "password" } } end follow_redirect! # assert_template 'users/show' # assert is_logged_in? end endこの状態で実際に新規ユーザーとして登録してみると、リダイレクトされてメールが生成される。
ただし、実際にメールが生成されるわけではないサーバーに以下のようなログが生成される
UserMailer#account_activation: processed outbound mail in 5.1ms Delivered mail 5d606e97b7a44_28872b106582df988776a@ip-172-31-25-202.mail (3.2ms) Date: Fri, 23 Aug 2019 22:54:15 +0000 From: noreply@example.com To: michael@michaelhartl.com Message-ID: <5d606e97b7a44_28872b106582df988776a@ip-172-31-25-202.mail> Subject: Account activation Mime-Version: 1.0 Content-Type: multipart/alternative; boundary="--==_mimepart_5d606e97b6f16_28872b106582df98876dd"; charset=UTF-8 Content-Transfer-Encoding: 7bit ----==_mimepart_5d606e97b6f16_28872b106582df98876dd Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 7bit Hi Michael Hartl, Welcome to the Sample App! Click on the link below to activate your account: https://0ebe1dc6d40e4a4bb06e0ca7fe138127.vfs.cloud9.us-east-2. amazonaws.com/account_activations/zdqs6sF7BMiDfXBaC7-6vA/ edit?email=michael%40michaelhartl.com ----==_mimepart_5d606e97b6f16_28872b106582df98876dd Content-Type: text/html; charset=UTF-8 Content-Transfer-Encoding: 7bit <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <style> /* Email styles need to be inline */ </style> </head> <body> <h1>Sample App</h1> <p>Hi Michael Hartl,</p> <p> Welcome to the Sample App! Click on the link below to activate your account: </p> <a href="https://0ebe1dc6d40e4a4bb06e0ca7fe138127.vfs.cloud9.us-east-2. amazonaws.com/account_activations/zdqs6sF7BMiDfXBaC7-6vA/ edit?email=michael%40michaelhartl.com">Activate</a> </body> </html> ----==_mimepart_5d606e97b6f16_28872b106582df98876dd--演習
新しいユーザーを登録したとき、リダイレクト先が適切なURLに変わったことを確認してみましょう。その後、Railsサーバーのログから送信メールの内容を確認してみてください。有効化トークンの値はどうなっていますか?
コンソールを開き、データベース上にユーザーが作成されたことを確認してみましょう。また、このユーザーはデータベース上にはいますが、有効化のステータスがfalseのままになっていることを確認してください。
>> user = User.find(101) >> user.activated? => false11.3 アカウントを有効化する
メールが生成できたら、今度はAccountActivationsコントローラのeditアクションを書いていく
アクションへのテストを書き、しっかりとテストできていることが確認できたら、AccountActivationsコントローラからUserモデルにコードを移していく作業(リファクタリング)にも取り掛かっていく11.3.1 authenticated?メソッドの抽象化
有効化トークンとメールはそれぞれ
params[:id]
params[:email]
で参照できる以下のコードで
パスワードのモデルと記憶トークンで学んだことを元に、次のようなコードでユーザーを検索して認証することにする。user = User.find_by(email: params[:email]) if user && user.authenticated?(:activation, params[:id])ただし、このメソッドは記憶トークン用なので今は正常に動作しない
# トークンがダイジェストと一致したらtrueを返す def authenticated?(remember_token) return false if remember_digest.nil? BCrypt::Password.new(remember_digest).is_password?(remember_token) endremember_digestはUserモデルの属性なので、モデル内では次のように書き換えることができる
self.remember_digest今回は、上のコードのrememberの部分をどうにかして変数として扱いたい
つまり、次のコード例のように、状況に応じて呼び出すメソッドを切り替えたいself.FOOBAR_digestこれから実装するauthenticated?メソッドでは、受け取ったパラメータに応じて呼び出すメソッドを切り替える手法を使う
この手法を「メタプログラミング」と呼ぶ
メタプログラミングを一言で言うと「プログラムでプログラムを作成する」ことここで重要なのは、sendメソッドの強力な機能
このメソッドは、渡されたオブジェクトに「メッセージを送る」ことによって、呼び出すメソッドを動的に決めることができる
例$ rails console >> a = [1, 2, 3]#aという配列を定義 >> a.length#aの配列の数 => 3 >> a.send(:length) #aオブジェクトにsend(:length(シンボル))を送る => 3 #a.lengthと等価に >> a.send("length")#aオブジェクトにsend("length"(文字列))を送る => 3 #a.lengthと等価にこのときsendを通して渡したシンボル:lengthや文字列"length"は、いずれもlengthメソッドと同じ結果になった。
つまり、どちらもオブジェクトにlengthメソッドを渡しているため、等価。もう1つ例
>> user = User.first #userに最初のUserインスタンスを代入 >> user.activation_digest #userのactivation_digestカラムの値 => "$2a$10$4e6TFzEJAVNyjLv8Q5u22ensMt28qEkx0roaZvtRcp6UZKRM6N9Ae" >> user.send(:activation_digest)#userに:activation_digestシンボルを送信 => "$2a$10$4e6TFzEJAVNyjLv8Q5u22ensMt28qEkx0roaZvtRcp6UZKRM6N9Ae"#同じ結果 >> user.send("activation_digest") => "$2a$10$4e6TFzEJAVNyjLv8Q5u22ensMt28qEkx0roaZvtRcp6UZKRM6N9Ae" >> attribute = :activation >> user.send("#{attribute}_digest") #文字列の式展開で[変数_digest]ができた => "$2a$10$4e6TFzEJAVNyjLv8Q5u22ensMt28qEkx0roaZvtRcp6UZKRM6N9Ae"最後の例では、シンボル:activationと等しいattribute変数を定義し、文字列の式展開(interpolation)を使って引数を正しく組み立ててから、sendに渡している。
文字列'activation'でも同じことができるが、Rubyではシンボルを使う方が一般的。
"#{attribute}_digest"
→"activation_digest"
sendメソッドの動作原理がわかったので、この仕組みを利用してauthenticated?メソッドを書き換える
def authenticated?(remember_token) digest = self.send("remember_digest") return false if digest.nil? BCrypt::Password.new(digest).is_password?(remember_token) end上のコードの各引数を一般化し、文字列の式展開も利用すると、次のようなコードになる。
def authenticated?(attribute, token) digest = self.send("#{attribute}_digest") return false if digest.nil? BCrypt::Password.new(digest).is_password?(token) end他の認証でも使えるように、上では2番目の引数tokenの名前を変更して一般化している点に注意
また、このコードはモデル内にあるのでselfは省略可能
最終的にRubyらしく書かれたコードは、以下def authenticated?(attribute, token) digest = send("#{attribute}_digest") return false if digest.nil? BCrypt::Password.new(digest).is_password?(token) endここまでできれば、次のように呼び出すことでauthenticated?の振る舞いができる
user.authenticated?(:remember, remember_token)以上の説明を実際のUserモデルに適用した、抽象化したauthenticated?メソッド
app/models/user.rb# トークンがダイジェストと一致したらtrueを返す def authenticated?(attribute, token) digest = send("#{attribute}_digest") return false if digest.nil? BCrypt::Password.new(digest).is_password?(token) endこの時点ではテストスイートは red
テストが失敗する理由は、current_userメソッドとnilダイジェストのテストの両方で、authenticated?が古いままで、引数も2つではなくまだ1つのままだから
これを解消するため、両者を更新して、新しい一般的なメソッドを使うようにする
app/helpers/sessions_helper.rbmodule SessionsHelper . . . # 現在ログイン中のユーザーを返す(いる場合) def current_user if (user_id = session[:user_id]) @current_user ||= User.find_by(id: user_id) elsif (user_id = cookies.signed[:user_id]) user = User.find_by(id: user_id) if user && user.authenticated?(:remember, cookies[:remember_token]) log_in user @current_user = user end end endtest/models/user_test.rbrequire 'test_helper' class UserTest < ActiveSupport::TestCase def setup @user = User.new(name: "Example User", email: "user@example.com", password: "foobar", password_confirmation: "foobar") end . . . test "authenticated? should return false for a user with nil digest" do assert_not @user.authenticated?(:remember, '') end end上のような変更を加えると、テストは green に変わる
演習
コンソール内で新しいユーザーを作成してみてください。新しいユーザーの記憶トークンと有効化トークンはどのような値になっているでしょうか? また、各トークンに対応するダイジェストの値はどうなっているでしょうか?
irb(main):002:0> user=User.create(name:"kiyoma",email:"aaa@com.com",password:"123456", => #<User id: 101, name: "kiyoma", email: "aaa@com.com", created_at: "2021-03-01 ... irb(main):004:0> user.remember_token => nil irb(main):005:0> user.activation_token => "5daKKPOcrSWPBx5Dv_tY7Q" irb(main):006:0> user.remember_digest => nil irb(main):007:0> user.activation_digest => "$2a$12$vMNDNH2/c4FhHbxzit/7MOrSa4fvvSKvkUlPWpx/fDe1Cyny571Tq"リスト 11.26で抽象化したauthenticated?メソッドを使って、先ほどの各トークン/ダイジェストの組み合わせで認証が成功することを確認してみましょう。
irb(main):008:0> user.remember_token = User.new_token => "p0dj7V57U5ukEawso3Kk1g" irb(main):009:0> user.update_attribute(:remember_digest, User.digest(user.remember_tok en)) => true irb(main):010:0> user.authenticated?(:remember,user.remember_token) => true11.3.2 editアクションで有効化
authenticated?の準備ができたので、やっとeditアクションを書く準備ができた。
editアクションは、paramsハッシュで渡されたメールアドレスに対応するユーザーを認証
ユーザーが有効であることを確認する中核は、次の部分if user && !user.activated? && user.authenticated?(:activation, params[:id])!user.activated?という記述に注目。このコードは、既に有効になっているユーザーを誤って再度有効化しないために必要。
正当であろうとなかろうと、有効化が行われるとユーザーはログイン状態になる。
もしこのコードがなければ、攻撃者がユーザーの有効化リンクを後から盗みだしてクリックするだけで、本当のユーザーとしてログインできてしまう上の論理値に基いてユーザーを認証するには、ユーザーを認証してからactivated_atタイムスタンプを更新する必要がある
user.update_attribute(:activated, true) user.update_attribute(:activated_at, Time.zone.now)上のコードをeditアクションで使う
app/controllers/account_activations_controller.rbclass AccountActivationsController < ApplicationController def edit user = User.find_by(email: params[:email]) if user && !user.activated? && user.authenticated?(:activation, params[:id]) user.update_attribute(:activated, true) user.update_attribute(:activated_at, Time.zone.now) log_in user flash[:success] = "Account activated!" redirect_to user else flash[:danger] = "Invalid activation link" redirect_to root_url end end endユーザーの有効化が使われるためには、ユーザーが有効である場合にのみログインできるようにログイン方法を変更する必要がある。
つまりuser.activated?がtrueの場合にのみログインを許可し、そうでない場合はルートURLにリダイレクトしてwarningで警告を表示する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]) if user.activated? log_in user params[:session][:remember_me] == '1' ? remember(user) : forget(user) redirect_back_or user else message = "Account not activated. " message += "Check your email for the activation link." flash[:warning] = message redirect_to root_url end else flash.now[:danger] = 'Invalid email/password combination' render 'new' end end def destroy log_out if logged_in? redirect_to root_url end end演習
コンソールから、11.2.4で生成したメールに含まれているURLを調べてみてください。URL内のどこに有効化トークンが含まれているでしょうか?
先ほど見つけたURLをブラウザに貼り付けて、そのユーザーの認証に成功し、有効化できることを確認してみましょう。また、有効化ステータスがtrueになっていることをコンソールから確認してみてください。11.3.3 有効化のテストとリファクタリング
アカウント有効化の統合テストを追加していく
test/integration/users_signup_test.rbrequire 'test_helper' class UsersSignupTest < ActionDispatch::IntegrationTest def setup ActionMailer::Base.deliveries.clear#deliveriesを初期化 解説 end 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' assert_select 'div#error_explanation' assert_select 'div.field_with_errors' end test "valid signup information with account activation" do get signup_path assert_difference 'User.count', 1 do post users_path, params: { user: { name: "Example User", email: "user@example.com", password: "password", password_confirmation: "password" } } end assert_equal 1, ActionMailer::Base.deliveries.size#解説 user = assigns(:user) assert_not user.activated? # 有効化していない状態でログインしてみる log_in_as(user) assert_not is_logged_in? # 有効化トークンが不正な場合 get edit_account_activation_path("invalid token", email: user.email) assert_not is_logged_in? # トークンは正しいがメールアドレスが無効な場合 get edit_account_activation_path(user.activation_token, email: 'wrong') assert_not is_logged_in? # 有効化トークンが正しい場合 get edit_account_activation_path(user.activation_token, email: user.email) assert user.reload.activated? follow_redirect! assert_template 'users/show' assert is_logged_in? end endリスト 11.33のコードは分量が多いが、本当に重要な部分は次の1行
assert_equal 1, ActionMailer::Base.deliveries.size上のコードは、配信されたメッセージがきっかり1つであるかどうかを確認する。
配列deliveriesは変数なので、setupメソッドでこれを初期化しておかないと、並行して行われる他のテストでメールが配信されたときにエラーが発生してしまうassignsメソッドは、対応するアクション内のインスタンス変数にアクセスできるようになる。
例えば、Usersコントローラのcreateアクションでは@userというインスタンス変数が定義されているが、テストでassigns(:user)と書くとこのインスタンス変数にアクセスできるようになる。テストがパスする
テストができたので、ユーザー操作の一部をコントローラからモデルに移動するというリファクタリングを行う準備ができた。
ここでは特に、activateメソッドを作成し、ユーザーの有効化属性を更新し、send_activation_emailメソッドを作成して有効化メールを送信するapp/models/user.rbclass User < ApplicationRecord # アカウントを有効にする def activate update_attribute(:activated, true) update_attribute(:activated_at, Time.zone.now) end # 有効化用のメールを送信する def send_activation_email UserMailer.account_activation(self).deliver_now #メールを送る endapp/controllers/users_controller.rbdef create @user = User.new(user_params) if @user.save @user.send_activation_email#定義したメールを送るメソッド flash[:info] = "Please check your email to activate your account." redirect_to root_url else render 'new' end endapp/controllers/account_activations_controller.rbclass AccountActivationsController < ApplicationController def edit user = User.find_by(email: params[:email]) if user && !user.activated? && user.authenticated?(:activation, params[:id]) user.activate#定義したアカウントを有効にするメソッド log_in user flash[:success] = "Account activated!" redirect_to user else flash[:danger] = "Invalid activation link" redirect_to root_url end end end
user.rb
ではuser.という記法を使っていない。Userモデルにはそのような変数はないので、これがあるとエラーになる-user.update_attribute(:activated, true) -user.update_attribute(:activated_at, Time.zone.now) +update_attribute(:activated, true) +update_attribute(:activated_at, Time.zone.now)(userをselfに切り替えるという手もあるが、selfはモデル内では必須ではない)
テストもパスするのでリファクタリングOK
演習
リスト 11.35にあるactivateメソッドはupdate_attributeを2回呼び出していますが、これは各行で1回ずつデータベースへ問い合わせしていることになります。リスト 11.39に記したテンプレートを使って、update_attributeの呼び出しを1回のupdate_columns呼び出しにまとめてみましょう。これでデータベースへの問い合わせが1回で済むようになります(注意!update_columnsは、モデルのコールバックやバリデーションが実行されない点がupdate_attributeと異なります)。また、変更後にテストを実行し、 green になることも確認してください。
# アカウントを有効にする def activate update_columns(activated: true, activated_at: Time.zone.now) end現在は、/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ここまでの演習課題で変更したコードをテストするために、/users と /users/:id の両方に対する統合テストを作成してみましょう。
users.ymlnon_activated: name: Non Activated email: non_activated@example.gov password_digest: <%= User.digest('password') %> activated: false activated_at: <%= Time.zone.now %>users_controller_test.rbtest "should not allow the not activated attribute" do log_in_as (@non_activated_user) # 非有効化ユーザーでログイン assert_not @non_activated_user.activated? # 有効化でないことを検証 get users_path # /usersを取得 assert_select "a[href=?]", user_path(@non_activated_user), count: 0 # 非有効化ユーザーが表示されていないことを確認 get user_path(@non_activated_user) # 非有効化ユーザーidのページを取得 assert_redirected_to root_url # ルートurlにリダイレクトされればtrue end11.4 本番環境でのメール送信
ここまでの実装で、development環境におけるアカウント有効化の流れは完成した。次は、本番環境で実際にメールを送信できるようにしてみる
まずは無料のサービスを利用してメール送信の設定を行い、続いてアプリケーションの設定とデプロイを行う本番環境からメール送信するために、「Mailgun」というHerokuアドオンを利用してアカウントを検証
本チュートリアルでは、「starter」というプランを使うアプリケーションでMailgunアドオンを使うには、production環境のSMTPに情報を記入する必要がある
を自分のHerokuのURLに設定し、利用するconfig/environments/production.rbRails.application.configure do . . . config.action_mailer.raise_delivery_errors = true config.action_mailer.delivery_method = :smtp host = '<your heroku app>.herokuapp.com' config.action_mailer.default_url_options = { host: host } ActionMailer::Base.smtp_settings = { :port => ENV['MAILGUN_SMTP_PORT'], :address => ENV['MAILGUN_SMTP_SERVER'], :user_name => ENV['MAILGUN_SMTP_LOGIN'], :password => ENV['MAILGUN_SMTP_PASSWORD'], :domain => host, :authentication => :plain, }この時点で、Gitのトピックブランチをmasterにマージ
$ rails test $ git add -A $ git commit -m "Add account activation" $ git checkout master $ git merge account-activation続いてリモートリポジトリにプッシュし、Herokuにデプロイ
$ rails test $ git push $ git push heroku $ heroku run rails db:migrateMailgunのHerokuアドオンを追加するために、次のコマンドを実行
$ heroku addons:create mailgun:starter注: herokuコマンドのバージョンが古いとここで失敗するかも。その場合は、Heroku Toolbeltを使って最新版に更新するか、次の古い文法のコマンドを試す
$ heroku addons:add mailgun:starterHerokuの環境変数を表示したい場合は、次のコマンドを実行
$ heroku config:get MAILGUN_SMTP_LOGIN $ heroku config:get MAILGUN_SMTP_PASSWORDメール設定にはMailgunアカウントのuser_nameとpassword設定を記入する行もあるが、そこには記入せず、必ず環境変数「ENV」に設定するよう十分注意
本番運用するアプリケーションでは、暗号化されていないIDやパスワードのような重要なセキュリティ情報は「絶対に」ソースコードに直接書き込まない。そのような情報は環境変数に記述し、そこからアプリケーションに読み込む。
今回の場合、そうした変数はMailgunアドオンが自動的に設定してくれる最後に、受信メールの認証を行う
以下のコマンドを打つと、Mailgun ダッシュボードのURLが表示されるのでブラウザで開く$ heroku addons:open mailgunMailGun公式ドキュメントに従い、受信するメールアドレスを認証していく。
画面左側の「Sending」→「Domains」のリストにある「sandbox」で始まるサンドボックスドメインを選択。
画面右側の「Authorized Recipients」から受信メールアドレスを認証し、本番環境でのメール送信準備は完了。HerokuへのデプロイとMailgunのアドオンが完了したら、先ほど受信認証したメールアドレスを使って、production(本番)環境でユーザー登録を行ってみる。
受信したメールに記されているメールをクリックすると、期待通りアカウントの有効化に成功するはず演習
実際に本番環境でユーザー登録をしてみましょう。ユーザー登録時に入力したメールアドレスにメールは届きましたか?
メールを受信できたら、実際にメールをクリックしてアカウントを有効化してみましょう。また、Heroku上のログを調べてみて、有効化に関するログがどうなっているのか調べてみてください。ヒント: ターミナルからheroku logsコマンドを実行してみましょう。herokuに接続できないため、(jqueryエラー中)のためスキップ
- 投稿日:2021-03-02T02:44:53+09:00
【Rails】世界一簡単なcreated.at(updated.at)日本語表記
はじめに
railsではロンドン?の時刻表示がデフォになっていますが、簡単に日本語表記で表すことができます。
本記事では簡単にやる方法を簡潔に書きます。
開発環境
- Ruby2.6.6
実際のコード
まずは、
application.rb
に以下を記述します。config/application.rbconfig.i18n.default_locale = :ja次に、config/locateの中に
ja.yml
のファイルを作り、以下を記述します。config/locate/ja.ymlja: datetime: distance_in_words: less_than_x_minutes: one: "1秒" other: "%{count}秒" x_minutes: one: "1分" other: "%{count}分" about_x_hours: one: "約1時間" other: "約%{count}時間" x_days: one: "1日" other: "%{count}日" about_x_months: one: "約1ヶ月" other: "約%{count}ヶ月" x_months: one: "1ヶ月" other: "%{count}ヶ月" about_x_years: one: "約1年" other: "約%{count}年" over_x_years: one: "1年以上" other: "%{count}年以上"あとは、表示させたいviewに追記部分を加えるだけ!
index.html.erb<h1>sample</h1> <h3>Tweet一覧</h3> <%= link_to "新規投稿へ", tweets_new_path %> <div class="tweets-container"> <% @tweets.each do |t| %> <div class="tweet"> <%= t.body %> #ここから <time datetime="<%= t.created_at %>"> <%= time_ago_in_words(t.created_at) %>前 </time> #ここまで </div> <% end %> </div>最後に
世界一簡単にRailsでcreated.atやupdated.atをTwitterのように(〇分前、〇日前)日本語表記する方法についてご紹介しました。
簡単に実装可能なのでぜひ試してみてみてくださいね!
参考記事:https://qiita.com/bellx2/items/30906a7832ef4ff4c886
最後まで読んでいただき、ありがとうございました。
- 投稿日:2021-03-02T01:34:18+09:00
[Ruby on Rails] No template for interactive request
なんか色々と長い内容のエラーに出くわした。
備忘録と言うよりは打つ間違いに気をつけろ!ってことで
自身への反省をこめて書きます。No template
なら、いつもの記述間違いか?ってなる所、
今回のエラーはいつもより長かった...
エラー内容↓
No template for interactive request
CatsController#show is missing a template for request formats: text/html
NOTE!訳(インタラクティブリクエスト用のテンプレートはありません
CatsController#showにリクエスト形式のテンプレートがありません:text / html
注意!)解決方法?
なのかは、分からないがshow.htmlのファイルを作った時にコンマが一個多かったorz
show..html.erb となっていた。
ただそれだけ。
まとめ
たまたま早期発見できたものの、今後templateエラーが出たら
コードとファイルの両方を細かくチェックするべきだと実感しました。