- 投稿日:2020-09-22T22:31:17+09:00
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.ymlversion: '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を追記します
Gemfilegroup :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' endgemをインストールします。
$ 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-22T22:01:44+09:00
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
- 投稿日:2020-09-22T21:26:15+09:00
本番環境で画像が表示されない
デプロイ後画像が表示されなくて困ったのでまとめてみました。
どなたかのお役に立てたら嬉しいです。
ruby '2.6.5'
rails '6.0.0'オリジナルアプリケーションを作っています。
本番環境はHerokuを使用します。
Herokuにデプロイして動作確認したらエラーログの出現しました。エラーログから本番環境ですと画像へのパスが変わってしまうのが原因と考えました。
ローカルだと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.rbconfig.assets.compile = false変更後
production.rbconfig.assets.compile = trueこちらを有効にしてみたら無事画像表示できました。
まとめ
<<初心者向け>>本番環境で画像表示されない場合のやってみて損はない対処法
①本番環境でのasset precompileの設定が有効か確認
29config.assets.compile = true②プリコンパイルでPublic内にAssetsファイルを作成
% rails assets:precompile③assets_pathを記述する
<%= image_tag asset_path('medicine3.jpeg'), class:"med-pic" %>
④デプロイ完了して無事、画像表示完了!!
asset precompileのやってることとか軽く調べておくとより良いかもです!
- 投稿日:2020-09-22T21:07:09+09:00
(ギリ)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.rbtest "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.rbtest "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.rbtest "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のテキストしか使用できないインターネットの電子メールでさまざまなフォーマット(書式)を扱えるようにする規格。
- 投稿日:2020-09-22T19:45:56+09:00
【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 であり、非常に低コストです。
上記の通り、高額な金額が発生するわけではなさそうなので試しに利用してみるのもありでしょう!
大体の流れ(イメージ)
- AWS SESの設定
- AWSのCSに制限解除のメールを送る
- IAMユーザーの設定
- railsに設定
1. AWS SESの設定
1-1. AWSにログイン
1-2. ヘッダーのサービスをクリック
1-3. SESと検索してエンター
1-4. IdentityManagement>Domainをクリック
1-5. Varufy a NeW Domianをクリック
※この段階で東京リージョン以外のリージョンを選択しておいたほうが良いです
1-6. Domainに接続したいドメイン名を入れる
1-7. Generate DKIM Settingsにチェック入れる
1-8. Varufy This Domianをクリック
1-9. User Route53をクリック
Route53を登録している場合上記表示になる。
登録している場合レコードの設定までを自動でやってくれるぽい
1-10. Create Record Setsをクリック
AWS SWSのsettingは一旦こちらで終了です!2. AWSのCSに制限解除のメールを送る
AWS SESをsettingをしただけではメールは送信されません。
設定初期ProductionAcsessがSandboxになっており、これを解除する必要があります。
2-1. Email Sending > Sending Statistics をクリック
2-2. ProductionAcsessがSandboxになっていることを確認する。
2-3. 右上のサポートをクリック
2-4. サポートセンターをクリック以降メールの送信手順に関しては下記記事でご丁寧にまとめられておりましたので
メールを送るまでは参考にしてみてください。
2-6.メール送信の手順
※2.解除申請を送るフォームに入力から行ってみてください。
メール送信して一日後、上記のような解除しましたとメールが来れば
下記がProductionAcsessがSandbox⇒Enabledに代わります。
2-7.IdentityManagement>Domain>SendaTestEmailをクリック
2-8.メールを送ってみる
最後にテストです。
メールを送り受信ボックスに届くか確認します。3. IAMユーザーの設定
AWS SESのsettingと制限解除だけではrailsにつなぎこめません。
最後にIAMユーザーを新規登録しなくてはいけません。
※筆者はrootアカウントでキーとIDを参照する方法を探してみましたがありませんでした。
※本来rootアカウントでは接続管理は好ましくなく、各アクションごとにIAMユーザー(権限の範囲)を指定して接続したほうがいいと参考書に書いてありました。
のであるかはわかりませんがとりあえずIAMユーザーで作成しておけってことですね。
3-1. ヘッダーのサービスをクリック
3-2. IAMと検索してエンター
3-3. ユーザーをクリック
3-4. ユーザーを追加をクリック
3-5. ユーザー名を入れる(※ここはわかりやすければなんでもOKです)
3-6. プログラムによるアクセスにチェック
3-7. 次のステップ:アクセス権限をクリック
3-8. 既存ポリシーを直接アタッチをクリック
3-9. ポリシーのフィルタにSESと入れる
3-10. AmazonSESFullAccessにチェックを入れる
3-11. 次ぐのステップ:タグをクリック
3-12. 次ぐのステップ:確認をクリック
キーと値は何も入れなくてOKです。
こちらで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-1IDと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 = :sesconfig/environments/development.rbに上記を追加
ドメイン名と書いてあるところは接続しようとしているドメイン名に変えてください。4-3 本番環境(のみ)の設定
config.action_mailer.default_url_options = { host: 'ドメイン名' } config.action_mailer.delivery_method = :sesconfig/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' endMailerクラスのfromの設定をします。
mailers/application_mailer.rbに上記を追加します。
ドメイン名と書いてあるところは接続しようとしているドメイン名に変えてください。
開発環境であればこちらで終了となります^^4-6. .envをアップロードする(本番環境のみの設定)
開発環境では正常に動き、本番環境で動かなかった際、こちら完全に忘れておりました、、
.envはpushされないのでEC2上にアップロードします
でないと本番環境ではSESの参照ができません!
流れは下記のとおりです。
1.EC2にログイン
2.アプリケーションのディレクトリに移動
3.viで.envにアクセスキーIDとシークレットアクセスキーの上書き保存
4.PUMAの再起動用語解説
後程追加いたします。
最後に
現在ポートフォリオを作成しておりますので
ご意見などいただければうれしいです!
NotePro
- 投稿日:2020-09-22T19:45:56+09:00
【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 であり、非常に低コストです。
上記の通り、高額な金額が発生するわけではなさそうなので試しに利用してみるのもありでしょう!
大体の流れ(イメージ)
- AWS SESの設定
- AWSのCSに制限解除のメールを送る
- IAMユーザーの設定
- railsに設定
1. AWS SESの設定
1-1. AWSにログイン
1-2. ヘッダーのサービスをクリック
1-3. SESと検索してエンター
1-4. IdentityManagement>Domainをクリック
1-5. Varufy a NeW Domianをクリック
※この段階で東京リージョン以外のリージョンを選択しておいたほうが良いです
1-6. Domainに接続したいドメイン名を入れる
1-7. Generate DKIM Settingsにチェック入れる
1-8. Varufy This Domianをクリック
1-9. User Route53をクリック
Route53を登録している場合上記表示になる。
登録している場合レコードの設定までを自動でやってくれるぽい
1-10. Create Record Setsをクリック
AWS SWSのsettingは一旦こちらで終了です!2. AWSのCSに制限解除のメールを送る
AWS SESをsettingをしただけではメールは送信されません。
設定初期ProductionAcsessがSandboxになっており、これを解除する必要があります。
2-1. Email Sending > Sending Statistics をクリック
2-2. ProductionAcsessがSandboxになっていることを確認する。
2-3. 右上のサポートをクリック
2-4. サポートセンターをクリック以降メールの送信手順に関しては下記記事でご丁寧にまとめられておりましたので
メールを送るまでは参考にしてみてください。
2-6.メール送信の手順
※2.解除申請を送るフォームに入力から行ってみてください。
メール送信して一日後、上記のような解除しましたとメールが来れば
下記がProductionAcsessがSandbox⇒Enabledに代わります。
2-7.IdentityManagement>Domain>SendaTestEmailをクリック
2-8.メールを送ってみる
最後にテストです。
メールを送り受信ボックスに届くか確認します。3. IAMユーザーの設定
AWS SESのsettingと制限解除だけではrailsにつなぎこめません。
最後にIAMユーザーを新規登録しなくてはいけません。
※筆者はrootアカウントでキーとIDを参照する方法を探してみましたがありませんでした。
※本来rootアカウントでは接続管理は好ましくなく、各アクションごとにIAMユーザー(権限の範囲)を指定して接続したほうがいいと参考書に書いてありました。
のであるかはわかりませんがとりあえずIAMユーザーで作成しておけってことですね。
3-1. ヘッダーのサービスをクリック
3-2. IAMと検索してエンター
3-3. ユーザーをクリック
3-4. ユーザーを追加をクリック
3-5. ユーザー名を入れる(※ここはわかりやすければなんでもOKです)
3-6. プログラムによるアクセスにチェック
3-7. 次のステップ:アクセス権限をクリック
3-8. 既存ポリシーを直接アタッチをクリック
3-9. ポリシーのフィルタにSESと入れる
3-10. AmazonSESFullAccessにチェックを入れる
3-11. 次ぐのステップ:タグをクリック
3-12. 次ぐのステップ:確認をクリック
キーと値は何も入れなくてOKです。
こちらで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-1IDと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 = :sesconfig/environments/development.rbに上記を追加
ドメイン名と書いてあるところは接続しようとしているドメイン名に変えてください。4-3 本番環境(のみ)の設定
config.action_mailer.default_url_options = { host: 'ドメイン名' } config.action_mailer.delivery_method = :sesconfig/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' endMailerクラスのfromの設定をします。
mailers/application_mailer.rbに上記を追加します。
ドメイン名と書いてあるところは接続しようとしているドメイン名に変えてください。
開発環境であればこちらで終了となります^^4-6. .envをアップロードする(本番環境のみの設定)
開発環境では正常に動き、本番環境で動かなかった際、こちら完全に忘れておりました、、
.envはpushされないのでEC2上にアップロードします
でないと本番環境ではSESの参照ができません!
流れは下記のとおりです。
1.EC2にログイン
2.アプリケーションのディレクトリに移動
3.viで.envにアクセスキーIDとシークレットアクセスキーの上書き保存
4.PUMAの再起動用語解説
後程追加いたします。
最後に
現在ポートフォリオを作成しておりますので
ご意見などいただければうれしいです!
NotePro
- 投稿日:2020-09-22T18:37:31+09:00
テーブルのデータ型とコマンドについての備忘録(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
- 投稿日:2020-09-22T18:15:25+09:00
投稿に対していいね機能を実装する
概要
今回、twitterのような投稿アプリに対して
いいねを押したり、取り消したりできる機能と、
いいねをカウントして件数を表示できる機能と、
いいねするリンクをハートにする実装についてまとめておきます。ちなみに、投稿にいいねをする機能に関しては、progateにカリキュラムがありますが、自分はそのまま実装しても思うような機能にならなかったので、qiitaをはじめとするサイトをたくさん検索しました。
それも踏まえてまとめようと思います。
機能の見た目
実装
likeモデルを用意する
rails g model likeマイグレーションファイルに
t.references, foreign_key: trueとして、user_idとpost_idのカラムを設定します。models/like.rbclass Like < ApplicationRecord validates :user_id, presence: true validates :post_id, presence: true endルーティングを設定する
routes.rbpost "likes/:post_id/create" => "likes#create" post "likes/:post_id/destroy" => "likes#destroy"これによってrails routesをすると次の画像のようになります。
これは、ビューファイルにリンクを指定するときに必要になるのであとで間違えないようにしましょう。コントローラーを作成する
controllers/likes_controller.rbclass 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.rbdef show @post = Post.find(params[:id]) @comment = Comment.new @comments = @post.comments.includes(:user) @like_count = Like.where(post_id: @post.id).count endwhereでその投稿についてるいいねをデータベースから検索して、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 %> ---省略---以上で完成です。
- 投稿日:2020-09-22T17:19:27+09:00
[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.rbclass ChangeIndexUniuqueToUsers < ActiveRecord::Migration[6.0] def change remove_index :users, :email add_index :users, [:email,:deleted_at], unique: true end endmigration
rails db:migrate3. Validation追加
アプリケーションレベルでのバリデーションを追加
app/models/user.rbclass User < ActiveRecord::Base acts_as_paranoid validates :email, uniqueness: { scope: :deleted_at }4. 動作確認
railsコンソールもしくはrailsサーバーを立ち上げ実際にユーザー作成、退会、同アドレスでの再登録を実施してDBを確認します。
無事同じアドレスでの再登録に成功しました!!
他何かいい方法などあれば是非教えてくださいませ~!
- 投稿日:2020-09-22T15:25:35+09:00
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を実行する」という手順を踏まないと、いくら(Ⅰ)の手順を踏んだとしましても、エラーが発生したままとなります。
- 投稿日:2020-09-22T14:14:57+09:00
めっちゃ便利な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] #=>nilStructクラスは配列、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 endStructクラス自体を継承したサブクラスを定義することは非推奨らしい。ここまだちょっとよく理解できてない。
継承元となる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とかでも気軽に試せるので興味が出た人は是非試してみてください。
- 投稿日:2020-09-22T12:26:02+09:00
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.rbtest "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.rbrespond_to do |format| # format.html { redirect_to @user } # format.js endFAIL["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>'
- 投稿日:2020-09-22T11:19:34+09:00
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.1Bootstrapの適用方法は省きます。
以下、記述例です
Gemfilegem 'kaminari', '~> 1.2.0'ターミナル% bundle install % rails g kaminari:views bootstrap4
config/kaminari_ja.ymlja: views: pagination: first: "« 最初" last: "最後 »" previous: "‹ 前" next: "次 ›" truncate: "…" 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-22T11:15:07+09:00
【Rails】[0日目]分からないところを無視しないオリジナルサービス制作
始めに
初めまして。名古屋在住のしんやです。転職で東京に行きたいです。
未経験からの転職のためにテックアカデミーのWebアプリケーションコースを受講し、2020年9月20日に受講終了しました。僕としてはスクールのカリキュラムは非常に分かりやすく、サポート体制も整っていたので快適に学習を進めることができました。
プログラミング勉強してる時、完全には納得できてないことがあっても、とりあえず「これはこういうものなんだ」として先に進めると、意外とあとから「そういうことだったのか」って納得できることありませんか??#プログラミング初心者 #駆け出しエンジニアと繋がりたい
— しんや@転職で東京行きたい (@shinyaeng) August 11, 2020唐突に自分の過去ツイートを引用しましたが、このスタンスで進めて受講終了に至りました。
受講終了した今気付いたのが、上記スタンスで進めるのはいいが、絶対に理解してから進めないといけないこともあるという当然のことでした。
簡単に言うと、とりあえずで進めたとこが多数あるが故に、カリキュラムは終えたけど全然理解できてねーなってとこが多数あるってことです。もちろんスクールの受講を終了した程度で全部理解できるとは思っていなかったですが、悔しいです。
今後の内容
悔しがっていてもしょうがないので、復習を兼ねたアウトプットをここでします。
一番重要視するのは、「分からないところを無視しない」という点です。ただただ自分のためのアウトプットであり、カリキュラムがあるわけではありません。
分からないところは徹底的に調べ、調べた内容はここにアウトプットします。自分だけのメモでやってろって話かもしれませんが、自分の認識などに誤りがある場合、ここでアウトプットすることで指摘をいただけるという可能性があります。
淡い期待です。最後に
現状、オリジナルサービスのワイヤーフレーム、サイトマップ、ER図の作成中です。
次回投稿を1回目とし、ここから進めていこうと思っています。よろしくお願いします!
- 投稿日:2020-09-22T11:15:07+09:00
【Rails】[0日目]分からないところを無視せずオリジナルサービス制作
始めに
初めまして。名古屋在住のしんやです。転職で東京に行きたいです。
未経験からの転職のためにテックアカデミーのWebアプリケーションコースを受講し、2020年9月20日に受講終了しました。僕としてはスクールのカリキュラムは非常に分かりやすく、サポート体制も整っていたので快適に学習を進めることができました。
プログラミング勉強してる時、完全には納得できてないことがあっても、とりあえず「これはこういうものなんだ」として先に進めると、意外とあとから「そういうことだったのか」って納得できることありませんか??#プログラミング初心者 #駆け出しエンジニアと繋がりたい
— しんや@転職で東京行きたい (@shinyaeng) August 11, 2020唐突に自分の過去ツイートを引用しましたが、このスタンスで進めて受講終了に至りました。
受講終了した今気付いたのが、上記スタンスで進めるのはいいが、絶対に理解してから進めないといけないこともあるという当然のことでした。
簡単に言うと、とりあえずで進めたとこが多数あるが故に、カリキュラムは終えたけど全然理解できてねーなってとこが多数あるってことです。もちろんスクールの受講を終了した程度で全部理解できるとは思っていなかったですが、悔しいです。
今後の内容
悔しがっていてもしょうがないので、復習を兼ねたアウトプットをここでします。
一番重要視するのは、「分からないところを無視しない」という点です。ただただ自分のためのアウトプットであり、カリキュラムがあるわけではありません。
分からないところは徹底的に調べ、調べた内容はここにアウトプットします。自分だけのメモでやってろって話かもしれませんが、自分の認識などに誤りがある場合、ここでアウトプットすることで指摘をいただけるという可能性があります。
淡い期待です。最後に
現状、オリジナルサービスのワイヤーフレーム、サイトマップ、ER図の作成中です。
次回投稿を1回目とし、ここから進めていこうと思っています。よろしくお願いします!
- 投稿日:2020-09-22T11:15:07+09:00
【Rails】[第0回]分からないところを無視しないオリジナルサービス制作
始めに
初めまして。名古屋在住のしんやです。転職で東京に行きたいです。
未経験からの転職のためにテックアカデミーのWebアプリケーションコースを受講し、2020年9月20日に受講終了しました。僕としてはスクールのカリキュラムは非常に分かりやすく、サポート体制も整っていたので快適に学習を進めることができました。
プログラミング勉強してる時、完全には納得できてないことがあっても、とりあえず「これはこういうものなんだ」として先に進めると、意外とあとから「そういうことだったのか」って納得できることありませんか??#プログラミング初心者 #駆け出しエンジニアと繋がりたい
— しんや@転職で東京行きたい (@shinyaeng) August 11, 2020唐突に自分の過去ツイートを引用しましたが、このスタンスで進めて受講終了に至りました。
受講終了した今気付いたのが、上記スタンスで進めるのはいいが、絶対に理解してから進めないといけないこともあるという当然のことでした。
簡単に言うと、とりあえずで進めたとこが多数あるが故に、カリキュラムは終えたけど全然理解できてねーなってとこが多数あるってことです。もちろんスクールの受講を終了した程度で全部理解できるとは思っていなかったですが、悔しいです。
今後の内容
悔しがっていてもしょうがないので、復習を兼ねたアウトプットをここでします。
一番重要視するのは、「分からないところを無視しない」という点です。ただただ自分のためのアウトプットであり、カリキュラムがあるわけではありません。
分からないところは徹底的に調べ、調べた内容はここにアウトプットします。自分だけのメモでやってろって話かもしれませんが、自分の認識などに誤りがある場合、ここでアウトプットすることで指摘をいただけるという可能性があります。
淡い期待です。最後に
現状、オリジナルサービスのワイヤーフレーム、サイトマップ、ER図の作成中です。
次回投稿を第1回とし、ここから進めていこうと思っています。よろしくお願いします!
- 投稿日:2020-09-22T10:53:18+09:00
Rails 6で認証認可入り掲示板APIを構築する #17 管理者権限の追加
←Rails 6で認証認可入り掲示板APIを構築する #16 policyの設定
管理者権限を用意する
前回までの実装で、投稿の編集や削除は投稿者本人だけできるようになりました。
ですがそこに拡張し、管理者としてログインしている時は誰の投稿でも編集・削除できるようにしてみます。そのためにはuserモデルにadminカラムを追加しましょう。
$ rails g migration AddAdminToUsers admin:booleandb/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.rbdef update? - mine? + mine? || admin? end def destroy? - mine? + mine? || admin? endなんとこれだけです。
traitを使ってadminを簡単に作る
これだけだと少し中身の薄い記事なので、factoryBotを触ってadminユーザーをもっと簡単に作れるようにします。
spec/factories/users.rbname { "MyString" } tokens { nil } admin { false } + + trait :admin do + admin { true } + end end
spec/policies/post_policy_spec.rbRSpec.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
のたびに複数カラムの初期値セットをしなくて済みます。ぜひご活用ください。
続き
- 投稿日:2020-09-22T10:23:15+09:00
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]()
- 投稿日:2020-09-22T09:54:14+09:00
剰余とべき乗(冪乗)の演算子
【概要】
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.ここから学んだこと(エラーの時に使用)
算数要素が混ざったプログラムは、プログラムの知識はもちろんのこと、工夫して計算する発想が求められることを学びました。工夫した計算にプログラムの知識も必要ですし、「このメソッドは一体どこで使うんだろう?」と思っているとピンポイントで使うこともあるので非常に大事です。
- 投稿日:2020-09-22T09:51:08+09:00
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.rb20.times do Book.create!( title: Faker::Book.title ) end
- 投稿日:2020-09-22T01:08:54+09:00
カウントダウンタイマーの実装
はじめに
ポートフォリオで実装予定のカウントダウンタイマーを実装する。
タイマーを実装するには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を実行して、データベースに保存する。
タイマーの実装
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(値)として受け取り、それを画面上で表示させることができる。これでようやく、タイマーを実装することができる。タイマーだけを表示させたい場合
なお、日時の記入欄を表示させたくない場合も考えられる。下の写真のように期限とタイマーの残り時間だけを表示させたいという場合には、
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
- 投稿日:2020-09-22T01:08:54+09:00
Rails6 カウントダウンタイマーの実装
はじめに
ポートフォリオで実装予定のカウントダウンタイマーを実装する。
タイマーを実装するにはjQueryが必要と書いてある記事もあるが、今回はjQueryを利用せずに実装を進めていくことにする。作成順序
具体的な手順としては
①制限時間のカラムであるdeadlineを作成する
②タイマーを画面上に表示させるため、HTMLの記述をする
③制限時間を表示させたいので、javascriptを使って記述を行うカラムの作成
まずは、いつものように
$rails generate migration Addカラム名Toテーブル名 カラム名:データ型でマイグレーションファイルを作成する。
今回の場合、投稿部分に制限時間を表示させたいのでdeadlineをカラム名として、
$rails generate migration AddDeadlineToMission deadline:datetimeとする。
db/migrate/20200903084112_create_missions.rbclass 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を実行して、データベースに保存する。
タイマーの実装
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(値)として受け取り、それを画面上で表示させることができる。これでようやく、タイマーを実装することができる。タイマーだけを表示させたい場合
なお、日時の記入欄を表示させたくない場合も考えられる。下の写真のように期限とタイマーの残り時間だけを表示させたいという場合には、
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
- 投稿日:2020-09-22T00:27:21+09:00
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
orinvalid
のパターンを検証。テスト作成において、ネストのイメージ(?)ですが、
model
>action
>attribute
>validates: presence:true
>valid or invalid
のような感じでグループ化を意識してspecを書きました。
(もう一点イメージとして、記事の最下部の$ bundle exec rspec
の実行結果が綺麗に見えるようにといった感じです。)地獄から抜け出せない理由
describe
、context
、it
の正しい使い分け。
工夫して他の人が読む場合を考慮してイケてるわかりやすく書いたつもりですが、
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.rbclass 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.rbFactoryBot.define do factory :user do name { "hoge" } email { "foo@bar.com" } end end以下が実際のテストコード。
spec/models/user_spec.rbrequire '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
を実行(グリーン)した出力は以下。
テストコードのdescribe
、context
、it
の使い分けは、以下の出力が見やすくなるようにを意識してネストした感じなります。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どうぞ、宜しくお願いします。