20200922のRailsに関する記事は23件です。

Docker開発環境でCapybaraを使ったブラウザテスト環境を構築する

概要

docker開発環境でRSpecテスト環境を構築するやり方をまとめました。
初心者で独学なので間違っている部分やもっといいやり方があると思いますが、その時はご指摘頂けると嬉しいです。

以下の記事を参考にしました。
Rails on DockerでRSpecのSystem testをSelenium Dockerを使ってやってみた。

はじめに

以前、Dockerを使ったRails開発でブラウザテストが実行できないでブラウザテストのエラー解消の記事を投稿しましたが、
コメントを頂き、docker-composeを使ってchrome自体をサービスの一つとして動かせば、
Rails環境を汚さずに簡単にテストができるとアドバイスを頂きましたので試してみました。
ご指摘ありがとうございました!

1.docker-compose.ymlにchromeのサービスを追加

使用するイメージはChromeが最初からインストールされたものである、standalone-chromeを使います。

docker-compose.yml
version: '3'
services:
  web:
    build: .
    command: bundle exec rails s -p 3000 -b '0.0.0.0'
    volumes:
      - .:/[app名]
    ports:
      - 3000:3000
    depends_on:
      - db
      - chrome # ←追加
    tty: true
    stdin_open: true
  db:
    image: mysql:5.7
    volumes:
      - db-volume:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: password
 # ↓追加
  chrome:
    image: selenium/standalone-chrome:latest
    ports:
      - 4444:4444
 # ↑追加
volumes:
  db-volume:

2.RSpecの導入

rspec-railsの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.1'  # 追加
  gem 'factory_bot_rails', '~>4.11'
end

gemをインストールします。

$ docker-compose build

※ビルドした後はコンテナの再起動を忘れずに

RSpecのインストール

$ docker-compose run web rails g rspec:install

次に、インストールすると作成されるファイルの中に、"rails_helper.rb"に設定を記述していきます。
RSpec実行時にdocker-seleniumのコンテナのブラウザを使用するように設定します。

/spec/rails_helper.rb
#~
Capybara.register_driver :remote_chrome do |app|
  url = "http://chrome:4444/wd/hub"
  caps = ::Selenium::WebDriver::Remote::Capabilities.chrome(
    "goog:chromeOptions" => {
      "args" => [
        "no-sandbox",
        "headless",
        "disable-gpu",
        "window-size=1680,1050"
      ]
    }
  )
  Capybara::Selenium::Driver.new(app, browser: :remote, url: url, desired_capabilities: caps)
end

#~

RSpec.configure do |config|

  config.before(:each, type: :system) do
    driven_by :rack_test
  end

  config.before(:each, type: :system, js: true) do
    driven_by :remote_chrome
    Capybara.server_host = IPSocket.getaddress(Socket.gethostname)
    Capybara.server_port = 4444
    Capybara.app_host = "http://#{Capybara.server_host}:#{Capybara.server_port}"
  end
#~
end

最後に.rspecを編集して、rails_helper.rbの設定を読み取るようにします。

.rspec
- --require spec_helper
+ --require rails_helper

以上です。

あとはテストを記述してテストを実行できるか確認してみます。

#コンテナ起動時
$ docker-compose exec web rspec [rspecテストファイルのpath]

テスト失敗時にブラウザのハードコピーを確認することができました。

スクリーンショット 2020-09-22 22.30.01.png

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

Ruby on Rails 環境構築(Windows10)で苦労したこと(SQLite3)

Railsのインストールがやっとできました。
苦労した点1つめをまとめます。

RubyとRailsをインストール後
Railsアプリを新規作成したところ、こんな感じのエラーがたくさんでました。

エラー: mingw32: キー "AD351C50AE085775EB59333B5F92EFC1A47D45A1" は不明です
・・・
エラー: データベース 'mingw64' は無効です (無効または破損したデータベース (PGP
鍵))

Gemfileで
gem 'sqlite3', '~> 1.4'
となっていたので
いろいろ修正してみたけれどだめ。

そこで
sqlite.dllをRubyのbinへ配置
node.jsのインストール
yarnのインストール
を行い

gem install sqlite3

を実施。それでもエラーが出てしまい・・
エラー内容をよく見てみると、こんな記述が・・

checking for sqlite3.h... no
sqlite3.h is missing. Install SQLite3 from http://www.sqlite.org/ first.

sqlite3.hのチェックで引っかかっている?!

そこで、sqlite3.hをRubyのbinへ置き、引数にsqlite.hの場所、sqlite3.dllの場所を指定してgem install sqlite3を実行。

それでもエラーが出て・・
引数にsqlite3のバージョンまで指定するとうまくいきました。

こんな感じです。

gem install sqlite3 --version 1.3.13 --platform=ruby -- --with-sqlite3-include=C:\Ruby27-x64\bin --with-sqlite3-lib=C:\Ruby27-x64\bin

最後に
Gemfileを
gem 'sqlite3', '~> 1.3.13'
に修正して
bundle install
を実行。
Railsアプリの新規作成を再度行うと無事に作成できました。

環境

Windows10 home

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

本番環境で画像が表示されない

デプロイ後画像が表示されなくて困ったのでまとめてみました。

どなたかのお役に立てたら嬉しいです。

ruby '2.6.5'
rails '6.0.0'

オリジナルアプリケーションを作っています。

本番環境はHerokuを使用します。
Herokuにデプロイして動作確認したらエラーログの出現しました。

12.png

エラーログから本番環境ですと画像へのパスが変わってしまうのが原因と考えました。

ローカルだとassets/images/直下におけばそのまま表示されますが
本番環境だとプリコンパイルされ表示されない為エラーになりました。

調べると、
productionでは、Railsはプリコンパイルされたファイルをpublic/assetsに置きます。プリコンパイルされたファイルは、Webサーバーによって静的なアセットとして扱われます。
app/assetsに置かれたファイルがそのままの形でproduction環境で使用されることは決してありません。

参考記事はこちら

上記の記事を参考に本番環境のパスを作ります。

_medicine.html.erb
   <%= image_tag asset_path('medicine3.jpeg'), class:"med-pic" %>

このようにしてパス再指定しましたが私の場合はまたエラーになりました。

本番環境でのasset precompileの設定がされていないのではないかとご指摘いただき確認。

変更前

production.rb
     config.assets.compile = false

変更後

production.rb
     config.assets.compile = true

こちらを有効にしてみたら無事画像表示できました。

まとめ

<<初心者向け>>本番環境で画像表示されない場合のやってみて損はない対処法

①本番環境でのasset precompileの設定が有効か確認

29
   config.assets.compile = true

②プリコンパイルでPublic内にAssetsファイルを作成

   % rails assets:precompile

③assets_pathを記述する

   <%= image_tag asset_path('medicine3.jpeg'), class:"med-pic" %>

   

④デプロイ完了して無事、画像表示完了!!

asset precompileのやってることとか軽く調べておくとより良いかもです!

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

(ギリ)20代の地方公務員がRailsチュートリアルに取り組みます【第13章】

前提

・Railsチュートリアルは第4版
・今回の学習は3周目(9章以降は2周目)
・著者はProgate一通りやったぐらいの初学者

基本方針

・読んだら分かることは端折る。
・意味がわからない用語は調べてまとめる(記事最下段・用語集)。
・理解できない内容を掘り下げる。
・演習はすべて取り組む。
・コードコピペは極力しない。

 
 さてさて第13章。早いもので残り2章となりました。今回はマイクロポストというユーザーのメッセージ投稿機能を実装していきます。要はTwitterよね。気張っていきましょう!!
 
本日のBGMはこちら。
ARIA vocal collection
ネオ・ヴェネツィアでカフェラテ飲みながらリモートワークできる時代が来ねえかなあ…

 

【13.1.1 Micropostモデル 演習】

1. RailsコンソールでMicropost.newを実行し、インスタンスを変数micropostに代入してください。その後、user_idに最初のユーザーのidを、contentに "Lorem ipsum" をそれぞれ代入してみてください。この時点では、 micropostオブジェクトのマジックカラム (created_atとupdated_at) には何が入っているでしょうか?

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

3. 先ほど作ったmicropostオブジェクトをデータベースに保存してみましょう。この時点でもう一度マジックカラム
→ まとめてドン

>> micropost = Micropost.new(user_id: 1, content: "Lorem ipsum")
=> #<Micropost id: nil, content: "Lorem ipsum", user_id: 1, created_at: nil, updated_at: nil>
>> micropost.created_at
=> nil
>> micropost.updated_at
=> nil

>> micropost.user
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
=> #<User id: 1, name: "Example User", email: "example@railstutorial.org", created_at: "2020-09-17 08:34:09", updated_at: "2020-09-17 08:34:09", password_digest: "$2a$10$.EZN.AXBx91cG82BFOaKp.qpuwRpmG5N1JASh6KIBnv...", remember_digest: nil, admin: true, activation_digest: "$2a$10$mQpfXtRYM2s5JyNF243gYOln7RRrGaHHlilpOHouLfk...", activated: true, activated_at: "2020-09-17 08:34:09", reset_digest: nil, reset_sent_at: nil>
>> micropost.user.name
=> "Example User"

>> micropost.save
   (0.1ms)  SAVEPOINT active_record_1
  SQL (1.3ms)  INSERT INTO "microposts" ("content", "user_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["content", "Lorem ipsum"], ["user_id", 1], ["created_at", "2020-09-21 02:33:40.343372"], ["updated_at", "2020-09-21 02:33:40.343372"]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> true
>> micropost.created_at
=> Mon, 21 Sep 2020 02:33:40 UTC +00:00
>> micropost.updated_at
=> Mon, 21 Sep 2020 02:33:40 UTC +00:00

 

【13.1.2 Micropostのバリデーション 演習】

1. Railsコンソールを開き、user_idとcontentが空になっているmicropostオブジェクトを作ってみてください。このオブジェクトに対してvalid?を実行すると、失敗することを確認してみましょう。また、生成されたエラーメッセージにはどんな内容が書かれているでしょうか?
→ 下記

>> micropost = Micropost.new(user_id: " ", content: " ")
=> #<Micropost id: nil, content: " ", user_id: nil, created_at: nil, updated_at: nil>
>> micropost.valid?
=> false
>> micropost.errors.messages
=> {:user=>["must exist"], :user_id=>["can't be blank"], :content=>["can't be blank"]}

 
2. コンソールを開き、今度はuser_idが空でcontentが141文字以上のmicropostオブジェクトを作ってみてください。このオブジェクトに対してvalid?を実行すると、失敗することを確認してみましょう。また、生成されたエラーメッセージにはどんな内容が書かれているでしょうか?
→ 下記

>> micropost = Micropost.new(user_id: " ", content: "a" * 141)
=> #<Micropost id: nil, content: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...", user_id: nil, created_at: nil, updated_at: nil>
>> micropost.valid?=> false
>> micropost.errors.messages=> {:user=>["must exist"], :user_id=>["can't be blank"], :content=>["is too long (maximum is 140 characters)"]}

 

【13.1.3 User/Micropostの関連付け メモと演習】

belongs_to: to以下に属する。これを設定した側はメソッド的に使えるようになる。
has_many: 紐付きの親になる側に設定。

1. データベースにいる最初のユーザーを変数userに代入してください。そのuserオブジェクトを使ってmicropost = user.microposts.create(content: "Lorem ipsum")を実行すると、どのような結果が得られるでしょうか?
→ 下記

>> user = User.first
  User Load (0.1ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> #<User id: 1, name: "Example User", email: "example@railstutorial.org", created_at: "2020-09-17 08:34:09", updated_at: "2020-09-17 08:34:09", password_digest: "$2a$10$.EZN.AXBx91cG82BFOaKp.qpuwRpmG5N1JASh6KIBnv...", remember_digest: nil, admin: true, activation_digest: "$2a$10$mQpfXtRYM2s5JyNF243gYOln7RRrGaHHlilpOHouLfk...", activated: true, activated_at: "2020-09-17 08:34:09", reset_digest: nil, reset_sent_at: nil>
>> micropost = user.microposts.create(content: "Lorem ipsum")                       
   (0.1ms)  SAVEPOINT active_record_1
  SQL (1.8ms)  INSERT INTO "microposts" ("content", "user_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["content", "Lorem ipsum"], ["user_id", 1], ["created_at", "2020-09-21 03:35:41.329488"], ["updated_at", "2020-09-21 03:35:41.329488"]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> #<Micropost id: 1, content: "Lorem ipsum", user_id: 1, created_at: "2020-09-21 03:35:41", updated_at: "2020-09-21 03:35:41">

 
2. 先ほどの演習課題で、データベース上に新しいマイクロポストが追加されたはずです。user.microposts.find(micropost.id)を実行して、本当に追加されたのかを確かめてみましょう。また、先ほど実行したmicropost.idの部分をmicropostに変更すると、結果はどうなるでしょうか?
→ 下記

>> user.microposts.find(micropost.id)
  Micropost Load (0.2ms)  SELECT  "microposts".* FROM "microposts" WHERE "microposts"."user_id" = ? AND "microposts"."id" = ? LIMIT ?  [["user_id", 1], ["id", 1], ["LIMIT", 1]]
=> #<Micropost id: 1, content: "Lorem ipsum", user_id: 1, created_at: "2020-09-21 03:35:41", updated_at: "2020-09-21 03:35:41">
>> user.microposts.find(micropost)
Traceback (most recent call last):
        1: from (irb):6
ArgumentError (You are passing an instance of ActiveRecord::Base to `find`. Please pass the id of the object by calling `.id`.)

 
3. user == micropost.userを実行した結果はどうなるでしょうか? また、user.microposts.first == micropost を実行した結果はどうなるでしょうか? それぞれ確認してみてください。
→ 下記

>> user == micropost.user
=> true
>> user.microposts.first == micropost
  Micropost Load (0.2ms)  SELECT  "microposts".* FROM "microposts" WHERE "microposts"."user_id" = ? ORDER BY "microposts"."id" ASC LIMIT ?  [["user_id", 1], ["LIMIT", 1]]
=> true

 

【13.1.4 マイクロポストを改良する メモと演習】

default_scope: あるスコープをモデルのすべてのクエリに適用したい場合に使用。
dependent: :destroy:has_manyに設定。親が削除されたときに、紐づいている子側のデータベースがすべて削除される。
 その他、いろいろ出てきたので用語集にまとめています。

1. Micropost.first.created_atの実行結果と、Micropost.last.created_atの実行結果を比べてみましょう。
→ 適当にmicropostを2つ作ってやってから下記(なんとなくsandbox使ってるので以前の演習データが残ってません。)

>> Micropost.first.created_at
  Micropost Load (0.1ms)  SELECT  "microposts".* FROM "microposts" ORDER BY "microposts"."created_at" DESC LIMIT ?  [["LIMIT", 1]]
=> Mon, 21 Sep 2020 04:56:04 UTC +00:00
>> Micropost.last.created_at
  Micropost Load (0.2ms)  SELECT  "microposts".* FROM "microposts" ORDER BY "microposts"."created_at" ASC LIMIT ?  [["LIMIT", 1]]
=> Mon, 21 Sep 2020 04:55:35 UTC +00:00

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

>> Micropost.first
  Micropost Load (0.2ms)  SELECT  "microposts".* FROM "microposts" ORDER BY "microposts"."created_at" DESC LIMIT ?  [["LIMIT", 1]]
=> #<Micropost id: 2, content: "jaoivhiua", user_id: 2, created_at: "2020-09-21 04:56:04", updated_at: "2020-09-21 04:56:04">
>> Micropost.last
  Micropost Load (0.1ms)  SELECT  "microposts".* FROM "microposts" ORDER BY "microposts"."created_at" ASC LIMIT ?  [["LIMIT", 1]]
=> #<Micropost id: 1, content: "krutinb", user_id: 1, created_at: "2020-09-21 04:55:35", updated_at: "2020-09-21 04:55:35">

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

>> user = User.first
  User Load (0.1ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> #<User id: 1, name: "Example User", email: "example@railstutorial.org", created_at: "2020-09-17 08:34:09", updated_at: "2020-09-17 08:34:09", password_digest: "$2a$10$.EZN.AXBx91cG82BFOaKp.qpuwRpmG5N1JASh6KIBnv...", remember_digest: nil, admin: true, activation_digest: "$2a$10$mQpfXtRYM2s5JyNF243gYOln7RRrGaHHlilpOHouLfk...", activated: true, activated_at: "2020-09-17 08:34:09", reset_digest: nil, reset_sent_at: nil>

>> user.microposts.first.id
  Micropost Load (0.2ms)  SELECT  "microposts".* FROM "microposts" WHERE "microposts"."user_id" = ? ORDER BY "microposts"."created_at" DESC LIMIT ?  [["user_id", 1], ["LIMIT", 1]]
=> 1

>> user.destroy
   (0.1ms)  SAVEPOINT active_record_1
  Micropost Load (0.1ms)  SELECT "microposts".* FROM "microposts" WHERE "microposts"."user_id" = ? ORDER BY "microposts"."created_at" DESC  [["user_id", 1]]
  SQL (0.1ms)  DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 1]]
  SQL (0.7ms)  DELETE FROM "users" WHERE "users"."id" = ?  [["id", 1]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> #<User id: 1, name: "Example User", email: "example@railstutorial.org", created_at: "2020-09-17 08:34:09", updated_at: "2020-09-17 08:34:09", password_digest: "$2a$10$.EZN.AXBx91cG82BFOaKp.qpuwRpmG5N1JASh6KIBnv...", remember_digest: nil, admin: true, activation_digest: "$2a$10$mQpfXtRYM2s5JyNF243gYOln7RRrGaHHlilpOHouLfk...", activated: true, activated_at: "2020-09-17 08:34:09", reset_digest: nil, reset_sent_at: nil>

>> Micropost.find(1)
  Micropost Load (0.2ms)  SELECT  "microposts".* FROM "microposts" WHERE "microposts"."id" = ? ORDER BY "microposts"."created_at" DESC LIMIT ?  [["id", 1], ["LIMIT", 1]]
Traceback (most recent call last):
        1: from (irb):13
ActiveRecord::RecordNotFound (Couldn't find Micropost with 'id'=1)

 

【13.2.1 マイクロポストの描画 演習】

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

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

>> helper.time_ago_in_words(3.weeks.ago)
=> "21 days"
>> helper.time_ago_in_words(6.months.ago)
=> "6 months"
>> helper.time_ago_in_words(1.year.ago)
=> "about 1 year"

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

s' for #<Class:0x0000000005968db8>)
>> user = User.first
  User Load (0.2ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> #<User id: 1, name: "Example User", email: "example@railstutorial.org", created_at: "2020-09-17 08:34:09", updated_at: "2020-09-17 08:34:09", password_digest: "$2a$10$.EZN.AXBx91cG82BFOaKp.qpuwRpmG5N1JASh6KIBnv...", remember_digest: nil, admin: true, activation_digest: "$2a$10$mQpfXtRYM2s5JyNF243gYOln7RRrGaHHlilpOHouLfk...", activated: true, activated_at: "2020-09-17 08:34:09", reset_digest: nil, reset_sent_at: nil>
>> microposts = user.microposts.paginate(page: nil)
  Micropost Load (0.1ms)  SELECT  "microposts".* FROM "microposts" WHERE "microposts"."user_id" = ? ORDER BY "microposts"."created_at" DESC LIMIT ? OFFSET ?  [["user_id", 1], ["LIMIT", 11], ["OFFSET", 0]]
=> #<ActiveRecord::AssociationRelation []>
>> microposts.class
=> Micropost::ActiveRecord_AssociationRelation

 

【13.2.2 マイクロポストのサンプル 演習】

1. (1..10).to_a.take(6)というコードの実行結果を推測できますか? 推測した値が合っているかどうか、実際にコンソールを使って確認してみましょう。
→ [1,2,3,4,5,6]という配列ができる。予想どおり。

>> (1..10).to_a.take(6)
=> [1, 2, 3, 4, 5, 6]

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

>> (1..10).take(6)
=> [1, 2, 3, 4, 5, 6]

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

>> Faker::University.name
=> "East Abshire University"
>> Faker::PhoneNumber.cell_phone
=> "1-315-982-9239"
>> Faker::Hipster.sentence
=> "Mumblecore pug tilde marfa drinking 8-bit."
>> Faker::ChuckNorris.fact
=> "Chuck Norris can binary search unsorted data."

>> Faker::Okinawa::Awamori.name
=> "瑞穂"

 

【13.2.3 プロフィール画面のマイクロポストをテストする メモと演習】

再び登場response.body。完全なHTMLが含まれる。assert_match でそのHTMLから該当の要素を探し出す。assert_selectよりも抽象的なメソッド。
'h1>img.gravatar'⇨h1タグの内側にあるgravatarクラス付きのimgタグ というネスト。

1. リスト 13.28にある2つの'h1'のテストが正しいか確かめるため、該当するアプリケーション側のコードをコメントアウトしてみましょう。テストが green から redに変わることを確認してみてください。
→ showビューのh1の中身をコメントアウトしよう。

show.thml.erb
      <h1>
        <%#= gravatar_for @user %>
        <%#= @user.name %>
      </h1>

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

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

 

【13.3.1 マイクロポストのアクセス制御 演習】

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

 

【13.3.2 マイクロポストを作成する 演習】

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

home.thml.erb
<% if logged_in? %>
  <%= render 'logged_in' %>
<% else %>
  <%= render 'not_logged_in' %>
<% end %>
_logged_in.html.erb
  <div class="row">
    <aside class="col-md-4">
      <section class="user_info">
        <%= render 'shared/user_info' %>
      </section>
      <section class="micropost_form">
        <%= render 'shared/micropost_form' %>
      </section>
    </aside>
  </div>
_not_logged_in.html.erb
  <div class="center jumbotron">
    <h1>Welcome to the Sample App</h1>

    <h2>
      This is the home page for the
      <a href="https://railstutorial.jp/">Ruby on Rails Tutorial</a>
      sample application.
    </h2>

    <%= link_to "Sign up now!", signup_path, class: "btn btn-lg btn-primary" %>
  </div>

  <%= link_to image_tag("rails.png", alt: "Rails logo"),
            'http://rubyonrails.org/' %>

 

【13.3.3 フィードの原型 演習】

1. 新しく実装したマイクロポストの投稿フォームを使って、実際にマイクロポストを投稿してみましょう。Railsサーバーのログ内にあるINSERT文では、どういった内容をデータベースに送っているでしょうか? 確認してみてください。
→ INSERT INTO "microposts" ("content", "user_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["content", "good morning."], ["user_id", 1], ["created_at", "2020-09-22 01:20:32.232820"], ["updated_at", "2020-09-22 01:20:32.232820"]]

 
2. コンソールを開き、user変数にデータベース上の最初のユーザーを代入してみましょう。その後、Micropost.where("user_id = ?", user.id)とuser.microposts、そしてuser.feedをそれぞれ実行してみて、実行結果がすべて同じであることを確認してみてください。ヒント: ==で比較すると結果が同じかどうか簡単に判別できます。
→ 全部一緒ですね。

>> Micropost.where("user_id = ?", user.id) == user.microposts
  Micropost Load (0.9ms)  SELECT "microposts".* FROM "microposts" WHERE "microposts"."user_id" = ? ORDER BY "microposts"."created_at" DESC  [["user_id", 1]]
  Micropost Load (0.2ms)  SELECT "microposts".* FROM "microposts" WHERE (user_id = 1) ORDER BY "microposts"."created_at" DESC
=> true
>> Micropost.where("user_id = ?", user.id) == user.feed
=> true
>> user.microposts == user.feed
  Micropost Load (0.3ms)  SELECT "microposts".* FROM "microposts" WHERE (user_id = 1) ORDER BY "microposts"."created_at" DESC
=> true

 

【13.3.4 マイクロポストを削除する メモと演習】

request.referrerメソッド: 一つ前のURLを返す。

1. マイクロポストを作成し、その後、作成したマイクロポストを削除してみましょう。次に、Railsサーバーのログを見てみて、DELETE文の内容を確認してみてください。
→ DELETE FROM "microposts" WHERE "microposts"."id" = ? ["id", 302] commit transaction

 
2. redirect_to request.referrer || root_urlの行をredirect_back(fallback_location: root_url)と置き換えてもうまく動くことを、ブラウザを使って確認してみましょう (このメソッドはRails 5から新たに導入されました)。
→ 動きます。

 

【13.3.5 フィード画面のマイクロポストをテストする 演習】

1. リスト 13.55で示した4つのコメント (「無効な送信」など) のそれぞれに対して、テストが正しく動いているか確認してみましょう。具体的には、対応するアプリケーション側のコードをコメントアウトし、テストが redになることを確認し、元に戻すと greenになることを確認してみましょう。
→ Yes, GREEN.

 
2. サイドバーにあるマイクロポストの合計投稿数をテストしてみましょう。このとき、単数形 (micropost) と複数形 (microposts) が正しく表示されているかどうかもテストしてください。ヒント: リスト 13.57を参考にしてみてください。
→ これでよかろうか。

microposts_interface_test.rb
  test "micropost sidebar count" do
    log_in_as(@user)
    get root_path
    assert_match "#{@user.microposts.count} microposts", response.body
    other_user = users(:malory)
    log_in_as(other_user)
    get root_path
    assert_match "0 microposts", response.body
    other_user.microposts.create!(content: "A micropost")
    get root_path
    assert_match "1 micropost", response.body
  end

 

【13.4.1 基本的な画像アップロード 演習】

1. 画像付きのマイクロポストを投稿してみましょう。もしかして、大きすぎる画像が表示されてしまいましたか? (心配しないでください、次の13.4.3でこの問題を直します)。
→ まあまあでした。

 
2. リスト 13.63に示すテンプレートを参考に、13.4で実装した画像アップローダーをテストしてください。テストの準備として、まずはサンプル画像をfixtureディレクトリに追加してください (コマンド例: cp app/assets/images/rails.png test/fixtures/)。リスト 13.63で追加したテストでは、Homeページにあるファイルアップロードと、投稿に成功した時に画像が表示されているかどうかをチェックしています。なお、テスト内にあるfixture_file_uploadというメソッドは、fixtureで定義されたファイルをアップロードする特別なメソッドです18 。ヒント: picture属性が有効かどうかを確かめるときは、11.3.3で紹介したassignsメソッドを使ってください。このメソッドを使うと、投稿に成功した後にcreateアクション内のマイクロポストにアクセスするようになります。
→ 下記。ここでテストがGREENにならない。間違ってそうな箇所が見当たらないので、調べたら解決。こちらの記事を参考にspringを再起動したらGREENになりました。

microposts_interface_test.rb
  test "micropost interface" do
    log_in_as(@user)
    get root_path
    assert_select 'div.pagination'
    assert_select 'input[type="file"]'
    # 無効な送信
    post microposts_path, params: { micropost: { content: "" } }
    assert_select 'div#error_explanation'
    # 有効な送信
    content = "This micropost really ties the room together"
    picture = fixture_file_upload('test/fixtures/rails.png', 'image/png')
    assert_difference 'Micropost.count', 1 do
      post microposts_path, params: { micropost: { 
                                      content: content,
                                      picture: picture } }
    end
    assert assigns(:micropost).picture?
    follow_redirect!
    assert_match content, response.body
    # 投稿を削除する
    assert_select 'a', text: 'delete'
    first_micropost = @user.microposts.paginate(page: 1).first
    assert_difference 'Micropost.count', -1 do
      delete micropost_path(first_micropost)
    end
    # 違うユーザーのプロフィールにアクセス (削除リンクがないことを確認)
    get user_path(users(:archer))
    assert_select 'a', text: 'delete', count: 0
  end

 

【13.4.2 画像の検証 演習】

1. 5MB以上の画像ファイルを送信しようとした場合、どうなりますか?

2. 無効な拡張子のファイルを送信しようとした場合、どうなりますか?
→ 両方とも、該当するファイルがあらへんよ…。多分エラーが出るでしょう。

 

【13.4.3 画像のリサイズ 演習】

1. 解像度の高い画像をアップロードし、リサイズされているかどうか確認してみましょう。画像が長方形だった場合、リサイズはうまく行われているでしょうか?
→ OK

 
2. 既にリスト 13.63のテストを追加していた場合、この時点でテストスイートを走らせると紛らわしいエラーメッセージが表示されることがあります。このエラーを取り除いてみましょう。ヒント: リスト 13.68にある設定ファイルを修正し、テスト時はCarrierWaveに画像のリサイズをさせないようにしてみましょう。
→ エラー吐いてなかったけど一応やっときました。

 

【13.4.4 本番環境での画像アップロード 演習】

1. 本番環境で解像度の高い画像をアップロードし、適切にリサイズされているか確認してみましょう。長方形の画像であっても、適切にリサイズされていますか?
→ よっしゃ!いけた!

 

第13章まとめ

・マイクロポストもリソース化。
・belongs_toとhas_manyでデータテーブル同士を紐付け。子側がメソッド的に使えるようになる。
・デフォルトスコープで表示順序を変更。使い方は気をつけた方がいいっぽい記事が調べると出てきた。
・dependent: :destroyオプションを使うと、関連付けされたオブジェクトと自分自身を同時に削除する。
・ActiveRecordの使用により、生のSQLを使うことはほとんどない。
・CarrierWaveで画像アップロードを実装。(今は標準がActive Storage?)

 
 マイクロポスト実装終わり〜〜。ここはけっこう分量ありました。特にテスト、やってること自体は復習が多いけど、量がありましたね。これでsample_appもそれらしくなってきました。
 さあいよいよラスト、第14章に入ります。ユーザーフォローからステータスフィードの実装まで、ラストスパートです!

 
⇦ 第12章はこちら
学習にあたっての前提・著者ステータスはこちら
 

なんとなくイメージを掴む用語集

・Proc
 ブロック( { }とかdo ~ end)をオブジェクト化したもの。ブロックはオブジェクトではないので、procでオブジェクト化する必要がある。こちらを参照

・scope(メソッド)
特定のクエリをまとめたもの。何回も使用するものをまとめてしまって、繰り返し長ったらしいコードを書くことを避ける。

・ラムダ式
 無名関数。その名のとおり、名前付けされていない関数のこと。

・SQLインジェクション
 アプリケーションのセキュリティ上の不備を意図的に利用し、アプリケーションが想定しないSQL文を実行させることにより、データベースシステムを不正に操作する攻撃方法のこと。また、その攻撃を可能とする脆弱性のこと。

・MIME(Multipurpose Internet Mail Extension(多目的インターネットメール拡張))
 規格上US-ASCIIのテキストしか使用できないインターネットの電子メールでさまざまなフォーマット(書式)を扱えるようにする規格。

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

【AWS】Amazon SESを用いてRuby on Railsのdeviseでメールを送信する

目次

・経緯
・今回の前提条件
・注意事項
・AWS SES(Simple Email Service)とは
・料金
・大体の流れ(イメージ)
・1. AWS SESの設定
・2. AWSのCSに制限解除のメールを送る
・3. IAMユーザーの設定
・4. railsに設定
・用語解説
・最後に

経緯

私がPortfolioを作成している段階でして、divise上のメールが飛んでいないことに気づきました。
改善策を模索している中、AWSSES(有料)なるものを見つけGmail(無料?)でも実装できることが分かったが、AWSでEC2なども利用していたので
・AWSでまとめて管理ができるのですっきりしそう
・いい経験になるかも
上記の理由で実装してみた。

私自身も初学者なので、完全に鵜呑みにせずイメージをつかむ感覚で読んでいただければと!

今回の前提条件

  • Ruby on railsでappがありdeviseを導入している
  • AWSのアカウントを所持している
  • Route53の設定が完了していて、ドメインでのアクセス可能なサイトがある。

くらいですかね、、

注意事項

私がだいぶ躓いたところなので、、
2020/09/22現在AWSSESを東京リージョン(ap-northeast-1)で本番環境で実装するとメールが飛びません!!!!!
最近のリリースなのでまだバグが多いから?!
解決方法わかる方教えていただきたいです
祝!Amazon SESが東京リージョンにやってきた!
私はアイルランドリージョン(eu-west-1)で実装しました。
AWS SES以外の設定は東京リージョンで済ませております。(EC2,Route53など)

あとサンドボックスの制限解除に1日かかります!
ご利用は計画的に!

AWS SES(Simple Email Service)とは

AWSで提供されているクラウドベースの電子メール送信サービスです。

料金

AWS SESは有料で、一定量送信毎での従量課金制ぽいです。

SES の料金は、送信する各 1,000 通の E メールにつき、0.10USD であり、非常に低コストです。

上記の通り、高額な金額が発生するわけではなさそうなので試しに利用してみるのもありでしょう!

大体の流れ(イメージ)

  1. AWS SESの設定
  2. AWSのCSに制限解除のメールを送る
  3. IAMユーザーの設定
  4. railsに設定

1. AWS SESの設定

AWS-マネジメントコンソール (2).png
1-1. AWSにログイン
1-2. ヘッダーのサービスをクリック
1-3. SESと検索してエンター

SES-Management-Console.png
1-4. IdentityManagement>Domainをクリック
1-5. Varufy a NeW Domianをクリック
この段階で東京リージョン以外のリージョンを選択しておいたほうが良いです

SES-Management-Console (2).png
1-6. Domainに接続したいドメイン名を入れる
1-7. Generate DKIM Settingsにチェック入れる
1-8. Varufy This Domianをクリック

SES-Management-Console (3).png
1-9. User Route53をクリック
Route53を登録している場合上記表示になる。
登録している場合レコードの設定までを自動でやってくれるぽい

SES-Management-Console (4).png
1-10. Create Record Setsをクリック
AWS SWSのsettingは一旦こちらで終了です!

SES-Management-Console (5).png
しばらくすると各項目がverifiedに代わると思います。

2. AWSのCSに制限解除のメールを送る

AWS SESをsettingをしただけではメールは送信されません。
設定初期ProductionAcsessがSandboxになっており、これを解除する必要があります。

SES-Management-Console (7).png
2-1. Email Sending > Sending Statistics をクリック  
2-2. ProductionAcsessがSandboxになっていることを確認する。

SES-Management-Console (6).png
2-3. 右上のサポートをクリック
2-4. サポートセンターをクリック

AWS-サポートダッシュボード.png
2-5. 中央にあるCreate Caseをクリック

以降メールの送信手順に関しては下記記事でご丁寧にまとめられておりましたので
メールを送るまでは参考にしてみてください。
2-6.メール送信の手順
※2.解除申請を送るフォームに入力から行ってみてください。

S__635158530.jpg
メール送信して一日後、上記のような解除しましたとメールが来れば
下記がProductionAcsessがSandbox⇒Enabledに代わります。

SES-Management-Console (8).png

SES-Management-Console (10).png
2-7.IdentityManagement>Domain>SendaTestEmailをクリック
2-8.メールを送ってみる
最後にテストです。
メールを送り受信ボックスに届くか確認します。

3. IAMユーザーの設定

AWS SESのsettingと制限解除だけではrailsにつなぎこめません。
最後にIAMユーザーを新規登録しなくてはいけません。
※筆者はrootアカウントでキーとIDを参照する方法を探してみましたがありませんでした。
※本来rootアカウントでは接続管理は好ましくなく、各アクションごとにIAMユーザー(権限の範囲)を指定して接続したほうがいいと参考書に書いてありました。
のであるかはわかりませんがとりあえずIAMユーザーで作成しておけってことですね。

AWS-マネジメントコンソール (3).png
3-1. ヘッダーのサービスをクリック
3-2. IAMと検索してエンター

IAM-Management-Console (7).png
3-3. ユーザーをクリック
3-4. ユーザーを追加をクリック

IAM-Management-Console (8).png
3-5. ユーザー名を入れる(※ここはわかりやすければなんでもOKです)
3-6. プログラムによるアクセスにチェック
3-7. 次のステップ:アクセス権限をクリック

IAM-Management-Console (10).png
3-8. 既存ポリシーを直接アタッチをクリック
3-9. ポリシーのフィルタにSESと入れる
3-10. AmazonSESFullAccessにチェックを入れる
3-11. 次ぐのステップ:タグをクリック

IAM-Management-Console (11).png
3-12. 次ぐのステップ:確認をクリック
キーと値は何も入れなくてOKです。

IAM-Management-Console (12).png
3-13. ユーザーの作成をクリック

IAM-Management-Console (13).png
こちらでIAMユーザーの作成が完了です。
※アクセスキーIDとシークレットアクセスキーは必ず控えておきましょう。
※アクセスキーIDは後程確認できますが、シークレットアクセスキーはこのページでしか確認ができず消したらもう見れません。

こちらでAWS上での設定は完了です。

4. railsに設定

お疲れ様です。
最後はrailsに設定して終わりです。
大体の作業は下記の流れです。
1. gemのインストール
2. ActionMailerの設定
3. 本番環境、開発環境の設定
4. deviseの設定変更
5. Mailerクラスのfromの設定
6. .envをアップロードする(本番環境用)

4-1. gemのインストール

gem 'aws-ses'

Gemfileにて上記を記載してbundle install

4-2. ActionMailerの設定

ActionMailer::Base.add_delivery_method :ses,
  AWS::SES::Base,
  access_key_id: ENV['AWS_ACCESS_KEY_ID'],
  secret_access_key: ENV['AWS_SECRET_ACCESS_KEY'],
  server: 'email.リージョンID.amazonaws.com'

config/initializers/aws.rbを新規で作成
リージョンIDと記載されているところはSESを作成したリージョンIDを入れてください。
※分からない場合はAWSにログインをするとURLの末尾にリージョンIDが記載されてます。


東京:ap-northeast-1
アイルランド:eu-west-1

IDとKEYはpushしてしまうとまずいので.envに新規で追加します。

AWS_ACCESS_KEY_ID = IDを入力
AWS_SECRET_ACCESS_KEY = KEYを入力

4-3 開発環境(のみ)の設定

config.action_mailer.default_url_options = { host: 'ドメイン名' }
config.action_mailer.delivery_method = :ses

config/environments/development.rbに上記を追加
ドメイン名と書いてあるところは接続しようとしているドメイン名に変えてください。

4-3 本番環境(のみ)の設定

config.action_mailer.default_url_options = { host: 'ドメイン名' }
config.action_mailer.delivery_method = :ses

config/environments/production.rbに上記を追加
ドメイン名と書いてあるところは接続しようとしているドメイン名に変えてください。

4-4. deviseの設定変更

# config.mailer_sender = 'please-change-me-at-config-initializers-devise@example.com'
config.mailer_sender = 'Testrooper <noreply@ドメイン名>'

config/initializers/devise.rbを変更します
ドメイン名と書いてあるところは接続しようとしているドメイン名に変えてください。

4-5. Mailerクラスのfromの設定

class ApplicationMailer < ActionMailer::Base
  default from: 'MyDomain <noreply@ドメイン名>'
  layout 'mailer'
end

Mailerクラスのfromの設定をします。
mailers/application_mailer.rbに上記を追加します。
ドメイン名と書いてあるところは接続しようとしているドメイン名に変えてください。
開発環境であればこちらで終了となります^^

4-6. .envをアップロードする(本番環境のみの設定)

開発環境では正常に動き、本番環境で動かなかった際、こちら完全に忘れておりました、、
.envはpushされないのでEC2上にアップロードします
でないと本番環境ではSESの参照ができません!
流れは下記のとおりです。
1.EC2にログイン
2.アプリケーションのディレクトリに移動
3.viで.envにアクセスキーIDとシークレットアクセスキーの上書き保存
4.PUMAの再起動

用語解説

後程追加いたします。

最後に

現在ポートフォリオを作成しておりますので
ご意見などいただければうれしいです!
NotePro

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

【AWS】AWS SESを用いてRuby on Railsのdeviseでメールを送信する

目次

・経緯
・今回の前提条件
・注意事項
・AWS SES(Simple Email Service)とは
・料金
・大体の流れ(イメージ)
・1. AWS SESの設定
・2. AWSのCSに制限解除のメールを送る
・3. IAMユーザーの設定
・4. railsに設定
・用語解説
・最後に

経緯

私がPortfolioを作成している段階でして、divise上のメールが飛んでいないことに気づきました。
改善策を模索している中、AWSSES(有料)なるものを見つけGmail(無料?)でも実装できることが分かったが、AWSでEC2なども利用していたので
・AWSでまとめて管理ができるのですっきりしそう
・いい経験になるかも
上記の理由で実装してみた。

私自身も初学者なので、完全に鵜呑みにせずイメージをつかむ感覚で読んでいただければと!

今回の前提条件

  • Ruby on railsでappがありdeviseを導入している
  • AWSのアカウントを所持している
  • Route53の設定が完了していて、ドメインでのアクセス可能なサイトがある。

くらいですかね、、

注意事項

私がだいぶ躓いたところなので、、
2020/09/22現在AWSSESを東京リージョン(ap-northeast-1)で本番環境で実装するとメールが飛びません!!!!!
最近のリリースなのでまだバグが多いから?!
解決方法わかる方教えていただきたいです
祝!Amazon SESが東京リージョンにやってきた!
私はアイルランドリージョン(eu-west-1)で実装しました。
AWS SES以外の設定は東京リージョンで済ませております。(EC2,Route53など)

あとサンドボックスの制限解除に1日かかります!
ご利用は計画的に!

AWS SES(Simple Email Service)とは

AWSで提供されているクラウドベースの電子メール送信サービスです。

料金

AWS SESは有料で、一定量送信毎での従量課金制ぽいです。

SES の料金は、送信する各 1,000 通の E メールにつき、0.10USD であり、非常に低コストです。

上記の通り、高額な金額が発生するわけではなさそうなので試しに利用してみるのもありでしょう!

大体の流れ(イメージ)

  1. AWS SESの設定
  2. AWSのCSに制限解除のメールを送る
  3. IAMユーザーの設定
  4. railsに設定

1. AWS SESの設定

AWS-マネジメントコンソール (2).png
1-1. AWSにログイン
1-2. ヘッダーのサービスをクリック
1-3. SESと検索してエンター

SES-Management-Console.png
1-4. IdentityManagement>Domainをクリック
1-5. Varufy a NeW Domianをクリック
この段階で東京リージョン以外のリージョンを選択しておいたほうが良いです

SES-Management-Console (2).png
1-6. Domainに接続したいドメイン名を入れる
1-7. Generate DKIM Settingsにチェック入れる
1-8. Varufy This Domianをクリック

SES-Management-Console (3).png
1-9. User Route53をクリック
Route53を登録している場合上記表示になる。
登録している場合レコードの設定までを自動でやってくれるぽい

SES-Management-Console (4).png
1-10. Create Record Setsをクリック
AWS SWSのsettingは一旦こちらで終了です!

SES-Management-Console (5).png
しばらくすると各項目がverifiedに代わると思います。

2. AWSのCSに制限解除のメールを送る

AWS SESをsettingをしただけではメールは送信されません。
設定初期ProductionAcsessがSandboxになっており、これを解除する必要があります。

SES-Management-Console (7).png
2-1. Email Sending > Sending Statistics をクリック  
2-2. ProductionAcsessがSandboxになっていることを確認する。

SES-Management-Console (6).png
2-3. 右上のサポートをクリック
2-4. サポートセンターをクリック

AWS-サポートダッシュボード.png
2-5. 中央にあるCreate Caseをクリック

以降メールの送信手順に関しては下記記事でご丁寧にまとめられておりましたので
メールを送るまでは参考にしてみてください。
2-6.メール送信の手順
※2.解除申請を送るフォームに入力から行ってみてください。

S__635158530.jpg
メール送信して一日後、上記のような解除しましたとメールが来れば
下記がProductionAcsessがSandbox⇒Enabledに代わります。

SES-Management-Console (8).png

SES-Management-Console (10).png
2-7.IdentityManagement>Domain>SendaTestEmailをクリック
2-8.メールを送ってみる
最後にテストです。
メールを送り受信ボックスに届くか確認します。

3. IAMユーザーの設定

AWS SESのsettingと制限解除だけではrailsにつなぎこめません。
最後にIAMユーザーを新規登録しなくてはいけません。
※筆者はrootアカウントでキーとIDを参照する方法を探してみましたがありませんでした。
※本来rootアカウントでは接続管理は好ましくなく、各アクションごとにIAMユーザー(権限の範囲)を指定して接続したほうがいいと参考書に書いてありました。
のであるかはわかりませんがとりあえずIAMユーザーで作成しておけってことですね。

AWS-マネジメントコンソール (3).png
3-1. ヘッダーのサービスをクリック
3-2. IAMと検索してエンター

IAM-Management-Console (7).png
3-3. ユーザーをクリック
3-4. ユーザーを追加をクリック

IAM-Management-Console (8).png
3-5. ユーザー名を入れる(※ここはわかりやすければなんでもOKです)
3-6. プログラムによるアクセスにチェック
3-7. 次のステップ:アクセス権限をクリック

IAM-Management-Console (10).png
3-8. 既存ポリシーを直接アタッチをクリック
3-9. ポリシーのフィルタにSESと入れる
3-10. AmazonSESFullAccessにチェックを入れる
3-11. 次ぐのステップ:タグをクリック

IAM-Management-Console (11).png
3-12. 次ぐのステップ:確認をクリック
キーと値は何も入れなくてOKです。

IAM-Management-Console (12).png
3-13. ユーザーの作成をクリック

IAM-Management-Console (13).png
こちらでIAMユーザーの作成が完了です。
※アクセスキーIDとシークレットアクセスキーは必ず控えておきましょう。
※アクセスキーIDは後程確認できますが、シークレットアクセスキーはこのページでしか確認ができず消したらもう見れません。

こちらでAWS上での設定は完了です。

4. railsに設定

お疲れ様です。
最後はrailsに設定して終わりです。
大体の作業は下記の流れです。
1. gemのインストール
2. ActionMailerの設定
3. 本番環境、開発環境の設定
4. deviseの設定変更
5. Mailerクラスのfromの設定
6. .envをアップロードする(本番環境用)

4-1. gemのインストール

gem 'aws-ses'

Gemfileにて上記を記載してbundle install

4-2. ActionMailerの設定

ActionMailer::Base.add_delivery_method :ses,
  AWS::SES::Base,
  access_key_id: ENV['AWS_ACCESS_KEY_ID'],
  secret_access_key: ENV['AWS_SECRET_ACCESS_KEY'],
  server: 'email.リージョンID.amazonaws.com'

config/initializers/aws.rbを新規で作成
リージョンIDと記載されているところはSESを作成したリージョンIDを入れてください。
※分からない場合はAWSにログインをするとURLの末尾にリージョンIDが記載されてます。


東京:ap-northeast-1
アイルランド:eu-west-1

IDとKEYはpushしてしまうとまずいので.envに新規で追加します。

AWS_ACCESS_KEY_ID = IDを入力
AWS_SECRET_ACCESS_KEY = KEYを入力

4-3 開発環境(のみ)の設定

config.action_mailer.default_url_options = { host: 'ドメイン名' }
config.action_mailer.delivery_method = :ses

config/environments/development.rbに上記を追加
ドメイン名と書いてあるところは接続しようとしているドメイン名に変えてください。

4-3 本番環境(のみ)の設定

config.action_mailer.default_url_options = { host: 'ドメイン名' }
config.action_mailer.delivery_method = :ses

config/environments/production.rbに上記を追加
ドメイン名と書いてあるところは接続しようとしているドメイン名に変えてください。

4-4. deviseの設定変更

# config.mailer_sender = 'please-change-me-at-config-initializers-devise@example.com'
config.mailer_sender = 'Testrooper <noreply@ドメイン名>'

config/initializers/devise.rbを変更します
ドメイン名と書いてあるところは接続しようとしているドメイン名に変えてください。

4-5. Mailerクラスのfromの設定

class ApplicationMailer < ActionMailer::Base
  default from: 'MyDomain <noreply@ドメイン名>'
  layout 'mailer'
end

Mailerクラスのfromの設定をします。
mailers/application_mailer.rbに上記を追加します。
ドメイン名と書いてあるところは接続しようとしているドメイン名に変えてください。
開発環境であればこちらで終了となります^^

4-6. .envをアップロードする(本番環境のみの設定)

開発環境では正常に動き、本番環境で動かなかった際、こちら完全に忘れておりました、、
.envはpushされないのでEC2上にアップロードします
でないと本番環境ではSESの参照ができません!
流れは下記のとおりです。
1.EC2にログイン
2.アプリケーションのディレクトリに移動
3.viで.envにアクセスキーIDとシークレットアクセスキーの上書き保存
4.PUMAの再起動

用語解説

後程追加いたします。

最後に

現在ポートフォリオを作成しておりますので
ご意見などいただければうれしいです!
NotePro

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

テーブルのデータ型とコマンドについての備忘録(Rails)

はじめに

テーブルを作る際、この場合データ型なんだったっけ?コマンドなんだったっけ?と調べ直してしまうことがままあるのでまとめることにしました。

データ型

データ型 種類
integer 数値(整数)
decimal 数値(精度の高い小数)
float 数値(浮動小数)
string 文字(短い文字列)
text 文字(長い文字列
date 日付
datetime 日時
time 時刻
timestamp タイムスタンプ
binary バイナリ
boolean 真偽

テーブル、カラム作成の際に使うコマンド

テーブル、カラムを作る時

$ rails g model モデル名 カラム名:データ型

使用例:Userモデルの作成と名前(name)と自己紹介文(introduction)のためのカラムを作成したいとき

$ rails g model User name:string introduction:text

テーブルを削除

$ rails d model モデル名

使用例

$ rails d model User

既存のテーブルに対しカラムの追加/削除するコマンド

カラムの追加

$ rails g migration Addカラム名Toテーブル名 カラム名:型名

使用例:Userテーブルにtitleカラムを追加したいとき

$ rails g migration AddTitleToUsers title:string

カラムの削除

$ rails g migration Removeカラム名Fromテーブル名 カラム名:型名

使用例:Userテーブルにtitleカラムを削除したいとき

$ rails g migration RemoveTitleFromUsers title:string

マイグレーション実行

作成、変更、削除が出来たらDBには反映させるため、db:migrateコマンドを実行する。

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

投稿に対していいね機能を実装する

概要

今回、twitterのような投稿アプリに対して
いいねを押したり、取り消したりできる機能と、
いいねをカウントして件数を表示できる機能と、
いいねするリンクをハートにする実装についてまとめておきます。

ちなみに、投稿にいいねをする機能に関しては、progateにカリキュラムがありますが、自分はそのまま実装しても思うような機能にならなかったので、qiitaをはじめとするサイトをたくさん検索しました。

それも踏まえてまとめようと思います。

機能の見た目

like-qiita.png

実装

likeモデルを用意する

rails g model like

マイグレーションファイルに
t.references, foreign_key: trueとして、user_idとpost_idのカラムを設定します。

models/like.rb
class Like < ApplicationRecord
  validates :user_id, presence: true
  validates :post_id, presence: true
end

ルーティングを設定する

routes.rb
post "likes/:post_id/create" => "likes#create"
post "likes/:post_id/destroy" => "likes#destroy"

これによってrails routesをすると次の画像のようになります。
これは、ビューファイルにリンクを指定するときに必要になるのであとで間違えないようにしましょう。

like-routes.png

コントローラーを作成する

controllers/likes_controller.rb
class LikesController < ApplicationController
  before_action :authenticate_user!



  def create
    @like = Like.new(user_id: current_user.id, post_id: params[:post_id])
    @like.save
    redirect_to "/posts/#{@like.post_id}"
  end

  def destroy
    @like = Like.find_by(user_id: current_user.id, post_id: params[:post_id])
    @like.destroy
    redirect_to("/posts/#{params[:post_id]}")
  end


end

保存や削除をした後にその投稿の詳細ページにリダイレクトされるようになっています。

ビューファイルを用意しよう

views/posts/show.html.erb
<div class="like-btn">
    <% if Like.find_by(user_id: @current_user.id, post_id: @post.id) %>

      <%=link_to("/likes/#{@post.id}/destroy", {method: :post}) do %>
        <span class="fa fa-heart like-btn-unlike"></span>
      <% end %>

    <% else %>

      <%= link_to("/likes/#{@post.id}/create", {method: :post}) do %>
        <span class="fa fa-heart like-btn"></span>
      <% end %>

    <% end %>
  </div>

まずは条件分岐でユーザーがいいねしているかしていないかを分岐し、
ボタンを押したときlikeをcreateするか、destroyするかを指定します。

そして、次に書きますが、link_to ~~~ doとする事で間にHTML文を挟むことができます。(理解が浅ければprogateのrailsカリキュラムを参照)

link_toのURLは間違えないようにしましょう。

先ほどルーティングでのせた画像の通り、/likes/post_id/create(またはdestroy)と記述します。

いいねをハートアイコンのボタンにする

この部分はprogateを参考にしたのでこれ以外知らないんですけど、
font-awesomeというものを使って、いいねするリンクをボタンにしていきます。

まずはfont-awesomeを使えるようにhead部分にリンクを読み込みます。

views/layouts/application.html.erb
 ---省略---

  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
 </head>

ビューファイルはもう適した形式で記述しているので、
最後にいいねしていたらピンク、していなかったらグレーとなるようにcssを記述します。

先ほど記述したviews/posts/show.html.erbを確認すればわかりますが、いいねするボタンと取り消すボタンでlike-btnとlike-btn-unlikeが分かれています。

assets/stylesheets/likes.scss
.like-btn {
  color: #8899a6;
}


.like-btn-unlike {
  color: #ff2581;
}


.posts-show-item .fa {
  font-size: 16px;
  margin-right: 3px;
}

これでハートボタンでいいねしたり取り消したりすることが可能になっているはずです。

最後に、いいねの件数をカウントして表示しましょう。

件数をカウントして表示する

postsコントローラーのshowメソッドに@like_countを定義してあげて、ビューファイルで表示するような手順になります。

controllers/posts_controller.rb
def show
    @post = Post.find(params[:id])
    @comment = Comment.new
    @comments = @post.comments.includes(:user)
    @like_count = Like.where(post_id: @post.id).count
 end

whereでその投稿についてるいいねをデータベースから検索して、countで数えていくように定義しています。

views/posts/show.html.erb
<div class="like-btn">

  <h3>いいね件数:<%= @like_count %></h3>

    <% if Like.find_by(user_id: @current_user.id, post_id: @post.id) %>
      <%=link_to("/likes/#{@post.id}/destroy", {method: :post}) do %>
---省略---

以上で完成です。

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

[Rails6]devise + paranoia + MySQL8系で実現するユーザー論理削除と一意制約の両立

はじめに

タイトルの環境にて、ユーザーの論理削除および一意制約の両立にかなり苦戦したので
解決方法を軽くまとめます。
deviseの一意制約をオーバーライドして独自の制約を持たせる方法などが出ていましたが
どれも面倒でもっと簡単にできないかと思い調べた結果です。

やりたいこと

先の環境にて(特にMySQL)、deviseデフォルトのユーザー物理削除ではなく論理削除を行い、
かつ退会したユーザーが同じアドレスやIDで再登録できるよう一意制約を担保したい。

PostgreSQL, SQLite

こちら2つのDBでは部分indexを活用することで簡単に実装できるそうです。
・実装記事
・add_index仕様1
・add_index仕様2

この、部分indexをMySQLは採用しておらず簡単に扱えない模様。
(情報が5.7ばかりだから8系では採用しているのか?どちらにしろRailsのmigrateからは無理そう)
軽くアンチMySQLになるかも…

結論(MySQLでの実現方法)

この記事みたらいけると思います。
https://vanhuyz.com/how-to-apply-unique-restriction-with-soft-delete-in-rails/
1. deleted_atカラムを追加後、
2. deleted_atにNullではなくアクティブユーザーにも特定の値をもたせることで、
 2カラムでのUNIQUEを実現し(dleted_atがNULLだと実現しない)
3. シンプルに論理削除と一意制約を両立

最高に優良な記事をどうもありがとう!!

実装

[環境]
- Ruby:2.6.6p146
- Rails:6.0.3.3
- MySQL:8.0.21
- mysql2:0.5.3
- devise:4.7.2
- paranoia:2.4.2

準備

環境構築やdevise, paranoia導入等の基本部分の実装は他に沢山の記事が出ているので割愛します。
devise標準の会員登録・退会機能等が動作する前提です。

1. 設定ファイルparanoia.rbを作る

config/initializers/paranoia.rb
# ユーザー退会後の論理削除・一意制約を両立させるための処理
# 非削除レコードはdeleted_at = '0000-01-01 00:00:00'
# 削除済みレコードはdeleted_at != '0000-01-01 00:00:00'
Paranoia.default_sentinel_value = DateTime.new(0)

2. migrationでのindex変更

デフォルトのUsersテーブルのindexを変更するマイグレーションファイルを作成

rails g Change_Index_To_Users

作成したマイグレーションファイルを以下のように変更

一度デフォルトで作成されたemailインデックスを削除し
新たに[email, deleted_at]でのuniqueインデックスを作成

db/migrate/202009220000000.rb
class ChangeIndexUniuqueToUsers < ActiveRecord::Migration[6.0]
  def change
    remove_index :users, :email
    add_index :users, [:email,:deleted_at], unique: true
  end
end

migration

rails db:migrate

3. Validation追加

アプリケーションレベルでのバリデーションを追加

app/models/user.rb
class User < ActiveRecord::Base
  acts_as_paranoid
  validates :email, uniqueness: { scope: :deleted_at }  

4. 動作確認

railsコンソールもしくはrailsサーバーを立ち上げ実際にユーザー作成、退会、同アドレスでの再登録を実施してDBを確認します。
論理削除_一意制約.PNG

無事同じアドレスでの再登録に成功しました!!
他何かいい方法などあれば是非教えてくださいませ~!

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

Ruby on Railsにて、Blocked Host:"ホスト名"が出てしまう時の対処法

背景

Ruby on Railsチュートリアルの1章の演習で、ずっと「Blocked Host:自分のホスト名」というエラーが出てしまい、長時間色々なことを試した結果、ある根本的なことをしていなかったことに気づき解決できたため、この投稿をさせてもらいました。

試したこと

Web上でよく書かれていることとしては、
1.environment/development.rbにて、「config.hosts.clear」を入れることです。
しかし、これを試しても、エラーが発生したままでした。
2.environment/development.rbにて「config.hosts<<"自分のホスト名"」を入れることです。
しかし、これを試しても、またしてもエラーが発生したままでした。

原因

(Ⅰ).違うアプリケーションの中の、development.rbに1と2のプログラムを追加していた。
例えば、自分が作っているアプリケーションがhello_appだとすると、違うアプリケーション(例えば、a_app)の中のdevelopment.rbに1と2のプログラムを追加していたということです。
これでは、a_appのホストが許可されただけで、hello_appのホストは許可されません。
これを間違えた理由としましては、階層構造がenvironment/hello_app/config/environment/development.rb、とenvironmentが2つあることにより、同じ環境内であれば、一つのdevelopment.rbを書き直せば、全てに反映されると勘違いしたことによるものでした。
development.rbの階層は上の通りであり、1つのアプリケーションにつき一つのファイルとなります。
決して、共通ではありません。
自分が作成しているアプリケーション内の、config/environment内のdevelopment.rbを1と2のように書き直すようにしましょう。

(Ⅱ).development.rbを書き直した後に、「rails server」を実行しなかった。
順序としては、「development.rb内のプログラムを書き直す→rails serverを実行する」という手順を踏まないと、いくら(Ⅰ)の手順を踏んだとしましても、エラーが発生したままとなります。

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

めっちゃ便利なRubyのStructクラスのお話

きっかけ

とちぎRuby会議09にリモートで参加し、そこで見たLTに今更聞けない! Struct の使い方と今後の可能性についてでRubyのStructクラスについて初めて知った。
なにこれめっちゃ便利じゃん!となったので啓蒙も兼ねて記事を書こう、となったのがきっかけ。
Rubyはかれこれ休み休み10年間は触っているけれど未だに発見がある。素敵!学んでいないだけでは??

Structクラスとは

Ruby 2.7.0 リファレンスマニュアル Structクラス

構造体クラス。Struct.new はこのクラスのサブクラスを新たに生成します。
個々の構造体はサブクラスから Struct.new を使って生成します。個々の構造体サブクラスでは構造体のメンバに対するアクセスメソッドが定義されています。

自分の解釈で説明すると

任意の名前のプロパティ(とメソッド)を持つオブジェクトをお手軽に作成できる便利構造体クラス

使い方

# 4つのパラメーターを持ったStructのサブクラスを生成する
BreakwaterClub = Struct.new(:id, :name, :grade, :age)
# 生成したサブクラスのインスタンスを作成
bucho = BreakwaterClub.new(1, 'kuroiwa', 3)

# 作成したインスタンスはパラメーター名のアクセサメソッドを持っている
p bucho.name
#=> "kuroiwa"
bucho.age = 17

# 初期化時にセットしていなかったageがセットされている
p bucho
#=>#<struct BreakwaterClub id=1, name="kuroiwa", grade=3, age=17>
# keyword_init: true を指定することで初期化時にキーワード引数を渡せるようになる
# キーワード引数のほうがわかりやすいけど文字数は多くなるのでどちらを使うかはお好みで
BreakwaterClub = Struct.new(:id, :name, :grade, :age, keyword_init: true)

hina = BreakwaterClub.new(name: 'tsurugi', grade: 1)

Hashや配列と比べて何が嬉しいの?

Rubyでちょっとした処理を書く時によく使いがちな配列やHash。
まだ頭の中で整理できてない処理とかをアウトプットしながら整理する時とかにも使ったりする。

# 配列
bucho = [1, 'kuroiwa', 3]
bucho[0] #=>1
bucho[1] #=>'kuroiwa'
bucho[2] #=>3

# Hash
bucho = {id: 1, name: 'kuroiwa', grade: 3}
bucho[:id]    #=>1
bucho[:name]  #=>'kuroiwa'
bucho[:grade] #=>3

定義してないパラメーターを指定するとエラーになる

Hashで定義してたりするとTypoしているのにそれに気付かず「なぜ動かない…合っているはずなのに…」とかあるからこれはありがたい。

senpai = BreakwaterClub.new(name: 'ohno', grade: 2)
senpai[:height] #=>NameError (no member 'height' in struct)
senpai.height   #=>NoMethodError
# Hashだと定義してなくても参照できてしまう
senpai[:height] #=>nil

Structクラスは配列、Hashと同じようにアクセス可能で型変換も可能

つまり上位互換って考えていいと思う。

natsumi = BreakwaterClub.new(name: 'hodaka', grade: 1)
# 配列のようにアクセスできる。indexの順番は`Struct.new`で定義した順番になる
natsumi[0] #=>nil
natsumi[1] #=>'hodaka'
# Hashのようにもアクセスできる
natsumi[:id]    #=>nil
natsumi[:grade] #=>1
# 配列にもHashにも変換できる
natsumi.to_a #=>[nil, "hodaka", 1, nil]
natsumi.to_h #=>{:id=>nil, :name=>"hodaka", :grade=>1, :age=>nil}

メソッド定義が可能

Struct.newの際ににブロックを指定することでメソッドを定義可能

BreakwaterClub = Struct.new(:id, :name, :grade, :age, keyword_init: true) do
  def gakunen
    "#{grade}年生"
  end
end
# `Struct.new`で生成したStructクラスを継承したサブクラスを作成することでも可能
Class BreakwaterClub < Struct.new(:id, :name, :grade, :age, keyword_init: true)
  def gakunen
    "#{grade}年生"
  end
end

Structクラス自体を継承したサブクラスを定義することは非推奨らしい。ここまだちょっとよく理解できてない。
継承元となるStructクラスが動的に生成した無名クラスなので不定なことに起因していると思う。
参考:
- 無名クラスから継承すると、何が問題なのか(Ruby)
- irbで2回以上loadすると失敗する
- 以下ドキュメント引用

ブロックを指定した場合

Struct.new にブロックを指定した場合は定義した Struct をコンテキストにブロックを評価します。また、定義した Struct はブロックパラメータにも渡されます。

Customer = Struct.new(:name, :address) do
  def greeting
    "Hello #{name}!"
  end
end
Customer.new("Dave", "123 Main").greeting # => "Hello Dave!"

Structをカスタマイズする場合はこの方法が推奨されます。無名クラスのサブクラスを作成する方法でカスタマイズする場合は無名クラスが使用されなくなってしまうことがあるためです。
[SEE_ALSO] Class.new

まとめ

Structクラスは便利。多種多様な記述方法があるRubyらしいクラスだと思う。
Structクラスまとめ
- 任意のパラメーター、メソッドを定義できる構造体クラス
- 配列やHashのようにアクセスでき、型変換も可能
- 初期化時に指定していないパラメーター名だとエラーになる(Hashだとnilになる)

どういう時に使うと便利?
- 配列やHashですませちゃってるけどClassとして定義したほうがいいよなってとき
- ちょっとコードを書いて検証とかしたいとき
- Classを定義するほど考えがまとまってないとき
- おいそれと叩けないAPIを介したテストをやりたいとき

まだ試してませんが、RailsのRspecでのテスト時などでも使えそうだと感じました。
irbとかでも気軽に試せるので興味が出た人は是非試してみてください。

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

Rails Tutorial 第14章 演習14.2.3.2 コメントアウトは2箇所しないとredにならない

演習14.2.2.3 strongタグにIDを指定

ネットの他の方はrespons.bodyでassert_matchをしていました。私はstrongタグにIDを指定して、assert_selectを使っています。

test/integration/users_profile_test.rb
  test "Home stat" do
    log_in_as(@user)
    get root_path
    assert_select 'strong#following', @user.following.count.to_s 
    assert_select 'strong#followers', @user.followers.count.to_s
  end
ネットの他の方
    assert_match @user.active_relationships.to_s, response.body
    assert_match @user.passive_relationships.to_s, response.body

演習14.2.3.2 コメントアウトは2箇所しないとredにならない

ネットの他の方はコメントアウトは1箇所でした。私は2箇所しないとredにならずgreenのままでした。

views/users/show_follow.html.erb
           <% @users.each do |user| %>
            <%#= link_to gravatar_for(user, size: 30), user %>
          <% end %>
..
      <ul class="users follow">
        <%#= render @users %>
      </ul>
ネットの他の方
    <% @users.each do |user| %>
        <%= #link_to gravatar_for(user, size:30), user %>
    <% end %>

演習14.2.6.1 ネットの他の人の答えが2種類

>各行を順にコメントアウトしていき、テストが正しくエラーを検知できるか
の意味をどうとるかで、ネットの他の人の答えが2種類ありました。
1.「順に」を1行だけずつ
2.1行の次は2行と2行を追加していく

私は1.を選びましたが、それだと次の設問で答えが得られなかったです。なので2.が正解のようです。
理由は、format.jsの行だけをコメントアウトしても、redにならずgreenのままだからです。

controllers/relationships_controller.rb
      respond_to do |format|
#        format.html { redirect_to @user }
#        format.js
      end
 FAIL["test_should_follow_a_user_with_Ajax", FollowingTest, 0.9658033109999451]
 test_should_follow_a_user_with_Ajax#FollowingTest (0.97s)
        "@user.following.count" didn't change by 1.
        Expected: 3
          Actual: 2
        test/integration/following_test.rb:36:in `block in <class:FollowingTest>'

ERROR["test_should_follow_a_user_the_standard_way", FollowingTest, 0.9935227480000322]
 test_should_follow_a_user_the_standard_way#FollowingTest (0.99s)
ActionController::UnknownFormat:         ActionController::UnknownFormat: ActionController::UnknownFormat
            app/controllers/relationships_controller.rb:7:in `create'
            test/integration/following_test.rb:31:in `block (2 levels) in <class:FollowingTest>'
            test/integration/following_test.rb:30:in `block in <class:FollowingTest>'
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails gem Kaminariでページネーション機能を簡単に作る

ページネーション機能を簡単に作るための記述例です。
参考になりましたら幸いです。
また、間違いなどあればご指摘いただけますと幸いです。

なお、環境は
・macOS Catalina
・Ruby on Rails 6.0.3.3
です。

haml使ってます。erbやslim使ってる方は読み換えてください。
Bootstrap 4.4.1をyarnを利用してインストールしています。
jqueryもpopperも依存関係にあるので一緒にインストールしています。
(あくまで一例なので、別の方法でBootstrapを適用している場合はこの限りではありません)

% yarn add bootstrap@4.4.1 jquery@3.5.1 popper.js@1.16.1 

Bootstrapの適用方法は省きます。

以下、記述例です

Gemfile
gem 'kaminari', '~> 1.2.0'
ターミナル
% bundle install
% rails g kaminari:views bootstrap4
config/kaminari_ja.yml
ja:
  views:
    pagination:
      first:    "&laquo; 最初"
      last:     "最後 &raquo;"
      previous: "&lsaquo; 前"
      next:     "次 &rsaquo;"
      truncate: "&hellip;"
  helpers:
    page_entries_info:
      one_page:
        display_entries:
          zero: ""
          one: "<strong>1-1</strong> / 1件中"
          other: "<strong>1-%{count}</strong> / %{count}件中"
      more_pages:
        display_entries: "<strong>%{first}-%{last}</strong> / %{total}件中"
ページネーションしたいビューファイル.haml
# 記述追加
= page_entries_info @events
= paginate @events

こんな感じになるはずです。
スクリーンショット 2020-09-22 11.18.24.png

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

【Rails】[0日目]分からないところを無視しないオリジナルサービス制作

始めに

初めまして。名古屋在住のしんやです。転職で東京に行きたいです。
未経験からの転職のためにテックアカデミーのWebアプリケーションコースを受講し、2020年9月20日に受講終了しました。

僕としてはスクールのカリキュラムは非常に分かりやすく、サポート体制も整っていたので快適に学習を進めることができました。

唐突に自分の過去ツイートを引用しましたが、このスタンスで進めて受講終了に至りました。

受講終了した今気付いたのが、上記スタンスで進めるのはいいが、絶対に理解してから進めないといけないこともあるという当然のことでした。
簡単に言うと、とりあえずで進めたとこが多数あるが故に、カリキュラムは終えたけど全然理解できてねーなってとこが多数あるってことです。

もちろんスクールの受講を終了した程度で全部理解できるとは思っていなかったですが、悔しいです。

今後の内容

悔しがっていてもしょうがないので、復習を兼ねたアウトプットをここでします。
一番重要視するのは、「分からないところを無視しない」という点です。

ただただ自分のためのアウトプットであり、カリキュラムがあるわけではありません。
分からないところは徹底的に調べ、調べた内容はここにアウトプットします。

自分だけのメモでやってろって話かもしれませんが、自分の認識などに誤りがある場合、ここでアウトプットすることで指摘をいただけるという可能性があります。
淡い期待です。

最後に

現状、オリジナルサービスのワイヤーフレーム、サイトマップ、ER図の作成中です。
次回投稿を1回目とし、ここから進めていこうと思っています。

よろしくお願いします!

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

【Rails】[0日目]分からないところを無視せずオリジナルサービス制作

始めに

初めまして。名古屋在住のしんやです。転職で東京に行きたいです。
未経験からの転職のためにテックアカデミーのWebアプリケーションコースを受講し、2020年9月20日に受講終了しました。

僕としてはスクールのカリキュラムは非常に分かりやすく、サポート体制も整っていたので快適に学習を進めることができました。

唐突に自分の過去ツイートを引用しましたが、このスタンスで進めて受講終了に至りました。

受講終了した今気付いたのが、上記スタンスで進めるのはいいが、絶対に理解してから進めないといけないこともあるという当然のことでした。
簡単に言うと、とりあえずで進めたとこが多数あるが故に、カリキュラムは終えたけど全然理解できてねーなってとこが多数あるってことです。

もちろんスクールの受講を終了した程度で全部理解できるとは思っていなかったですが、悔しいです。

今後の内容

悔しがっていてもしょうがないので、復習を兼ねたアウトプットをここでします。
一番重要視するのは、「分からないところを無視しない」という点です。

ただただ自分のためのアウトプットであり、カリキュラムがあるわけではありません。
分からないところは徹底的に調べ、調べた内容はここにアウトプットします。

自分だけのメモでやってろって話かもしれませんが、自分の認識などに誤りがある場合、ここでアウトプットすることで指摘をいただけるという可能性があります。
淡い期待です。

最後に

現状、オリジナルサービスのワイヤーフレーム、サイトマップ、ER図の作成中です。
次回投稿を1回目とし、ここから進めていこうと思っています。

よろしくお願いします!

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

【Rails】[第0回]分からないところを無視しないオリジナルサービス制作

始めに

初めまして。名古屋在住のしんやです。転職で東京に行きたいです。
未経験からの転職のためにテックアカデミーのWebアプリケーションコースを受講し、2020年9月20日に受講終了しました。

僕としてはスクールのカリキュラムは非常に分かりやすく、サポート体制も整っていたので快適に学習を進めることができました。

唐突に自分の過去ツイートを引用しましたが、このスタンスで進めて受講終了に至りました。

受講終了した今気付いたのが、上記スタンスで進めるのはいいが、絶対に理解してから進めないといけないこともあるという当然のことでした。
簡単に言うと、とりあえずで進めたとこが多数あるが故に、カリキュラムは終えたけど全然理解できてねーなってとこが多数あるってことです。

もちろんスクールの受講を終了した程度で全部理解できるとは思っていなかったですが、悔しいです。

今後の内容

悔しがっていてもしょうがないので、復習を兼ねたアウトプットをここでします。
一番重要視するのは、「分からないところを無視しない」という点です。

ただただ自分のためのアウトプットであり、カリキュラムがあるわけではありません。
分からないところは徹底的に調べ、調べた内容はここにアウトプットします。

自分だけのメモでやってろって話かもしれませんが、自分の認識などに誤りがある場合、ここでアウトプットすることで指摘をいただけるという可能性があります。
淡い期待です。

最後に

現状、オリジナルサービスのワイヤーフレーム、サイトマップ、ER図の作成中です。
次回投稿を第1回とし、ここから進めていこうと思っています。

よろしくお願いします!

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

Rails 6で認証認可入り掲示板APIを構築する #17 管理者権限の追加

Rails 6で認証認可入り掲示板APIを構築する #16 policyの設定

管理者権限を用意する

前回までの実装で、投稿の編集や削除は投稿者本人だけできるようになりました。
ですがそこに拡張し、管理者としてログインしている時は誰の投稿でも編集・削除できるようにしてみます。

そのためにはuserモデルにadminカラムを追加しましょう。

$ rails g migration AddAdminToUsers admin:boolean
db/migrate/xxxxxxxxxxxxxx_add_admin_to_users.rb
# frozen_string_literal: true

class AddAdminToUsers < ActiveRecord::Migration[6.0]
  def change
    add_column :users, :admin, :boolean, default: false, null: false
  end
end

さて、今までは機能を実装してからテストを書いていましたが、punditの基本挙動は理解できたと思うので、先にテストから書いてみます。

管理者権限のない別ユーザーでログインしていた際に編集削除できないテストはすでに書かれているので、

  • 管理者権限のある際に別ユーザーの投稿を編集できること
  • 管理者権限のある際に別ユーザーの投稿を削除できること

を実装してみます。
厳密にはpolicyテストとrequestテスト両方実装すると万全ですが、ここはpolicyだけ実装してみます。

もし良ければここから書いてあるサンプルを見ずに、まずは自力で実装してみて見比べてみると、コードを書く練習になるのでやってみてください。

post_policy_spec.rbの編集

spec/policies/post_policy_spec.rb
...
 RSpec.describe PostPolicy, type: :policy do
   let(:user) { create(:user) }
+  let(:admin_user) { create(:user, admin: true) }
   let(:post) { create(:post) }

...
     it "ログインしているが別ユーザーの時に不許可" do
       expect(subject).not_to permit(user, post)
     end
+    it "adminユーザーでログインしている時に許可" do
+      expect(subject).to permit(admin_user, post)
+    end
...
spec/factories/posts.rb
...
     remember_created_at { nil }
     name { "MyString" }
     tokens { nil }
+    admin { false }
   end
...

ここまで実装したらrubocopとrspecを動かして確認。
まだpolicyファイルを実装していないのでテストはコケます。

policyの実装

まずは管理者であることを判定するprivateメソッドをapplication_policy.rbに生やします。

app/policies/application_policy.rb
...
   private

   def mine?
     @record.user == @user
   end
+
+  def admin?
+    @user.present? && @user.admin?
+  end
+

あとはpost_policy.rbのupdate?destroy?の2つの判定を変えるだけですね。

app/policies/post_policy.rb
   def update?
-    mine?
+    mine? || admin?
   end

   def destroy?
-    mine?
+    mine? || admin?
   end

なんとこれだけです。

traitを使ってadminを簡単に作る

これだけだと少し中身の薄い記事なので、factoryBotを触ってadminユーザーをもっと簡単に作れるようにします。

spec/factories/users.rb
     name { "MyString" }
     tokens { nil }
     admin { false }
+
+    trait :admin do
+      admin { true }
+    end
   end
spec/policies/post_policy_spec.rb
 RSpec.describe PostPolicy, type: :policy do
   let(:user) { create(:user) }
-  let(:admin_user) { create(:user, admin: true) }
+  let(:admin_user) { create(:user, :admin) }
   let(:post) { create(:post) }

これでrspecを動かしてみるとどうでしょうか。
ミスなく書けていればテスト通過するはずです。

traitはご覧の通り、factoryBotのcreate等の第2引数に別名として渡すことで使うことができます。
adminフラグを立てるだけなら恩恵も少なく意味もあまりないのですが、例えばadminユーザーの場合に必ずセットで初期値を持っておきたいカラムがあったりすれば、いちいちcreateのたびに複数カラムの初期値セットをしなくて済みます。

ぜひご活用ください。

続き

Rails 6で認証認可入り掲示板APIを構築する #18・終 user controllerの実装
連載目次へ

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

railsアプリ の謎を解消してみた

今日は既存のrailsアプリについて復習しました 

一度実装してみてから何だかよくわかりきっていない部分がありました、、、

#具体的にいうと、#
1.検索機能実装の仕組み

です

ではまずはじめに検索機能に関する謎についてわかったことをまとめて行きたいとおもいます。

ポイント1
 たいていのアプリケーションにおける検索機能は、一つのデータを取得しようとしているわけではない。イメージしやすいのはインスタやツイッターなのである。

多くのアカウントがそれぞれデータベースに保存されている状況で、何億といるユーザーの情報をひっぱってきているわけだが、これらは常に含まれているワードに基づいて結果を出しています。

つまり複数の情報をとってきているということ

railsのアプリケーションには7つのアクションがあります

index
new
create
show
edit
update
destroy
といった具合です。
しかし、searchアクションというのはありません。

そこで登場するののが

colectionとmemberです

これら2つはかんたんに言うとアクションを新しく自分のなりのカスタムで作ることができるものです!

これらは、どちらかを使ってアクションを生成しますが、ルーティングのurlと実行されるコントローラーをモデルに記述することで作成することが出来ます。((ビジネスロジックという)

しかし一つ違いがあります。

それは「collectionはルーティングに:idがつかない、memberは:idがつく」という違いがあるということです。

ただ、idがつかないだけじゃん。だから何なの?って思う方も多いと思います。
ここで先程の話に少し「戻ります。

検索機能は多くのデータを取得している。

idをつける=(特定のデータ、一つのデータ)
なのでこのような検索機能においてはcollectionのほうが適しているということになりますね。

一つ疑問が解消したところで
もう一つなぞだった問題について明らかにしていこうとおもいます。[Something went wrong]()

その謎とは、、、

なぜsearchアクションはコントローラーに記載しないのか

ということです。

ほかのアクションはちゃんとコントローラに記述しているのになぜ、searchアクションだけ
モデルに記載しているのか、気になりませんか?

答え、、、

結論

ビジネスロジックという仕組みが働いているから
です!!!

ビジネスロジックとはデータに対する処理などをプログラムすることいいます。
処理の内容を
主にモデルに記載することによって、処理をよみこませることができるのです。(まあかんたんにいうとシステムにおける実際のお仕事部分であると言えますね)
今回の検索機能などにおいてはカスタムのような機能であるため、モデルに詳細を記述しているといったような
ことが起こっているのです。

初学者にとってはなぜというところが大きかったとは思いますが、
このビジネスロジックが働いていたというところが大きかったようです。

[Something went wrong]()

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

剰余とべき乗(冪乗)の演算子

【概要】

1.結論

2.どのように使用するか

3.ここから学んだこと

1.結論

剰余:%
べき乗:**
を使う!


2.どのように使用するか

今回は2桁の整数(桁数制限するプログラムはしていません。)入力「の10の位と1の位を足した合計を求めています。

def addition(a, b)
  a + b
end

def calculation(num)
 #10の位
 no1 = (num / 10) % 10
 #1の位
 no2 = (num / 1) % 10
  return no1,no2
end

puts "2桁の整数を入力"
num = gets.to_i
a, b = calculation(num)
add_sum = addition(a, b)
puts "10の位と1の位の合計は#{add_sum}"

10の位を出すために10で割った数値をさらに10で割った余りを出しています
1の位も同様に1で割った数値をさらに10で割った余りを出しています。その際に"%"を使ってあまりを出しています。

(例)28の場合
(num / 10)・・・・28 / 10 = 2.8
2.8 % 10 ・・・・0余り2 の"2"を返す。

(num / 1)・・・・28 / 1 = 28
28 % 10・・・・2余り8 の"8"を返す。


3.ここから学んだこと(エラーの時に使用)

算数要素が混ざったプログラムは、プログラムの知識はもちろんのこと、工夫して計算する発想が求められることを学びました。工夫した計算にプログラムの知識も必要ですし、「このメソッドは一体どこで使うんだろう?」と思っているとピンポイントで使うこともあるので非常に大事です。

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

db/seeds.rbを使うようになったので備忘録

この記事について

Vue.jsの勉強をしているとデータを事前に準備する事があり、db/seeds.rbを使う機会が多くなったので備忘録めも

db/seeds.rbとは

seeds.rbはテストデータを定義しておくファイル。
こちらにテストデータ作成用のコードを書いてrails db:seedコマンドを実行すると、データベースにテストデータを作成することができる。

記述の仕方

db/seeds.rb
モデル名.create

Book.create(title: '吾輩は猫である')

Fakerを使って複数のデータを一気に作成する方法

db/seeds.rb
20.times do
  Book.create!(
    title: Faker::Book.title
  )
end

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

カウントダウンタイマーの実装

はじめに

ポートフォリオで実装予定のカウントダウンタイマーを実装する。
タイマーを実装するにはjQueryが必要と書いてある記事もあるが、今回はjQueryを利用せずに実装を進めていくことにする。

カラムの作成

まずは、いつものように

rails generate migration Addカラム名Toテーブル名 カラム名:データ型

でマイグレーションファイルを作成する。
今回の場合、制限時間を表示させる場所を表示させたいのでdeadlineをカラム名として、

rails generate migration AddDeadlineToMission deadline:datetime

とする。

class CreateMissions < ActiveRecord::Migration[6.0]
  def change
    create_table :missions do |t|
      t.integer :user_id
      t.text    :content
      t.string  :penalty
      t.datetime   :deadline


      t.timestamps
    end
  end
end

上記のマイグレーションファイルの7行目にt.datetime :deadlineを追加する。
そして、親の顔より見た

$ rails db:migrate

を実行して、データベースに保存する。

タイマーの実装

スクリーンショット 2020-09-22 0.33.23.png

HTMLの記述

上記のような赤字のタイマーを設定するには、

app/views/missions/new.html.erb
 <p>
     <%= f.hidden_field :deadline, :id => "deadline.id" %>
        <input type="text" id="userYear" >年
        <input type="text" id="userMonth">月
        <input type="text" id="userDate" >日
        <input type="text" id="userHour" >時
        <input type="text" id="userMin"  >分
        <input type="text" id="userSec"  >秒
 </p>
       <p id="RealtimeCountdownArea" ></p> #ここにタイマーが表示される

とする。
ここで、:id => deadline.id は後のjavascriptの記述において効果を発揮するため、記述している。

javascriptの記述

今回は、app/views/missions/new.html.erbのscriptタグにjavascriptを記述することとする。
以下の通りである。

app/views/missions/new.html.erb
<script>

function set2fig(num) {
   // 数値が1桁だったら2桁の文字列にして返す
   var ret;
   if( num < 10 ) { ret = "0" + num; }
   else { ret = num; }
   return ret;
}
function isNumOrZero(num) {
   // 数値でなかったら0にして返す
   if( isNaN(num) ) { return 0; }
   return num;
}
function showCountdown() {
   // 現在日時を数値(1970-01-01 00:00:00からのミリ秒)に変換
   var nowDate = new Date();
   var dnumNow = nowDate.getTime();
 
   // 指定日時を数値(1970-01-01 00:00:00からのミリ秒)に変換
   var inputYear  = document.getElementById("userYear").value;
   var inputMonth = document.getElementById("userMonth").value - 1;
   var inputDate  = document.getElementById("userDate").value;
   var inputHour  = document.getElementById("userHour").value;
   var inputMin   = document.getElementById("userMin").value;
   var inputSec   = document.getElementById("userSec").value;
   var targetDate = new Date( isNumOrZero(inputYear), isNumOrZero(inputMonth), isNumOrZero(inputDate), isNumOrZero(inputHour), isNumOrZero(inputMin), isNumOrZero(inputSec) );
   var dnumTarget = targetDate.getTime();
 
   // 表示を準備
   var dlYear  = targetDate.getFullYear();
   var dlMonth = targetDate.getMonth() + 1;
   var dlDate  = targetDate.getDate();
   var dlHour  = targetDate.getHours();
   var dlMin   = targetDate.getMinutes();
   var dlSec   = targetDate.getSeconds();
   var msg1 = "期限の" + dlYear + "/" + dlMonth + "/" + dlDate + " " + set2fig(dlHour) + ":" + set2fig(dlMin) + ":" + set2fig(dlSec);
 
   // 引き算して日数(ミリ秒)の差を計算
   var diff2Dates = dnumTarget - dnumNow;
   if( dnumTarget < dnumNow ) {
      // 期限が過ぎた場合は -1 を掛けて正の値に変換
      diff2Dates *= -1;
   }
 
   // 差のミリ秒を、日数・時間・分・秒に分割
   var dDays  = diff2Dates / ( 1000 * 60 * 60 * 24 );   // 日数
   diff2Dates = diff2Dates % ( 1000 * 60 * 60 * 24 );
   var dHour  = diff2Dates / ( 1000 * 60 * 60 );   // 時間
   diff2Dates = diff2Dates % ( 1000 * 60 * 60 );
   var dMin   = diff2Dates / ( 1000 * 60 );   // 分
   diff2Dates = diff2Dates % ( 1000 * 60 );
   var dSec   = diff2Dates / 1000;   // 秒
   var msg2 = Math.floor(dDays) + "日"
            + Math.floor(dHour) + "時間"
            + Math.floor(dMin) + "分"
            + Math.floor(dSec) + "秒";
 
   // 表示文字列の作成
   var msg;
   if( dnumTarget > dnumNow ) {
      // まだ期限が来ていない場合
      msg = msg1 + "までは、あと" + msg2 + "です。";
   }
   else {
      // 期限が過ぎた場合
      msg = msg1 + "は、既に" + msg2 + "前に過ぎました。";
   }
 
   // 作成した文字列を表示
   document.getElementById("RealtimeCountdownArea").innerHTML = msg;
   document.getElementById("deadline.id").value =  targetDate; #最重要記述

}
// 1秒ごとに実行
setInterval('showCountdown()',1000);


</script>

ここで、先程の:id => deadline.id が活きてくる。このような記述を追加することで初めて、
javascriptで処理された結果をvalue(値)として受け取り、それを画面上で表示させることができる。これでようやく、タイマーを実装することができる。

タイマーだけを表示させたい場合

なお、日時の記入欄を表示させたくない場合も考えられる。下の写真のように期限とタイマーの残り時間だけを表示させたいという場合には、
スクリーンショット 2020-09-22 0.49.38.png

app/views/missions/show.thml.erb
      <p>期限 <%= @mission.deadline %>
      <br>
        <input type="hidden" id="userYear" value = "<%= @mission.deadline.year %>"  > 
        <input type="hidden" id="userMonth"value = "<%= @mission.deadline.month %>" >
        <input type="hidden" id="userDate" value = "<%= @mission.deadline.day %>" >
        <input type="hidden" id="userHour" value = "<%= @mission.deadline.hour %>" >
        <input type="hidden" id="userMin"  value = "<%= @mission.deadline.min %>" >
        <input type="hidden" id="userSec"  value = "<%= @mission.deadline.sec %>" >
      </p>
      <p id="RealtimeCountdownArea" ></p>

<script>



function set2fig(num) {
   // 数値が1桁だったら2桁の文字列にして返す
   var ret;
   if( num < 10 ) { ret = "0" + num; }
   else { ret = num; }
   return ret;
}
function isNumOrZero(num) {
   // 数値でなかったら0にして返す
   if( isNaN(num) ) { return 0; }
   return num;
}
function showCountdown() {
   // 現在日時を数値(1970-01-01 00:00:00からのミリ秒)に変換
   var nowDate = new Date();
   var dnumNow = nowDate.getTime();
 
   // 指定日時を数値(1970-01-01 00:00:00からのミリ秒)に変換
   var inputYear  = document.getElementById("userYear").value;
   var inputMonth = document.getElementById("userMonth").value - 1;
   var inputDate  = document.getElementById("userDate").value;
   var inputHour  = document.getElementById("userHour").value;
   var inputMin   = document.getElementById("userMin").value;
   var inputSec   = document.getElementById("userSec").value;
   var targetDate = new Date( isNumOrZero(inputYear), isNumOrZero(inputMonth), isNumOrZero(inputDate), isNumOrZero(inputHour), isNumOrZero(inputMin), isNumOrZero(inputSec) );
   var dnumTarget = targetDate.getTime();
 
   // 表示を準備
   var dlYear  = targetDate.getFullYear();
   var dlMonth = targetDate.getMonth() + 1;
   var dlDate  = targetDate.getDate();
   var dlHour  = targetDate.getHours();
   var dlMin   = targetDate.getMinutes();
   var dlSec   = targetDate.getSeconds();
   var msg1 = "期限の" + dlYear + "/" + dlMonth + "/" + dlDate + " " + set2fig(dlHour) + ":" + set2fig(dlMin) + ":" + set2fig(dlSec);
 
   // 引き算して日数(ミリ秒)の差を計算
   var diff2Dates = dnumTarget - dnumNow;
   if( dnumTarget < dnumNow ) {
      // 期限が過ぎた場合は -1 を掛けて正の値に変換
      diff2Dates *= -1;
   }
 
   // 差のミリ秒を、日数・時間・分・秒に分割
   var dDays  = diff2Dates / ( 1000 * 60 * 60 * 24 );   // 日数
   diff2Dates = diff2Dates % ( 1000 * 60 * 60 * 24 );
   var dHour  = diff2Dates / ( 1000 * 60 * 60 );   // 時間
   diff2Dates = diff2Dates % ( 1000 * 60 * 60 );
   var dMin   = diff2Dates / ( 1000 * 60 );   // 分
   diff2Dates = diff2Dates % ( 1000 * 60 );
   var dSec   = diff2Dates / 1000;   // 秒
   var msg2 = Math.floor(dDays) + "日"
            + Math.floor(dHour) + "時間"
            + Math.floor(dMin) + "分"
            + Math.floor(dSec) + "秒";
 
   // 表示文字列の作成
   var msg;
   if( dnumTarget > dnumNow ) {
      // まだ期限が来ていない場合
      msg =  "Mission終了まで、あと" + msg2 ;
   }
   else {
      // 期限が過ぎた場合
      msg = msg1 + "は、既に" + msg2 + "前に過ぎました。";
   }
 
   // 作成した文字列を表示
   document.getElementById("RealtimeCountdownArea").innerHTML = msg;
   document.getElementById("deadline.id").value =  targetDate;

}
// 1秒ごとに実行
setInterval('showCountdown()',1000);

</script>


上記のようにinput type = "hidden"とすれば、記入欄が画面上に表示されないものの、
<input type = ・・・>の6つが削除されている訳ではないためこれで正常に起動する。

まとめ

なかなか難易度が高かったが、達成感はすごかった。少しでも参考にしていただけたら幸いである。
参考記事
https://www.nishishi.com/javascript-tips/realtime-countdown-deadline.html

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

Rails6 カウントダウンタイマーの実装

はじめに

ポートフォリオで実装予定のカウントダウンタイマーを実装する。
タイマーを実装するにはjQueryが必要と書いてある記事もあるが、今回はjQueryを利用せずに実装を進めていくことにする。

作成順序

具体的な手順としては
①制限時間のカラムであるdeadlineを作成する
②タイマーを画面上に表示させるため、HTMLの記述をする
③制限時間を表示させたいので、javascriptを使って記述を行う

カラムの作成

まずは、いつものように

$rails generate migration Addカラム名Toテーブル名 カラム名:データ型

でマイグレーションファイルを作成する。

今回の場合、投稿部分に制限時間を表示させたいのでdeadlineをカラム名として、

$rails generate migration AddDeadlineToMission deadline:datetime

とする。

db/migrate/20200903084112_create_missions.rb
class CreateMissions < ActiveRecord::Migration[6.0]
  def change
    create_table :missions do |t|
      t.integer :user_id
      t.text    :content
      t.string  :penalty
      t.datetime   :deadline


      t.timestamps
    end
  end
end

上記のマイグレーションファイルの7行目にt.datetime :deadlineを追加する。
そして、親の顔より見た

$ rails db:migrate

を実行して、データベースに保存する。

タイマーの実装

スクリーンショット 2020-09-22 0.33.23.png

HTMLの記述

上記のような赤字のタイマーを設定するには、

app/views/missions/new.html.erb
<p>
     <%= f.hidden_field :deadline, :id => "deadline.id" %>
        <input type="text" id="userYear" >年
        <input type="text" id="userMonth">月
        <input type="text" id="userDate" >日
        <input type="text" id="userHour" >時
        <input type="text" id="userMin"  >分
        <input type="text" id="userSec"  >秒
 </p>
       <p id="RealtimeCountdownArea" ></p> #ここにタイマーが表示される

とする。
ここで、:id => deadline.id は後のjavascriptの記述において効果を発揮するため、記述している。

javascriptの記述

今回は、app/views/missions/new.html.erbのscriptタグにjavascriptを記述することにする。

以下の通りである。

app/views/missions/new.html.erb
<script>

function set2fig(num) {
   // 数値が1桁だったら2桁の文字列にして返す
   var ret;
   if( num < 10 ) { ret = "0" + num; }
   else { ret = num; }
   return ret;
}
function isNumOrZero(num) {
   // 数値でなかったら0にして返す
   if( isNaN(num) ) { return 0; }
   return num;
}
function showCountdown() {
   // 現在日時を数値(1970-01-01 00:00:00からのミリ秒)に変換
   var nowDate = new Date();
   var dnumNow = nowDate.getTime();
 
   // 指定日時を数値(1970-01-01 00:00:00からのミリ秒)に変換
   var inputYear  = document.getElementById("userYear").value;
   var inputMonth = document.getElementById("userMonth").value - 1;
   var inputDate  = document.getElementById("userDate").value;
   var inputHour  = document.getElementById("userHour").value;
   var inputMin   = document.getElementById("userMin").value;
   var inputSec   = document.getElementById("userSec").value;
   var targetDate = new Date( isNumOrZero(inputYear), isNumOrZero(inputMonth), isNumOrZero(inputDate), isNumOrZero(inputHour), isNumOrZero(inputMin), isNumOrZero(inputSec) );
   var dnumTarget = targetDate.getTime();
 
   // 表示を準備
   var dlYear  = targetDate.getFullYear();
   var dlMonth = targetDate.getMonth() + 1;
   var dlDate  = targetDate.getDate();
   var dlHour  = targetDate.getHours();
   var dlMin   = targetDate.getMinutes();
   var dlSec   = targetDate.getSeconds();
   var msg1 = "期限の" + dlYear + "/" + dlMonth + "/" + dlDate + " " + set2fig(dlHour) + ":" + set2fig(dlMin) + ":" + set2fig(dlSec);
 
   // 引き算して日数(ミリ秒)の差を計算
   var diff2Dates = dnumTarget - dnumNow;
   if( dnumTarget < dnumNow ) {
      // 期限が過ぎた場合は -1 を掛けて正の値に変換
      diff2Dates *= -1;
   }
 
   // 差のミリ秒を、日数・時間・分・秒に分割
   var dDays  = diff2Dates / ( 1000 * 60 * 60 * 24 );   // 日数
   diff2Dates = diff2Dates % ( 1000 * 60 * 60 * 24 );
   var dHour  = diff2Dates / ( 1000 * 60 * 60 );   // 時間
   diff2Dates = diff2Dates % ( 1000 * 60 * 60 );
   var dMin   = diff2Dates / ( 1000 * 60 );   // 分
   diff2Dates = diff2Dates % ( 1000 * 60 );
   var dSec   = diff2Dates / 1000;   // 秒
   var msg2 = Math.floor(dDays) + "日"
            + Math.floor(dHour) + "時間"
            + Math.floor(dMin) + "分"
            + Math.floor(dSec) + "秒";
 
   // 表示文字列の作成
   var msg;
   if( dnumTarget > dnumNow ) {
      // まだ期限が来ていない場合
      msg = msg1 + "までは、あと" + msg2 + "です。";
   }
   else {
      // 期限が過ぎた場合
      msg = msg1 + "は、既に" + msg2 + "前に過ぎました。";
   }
 
   // 作成した文字列を表示
   document.getElementById("RealtimeCountdownArea").innerHTML = msg;
   document.getElementById("deadline.id").value =  targetDate; #最重要記述

}
// 1秒ごとに実行
setInterval('showCountdown()',1000);


</script>

ここで、先程の:id => deadline.id が活きてくる。このような記述を追加することで初めて、
javascriptで処理された結果をvalue(値)として受け取り、それを画面上で表示させることができる。これでようやく、タイマーを実装することができる。

タイマーだけを表示させたい場合

なお、日時の記入欄を表示させたくない場合も考えられる。下の写真のように期限とタイマーの残り時間だけを表示させたいという場合には、
スクリーンショット 2020-09-22 0.49.38.png

app/views/missions/show.thml.erb
      <p>期限 <%= @mission.deadline %>
      <br>
        <input type="hidden" id="userYear" value = "<%= @mission.deadline.year %>"  > 
        <input type="hidden" id="userMonth"value = "<%= @mission.deadline.month %>" >
        <input type="hidden" id="userDate" value = "<%= @mission.deadline.day %>" >
        <input type="hidden" id="userHour" value = "<%= @mission.deadline.hour %>" >
        <input type="hidden" id="userMin"  value = "<%= @mission.deadline.min %>" >
        <input type="hidden" id="userSec"  value = "<%= @mission.deadline.sec %>" >
      </p>
      <p id="RealtimeCountdownArea" ></p>

<script>



function set2fig(num) {
   // 数値が1桁だったら2桁の文字列にして返す
   var ret;
   if( num < 10 ) { ret = "0" + num; }
   else { ret = num; }
   return ret;
}
function isNumOrZero(num) {
   // 数値でなかったら0にして返す
   if( isNaN(num) ) { return 0; }
   return num;
}
function showCountdown() {
   // 現在日時を数値(1970-01-01 00:00:00からのミリ秒)に変換
   var nowDate = new Date();
   var dnumNow = nowDate.getTime();
 
   // 指定日時を数値(1970-01-01 00:00:00からのミリ秒)に変換
   var inputYear  = document.getElementById("userYear").value;
   var inputMonth = document.getElementById("userMonth").value - 1;
   var inputDate  = document.getElementById("userDate").value;
   var inputHour  = document.getElementById("userHour").value;
   var inputMin   = document.getElementById("userMin").value;
   var inputSec   = document.getElementById("userSec").value;
   var targetDate = new Date( isNumOrZero(inputYear), isNumOrZero(inputMonth), isNumOrZero(inputDate), isNumOrZero(inputHour), isNumOrZero(inputMin), isNumOrZero(inputSec) );
   var dnumTarget = targetDate.getTime();
 
   // 表示を準備
   var dlYear  = targetDate.getFullYear();
   var dlMonth = targetDate.getMonth() + 1;
   var dlDate  = targetDate.getDate();
   var dlHour  = targetDate.getHours();
   var dlMin   = targetDate.getMinutes();
   var dlSec   = targetDate.getSeconds();
   var msg1 = "期限の" + dlYear + "/" + dlMonth + "/" + dlDate + " " + set2fig(dlHour) + ":" + set2fig(dlMin) + ":" + set2fig(dlSec);
 
   // 引き算して日数(ミリ秒)の差を計算
   var diff2Dates = dnumTarget - dnumNow;
   if( dnumTarget < dnumNow ) {
      // 期限が過ぎた場合は -1 を掛けて正の値に変換
      diff2Dates *= -1;
   }
 
   // 差のミリ秒を、日数・時間・分・秒に分割
   var dDays  = diff2Dates / ( 1000 * 60 * 60 * 24 );   // 日数
   diff2Dates = diff2Dates % ( 1000 * 60 * 60 * 24 );
   var dHour  = diff2Dates / ( 1000 * 60 * 60 );   // 時間
   diff2Dates = diff2Dates % ( 1000 * 60 * 60 );
   var dMin   = diff2Dates / ( 1000 * 60 );   // 分
   diff2Dates = diff2Dates % ( 1000 * 60 );
   var dSec   = diff2Dates / 1000;   // 秒
   var msg2 = Math.floor(dDays) + "日"
            + Math.floor(dHour) + "時間"
            + Math.floor(dMin) + "分"
            + Math.floor(dSec) + "秒";
 
   // 表示文字列の作成
   var msg;
   if( dnumTarget > dnumNow ) {
      // まだ期限が来ていない場合
      msg =  "Mission終了まで、あと" + msg2 ;
   }
   else {
      // 期限が過ぎた場合
      msg = msg1 + "は、既に" + msg2 + "前に過ぎました。";
   }
 
   // 作成した文字列を表示
   document.getElementById("RealtimeCountdownArea").innerHTML = msg;
   document.getElementById("deadline.id").value =  targetDate;

}
// 1秒ごとに実行
setInterval('showCountdown()',1000);

</script>


上記のようにinput type = "hidden"とすれば、記入欄が画面上に表示されないものの、<input type = ・・・>の6つが削除されている訳ではないためこれで正常に起動する。

 また、<input type="hidden" id="userYear" value = "<%= @mission.deadline.year %>" >
value="<%= @mission.deadline.year %>"の部分がなぜそのような記述となるかについて説明する。これは、deadlineはdatetimeというデータ型をとるカラムであるのだが、datetimeには年・月・日・時・分・秒といった日時の情報が保存されているからである。したがって、上記のような記述でnew.html.erbの部分で記述した期限の日時が取り出せることになる。

まとめ

なかなか難易度が高かったが、達成感はすごかった。少しでも参考にしていただけたら幸いである。

参考記事
https://www.nishishi.com/javascript-tips/realtime-countdown-deadline.html

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

RSpec - 'users validation'のテスト

経緯

初めまして。Qiitaの投稿、rails、rspecの初心者です。お手柔らかにお願いします。
railstutorial で学習しながら、app/models/user.rbの'model spec'を書いています。
イケてる綺麗なテストコードにしたいと思い、色々な方のrspecの書き方の記事を拝見させてもらいました。
specを自分なりに工夫しながら書いていて、この書き方「結構イケてるのでは?」と思う反面、
いざ記事を参考にしながら自分のコードを見返していると何か微妙に感じてきてしまい、
「ルールに従ったテストコードがなんなのか」と迷子になり、
なかなかcommitできず、地獄を彷徨っています。
もしよければ、テストコードのレビュー、指摘をしていただけると幸いです。

参考

テストコードは、主に jnchito さんの以下を参考にさせてもらいました。

上記を拝見させて頂いて自分なりに工夫して書いたコードになります。
以下は参考にしたわけではないですが、拝見して自分のコードがゴミに思えた記事。

ネストイメージ

  • describe: User(model)#create(action) のとき attribute: name は、
  • context:  name should be present名前が存在するべき。
  • it:     valid or invalid のパターンを検証。

テスト作成において、ネストのイメージ(?)ですが、
model > action > attribute > validates: presence:true > valid or invalid
のような感じでグループ化を意識してspecを書きました。
(もう一点イメージとして、記事の最下部の$ bundle exec rspecの実行結果が綺麗に見えるようにといった感じです。)

地獄から抜け出せない理由

  • describecontextitの正しい使い分け。
    工夫して他の人が読む場合を考慮してイケてるわかりやすく書いたつもりですが、
    RSpecを綺麗に書くための基本Rule を拝見して、 「これじゃダメだ。」となった。
    (そもそもルールがあるのでしょうか?ある程度のルールを自分の中で作って作成する認識でいるのでそれが問題かもしれません。)
  • プロから見た僕のテストコード
    書いたテストコードがグリーンで「問題ない」と思ってしまっているので、
    「俺ならこうする!」が知りたいです。
  • いちいち、「保存できることorできないこと」は、各テストケースで実施しなくても良いものなのか。
    基本的にはuser.valid?として、確実に対象のテストで落ちていることを保証するために、
    エラー(errors[:name])の中身をinclude("can't be blank")のように確認していますが、
    その後、user.save!として、、(!これは例外で落ちてしまう気がしてますが、、)
    以下を確認するべきなのかを迷っています。
    • validのケースは、保存できること
    • invalidのケースは、保存できないこと

以下が「保存できることorできないこと」を確認しているコードの抜粋です。

# nameが存在していない場合は、無効であること
it 'is invalid because the name value does not exists' do
  user.name = nil
  user.valid?
  expect(user.errors[:name]).to include("can't be blank")

  expect(user.save).to be_falsey # ...この1行で保存できないことを確認

end

テストコード

テストの内容は、よくあるusers.name, user.emailに関するバリデーションです。
railstutorial いうところの こちら になります。

では、早速ですが、、

以下が現状、掛かっているバリデーション。

app/models/user.rb
class User < ApplicationRecord
  before_save :downcase_email

  validates :name,
            presence: true,
            length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email,
            presence: true,
            length: { maximum: 255 },
            format: { with: VALID_EMAIL_REGEX },
            uniqueness: { case_sensitive: false }

  private

    def downcase_email
      self.email.downcase!
    end
end

以下がFactoryBotで定義しているuser

spec/factories/users.rb
FactoryBot.define do
  factory :user do
    name { "hoge" }
    email { "foo@bar.com" }
  end
end

以下が実際のテストコード。

spec/models/user_spec.rb
require 'rails_helper'
require 'pry'

RSpec.describe User, type: :model do
  let(:user) { FactoryBot.build(:user) }

  describe '#create' do
    describe 'attribute: name' do
      context 'name should be present' do
        # nameが存在している場合は、有効であること
        it 'is valid because the name has the correct value' do
          expect(user).to be_valid
          expect(user.save).to be_truthy
        end
        # nameが存在していない場合は、無効であること
        it 'is invalid because the name value does not exists' do
          user.name = nil
          user.valid?
          expect(user.errors[:name]).to include("can't be blank")
          expect(user.save).to be_falsey
        end
      end

      context 'name should be length is 50 characters or less' do
        # nameが50文字以下の場合は、有効であること
        it 'is valid because the name length is 50 characters or less' do
          user.name = 'a' * 50
          user.valid?
          expect(user.errors[:name]).not_to include('is too long (maximum is 50 characters)')
          expect(user.save).to be_truthy
        end
        # nameが50文字を超える場合は、無効であること
        it 'is invalid because the name length is 50 characters over' do
          user.name = 'a' * 51
          user.valid?
          expect(user.errors[:name]).to include('is too long (maximum is 50 characters)')
          expect(user.save).to be_falsey
        end
      end

      context 'name should be not blank' do
        # nameが空白の場合は、無効であること
        it 'is invalid because the name is blank' do
          user.name = ' '
          user.valid?
          expect(user.errors[:name]).to include("can't be blank")
          expect(user.save).to be_falsey
        end
      end
    end

    context 'attribute: email' do
      context 'email should be present' do
        # emailが存在していない場合は、無効であること
        it 'is invalid because the email value does not exists' do
          user.email = nil
          user.valid?
          expect(user.errors[:email]).to include("can't be blank")
          expect(user.save).to be_falsey
        end
      end

      context 'email should be length is 50 characters or less' do
        # emailが255文字以下の場合は、有効であること
        it 'is valid because the email length is 255 characters or less' do
          user.email = 'a' * 243 + '@example.com'
          user.valid?
          expect(user.errors[:email]).not_to include('is too long (maximum is 255 characters)')
          expect(user.save).to be_truthy
        end
        # emailが255文字を超える場合は、無効であること
        it 'is invalid because the email length is 255 characters over' do
          user.email = 'a' * 244 + '@example.com'
          user.valid?
          expect(user.errors[:email]).to include('is too long (maximum is 255 characters)')
          expect(user.save).to be_falsey
        end
      end

      context 'email should be correct format' do
        # emailの形式が正しい場合は、有効であること
        it 'is valid because the email is the correct format' do
          user.email = 'user@example.com'
          expect(user).to be_valid
          expect(user.save).to be_truthy

          user.email = 'USER@foo.COM'
          expect(user).to be_valid
          expect(user.save).to be_truthy

          user.email = 'A_US-ER@foo.bar.org'
          expect(user).to be_valid
          expect(user.save).to be_truthy

          user.email = 'first.last@foo.jp'
          expect(user).to be_valid
          expect(user.save).to be_truthy

          user.email = 'alice+bob@baz.cn'
          expect(user).to be_valid
          expect(user.save).to be_truthy
        end
        # emailの形式が正しくない場合は、無効であること
        it 'is invalid because the email format is incorrect' do
          user.email = 'user@example,com'
          expect(user).not_to be_valid
          expect(user.save).to be_falsey

          user.email = 'user_at_foo.org'
          expect(user).not_to be_valid
          expect(user.save).to be_falsey

          user.email = 'user.name@example.'
          expect(user).not_to be_valid
          expect(user.save).to be_falsey

          user.email = 'foo@bar_baz.com'
          expect(user).not_to be_valid
          expect(user.save).to be_falsey

          user.email = 'foo@bar+baz.com'
          expect(user).not_to be_valid
          expect(user.save).to be_falsey
        end
      end

      context 'email should be unique' do
        # 同一のemailが既に登録されている場合は、無効であること
        it 'is Invalid because the same email already exists' do
          dup_user = user.dup
          dup_user.email = dup_user.email.upcase
          dup_user.save!
          user.valid?
          expect(user.errors[:email]).to include('has already been taken')
          expect(user.save).to be_falsey
        end
      end

      # emailが小文字で保存されていること
      context 'email should be saved in lowercase' do
        it 'is valid because the email saved in lowercase' do
          user.email = 'Foo@Example.Com'
          user.save!
          expect(user.reload.email).to eq 'foo@example.com'
        end
      end

      context 'email should be not blank' do
        # emailが空白の場合は、無効であること
        it 'is invalid because the email is blank' do
          user.email = ' '
          user.valid?
          expect(user.errors[:email]).to include("can't be blank")
          expect(user.save).to be_falsey
        end
      end
    end

  end
end

上記のテストは、$ bundle exec rspecを実行(グリーン)した出力は以下。
テストコードのdescribecontextitの使い分けは、以下の出力が見やすくなるようにを意識してネストした感じなります。

User
  #create
    attribute: name
      name should be present
        is valid because the name has the correct value
        is invalid because the name value does not exists
      name should be length is 50 characters or less
        is valid because the name length is 50 characters or less
        is invalid because the name length is 50 characters over
      name should be not blank
        is invalid because the name is blank
    attribute: email
      email should be present
        is invalid because the email value does not exists
      email should be length is 50 characters or less
        is valid because the email length is 255 characters or less
        is invalid because the email length is 255 characters over
      email should be correct format
        is valid because the email is the correct format
        is invalid because the email format is incorrect
      email should be unique
        is Invalid because the same email already exists
      email should be saved in lowercase
        is valid because the email saved in lowercase
      email should be not blank
        is invalid because the email is blank

どうぞ、宜しくお願いします。

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