大ちゃんの駆け出し技術ブログ

RUNTEQ受講生のかわいいといわれるアウトプットブログ

RSpecでページのステータスコードを取得する方法

はじめに

こんにちは!大ちゃんの駆け出し技術ブログです。

今回は超短いアウトプットです!!

いつも6000字は超えるようにしてるんですけど、今回は3000字ぐらいになります。

というのも紹介することがシンプルなんですよね。

タイトルにあるとおり、今回は「RSpecでページのステータスコードを取得する方法」を紹介します。

Capybaraではテストでステータスコードを取得できない

RSpecのテスト方法にはCapybaraを使ったテスト方法が主流かなと思っています。

テスト時に実際にブラウザを起動させて行うテストで、エラーがあったときにはエラー時のスクリーンショットを取ってくれる便利なやつです。

設定ファイルは以下のようになります。

# spec/support/capybara.rb
RSpec.configure do |config|
  config.before(:each, type: :system) do
    driven_by :selenium, using: :chrome, screen_size: [1920, 800]
  end
end

spec/supportディレクトリを生成し、spec_helper.rbとは違うファイルでテストの設定を行えるようにしています。

一見完璧に見えるCapybaraなのですが、意外と欠点があるんですよね。

それが「ページのステータスコードを取得できない」です。

例えば、権限がないページにユーザーがアクセスしたときに、403エラー(:forbidden)が発生します。そして、エラーが発生した時のステータスコードは403となるはずです。ですので、ステータスコードを検証したいのですが、それがCapybaraだとできないんです。

driven_by(:rack_test)

ステータスコードを取得するテストはどうすれば良いのかというと、実は簡単でCapybaraではなく、テストの設定をdriven_by(:rack_test)に変えるだけです。

この設定は実はシステムスペックファイルを作成するコマンド実行時にデフォルトで生成されたシステムスペックファイルに記述されています。

$ bin/rails g rspec:system sample
      create  spec/system/samples_spec.rb

作成されたシステムスペックファイル

# spec/system/samples_spec.rb
require 'rails_helper'

RSpec.describe "Samples", type: :system do
  before do
    driven_by(:rack_test) # ここ!
  end

  pending "add some scenarios (or delete) #{__FILE__}"
end

これでステータスコードを取得できるんですね。

実際にテストコードに書いてみます。

RSpec.describe "Pundit", type: :system do
  let(:writer) { create(:user, :writer) }
  before do
    driven_by(:rack_test) # ここに記載
  end

  describe 'Writer' do
    before { login_as(writer) }
    describe 'Not authorized pages' do
      context 'When writer accesses to admin_categories_path' do
        before { visit admin_categories_path }
        fit 'Current_page is 403 error page' do
          expect(page).to have_content('You are not authorized to perform this action.'), '403エラーページが表示されていません'
          expect(page.status_code).to eq(403), 'ステータスコードが403ではありません'
        end
      end
    end
  end
end

今回はadmin_categories_pathに権限を持たないwriterがアクセスするとステータスコードが403になるかを検証します。driven_by(:rack_test)を使うとステータスコードを取得するpage.status_codeが使用できるようになります。実際に使ってみると403が返ってきます。

[1] pry(#<RSpec:・・・省略)> page.status_code
=> 403

しかし、driven_by(:rack_test)を記述すると、Capybaraのテスト設定がオーバーライドされてしまい、スクリーンショットをとることができなくなるのでご注意を。

じゃあCapybaraでpage.status_codeを実行すればいいのでは?と思う方もいると思いますが、残念ながらCapybaraのテストでpage.status_codeを実行するとエラーが返ってきます。

[1] pry(#<RSpec:・・・省略)> page.status_code
Capybara::NotSupportedByDriverError: Capybara::Driver::Base#status_code

なのでステータスコードを取得することを検証するシステムスペックファイルにのみ、driven_by(:rack_test)を記載しましょう。

終わりに

これで連続投稿8日目となりました!

どこまで行けるかわかりませんが、少なくとも今月中は毎日アウトプットを繰り返してみようと思います。

以上、大ちゃんの駆け出し技術ブログでした!

transientとafter(:build)

はじめに

こんにちは!大ちゃんの駆け出し技術ブログです。

RSpecで少し詰まった箇所があったので、今回はそちらを記事にします。今回の記事は試験的に少し雑なアウトプットをさせていただきます。いつもの記事も雑かもしれませんが、、、

理由は下記2つです。

  • そもそも読者想定は初学者ですが、ある程度勉強を進めているので1からアプリを用意するということはもうすでに必要ないことなのかなと思った。
  • 2月は毎日投稿を目指していますが、仕事しながらだとRUNTEQの課題や転職用のポートフォリオのために使う時間が撮れないと感じた。

2つ目の理由についてはネガティブな理由ではあるものの、やはり投資時間を分散させるために試験的に行ってみたいと思います。

トピックは記事のタイトル通り、transientafter(:build)です。

RSpecを書く上でとっても役に立つと思ったので是非みなさんも使ってみてください。

テストの概要

早速本題に入らせていください。

RUNTEQの応用編に現在取り組んでいますが、その課題でRSpecを作成する課題があります。

どのようなページで検証するのかと言うと、記事(Article)について、著者がいる記事Aと著者がいない記事Bが記事一覧ページにて検証します。

Image from Gyazo

検証する内容は下記の通りです。

記事Aの著者で検索をすると記事Aだけが表示される。

このページには検索窓がありそれが下記の画像のような感じ。

https://i.gyazo.com/7c0d3f4ea882d8489b2fd64347ee9acc.png

この著者のセレクトボックスで著者Aを選択肢検索すると記事Aだけが表示されるということです。

テストのシステムスペックファイル

よってシステムスペックのシナリオは以下の通りになります。

# spec/system/admin_article_searches_spec.rb
describe '著者検索' do
    let(:admin_user) { create(:user, :admin) }
        let(:article_with_author_A) { create(:article, :with_author, author_name: '著者A') }
      let(:article_with_author_B) { create(:article, :with_author, author_name: '著者B') }
    before do
      article_with_author_A
      article_with_author_B
      login_as(admin_user) # ポイント1
      visit admin_articles_path # ポイント2
    end
    context "著者Aで検索される時" do
      before do
        within('.form-inline') do
          select article_with_author_A.author.name, from: 'q[author_id]' # ポイント3
          click_button '検索'
        end
      end
      it '著者Aの記事のみが表示される' do # ポイント3
        expect(page).to have_content(article_with_author_A.title), '著者Aの記事が表示されていません'
        expect(page).not_to have_content(article_with_author_B.title), '著者A以外の記事が表示されています'
      end
    end
  end

letの部分の説明は今回の記事の肝ですので、後で説明します。最初にシナリオの流れだけざっくり説明します。

[ポイント1] login_as(admin_user)でまずは管理者画面にログインします。login_asメソッドはモジュールで独自に定義してあります。

# spec/support/login_support.rb
module LoginSupport
  def login_as(user)
    visit admin_login_identifier_path
    fill_in 'user_name', with: user.name
    click_button '次へ'
    expect(current_path).to eq admin_login_password_path
    fill_in 'user_password', with: 'password'
    click_button 'ログイン'
  end
end

[ポイント2] Article(記事)モデルとAuthor(著者)モデルの関係性は多:1です。Authorが複数の記事を持つということです。

ですので、セレクトボックスから著者の名前を取り出す方法はarticle_with_author_A.author.nameとなります。

[ポイント3] そして、検証部分では著者Aの記事だけが表示され、著者Bの記事は表示されないことを想定しています。

expect(page).to have_content(article_with_author_A.title), '著者Aの記事が表示されていません'
expect(page).not_to have_content(article_with_author_B.title), '著者A以外の記事が表示されています'

問題なのはletの部分です。

admin_userに関しては簡単です。createの第二引数でtraitを指定し、管理者ユーザーが作成されるように指定しています。UserモデルのFactoryBotの中身を見てみると、trait:adminの記述があるのが分かります。

# spec/factories/users.rb
FactoryBot.define do
  factory :user do
・
・
    trait :admin do
      sequence(:name) { |n| "writer-#{n}" }
      role { :admin }
    end
・
・
  end
end

一方でarticle_with_author_Aarticle_with_author_Bはどうでしょうか。

let(:article_with_author_A) { create(:article, :with_author, author_name: '著者A') }
let(:article_with_author_B) { create(:article, :with_author, author_name: '著者B') }

createの第三引数にauthor_nameの指定がありました。自分はRSpecにおけるcreateの第三引数自体が初見だったので少し混乱しました。

ArticleのFactoryBotでtraitが:with_authorの部分を見てみると見慣れない記述があります。

FactoryBot.define do
  factory :article do

    trait :with_author do
      transient do
        sequence(:author_name) { |n| "test_author_name_#{n}" }
        sequence(:author_slug) { |n| "test_author_slug_#{n}" }
      end
  
      after(:build) do |article, evaluator|
        article.author = build(:author, name: evaluator.author_name, slug: evaluator.author_slug)
      end
    end

これが自分にとって初となるtransientafter(:build)の出会いでしたww

なんだこの記述ってすごく思ったんですけど、理解するのは意外と簡単なんです!

transient

まずtransientから説明していきます!

lettraitを指定すると、同時にtraitのブロック内に存在するtransient内のの値が取得できます。例えば、下記のようにcreateに第三引数を渡さずにFactoryBotを呼び出す記述を書くとします。

let(:article_with_test_author) { create(:article, :with_author) }

このFactoryBotが呼び出されるとtransient内の中の値、つまりauthor_nameauthor_slugも呼び出されます。

試しに、binding.pryで止めて値を確かめてみます。

transient do
  sequence(:author_name) { |n| "test_author_name_#{n}" }
  sequence(:author_slug) { |n| "test_author_slug_#{n}" }
  binding.pry # ここで止める!
end

するとそれぞれの値は以下のようになっていました。

[1] pry(#<FactoryBot::Declaration::Implicit>)> author_name
NameError: undefined local variable or method `author_name' for #<FactoryBot::Declaration::Implicit:0x00007f852fc39ba8>
Did you mean?  Pathname
from (pry):5:in `__pry__'
[2] pry(#<FactoryBot::Declaration::Implicit>)> author_slug
NameError: undefined local variable or method `author_slug' for #<FactoryBot::Declaration::Implicit:0x00007f852fc39ba8>
from (pry):6:in `__pry__'

ありゃりゃ、取得できていませんね。

ご安心ください。値自体はafter(:build)の部分で確認することができます!

補足しておくと、letcreateの第三引数であった:author_name"test_author_name_#{n}"の値ではなく別の値を格納したい時に指定します。今回の場合は"著者A"、"著者B"といった具合ですね。

after(:build)

続いてafter(:build)の中を覗いてみましょう。

after(:build) do |article, evaluator|
  article.author = build(:author, name: evaluator.author_name, slug: evaluator.author_slug)
end

evaluatorとは何なんでしょうか??

実はさっきのtrasient内の値はこの中に格納されています。

[2] pry(#<FactoryBot::SyntaxRunner>)> evaluator.author_name
=> "test_author_name_1"
[3] pry(#<FactoryBot::SyntaxRunner>)> evaluator.author_slug
=> "test_author_slug_1"

そしてこの値がarticle.authorというAuthorモデルの作成時に渡されています。

ブロック内の部分はとても単純ですよね。AuthorモデルのFactoryBotを作成しています。

build(:author, name: evaluator.author_name, slug: evaluator.author_slug)

だんだん見えて理解できてきましたでしょうか。

本来であれば、アソシエーション関係のあるFactoryBotの呼び出しをletのみで行う場合、letを2回使う必要がありました。今回の場合、記事が1つとその記事の著者が1つです。

しかし、transientを使用した場合、let1回の呼び出しで2つのFactoryBotを作成しています!

let(:article_with_author_A) { create(:article, :with_author, author_name: '著者A') }
let(:article_with_author_B) { create(:article, :with_author, author_name: '著者B') }

これによりletをシステムスペックファイルに記述する量を減らしているのです。今回の場合、著者Aの記事だけでなく著者Bの記事も必要だったため、letのみでFactoryBotを作成しようとすると、合計で4回も呼び出さなければなりません。すごい冗長ですよね、、、。

終わりに

今回の説明でtransientafter(:build)の概要は何となく理解できたかなと思います。

たくさんのletの使用は冗長化につながりますので、1回のletの使用でアソシエーション関係にあるFactoryBotの作成は是非とも使ってみてください!

以上、以上、大ちゃんの駆け出し技術ブログでした!

【Rails】content_forとyieldで動的にタイトルを表示

はじめに

こんにちは!大ちゃんの駆け出し技術ブログです。

今回はRailsでcontent_forとyieldを使用して、タイトルを動的に表示させる方法」について紹介します!!

「動的とは?」となった人(自分もそうでした笑)もいるかもしれませんが、プログラミングでは高い頻度で出てくる概念です。その意味は 「状態や構成が状況に応じて変化したり、状況に合わせて選択できたりする柔軟性を持っていること。」

この記事で紹介するcontent_forとyieldを使った実装内容はどちらかといえば主に文の後ろ側、「状況に合わせて選択できたりする柔軟性」に当たります。

その柔軟性について詳しく見ていきましょう。

成果物の確認

以下が実装した成果物になります。

トップページのタイトル top.png

アクセスページのタイトル access.png

Webサイトがサイト内で複数のページを持つのは当然のことです。 例えば、家電量販店サイトにアクセスしたときに表示されるトップページ、お店への行き方を紹介するアクセスページといった具合です。 そして各ページでのタイトルはページ内容に則していることが普通です。

本記事の実装でも上の図で示したように各ページでサイト名を変えています。

  • トップページでは「AAA 電機」(家電量販店の名前)
  • アクセスページではAccess | AAA 電機」(ページ内容| 家電量販店の名前)

Googleでもタイトルは 「検索した内容 + "- Google 検索"」 の仕様となってるみたいですので確認してみてください。

実装方法を解説

まず、全てのビューのの共通レイアウトであるapplication.html.erbのheadタグ内にyield文を記載。

<title><%= page_title(yield(:title)) %></title>

yield(:title)ですが、()内の:titleは他のコードでも構いません。(:notice、:info、etc.) 後ほど記載するcontent_forでは()内のコードをここで記載したものと共通にする必要があります。 (ちなみに:titleはrubyの記法でシンボルというもの。ここでの理解はただの文字列でも問題ないです。)

ここで定義しているpage_titleメソッドはapp/helpers/application_helper.rbで定義しています。

module ApplicationHelper

  # 引数が渡されなかった場合は空文字をデフォルト値とする
  def page_title(page_title = '')
    base_title = 'AAA 電機'

    # 三項演算子
    page_title.empty? ? base_title : page_title + " | " + base_title
  end
end

page_titleの引数にはデフォルト値として空文字を指定しています。 これにより引数が渡されなかったとしても空文字(””)がpage_titleに代入され、メソッド内の演算で使用されます。

そして、三項演算子によって、引数が渡されていなければ「AAA 電機(base_title)」、引数が渡されていれば「引数の文字(page_title) | AAA 電機」を返します。

三項演算子ですがここでは詳しく説明しませんが、一言で説明すると「if文を一行で記載する演算子です。つまり、上の文はif文と同じ意味になります。(最初見たときはこれがifと同じ意味とは思いませんでした、、。)

条件式 ? trueの処理 : falseの処理

念のためif文で書き換えたものを記載しておくので、三項演算子をスキップしたい人はこちらを見て意味を確認してください。

# page_title.empty? ? base_title : page_title + " | " + base_title をif文で書き換え
if page_title.empty?
  base_title
else
  page_title + " | " + base_title
end

話が少し脱線しましたが、このpage_titleメソッドを定義することでapplication.html.erbで使用することが可能になります。

では、page_titleメソッドの引数であるyiield(:title)は果たしてどのように渡されるのかというと、 そう、content_forメソッドの出番です。

しかもその実装方法はとても簡単! 各ページにcontent_forメソッドを使用し、第一引数に共通のシンボル、第二引数に渡したい文字列を記載するだけ!

成果物のように実装する場合、

<% content_for(:title, 'Access' ) %>

これにより、文字列’Access’が共通レイアウトのページにあるyeildに渡され、page_titleメソッドの引数が’Access’となり、成果物のようにタイトルが「「Access | AAA 電機」(ページ内容| 家電量販店の名前)」となります。

逆に、トップページとしてルーティングされているページのビューに何も記載しなければ、そのページのタイトルは「AAA 電機」となります。

動的であるメリット

冒頭で動的(状況に合わせて選択できたりする柔軟性)の意味を説明しましたが、具体的にどのような柔軟性が今回の実装にあるのでしょう?

例えば、AAA 電機が社名変更でBBB 電機となったとしましょう。 当然タイトルも新しい会社名に変更しなければなりません。 どのようにコードに変更を加えるべきでしょうか?

答えは"ヘルパーメソッドのbase_titleに入れる文字を変える"だけです。

module ApplicationHelper

  def page_title(page_title = '')
    # 変更箇所 -----------
    base_title = 'BBB 電機'
    # --------------------

    page_title.empty? ? base_title : page_title + " | " + base_title
  end
end

本当にたったこれだけ! これだけでcontent_forが使われる他の全ページのタイトルが「AAA 電機」から「BBB 電機」に変わります。 メンテナンスが驚くほどに簡単になるのです。

逆に、content_forとyieldを使わない実装方法を考えてみましょう。 各ページにはそれぞれのページのタイトルが静的に表示されています。

<title>AAA 電機</title>
<title>Access | AAA 電機</title>

そして先ほどの例のように会社名が変更になったとしましょう。

さて、変更を加える箇所はいくつあるでしょうか。 Accessページだけでなく、他のページにも+ | AAA 電機と実装していた場合、その数だけ修正をしなければなりません。 content_forとyieldを使った実装なら一箇所で済むのに、です。

これが動的であることの威力です。 状況に合わせて対応を選択できる柔軟性です。

最後に

今回紹介した例はとても簡単な実装方法になります。 しかし、他にも幅広い方法でcontent_forとyieldは活用できるので、是非色々試してみてください! 自分も試します!

この記事が少しでも皆様の学習の役に立てば幸いです。

以上、大ちゃんの駆け出し技術ブログでした!

【Rails】独自のRakeタスクの定期的実行

はじめに

こんにちは!大ちゃんの駆け出し技術ブログです。

先日にRakeタスクに関する記事を書きました。

sakitadaiki.hatenablog.com

記事ではデフォルトで実装されているRakeタスクの表示方法[ $ rake -T ]や、開発環境を表示するRakeタスク[ $ rake about ]の中身を実際に見てみました。

Rakeタスクの概要はざっくりと理解できたかなと思います。

このざっくりとした理解をより凝固なものにするために、今回は前回学んだRakeタスクを独自に作成する方法について発信します。

実際にRakeタスクを自分で実装することでより理解が深まると思います。

それでは本記事の内容に入っていきます。

※なお、今回使うアプリケーションは前回と同じファイルで行います(ファイル名が「scaffold_rake」とトピックが同じであるため)

Rakeタスクファイルの作成

それではRakeタスクファイルを作成してみます。

作成方法はシンプルで以下のようになります。

$ rails g task ファイル名

今回はファイル名をsample_taskとしてコマンドを実行しましょう。

$ rails g task sample_task
      create  lib/tasks/sample_task.rake

パス: lib/tasks/配下に新規ファイルが作成されました。

.rake とあるようにRakeファイルであることがわかります。

このように、Rakeタスクファイルはlib/tasks/配下に置かれていきます。

作成時のファイル状態は以下のようになります。

# lib/tasks/sample_task.rake
namespace :sample_task do
end

ここで簡単なRakeタスクの作成に入る前に、Rakeタスクファイルの構成について再度確認します。

name_space :ファイル名 do
    desc "Rakeタスクの処理内容の説明"
    task Rakeタスク名: :environment do
        処理内容(Rubyで記述)
    end
end

desc内にはタスクの説明を記述するんでしたよね。rake aboutなら "List versions of all Rails frameworks and the environment"といった具合ですね。

そしてその下にtaskを使ってRakeタスク名を定義します。

:environmentの記載は必須でしたね。

そしてdo~endのブロック内に実行したい処理をRubyで記述します。

それではsample_task.rake内に記載していきましょう。

実行する処理は「コンソールに"Hello"を出力する」ことにしましょう。

# lib/tasks/sample_task.rake
namespace :sample_task do
  desc "'Hello'をコンソールに出力"
    task output_hello: :environment do
        puts 'Hello'
    end
end

ここでrake -Tを使ってRakeタスク一覧をターミナル上に表示しましょう上で定義したRakeタスクが表示されると思います。

$ rake -T
・
・
rake restart                            # Restart app by touching tmp/restart.txt
rake sample_task:output_hello           # 'Hello'をコンソールに出力
rake secret                             # Generate a cryptographically secure secret key (this is typically used to generate a secret for cook...
・
・

実はタスクの実装自体も既に完了しております。

$ rake sample_task:output_helloを実行してみましょう。

$ rake sample_task:output_hello 
Hello

文字列「Hello」を出力できたかと思います。

Rakeタスクを簡単に実装できることが分かったのではないでしょうか。

cronとwhenever

独自のRakeタスクのことはある程度理解しましたが、用途についてはまだ説明していません。

色々とあると思いますが、今回はRUNTEQで学習した定期的に登録したRakeタスク(ジョブ)を実行する方法について発信します。

どのように定期タスクを登録するのかというとcronに登録します。

cronについて下記サイトでは以下のように説明しています。

cronとは、多くのUNIX系OSで標準的に利用される常駐プログラム(デーモン)の一種で、利用者の設定したスケジュールに従って指定されたプログラムを定期的に起動してくれるもの。

引用元: https://e-words.jp/w/cron.html

つまり UnixOS上にある定期的に何かのプログラムを自動で実行する設定のことを指します。

そして、このcronの設定にRakeタスクを登録するために使うgemがあります。それがwheneverです。

wheneverはRakeタスクがどれぐらいの頻度でどのRakeタスクを実行するかを設定することができます。つまり、wheneverで作成したファイルにRakeタスクを指定し、それをcronに登録するということです。cron上では頻度やRakeタスク自体の変更はできません。

Image from Gyazo

では実際にwheneverからインストールしていきましょう。

# Gemfile
gem 'whenever', require: false
$ bundle install
Fetching whenever 1.0.0
Installing whenever 1.0.0

wheneverを起動するために$ bundle exec wheneverize .をターミナル上で実行してください。

$ bundle exec wheneverize .
[add] writing `./config/schedule.rb'
[done] wheneverized!

するとconfig/配下にschedule.rbというファイルが作成されました。これが先ほど説明した、Rakeタスクがどれぐらいの頻度でどのRakeタスクを実行するかを記述するファイルになります。

中身を見てみましょう!!!

# Use this file to easily define all of your cron jobs.
#
# It's helpful, but not entirely necessary to understand cron before proceeding.
# http://en.wikipedia.org/wiki/Cron

# Example:
#
# set :output, "/path/to/my/cron_log.log"
#
# every 2.hours do
#   command "/usr/bin/some_great_command"
#   runner "MyModel.some_method"
#   rake "some:great:rake:task"
# end
#
# every 4.days do
#   runner "AnotherModel.prune_old_records"
# end

# Learn more: http://github.com/javan/whenever

全てコメントアウトされたファイルが作成されています。しかしながら、何を設定しているのかが見て理解できるファイル構造になっています。以下の箇所を参照して説明します。

# every 2.hours do
#   command "/usr/bin/some_great_command"
#   runner "MyModel.some_method"
#   rake "some:great:rake:task"
# end

every 2.hoursとありますが、これは言うまでもなくタスクの実行頻度でしょう。

そして、ブロック内にcommandrunnerrakeの3つのメソッドがあります。実はこのファイルでは、Rakeタスクだけ登録できると言うわけではなく、ターミナルのコマンドや定義されたメソッドも実行することができます。

  • command/usr/bin/some_great_commandからわかる通り、ターミナル上で実行するタスクを登録
  • runnerMyModel.some_methodからわかる通り、クラスメソッドを登録
  • rakesome:great:rake:taskからわかる通り、Rakeタスクを登録

今回使用するのはもちろんrakeです!

今回は先ほど作成したRakeタスクoutput_helloを毎時実行されるようにします。

# config/schedule.rb
every :hour do
  rake "sample_task:output_hello"
end

それではいよいよcronに登録してみましょう。

登録する前に、現在cronに登録されているタスクが何もないことを確認しましょう。cronに登録されているタスクを確認するためには、$ crontab -lをターミナル上で実行します。

$ crontab -l

何も表示されていないことを確認したら、次は以下のコマンドで設定ファイルをcronに反映します。

$ bundle exec whenever --update-crontab
[write] crontab file updated

再度$ crontab -lをターミナル上で実行します。

$ crontab -l
# Begin Whenever generated tasks for: /Users/sakidendaiki/Downloads/RUNTEQ/for_blog/scaffold_rake/config/schedule.rb at: 2021-02-04 14:39:04 +0900
0 * * * * /bin/bash -l -c 'cd /Users/sakidendaiki/Downloads/RUNTEQ/for_blog/scaffold_rake && RAILS_ENV=production bundle exec rake sample_task:output_hello --silent'

# End Whenever generated tasks for: /Users/sakidendaiki/Downloads/RUNTEQ/for_blog/scaffold_rake/config/schedule.rb at: 2021-02-04 14:39:04 +0900

これで毎時登録したRakeタスクが実行されるようになりました。(サーバーが起動している時にのみ実行されます。つまり、$ rails sを実行してから1時間待つ必要がありますので、「Hello」をターミナル上で確認したい方は、1時間待つかww、設定ファイルを:hourから:minuteに変えて、再度cronに登録してください。)

終わりに

今回の内容はRUNTEQで学習したことの簡単なアウトプットです。 RUNTEQではもっと実用的な実装をしています。本当このスクールレベル高いなと思いながら毎日を生きています。

今回はただターミナル上に「Hello」を出力するだけのメソッドでしたが、本来であれば定期的にメールを送信するメソッドだったり、定期的にDBを更新するメソッドが使われます。 もしそんなメソッドを使う機会がアプリ作成時にあれば是非今回紹介した独自のRakeタスクの定期的実行方法を参考にしてみてください。

以上、大ちゃんの駆け出し技術ブログでした!

【scope】EverydayRailsで初学者が理解できなそうなところ

なんだこの記述は、、!?

こんにちは!大ちゃんの駆け出し技術ブログです。

RSpec学習には以下の有名教材があります。 Everyday Rails - RSpec による Rails テスト入門 テスト駆動開発の習得に向けた実践的アプローチ

Aaron Sumnerさんという方が書いた本を日本語訳されています。 翻訳した方々の中にはあの「プロを目指す人のためのRuby入門 言語仕様からテスト駆動開発・デバッグ技法まで」を書かれた伊藤淳一もいらっしゃいます。

自分もプログラミングスクールの課題のために読み進めていますが、かなり序盤に出てくる以下の記述に驚きました。

class Note < ApplicationRecord
  scope :search, ->(term) {
    where("LOWER(message) LIKE ?", "%#{term.downcase}%")
  }
end

うわ、なんだこの見たこともない記載方法は、、!?。scope? whereの中身がなんか変!

この文章を初学者の人は理解できるのかなと思いました。 自分は初見では全くわからずぐぐりましたね。

ということで今回は上記の文をまるっと全部説明したいと思います!

本記事を読む読者のレベル

本記事は「Everyday Rails」を読んでいる読者を想定しています。 したがって、本記事の対象者のレベルは「Everyday Rails」に書かれている読者レベルと同じです。 つまり、

  • Rails で使われているサーバーサイドの Model-View-Controller
  • gem の依存関係を管理する Bundler
  • Rails コマンドの実行方法
  • リポジトリのブランチを切り替えられるぐらいの Git 知識

なお、SQLについての知識、特に、where、ワイルドカード、LIKEについて知識があるとより理解が簡単になると思います。

使用方法の説明

具体的な説明に入る前に、全体像を掴むために、今回説明するものがいったいどのようにして使われるのか、どんな機能を持つのか見ておきましょう。 上記のscope〜を記述することで以下のような使われ方をします。

@notes = Note.search("first")

この形どこかで見たことありませんか? そう、コントローラでDBからモデルの対象のレコードを取得する方法です。 これはクラスに対して実行するクラスメソッドというものですね。

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

Noteはモデルです。 ファイルを見てみるとtext型でmessageカラムがあります。

  create_table "notes", force: :cascade do |t|
    t.text "message"
    .
    .
    .
  end

具体的にNote.search("first")が何をしているのかというと、searchとあるように、Noteモデルのmessageカラムに文字列"first"を含んだ文字列があるかどうか検索し、あればそのモデルを取得します。

例えば、DBに以下の3つのnoteが登録されているとします。

  • note1・・・messageカラムが"first"
  • note2・・・messageカラムが"first name"
  • note3・・・messageカラムが"second"

そして、@notes = Note.search("first")とした場合、インスタンス変数@notesにはnote1とnote2が格納されますが、note3は文字列"first"を含まないので取得されません。

ここまで全体像を説明してきましたが、ポイントは2つです。

① scopeを使うことでクラスメソッドが使えるようになる

② whereの箇所では、指定した文字列を含む文字列がmessageカラムにあるかを検索する機能がある

では①、②の順番で詳しく説明していきます。

scopeについて

まずはscopeの部分。

class Note < ApplicationRecord
  # -----------------------
  scope :search, ->(term) { 
  # -----------------------
    where("LOWER(message) LIKE ?", "%#{term.downcase}%")
  }
end

説明したように、上記はモデル内で定義し、コントローラで使われます。

言い換えれば、モデル内で定義をしなくても、コントローラで使用することが可能です。

term = "first"
@notes = Note.where("LOWER(message) LIKE ?", "%#{term.downcase}%")

ですが、見てわかる通り、こちら長くないですか? まず、term = "first"のように検索したい文字列変数として定義しなければなりません。 Note.where以降も長くて理解しづらいかと思います。

そこでモデルに定義することで上の文が、あらスッキリ、生まれ変わります。

@notes = Note.search("first")

文字列の定義をせず引数として文字列を指定! whereメソッド自体もモデルに定義されているので長い文をコントローラに記載せずに使用! 可読性がすごくあがったのをお分かりいただけたかと思います。 search("first")で何をしているのかが直感的にも理解できますしね。

このようにメソッドとして切り出すことがscopeを使うメリットです。

ここまで説明してscope :search, ->(term)の部分に関して以下のことも 理解できたのではないのでしょうか。

① searchの部分はクラスメソッドを定義していること

② termの部分はクラスメソッドの引数であること

本来であれば、search(term)みたいな記述だと理解しやすいのですが、scope独特の記述方法によって、引数が離れてしまっていますし、searchの箇所も:searchとシンボルで記載しなければなりません。

ここまでわかればもうscope :search, ->(term)について疑問はないでしょう。

総括すると、 本来であればDBからのレコード取得時にすごく長い文章を書かなければならない場合、scopeでクラスメソッドを定義し、短文でコントローラで使用することができます。

where("LOWER(message) LIKE ?", "%#{term.downcase}%")

そしてとっても難解なこの文章はなんなんでしょう。

class Note < ApplicationRecord
  scope :search, ->(term) { 
  # ----------------------------------------------------
    where("LOWER(message) LIKE ?", "%#{term.downcase}%")
  # ----------------------------------------------------
  }
end

3つに分けて説明します。

①whereメソッドについて

②"LOWER(message) LIKE ?"について

③"%#{term.downcase}%"について

whereメソッドについて

まず、whereメソッドから簡単に説明します。

where(カラム名: 値)

whereメソッドはfindfind_by同様にDBからレコードを取得します。 決定的に違うのはfindfind_byは1つのレコードを取得するのに対し、whereメソッドは複数のレコードを取得するのです。

例えば、1番最初に紹介した例と同様にDBに以下の3つのnoteが登録されているとします。

  • note1・・・messageカラムが"first"
  • note2・・・messageカラムが"first name"
  • note3・・・messageカラムが"second"

そして、find_byで文字列"first"を含むmessageカラムがあるレコードを取得する時は以下のようになると思います。

Note.find_by(message: "first")

ですが、当然これではnote1, note2は同時に取得できません。 理由は簡単。 find_byはレコードを1つしか取得できないからです。

よってwhereを使って複数取得します。

Note.where(message: "first")

しかしながら、"LOWER(message) LIKE ?"は本来whereの第一引数であるカラム名とは少し違うようです。

"LOWER(message) LIKE ?"について

まずwhereの第一引数の"LOWER(message) LIKE ?"がそもそも何なのかですが、これはSQLの文章です。

まずLOWERですがこれはSQLの関数になります。 意味は「引数の文字列を全て小文字にする」、です。

LOWER("FIRST") --> "first"

LOWERの引数はmessageとなっています。 したがって、messageカラムにある文字列を全て小文字にしているということになります。 なぜ小文字にする必要があるのか?理由は後ほど説明します。

次にLIKEの部分ですが、これはLIKE演算子の「あいまい検索」と言われるものです。

例えば、DB上の都道府県のテーブルから"島"という文字列を含んだ件名を取得したいとします。 そのような場合SQLの文章は以下のようになります。

SELECT 都道府県名 FROM 都道府県テーブル WHERE 都道府県名 LIKE "%島%"

LIKE以降にある文字列がLIKEの手前の文字列に含まれているかを判別しています。

ちなみに%ワイルドカードと呼ばれるもので、0個以上の任意の文字を表します。 よって、%島%は島を含んだ文字列となります。

LIKEの手前はLOWER(message)です。では後ろは? 後ろはですね笑

って何と思われるかもしれませんが、じつはには第二引数である"%#{term.downcase}%"が入ります。

実はこのですがよくrailsでサーバを起動している際に頻出しています。

SELECT "users".* FROM "users" WHERE "users"."id" = ? ORDER BY "users"."id" ASC LIMIT ? [["id", 1], ["LIMIT", 1]]

“users”.“id” = ? の?は[“id”, 1]の1の値が代入される LIMIT ? の?には[“LIMIT”, 1]の1の値が代入される

上記の"LOWER(message) LIKE ?"?は代入される値をSQLの後ろで指定している方法と同じ考え方です。

つまり、where("LOWER(message) LIKE ?", "%#{term.downcase}%")は以下のSQLの条件に当てはまるレコードを取得しているのです。

WHERE LOWER(message) LIKE "%#{term.downcase}%"

"%#{term.downcase}%"について

そして、最後に"%#{term.downcase}%"の部分です。

両端の%は先ほど説明したように、0個以上の任意の文字を表します。

#{term.downcase}は式展開で、引数のtermに格納された文字列をメソッドであるdowncaseで全て小文字にしています。

term = "FIRST"
"#{term.downcase}" --> # "first"

ここで先ほど紹介したLOWER(message)でなぜmessageカラムの値を全て小文字にしているのかの答えにつながります。

引数のtermが"First"、messageカラムの値が"FIRST"の時、それは文字列が一致しないということになり、結果そのmessageカラムのレコードは取得されません。 しかし、完全に一致する検索より、小文字大文字を区別しないほうが使い勝手が良さそうです。 なので、両方を小文字にすることで大文字と小文字を区別しないようにしているのです。

よって"%#{term.downcase}%"「termを含んだ文字列」となります。

結論: scopeとwhere("LOWER(message) LIKE ?", "%#{term.downcase}%")とは

長い文章を読んでいただきありがとうございました。

最後に結論として今まで記述したことをまとめて終わりとします。

class Note < ApplicationRecord
  scope :search, ->(term) {
    where("LOWER(message) LIKE ?", "%#{term.downcase}%")
  }
end

scopeの部分の説明は再掲します。

① searchの部分はクラスメソッドを定義していること ② termの部分はクラスメソッドの引数であること

よって、コントローラで以下のように使われることができます。

@notes = Note.search("first")

そしてsearchの内容であるwhere("LOWER(message) LIKE ?", "%#{term.downcase}%")ですが、意味は

モデルのmessageカラムにtermを含んだ文字列があるかどうか検索し、あればそのモデルを取得

という意味になります。

終わりに

scopewhereメソッドを説明しました。 この2つは今回のようにモデルで使用されることが多く、そのため2つセットで使われる場合が多い気がしています。

見た目は自分の知っているRailsの記述ではなかったので理解に苦労しましたが、仕組みを理解できれば簡単です。 初学者のうちから逃げずにしっかりと理解しておきましょう!

以上、大ちゃんの駆け出し技術ブログでした!

【Rails】Action Mailer

はじめに

こんにちは!大ちゃんの駆け出し技術ブログです。

今回はRails標準機能であるAction Mailerという機能について紹介します!名前から類推できる通りメールの送信機能になります。

※Action Mailboxという別の標準機能もありますが、こちらはメールの受信機能です。

導入方法

今回はアプリケーションからユーザーに対しアカウントアクティベーションのメールを送ることを想定してます。アカウントアクティベーションとはよくある本人確認方法の1つですね。Webサイトなどでアカウント登録時に登録したメールに対し、本人確認メールが送られるシチュエーションです。

では早速実装していきましょう。

いつものようにscafflodを使用してアプリの雛形を作成します。

$ rails new action_mailer_sample
$ cd action_mailer_sample
$ rails g scaffold user name:string email:string
$ rails db:create
$ rails db:migrate

次にmailerのファイルを作成します。

$ rails g mailer UserMailer
      create  app/mailers/user_mailer.rb
      invoke  erb
      create    app/views/user_mailer
      invoke  test_unit
      create    test/mailers/user_mailer_test.rb
      create    test/mailers/previews/user_mailer_preview.rb

作成されたapp/mailers/user_mailer.rbに対してactivation_emailメソッドを定義します。

# app/mailers/user_mailer.rb
class UserMailer < ApplicationMailer
  def activation_email
    @user = params[:user]
    @url  = 'http://example.com/login'
    mail(to: @user.email, subject: 'アカウントのアクティベーションメール')
  end
end

インスタンス変数である@user@urlを定義しています。

このインスタンス変数はRailsのコントローラと同様に、ビューに渡されるようになっています。このビューがメール本文自体を表示するようになっています。

mail(to: @user.email, subject: 'アカウントのアクティベーションメール')は見て理解できると思います。to: の部分で宛先を、subject: の部分で件名を指定しています。

次はメール本文にあたるビューを作成します。メソッド名に対応したerbファイルを作成する必要があるため、ファイル名は「activation_email~」となります。

しかし、先ほど実行したコマンドではビューのフォルダであるapp/views/user_mailerのみ作成しています。従って、ビュー自体は雛形が作成されていないので手動でファイルを作成し、本文も手動で実装します。テキスト形式とHTML形式の2つのフォーマットで表示させるために、activation_email.html.erbactivation_email.text.erbを作成します。 ※メールの閲覧方法はアプリ上やwebブラウザ上など表示するフォーマットが異なる場合があるため、

# app/views/user_mailer/activation_email.html.erb

<p><%= @user.name %>様</p>

<p>この度は、「○○○○○○」にお申し込み頂きまして誠にありがとうございます。</p>

<p>お申し込み頂きましたアカウント情報は以下となります。</p>

<p>ログインID:○○○○○</p>
<p>パスワード:個人情報のため表示を伏せています</p>

<p>下記URLをクリックしてログインしてください。</p>
<p><%= @url %></p>

# app/views/user_mailer/activation_email.text.erb

<%= @user.name %>様

この度は、「○○○○○○」にお申し込み頂きまして誠にありがとうございます。

お申し込み頂きましたアカウント情報は以下となります。

ログインID:○○○○○
パスワード:個人情報のため表示を伏せています

下記URLをクリックしてログインしてください。
<%= @url %>

erbファイルなので<%= %>の記法が使用できます。

ここでプレビュー機能を使ってメール本文を確認してみたいと思います。

プレビュー機能を使うために、コンソール上で前もって1つのユーザーを登録しておきましょう。

$ rails c

irb(main):001:0> User.create(name: "dai-chan", email: "daichan@example.com")
   (0.1ms)  begin transaction
  User Create (1.3ms)  INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["name", "dai-chan"], ["email", "daichan@example.com"], ["created_at", "2021-02-02 10:58:11.562336"], ["updated_at", "2021-02-02 10:58:11.562336"]]
   (0.9ms)  commit transaction
=> #<User id: 1, name: "dai-chan", email: "daichan@example.com", created_at: "2021-02-02 10:58:11", updated_at: "2021-02-02 10:58:11">

プレビュー機能を使うためにはUserMailer作成時に作成されたtest/mailers/previews/user_mailer_preview.rbにメソッドを追記します。

class UserMailerPreview < ActionMailer::Preview
  def activation_email
    UserMailer.with(user: User.first).activation_email
  end
end

rails sを実行して[http://localhost:3000/rails/mailers/user_mailer/activation_email.html]にアクセスしましょう。すると下の画像のようなプレビューを見ることができると思います。こちらでどのような見た目で送信されているのかが確認できますね。

Image from Gyazo

最後にUsersコントローラのユーザー作成アクションであるcreateアクションで、ユーザー作成時にメールが送信されるように記述します。

def create
    @user = User.new(user_params)

    respond_to do |format|
      if @user.save
        UserMailer.with(to: @user.email, name: @user.name).activation_email.deliver_now # 追記
        format.html { redirect_to @user, notice: "User was successfully created." }
        format.json { render :show, status: :created, location: @user }
      else
        format.html { render :new, status: :unprocessable_entity }
        format.json { render json: @user.errors, status: :unprocessable_entity }
      end
    end
  end

これでユーザー作成時にメールを送信するように実装できました。

終わりに

ActionMailerなどのメール機能は他にもたくさんあるそうです。 正直プログラミング言語は勉強しきれないので、他にもたくさんあるそうですとしか言っていませんが、、、、

最近は技術ブログ多めなので、技術ブログだけでなく、個人的な体験談も近日アップします!

以上、大ちゃんの駆け出し技術ブログでした!

【Rails】Rakeタスク

はじめに

こんにちは!大ちゃんの駆け出し技術ブログです。

Rubyを学習していると"rake"という言葉に遭遇すると思います。

例えば $ rake routes$ rails routes と同じようにルーティングをターミナル表示してくれます。

$ rake routes
     Prefix Verb   URI Pattern                                                                              Controller#Action
       root GET    /                                                                                        users#index
      users GET    /users(.:format)                                                                         users#index
            POST   /users(.:format)    
・
・
略

そもそもRakeって何なんでしょうか?

Rubyを勉強し始めてなんとなくルーティングを表示できるという理解で留めていたので、今回はRakeについてまとめます。

Rakeとは

Railsドキュメントでは以下のようにて説明しています。

Rubyで記述されたビルドツール Rails5以降はrailsコマンドでrakeを呼び出せるようになっています

railsdoc.com

短いし説明が全然されていないのであんまり理解できませんでした、、、

Rails5以降ではrailsコマンドでrakeコマンドを呼び出せる仕様になっていることから、現在rakeコマンドはrailsコマンドの一部になっているという理解で良さそうです。よって$ rails routesも元々はrakeコマンドの機能だったということです。しかしもっと説明が欲しいですね笑

次にTechAcademyマガジンの説明を引用します。

Linux環境でにおけるビルドツールとしてmakeがありますが、実はRubyにおいてもビルドツールがあります。それがRakeです。Rakeを使うとRakefileに一連の処理として定義されたタスクを実行することができます。

techacademy.jp

ビルドというのはプログラミング言語において、テキストファイルに従った処理を実行することです。わかりやすくいうとJavaなどがコンパイルなどもビルドの一部に該当します。

このテキストファイルにを読み込んで処理をすることは、アプリケーションの複雑化に伴い人間で管理することが難しくなりました。例えば、ファイルを読み込む順序の工数が膨大になために、手動でファイルを実行すると順番を間違えたりしてしまう恐れがあります。そこで、テキストファイルを自動で読み込むツールが必要となり、そのツールがビルドツールにあたります。

Linux環境ではmakeというビルドツールがありますが、Rubyでも同様にビルドツールがあります。それがRakeということになります。Makeの頭文字をRに変更したということが名前の由来っぽいですね。

Rakeタスク

Rakeがアプリのファイルを自動で読み込んでくれるツールであることは理解できました。

しかし、実際どのような処理を自動でしてくれているのかがわからないですね、、、

実はRakeが自動で行ってくれている処理(これをRakeタスクといいます)はターミナル上で確認することができます。

まず、簡単なアプリを作成します。

$ cd 任意のディレクトリ
$ rails new scaffold_rake
$ cd scaffold_rake
$ rails  db:create
$ rails generate scaffold user name:string email:string
$ rails db:migrate

コマンドライン上で以下のコマンドを打ち込んでください。

$ rake -T

たくさんの文字がターミナル上に表示されたかと思います。これらは全て自動でファイルを読み込み処理を行なってくれるRakeタスクです。

rake about                              # List versions of all Rails frameworks and the environment
rake action_mailbox:ingress:exim        # Relay an inbound email from Exim to Action Mailbox (URL and INGRESS_PASSWORD required)
rake action_mailbox:ingress:postfix     # Relay an inbound email from Postfix to Action Mailbox (URL and INGRESS_PASSWORD required)
rake action_mailbox:ingress:qmail       # Relay an inbound email from Qmail to Action Mailbox (URL and INGRESS_PASSWORD required)
rake action_mailbox:install             # Copy over the migration
rake action_text:install                # Copy over the migration, stylesheet, and JavaScript files
rake active_storage:install             # Copy over the migration needed to the application
rake app:template                       # Applies the template supplied by LOCATION=(/path/to/template) or URL
rake app:update                         # Update configs and some other initially generated files (or use just update:configs or update:bin)
rake assets:clean[keep]                 # Remove old compiled assets
rake assets:clobber                     # Remove compiled assets
・
・
・

次にrakeタスクの定義場所を見てみましょう。

今回はrake aboutの定義場所を覗きにいきましょう。rake aboutは説明部分にある通り、「すべてのRailsフレームワークと環境のバージョンを一覧表示する」処理を行います。

$ rake about
About your application's environment
Rails version             6.0.3.4
Ruby version              ruby 2.6.5p114 (2019-10-01 revision 67812) [x86_64-darwin19]
RubyGems version          3.0.3
Rack version              2.2.3



.gitignoreによってエディタで検索できない場合があります。今回に限り.gitignore/.bundleコメントアウトしましょう。

※通常.gitignoreコメントアウトすることはありません。今回はRakeタスクの中身を見るために仕方なく行なっています。

.gitignore

# Ignore bundler config.
# /.bundle

それではrake aboutのファイルを覗いてみましょう。文字検索する場合、検索は「rake about」ではなく、$ rake -Tで表示したrake aboutの説明部分にあたる「List versions of~~~」で検索してみてください。

すると以下のファイルで定義されていることがわかりました。

vendor/bundle/ruby/2.6.0/gems/railties-6.0.3.4/lib/rails/tasks/misc.rake

desc "List versions of all Rails frameworks and the environment"
task about: :environment do
  puts Rails::Info
end

このファイルから定義箇所の構成はこのようになっています。

desc "Rakeタスクの処理内容の説明"
task Rakeタスク名: :environment do
    処理内容(Rubyで記述)
end

descはおそらくですがdescriptionの略でしょうか。処理の説明を記述しています。$ rake -Tで実行すると説明部分が# の後ろに表示されます。Rakeタスク名も表示されていますね。

rake about    # List versions of all Rails frameworks and the environment

:environmentの部分ですが、Qiitaの記事で詳しく説明されています。説明する量が多いため、詳しく知りたい方は記事を参照してください。

qiita.com

記事が難しいと思った方のために雑に説明すると、:environmentは必ずrakeタスクの定義では記載します。もし記載しないとエラーが起きてしまいます。試しに削除してみましょう。

desc "List versions of all Rails frameworks and the environment"
task about: do
  puts Rails::Info
end

ターミナル上実行すると、SyntaxError、つまり記述ミスのエラーが出ます。

$ rake about
rails aborted!
SyntaxError: /Users/sakidendaiki/Downloads/RUNTEQ/for_blog/scaffold_rake/vendor/bundle/ruby/2.6.0/gems/railties-6.0.3.4/lib/rails/tasks/misc.rake:10: syntax error, unexpected do (for block)
task about: do

※削除した部分は必ず元に戻しておいてください。

また、railsコマンドでrakeを呼び出せるので、$ rails aboutでも同じ処理ができます。

$ rails about
About your application's environment
Rails version             6.0.3.4
Ruby version              ruby 2.6.5p114 (2019-10-01 revision 67812) [x86_64-darwin19]
RubyGems version          3.0.3
Rack version              2.2.3



終わりに

以上、Rakeについて説明させていただきました。

今回説明したrake about以外にもRakeタスクが定義されています。 コマンドを実行したり、ファイルを覗いてみたり、いろいろ調べてみてください。

次回の記事ではRakeタスクを自分で定義する方法について発信します!

以上、大ちゃんの駆け出し技術ブログでした!

参考文献

Rake | Railsドキュメント

RubyのRakeライブラリについてを現役エンジニアが解説【初心者向け】 | TechAcademyマガジン

Rails における rake タスクの :environment について - Qiita

【Gem】パンくずリスト "gretel"

はじめに

こんにちは!大ちゃんの駆け出し技術ブログです。

本日はパンくずリスト gretelというRailsのgemについて紹介したいと思います!

名前がパンくず!?読み方は”グレてる”!?

本当に読み方が印象的だなと思いますwww

ですが、こちらのパンくずリストは皆さんもウェブサイトでみたことがあるものだと思います! では説明をしていきます!

gretelの概要

gemのgretel公式のREADMEで以下のように説明されています。

Gretel is a Ruby on Rails plugin that makes it easy yet flexible to create breadcrumbs. It is based around the idea that breadcrumbs are a concern of the view, so you define a set of breadcrumbs in config/breadcrumbs.rb (or multiple files; see below) and specify in the view which breadcrumb to use. Gretel also supports semantic breadcrumbs (those used in Google results).

これを日本語訳すると以下のようになります!(翻訳の仕事をしたことはないので多めにみていただけると幸いですmm)

GretelはRuby on Railsプラグインで、パンくずリストの作成を簡単かつ柔軟に行うことができます。パンくずリストはビューに基づいているので、config/breadcrumbs.rb (または複数のファイル。以下を参照) でパンくずリストのセットを定義し、どのパンくずリストを使うかをビューで指定します。Gretelはセマンティックパンくずリスト (Google の結果で使用されるパンくずリスト) もサポートしています。

めちゃくちゃ分かりにくい訳でごめんなさいm( _ _ )m

最後の文の訳にのみ触れておきます。なぜなら、パンくずリストが何であるかがわかるからです。

Gretelはセマンティックパンくずリスト (Google の結果で使用されるパンくずリスト) もサポートしています。

この文で注目して欲しい部分はGoogle の結果で使用されるパンくずリストの部分です。

実はパンくずリストGoogleの検索結果のページにもあります。例えば、「Amazon Prime」と検索してみましょう。

Image from Gyazo

どれがパンくずリストなのか分かりますか?実はこちらがパンくずリストです。

Image from Gyazo

Webページのタイトルの部分の上にある小さい文字の部分です。 Google検索すると必ず表示されるので、絶対に見たことがあるはずです。

このリストは何を意味しているのかは直感的にわかるでしょう。 回答は左のページ(www.amazon.co.jp)は右のページ(Prime-Video)の親ページということになります。 (Amazon Primeは元々あったAmazonのHomeページ(親ページ)に付け足されたページ(子ページ)ということですね)

このようにパンくずリストはページごとの親子関係をリストで表示するという機能を持っています。

gretelのインストール手順

では実際にRailsgretelをインストールします。 なお、本記事で取り扱うサンプルアプリのRailsRubyのバージョンは以下になります。

ではさっそくアプリを作成するディレクトリに移り、$ Rails newをつかって初期アプリを作成しましょう! 本記事ではscaffoldを使用して雛形のアプリを作成しましょう。

$ cd 任意のディレクトリ
$ rails new scaffold_gretel
$ cd scaffold_gretel
$ rails  db:create
$ rails generate scaffold user name:string email:string
$ rails db:migrate

今回はroot_pathをユーザー一覧画面とします。 そのため、config/routes.rbを編集します。

Rails.application.routes.draw do
  root to: 'users#index'
  resources :users
end

念のため、サーバーを起動しルートパスがユーザー一覧画面となっているか確認してください!

Image from Gyazo

次にGemfilegretelを追加し、bundle installします。

Gemfile

gem 'gretel'

ターミナル

$ bundle install
・
・
Fetching gretel 4.2.0
Installing gretel 4.2.0
・
・
Bundle complete!

次にgenerateコマンドでインストールしたら、config/breadcrumbs.rbが作成されます。

$ rails generate gretel:install
      create  config/breadcrumbs.rb

最初のインストール手順は終了です。

実装方法

いよいよ実装に移ります!

まず、先程作成したの中身を見てみましょう!

config/breadcrumbs.rb

crumb :root do
  link "Home", root_path
end

# crumb :projects do
#   link "Projects", projects_path
# end

# crumb :project do |project|
#   link project.name, project_path(project)
#   parent :projects
# end

# crumb :project_issues do |project|
#   link "Issues", project_issues_path(project)
#   parent :project, project
# end

# crumb :issue do |issue|
#   link issue.title, issue_path(issue)
#   parent :project_issues, issue.project
# end

# If you want to split your breadcrumbs configuration over multiple files, you
# can create a folder named `config/breadcrumbs` and put your configuration
# files there. All *.rb files (e.g. `frontend.rb` or `products.rb`) in that
# folder are loaded and reloaded automatically when you change them, just like
# this file (`config/breadcrumbs.rb`).

上のファイルに追記します。

crumb :root do
  link 'Home', root_path
end
crumb :users do
  link "ユーザ一覧", users_path
end
crumb :user_show do |user|
  link user.name, user_path(user)
  parent :users
end

追記したファイルのそれぞれの意味は後ほど説明します。

ビューにパンくずリスト表示部分を追加します。 まず、application.html.erbに以下の記述を足してください。

scaffold_gretel/app/views/layouts/application.html.erb

  <body>
    <%= breadcrumbs separator: " &rsaquo; " %> # 追記
    <%= yield %>
  </body>

そして、以下の記述をルートパスであるユーザー一覧画面のビューに記載しましょう。

scaffold_gretel/app/views/layouts/application.html.erb

<% breadcrumb :users %>

そうして再度ルートパスを読み込むと、リンクタグが表示されたかと思います。

Image from Gyazo

まずroot_pathである"Home"が親ページとして表示され、その子ページとして"ユーザー一覧"が表示されているかと思います。 breadcrumb :usersの記載は:usersと対応したconfig/breadcrumbs.rbの以下の部分を表示しています。

crumb :users do
  link "ユーザ一覧", users_path
end

これはlink_toではなくlinkですが、意味はほとんど同じです。 テキストがユーザー一覧でユーザー一覧画面のリンク(users_path)を含んでいます。

次にユーザー詳細画面であるscaffold_gretel/app/views/users/show.html.erbにも追記します。

scaffold_gretel/app/views/users/show.html.erb

<% breadcrumb :user, @user %>

ユーザー詳細画面に移動しましょう。

Image from Gyazo

このパンくずリストを表示しているのは以下の部分です。

crumb :user_show do |user|
  link user.name, user_path(user)
  parent :users
end

<% breadcrumb :user, @user %>@userは上の記載のuserの値となるので、 テキストがuser.nameでユーザー一覧画面のリンク(user_path(user))を含んでいます。

また、parent :usersと記載したおうに親ページのユーザー一覧のリンクも表示されています。 こちらのリンクをクリックするとユーザー一覧画面に遷移できます。

Image from Gyazo

最後に

以上ざっくりとパンくずリストについて説明しました。 導入するのはとても簡単なのでぜひ使ってみてください!

以上、大ちゃんの駆け出し技術ブログでした!

【Rails】resources と resource

こんにちは!大ちゃんの駆け出し技術ブログです。

今回は初心に戻って、基礎編で学習したresourceresourcesの違いについてアウトプットします。

resources

Ruby on Railsでルーティングを行う際に必ず使うであろうresourcesresourcesはモデルに対してアプリケーションにおける基本メソッド7つを自動的にルーティングしてくれます。

例えばUserモデルに対してresourcesを使ってルーティングを記載した場合

Rails.application.routes.draw do
  resources :users
end

$ rails routesでルーティングを表示させると以下のようになります。

$ rails routes
   Prefix Verb   URI Pattern              Controller#Action
    users GET    /users(.:format)                    users#index                                               
          POST   /users(.:format)                    users#create                                                     
 new_user GET    /users/new(.:format)                users#new                                                  
edit_user GET    /users/:id/edit(.:format)           users#edit                                                   
     user GET    /users/:id(.:format)                users#show                                                 
          PATCH  /users/:id(.:format)                users#update                                           
          DELETE /users/:id(.:format)              users#destroy

このようにして、たった1行記載するだけで、簡単にルーティングをしてくれます。

ただ、resourcesが使われるのは基本的にモデルに複数のリソースがある時です。 例えば、Userモデルにはアプリケーションのユーザのモデルがたくさん登録されると思います。 他にも、ユーザの投稿モデルであるPostモデルも、アプリケーション内に複数の投稿が存在するので、resourcesを使用することができます。

resource

では、複数リソースに対してではなく単数のリソースにルーティングする場合はどうでしょうか?

単数のリソースとはつまり、アプリケーション内のページにおいて、ログインユーザだけが使用するリソースです。 例えば、Instagramの設定にあるプロフィールページは自分だけが使用できますよね? 他の人はあなたのプロフィール設定ページににアクセスすることはできません。(できたらアカウントが乗っ取られてしまいます。笑)

そのような単一リソースに対してルーティングを設定する際はresourcesではなく、単数形resourceを使います。 例えば、プロフィール画面をルーティングするとしましょう。

Rails.application.routes.draw do
  resources :users
  # 追記
  resource :profile
end
$ rails routes
      Prefix Verb   URI Pattern               Controller#Action
 new_profile GET    /profile/new(.:format)             profiles#new
edit_profile GET    /profile/edit(.:format)            profiles#edit
     profile GET    /profile(.:format)                 profiles#show
             PATCH  /profile(.:format)                 profiles#update
             DELETE /profile(.:format)                 profiles#destroy
             POST   /profile(.:format)                 profiles#create

resourcesと何かが違いますね、、。

①indexアクションがない

なぜ?、と思うかもしれませんが、これはよくよく考えてみると簡単です。

上述したように、resourceを使用する場合、単一リソースが使用対象をなることを説明しました。 しかし、indexアクションはモデルのリソース全てを表示することを意図しています。 結果、単一リソースであるプロフィール画面を扱う際に複数を表示するindexアクションは不要となるわけです。

②:idがない

4つのアクション(edit、show、update、destroy)から:idという部分が消えています。

実はこれもindexアクションがない理由と同じになります。

resourcesにおいて4つのアクションに:idが必要だった理由は、複数あるリソースの中からどのリソースかを指定するためです。 例えば、自分の投稿を編集する場合、どのidの投稿かを指定しないと、どの投稿を編集していいのかがわかりません。投稿の詳細閲覧、投稿の更新、投稿の削除も同様に、どの投稿であるかを指定しないといけません。

しかし、プロフィール設定画面はどうでしょうか。 単一リソースであるプロフィールは、指定せずとも一つしかないのだからわかるという仕組みです。 ですので、idを指定する必要がそもそもないという理由で:idが消えています。

終わりに

1つのアプリケーションを作成する中で単一リソースを使う頻度はそこまで多くありません。 しかし、プロフィール画面などはアプリケーションによく使われるので、念のため違いについては今一度再確認しておきましょう。

以上、大ちゃんの駆け出し技術ブログでした!

【Rails】日付や時刻を扱うRubyのクラス

はじめに

こんにちは!大ちゃんの駆け出し技術ブログです。

今回は日付、時刻を扱うRubyのクラスについて紹介したいと思います。

早速説明に入ります。皆さんは日付や時刻を表すRubyのクラスを知っていますか。

日付や時刻を表すクラスとして、Rubyの標準ライブラリには以下の3つが定義されています。

  • Timeクラス
  • Dateクラス
  • DateTimeクラス

Dateクラスは日付を扱うクラスで、TimeクラスとDateTimeクラスは日付と時刻を扱うクラスです。

(引用: プロを目指す人のためのRuby入門-言語仕様からテスト駆動開発・デバッグ技法まで)

今回は3つのクラスの違いにたまに触れつつ、日付に関わるクラスの使用方法や使えるメソッドなどについて紹介をしていきます。

準備としてテキトーなアプリケーションでコンソールを起動してください。いろいろな入力をしていくので、念のため変更内容が反映されないようオプションに--sandboxをつけておきましょう。(もしくはirbでもいいと思います)

$ rails c --sandbox

現在時刻を取り出す

まずは簡単に現在時刻をクラスメソッドを使用して出力してみましょう。

Time.new
=> 2021-01-27 08:33:49 +0900

2021-01-27の部分は本日の日付を表しており、08:33:49の部分は現在時刻です。では+0900とはなんでしょうか。察しの良い方はわかると思いますが、これは日本標準時協定世界時の時差を表しています。つまり、協定世界時より9時間先に進んでいるということになります。

ここでDate.newDateTime.newを入力すると少し違う形で出力されます。

Date.new
=> Mon, 01 Jan -4712
DateTime.new
=> Mon, 01 Jan -4712 00:00:00 +0000

上述したように、Dateは日付を扱いとDateTimeは日付と時刻を扱います。よってその違いは時刻の有無となります。

Mon, 01 Jan(つまり、1/1(月))とあるように、現在時刻を出力してくれないのです。これは暦日付に相当する日付オブジェクトを生成しているためです。暦日付について詳しくは説明しませんが、要は時間の出力方法が違うということですね。

Dateクラスを使って現在時刻を出力するにはtodayメソッドを使います。DateTimeクラスの場合はnowメソッドを使います。(Timeクラスもnowメソッドを使用すれば現在時刻を出力できます!出力結果はTime.newと同じになります。Timeが付けばnowと覚えておきましょう。)

Date.today
=> Wed, 27 Jan 2021
DateTime.now
=> Wed, 27 Jan 2021 10:18:01 +0900

特定の時刻を出力

現在時刻ではなく特定の時刻を引数を与えることで出力することができます。

例えば去年のクリスマスの日付を取り出したいのであれば、Date.newに対して2020, 12, 24引数として指定します。

Date.new(2020, 12, 24)
=> Fri, 24 Dec 2020

遊びの待ち合わせ時刻も出力したいのであればTimeもしくはDateTimeを使用します。

Time.new(2020,12,24,11, 0, 0)
=> 2020-12-24 11:00:00 +0900
DateTime.new(2020,12,24,11, 0, 0)
=> Thu, 24 Dec 2020 11:00:00 +0000

上記のTimeDateTimeを見てみると、2つの違いがわかると思います。

DateTimeの方はThuとあるようにその日の曜日も出力しています。また、+0000から日本標準時ではなく世界標準時を出力しています。 あともう1つ。引数を渡さなければデフォルト値が出力されます。例えば、Time.new(2020,12,24,11, 0, 0)の0の部分はデフォルト値ですので、Time.new(2020,12,24,11)で同じ値が出力できます。

Time.new(2020,12,24,11)
=> 2020-12-24 11:00:00 +0900

クラスの型の変更

上記でTimeクラスとDateTimeクラスの違いを上述しましたが、TimeクラスのからDateTimeクラスの出力に変更することもできます。Timeクラスの出力にto_datetimeメソッドを使用します。

Time.new(2020,12,24,11).to_datetime
=> Thu, 24 Dec 2020 11:00:00 +0900

to_datetimeメソッドがあるということは、、、

そうです。to_dateメソッドもto_timeメソッドもあります。

Time.new(2020,12,24,11).to_datetime.to_date
=> Thu, 24 Dec 2020
Time.new(2020,12,24,11).to_datetime.to_date.to_time
=> 2020-12-24 00:00:00 +0900

日付・時刻の各要素を出力

次は年・月・日のように日付や時刻を表している各要素を出力する方法を紹介します。と言ってもめちゃくちゃシンプルな方法です。なんと、yearメソッド、monthメソッドなどが標準で定義されています。さすがRuby

xmas_time = Time.new(2020,12,24,11)
=> 2020-12-24 11:00:00 +0900

xmas_time.year
=> 2020 # 年
xmas_time.month
=> 12   # 月
xmas_time.day
=> 24   # 日
xmas_time.hour
=> 11   # 時
xmas_time.min
=> 0    # 分
xmas_time.sec
=> 0    # 秒

strftimeメソッド

今までの日付・時刻の出力(2020-12-24 11:00:00 +0900など)は見た目で何を表しているのかがわかりますが、「2020/12/24」と出力したいとします。

そんな時に便利なメソッドがstrftimeメソッドです。引数に出力したいフォーマットを指定します。

Time.new(2020,12,24,11).strftime("%Y/%m/%d")
=> "2020/12/24"

フォーマットの引数を見ると少し複雑ですが覚える必要はありません。公式リファレンスを参照すれば出力したいフォーマットに合わせた引数をリストとして載せているのでそちらを参照すれば大丈夫です。下記は公式の使い勝手が良さそうなフォーマットの例になります。

t = Time.new(2001,2,3,4,5,6,"+09:00")
p t.strftime("%Y%m%d")           # => 20010203                 
p t.strftime("%F")               # => 2001-02-03                
p t.strftime("%T")               # => 04:05:06                  

まだまだあります!

今回紹介した以外にもまだまだたくさんのメソッドによる出力方法があります。

例えば、end_of_daybeginning_of_yearなどです。本当に直感的ですよねww

また機会があれば他のメソッドも紹介していきたいと思います!

以上!大ちゃんの駆け出し技術ブログでした!