20210502のRubyに関する記事は20件です。

Rubyのmoduleとは?

moduleとは クラスと似ており、変数やメソッドを定義することができます。 ただし、クラスのようにインスタンスは生成できませんし、継承もできません。 ・moduleでインスタンスを作ろうとした場合 moduleでインスタンスは作成できないため、エラーとなります。 module Test def hoge put "hoge" end end tester = Test.new # エラーメッセージ undefined method `new' for Test:Module (NoMethodError) ・moduleを継承して新しいmoduleを作成することは出来ないのでエラーとなります。 module Test def hoge "hoge" end end module Test2 < Test end # エラーメッセージ syntax error, unexpected '<' moduleを使うメリット 上記のようにclassと違って融通の効かない感じがありますが、moduleを使うことで以下のようなメリットがあります。 ・多重継承ができる Rubyは単一継承しか出来ません。しかし、moduleを用いることで多重継承に似た機能を実装できます。 以下のようにincludeしてmoduleを追加することをミックスインと言います。 module Test def hoge puts "Testmoduleが呼び出されている" end end class User # Test moduleを読み込んでいる include Test def get_user # Testモジュールをincludeしているので、hogeメソッドが使える hoge() puts "get_userが呼び出されている" end end # Userインスタンスの作成 user = User.new # Userクラスのget_userメソッドを呼び出す。 # Userクラスで、Testモジュールをincludeしているので、hogeメソッドが使える user.get_user
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

最小公倍数

最小公倍数を求める計算機 https://www.nap.st/least_common_multiple/?lang=ja 最小公倍数の計算 https://calculator.jp/science/lcm/ 最大公約数と最小公倍数 https://keisan.casio.jp/exec/system/1161228772 最小公倍数の計算アプリ。手計算する場合の簡単な求め方も紹介。 https://kumiko-jp.com/archives/306553.html 1.249629367105416e100 Ruby で任意個の整数の最大公約数・最小公倍数を求める https://qiita.com/scivola/items/5a26a2a38d82bcd591f4 num.rb numbers = [2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99] # 最小公倍数 puts numbers.inject(:lcm) bash $ ruby num.rb 69720375229712477164533808935312303556800
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

最小公倍数を使う題材

最小公倍数に関する話題。 各数に最小公倍数をかけて、加算して、最小公倍数との差が1なら、誤差のうち。同じなら1。 最小公倍数を求める計算機 https://www.nap.st/least_common_multiple/?lang=ja 最小公倍数の計算 https://calculator.jp/science/lcm/ 最大公約数と最小公倍数 https://keisan.casio.jp/exec/system/1161228772 最小公倍数の計算アプリ。手計算する場合の簡単な求め方も紹介。 https://kumiko-jp.com/archives/306553.html 1.249629367105416e100 Ruby で任意個の整数の最大公約数・最小公倍数を求める https://qiita.com/scivola/items/5a26a2a38d82bcd591f4 2桁までの数の最小公倍数は num2.rb numbers = [2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99] # 最小公倍数 puts numbers.inject(:lcm) bash $ ruby num2.rb 69720375229712477164533808935312303556800 じゃ、3桁までの数だったら。 num3.rb numbers = [2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,381,382,383,384,385,386,387,388,389,390,391,392,393,394,395,396,397,398,399,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,435,436,437,438,439,440,441,442,443,444,445,446,447,448,449,450,451,452,453,454,455,456,457,458,459,460,461,462,463,464,465,466,467,468,469,470,471,472,473,474,475,476,477,478,479,480,481,482,483,484,485,486,487,488,489,490,491,492,493,494,495,496,497,498,499,500,501,502,503,504,505,506,507,508,509,510,511,512,513,514,515,516,517,518,519,520,521,522,523,524,525,526,527,528,529,530,531,532,533,534,535,536,537,538,539,540,541,542,543,544,545,546,547,548,549,550,551,552,553,554,555,556,557,558,559,560,561,562,563,564,565,566,567,568,569,570,571,572,573,574,575,576,577,578,579,580,581,582,583,584,585,586,587,588,589,590,591,592,593,594,595,596,597,598,599,600,601,602,603,604,605,606,607,608,609,610,611,612,613,614,615,616,617,618,619,620,621,622,623,624,625,626,627,628,629,630,631,632,633,634,635,636,637,638,639,640,641,642,643,644,645,646,647,648,649,650,651,652,653,654,655,656,657,658,659,660,661,662,663,664,665,666,667,668,669,670,671,672,673,674,675,676,677,678,679,680,681,682,683,684,685,686,687,688,689,690,691,692,693,694,695,696,697,698,699,700,701,702,703,704,705,706,707,708,709,710,711,712,713,714,715,716,717,718,719,720,721,722,723,724,725,726,727,728,729,730,731,732,733,734,735,736,737,738,739,740,741,742,743,744,745,746,747,748,749,750,751,752,753,754,755,756,757,758,759,760,761,762,763,764,765,766,767,768,769,770,771,772,773,774,775,776,777,778,779,780,781,782,783,784,785,786,787,788,789,790,791,792,793,794,795,796,797,798,799,800,801,802,803,804,805,806,807,808,809,810,811,812,813,814,815,816,817,818,819,820,821,822,823,824,825,826,827,828,829,830,831,832,833,834,835,836,837,838,839,840,841,842,843,844,845,846,847,848,849,850,851,852,853,854,855,856,857,858,859,860,861,862,863,864,865,866,867,868,869,870,871,872,873,874,875,876,877,878,879,880,881,882,883,884,885,886,887,888,889,890,891,892,893,894,895,896,897,898,899,900,901,902,903,904,905,906,907,908,909,910,911,912,913,914,915,916,917,918,919,920,921,922,923,924,925,926,927,928,929,930,931,932,933,934,935,936,937,938,939,940,941,942,943,944,945,946,947,948,949,950,951,952,953,954,955,956,957,958,959,960,961,962,963,964,965,966,967,968,969,970,971,972,973,974,975,976,977,978,979,980,981,982,983,984,985,986,987,988,989,990,991,992,993,994,995,996,997,998,999] # 最小公倍数 puts numbers.inject(:lcm) bash $ ruby num3.rb 7128865274665093053166384155714272920668358861885893040452001991154324087581111499476444151913871586911717817019575256512980264067621009251465871004305131072686268143200196609974862745937188343705015434452523739745298963145674982128236956232823794011068809262317708861979540791247754558049326475737829923352751796735248042463638051137034331214781746850878453485678021888075373249921995672056932029099390891687487672697950931603520000 話題提供してくださった、星野利夫さんなら、分母が素数だけのいい例を提供してくれると思う。 文書履歴(document hisotry9 ver. 0.01 初稿 2桁まで計算。 20210502 ver. 0.02 3桁まで計算 20210503
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

URL直打ち対策(ログアウト/ログイン別まとめ)

はじめに アプリ実装中、"URL直打ち"対策を行なったので、記録として残します。 Ruby 2.6.5 目次 1. "URL直打ち"とは 2. 対 ログアウトユーザー 3. 対 ログインユーザー 4. 対 ログインしている自分 5. まとめ 1. "URL直打ち"とは "URL直打ち"とは、その名前の通りURLに直接書き込むことです。 例えば、他のユーザーが投稿したつぶやきや写真・動画などはこちらが勝手に編集・削除はできませんよね。 それに対し、自分が投稿したものに対してはいつでも編集・削除ができます。 これは自分自身と他のユーザーとで画面上でのボタンの表示の切り替えがされるように実装されているからです。 なので、普通なら他ユーザーの投稿は編集・削除ができません。 しかし、URLの仕組みを理解してると、URLに直接書き込むことで他のユーザーの編集画面ページに遷移することができてしまいます! なので、この対策はしっかりおこなわなければなりません! それが"URL直打ち"対策というわけです。 今回登場するモデルとテーブルの関係 モデルとテーブルの関係を簡単にではありますが、記載しておきます。 2. 対 ログアウトユーザー まずは、ログアウトユーザーへの対策です。 これはすごくシンプルで、以下のコードを対策したいコントローラー内に設定していれば対策できています! before_action :authenticate_user!, except: [:index, :show] 実際にコントローラー内に記述したらこんな感じ。 items_controller.rb class ItemsController < ApplicationController before_action :authenticate_user!, except: [:index, :show] def index end def show end end 記述したコードを1つずつ要約すると以下の通りです。 記述 説明 before_action このコードの後ろに書かれたメソッドは、このコントローラー内の処理が動く前に実行される.つまり、一番はじめに実行される. :authenticate_user! ログイン済みユーザーの認証を行う。ログインしてなければ、ログイン画面にとばす!devise機能を実装することで使えるメソッドです。(作成するモデル名によってuserの部分が変わります。今回はモデル名にUserを使用しています) except: [:index, :show] index(一覧)とshow(詳細)は除く これを踏まえて、このコードを翻訳すると 「ログインしてなかったら弾き飛ばす。でも、indexとshowはログインしてなくても見れるよ。」 というような感じです。 大体の投稿アプリは、以下のような機能はログアウトしていてもできると思います。 他の投稿者のツイートや写真・動画投稿の閲覧 その投稿者の詳細ページ閲覧 検索 しかし、いざ投稿に対してのコメントや自分が投稿しようとしたらログイン画面に飛ばされてしまいますよね? それはこの対策がしっかりされているからです。 もちろん、URLを直打ちしてページ遷移をすることも防いでくれています! ちなみに、exceptはその後に指定されたものを「除く」という意味ですが、これと反対でonlyというものもがあります。こちらは、その後に指定したもの「のみ対象」という意味になります。 before_action :authenticate_user!, only: [:index, :show] # indexとshowのみログアウトしてたら見れないよ これで、対ログアウトユーザーへの対策はできました! 3. 対 ログインユーザー 次はログインしている自分以外のユーザーに対しての対策です。 例えば自身が投稿した内容はいつでも編集(edit, update)・削除(destroy)できますよね。 画面上ではログイン/ログアウト、自身と他ユーザーによってボタン表示の切り替えはバッチリできていても、URLの対策をしていないと直接URLに打ち込まれてあっさり編集画面に侵入...なんてことが起きてしまいます。 しっかり対策しておきましょう! 他にも色々あると思いますが、シンプルな例として以下のことが挙げられます。 自身が投稿(出品)した内容を他ユーザーに編集・削除されたくない これに対しては以下のようにコードを記述することで対策ができます。 # 商品情報のidを取得 @item = Item.find(params[:id]) # 出品者本人かどうかの分岐 if @item.user_id != current_user.id redirect_to root_path end 記述したコードを1つずつ要約すると以下の通りです。 記述 説明 @item.user_id itemsテーブルにあるuser_idを取得。itemモデルとuserモデルでアソシエーションを組んでいるため、このような記述ができます。 != 「等しくない時」という意味。 current_user.id 【current_user】devise機能を実装することで使えるメソッドです。(作成するモデル名によってuserの部分が変わります。今回はモデル名にUserを使用しています)ログインしているユーザーを取得してくれます。.idをつけることで現在ログインしているユーザーのidを取得してくれます。 これを踏まえて、このコードを翻訳すると 「投稿(出品)したユーザーと現在のユーザーのidが違えばトップページに飛ばす。」 というふうになります。 実際にコントローラー内のeditだけに適応したければこんな感じになります。 items_controller.rb # editアクションのみに対応 def edit @item = Item.find(params[:id]) if @item.user_id != current_user.id redirect_to root_path end end けれど他にも、destroyアクション、updateアクションにも適応させたいです。 しかし、それぞれのアクション内に上記のコードを記述していくと非常に非常に冗長でかつ見にくいコードになってしまいます。 というか普通に邪魔くさいです。 items_controller.rb # それぞれのアクションに記述 # かなり見にくいですよね def edit if @item.user_id != current_user.id redirect_to root_path end end def update if @item.user_id != current_user.id redirect_to root_path end end def destroy if @item.user_id != current_user.id redirect_to root_path end end なので、複数同じコードを記述する場合は コードを一箇所にまとめて、適応させたいアクションのみ指定してあげる とした方がスッキリます! ちなみに、editアクションを防いだ時点でupdateアクションは呼ばれないはずです。 しかし、検証ツールからボタンを表示させる、URLを直接入力をするといったことにより、updateアクションにたどり着いてしまう可能性があります! なので、 edit・updateアクション new・createアクション はセットで記述した方がいいです。 コードを1つにまとめるとこんな感じになります。 before_actionで呼び出しているprevent_urlは私が勝手に考えた名前ですので、ここの命名は自由で大丈夫です! items_controller.rb class ItemsController < ApplicationController before_action :set_furima, only: [:edit, :update, :destroy] before_action :prevent_url, only: [:edit, :update, :destroy] def edit end def update end def destroy end private def set_furima @item = Item.find(params[:id]) end def prevent_url if @item.user_id != current_user.id redirect_to root_path end end end これで、対ログアウトユーザーへの対策はできました! 4. 対 ログインしている自分 最後は、ログインしている自分自身にもURLを直打ちしてページ遷移させないようにする実装です。 個人的にここの実装がかなり手こずりました。 実装する内容としては、以下の2つです。 ① ログイン状態の出品者が自身の出品した商品の購入画面に遷移できないようにする ② ログイン状態の出品者が売却済みの自身が出品した商品に対して、商品購入画面に遷移できないようにする 特に②の売却済みの商品という表現に苦戦しました。 結論から言うと、以下のコードを記述して①と②の対策をしました。 purchases_controller.rb class PurchasesController < ApplicationController before_action :set_furima, only: [:index, :create] before_action :prevent_url, only: [:index, :create] def index end def create end private def set_furima @item = Item.find(params[:item_id]) end def prevent_url if @item.user_id == current_user.id || @item.purchase != nil redirect_to root_path end end end 上記のコードは論理演算子||(または)を用いて2つに分けています。 ① ログイン状態の出品者が自身の出品した商品の購入画面に遷移できないようにする @item.user_id == current_user.id これは対 ログインユーザーで説明したコードとほぼ同じです。内容としては、 「投稿(出品)したユーザーと現在のユーザーのidが同じであればトップページに飛ばす。」 となります。 ② ログイン状態の出品者が売却済みの自身が出品した商品に対して、商品購入画面に遷移できないようにする @item.purchase != nil このコードを1つずつ要約すると以下の通りです。 記述 説明 @item.purchase itemsモデルに紐づくpurchasesモデルのitem_idを取得 != 「等しくない時」という意味。 nil 「何もない、空」という意味。 つまり、②のコードを翻訳すると 「purchasesテーブルにあるitem_idがnil(空)でない時」 というふうになります。purchasesテーブルは商品の購入情報を保存するテーブルであるため、 itemsテーブルのidとpurchasesテーブルのitem_idが一致する = 商品は購入されている ということになります。 逆にいうと、itemsテーブルのidとpurchasesテーブルのitem_idが一致しなければ、その商品はまだ販売中ということになります。 また、売却済みの自身が出品した商品の編集画面にも遷移できないように実装したいので、itemsコントローラーのところにもコードを追加しました。 items_controller.rb class ItemsController < ApplicationController before_action :set_furima, only: [:edit, :update, :destroy] before_action :prevent_url, only: [:edit, :update, :destroy] def edit end def update end def destroy end private def set_furima @item = Item.find(params[:id]) end def prevent_url if @item.user_id != current_user.id || @item.purchase != nil # コードを追加 redirect_to root_path end end end これで出品者本人に対するURL直打ち対策ができました! 5. まとめ 画面上のボタンの表示切り替えはしっかりできていても、URLの直打ち対策をうっかり忘れてしまう...なんてことがないようにしないといけませんね。 今回の実装でかなり勉強になりました!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails] deviseによるユーザー管理機能の導入手順とメソッドまとめ

記事の概要 devise gemによるユーザー管理機能の導入手順をまとめました。 ユーザー管理機能を実装するためのdeviseメソッドをまとめました。 deviseとは?  ユーザー管理機能を簡単に実装するためのGemです。 1. devise導入手順まとめ ① deviseのインストール  Gemfileの最下部にdeviseを記述する。 Gemfile gem 'devise'  次に、ターミナルでbundle installを実行する。 % bundle install  ここでGemのインストールを反映するために、ローカルサーバーを再起動すること。 ② deviseの設定ファイル作成  rails g devise:installコマンドを実行し、devise Gemの設定関連に使用するファイルを自動で生成する。 % rails g devise:install ③ deviseのUserモデル作成  rails g deviseコマンドを実行し、ユーザー管理機能のモデルとマイグレーションの生成、ルーティングの設定をまとめて行う。  ※通常のモデル作成方法と異なる点に注意。 % rails g devise user ④ ユーザーテーブル作成  先ほど自動生成されたマイグレーションファイルを確認し、必要に応じてファイルに追記し、テーブル設計を反映させる。メールアドレスとパスワードのカラムはデフォルトで記述されている。 db/migrate/XXXXXXXXX_devise_create_users.rb class DeviseCreateUsers < ActiveRecord::Migration[6.0] def change create_table :users do |t| ## Database authenticatable t.string :email, null: false, default: "" t.string :encrypted_password, null: false, default: "" t.string :nickname, null: false # 左記のように必要に応じてカラム追加 # 省略 end # 省略 end end  テーブル設計を確認後、マイグレーションを実行。 % rails db:migrate  テーブル情報を変更したので、ここでローカルサーバーを再起動すること。 ⑤ deviseのビューファイル作成  rails g devise:viewsコマンドを実行し、deviseが元々用意しているログイン/サインアップ画面のビューファイルを生成する。 % rails g devise:views  サインアップ画面はapp/views/devise/registrations/new.html.erb、ログイン画面のビューはapp/views/devise/sessions/new.html.erbにそれぞれ対応。 ⑥ [補足]コントローラーについて  deviseの処理を行うコントローラーはGem内に記述されているため編集ができない。従い、必要な処理(ストロングパラメーターなど)は全てのコントローラーの継承元であるapplicationコントローラーに追記すること。 2. deviseメソッドまとめ  deviseが提供するメソッドのうち、よく使うもの。これらを用いてユーザー管理機能を実装していく。 メソッド 説明 devise_for ユーザー機能に必要なルーティングを一度に生成する devise_parameter_sanitizer ログインや新規登録などのリクエストからパラメーターを取得する user_signed_in? ユーザーがログイン状態かどうかを判定する current_user 現在ログインしているユーザーの情報を取得する authenticate_user! ユーザーがログインしていなければ、ログイン画面に遷移させる ① devise_for  ユーザー機能に必要なルーティングをまとめて生成する。rails g deviseコマンド実行時に下記のように自動記述される。 config/routes.rb Rails.application.routes.draw do devise_for :users end ② devise_parameter_sanitizer  deviseにおけるparamsのようなメソッドで、ストロングパラメーターの設定に用いる。permitメソッドを組み合わせることで、deviseに定義されているストロングパラメーターに対して、追加したいカラムも指定して含めることもできる。なお、上述の通り、applicationコントローラーに記述すること。 application_controller.rb private def configure_permitted_parameters # ※メソッド名は慣習なのでこの限りでない devise_parameter_sanitizer.permit(:deviseの処理名, keys: [:許可するキー名]) end  deviseの処理名には、sign_in, sign_up, account_updateを選択して指定する。 ③ user_signed_in?  ユーザーのログイン状態を判定する。ユーザーがログイン状態であればtrueを、ログアウト状態であればfalseを返す。用途例として、下記のようにif文と組み合わせることで「ログイン」「ログアウト」「新規登録」などのリンクボタンの切り替え表示が実装できる。 <% if user_signed_in? %> <%= link_to "ログアウト", destroy_user_session_path, method: :delete %> <% else %> <%= link_to "ログイン", new_user_session_path %> <%= link_to "新規登録", new_user_registration_path %> <% end %> ④ current_user  現在ログインしているユーザーの情報を取得できる。例えばログイン中のユーザーのidを取得したい場合、 current_user.id で取得できる。 ⑤ authenticate_user!  このメソッドが呼ばれた時点でユーザーがログインしていなければ、そのユーザーをログイン画面に遷移させる。下記のようにbefore_actionで呼び出すことで、特定のアクションを実行する前にログインしていなければ、ログイン画面に強制遷移させることができる。 xxx_controller.rb class XxxController < ApplicationController before_action :authenticate_user!, only: [:new, :create, :edit, :update, :destroy] end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RubyでTwitterBotを作って毎日天気予報をツイートする

はじめに WebAPIとRubyの勉強を目的に、TwitterBotを作りました。 WebアプリとしてHerokuにデプロイし、HerokuSchedulerを用いて、毎日7:30に東京の天気を画像付きでツイートします。 背景 女児向けアニメの『アイカツ!』という作品内で、主人公の大空あかりちゃんがお天気キャスターとして活躍するのですが、それをBotとして作った次第です。 朝に東京の天気もわかるし、推しの画像も見られるので一石二鳥です。 TwitterBotの導入 下記の記事を参考にしました。 ・導入記事1 ・導入記事2 インストールしたGem source 'https://rubygems.org' ruby '2.5.1' gem 'sinatra' gem 'twitter' //TwitterAPIと通信用 gem 'httpclient' //WebAPIと通信用 gem 'dotenv' //環境変数設定用 TwitterBotのコード ディレクトリの構成 (一部抜粋) ~twitter_bot |- images        // ツイートに添付する画像フォルダ |- otenki0.jpg |- ... |- otenki22.jpg // 23枚ほど用意 |- src // 天気情報を取得するフォルダ |- Weather.rb // API接続からデータ加工処理を行うファイル |- WeatherInfo.rb // Weather.rbの関数ファイル |- .env // TwitterAPIのコンシューマーキーやアクセストークンを格納 |- app.rb // Webアプリのメインファイル |- config.ru // Webアプリの設定ファイル |- Gemfile // 使用するgemを管理するファイル |- tweet.rb       // ツイート処理ファイル tweet.rb ツイート処理ファイルです。 画像を添付してツイートするのに加えて、srcフォルダから天気情報を取得しています。 .envに書いたキーとトークンをENV['hoge_hoge']で呼び出しています。 tweet.rb require 'date' require 'twitter' require 'dotenv/load' require "./src/Weather" require "./src/WeatherInfo" class Tweet def initialize # 投稿内容の初期化 @text = "" # クライアントの生成 @client = Twitter::REST::Client.new do |config| config.consumer_key = ENV['CONSUMER_KEY'] config.consumer_secret = ENV['CONSUMER_SECRET'] config.access_token = ENV['ACCESS_TOKEN'] config.access_token_secret = ENV['ACCESS_TOKEN_SECRET'] end end # Tweetの投稿処理呼び出し def send_tweet create_text update end # ツイート本文の生成 def create_text # 天気情報取得 weatherobj = Weather.new info = weatherobj.doProcess() # ツイート部分 @text = "みなさんおはようございます!時刻は7時30分!\n" @text += "今日のお空はどんな空~❓\n大空お天気の時間です✨\n" @text += "今日の都心部は#{info.todayTelop()}、最高気温は#{info.todayTempMax()}℃です!\n" @text += "それでは通勤・通学気をつけて、行ってらっしゃ~い!" end private # Tweet投稿処理 def update begin # 画像添付部 images = [] range = 0..22 range.each{|i| images << File.new("./images/otenki#{i}.jpg") } @client.update_with_media(@text,images.sample) rescue => e p e # エラー時はログを出力 end end end # ツイートを実行 if __FILE__ == $0 Tweet.new.send_tweet end srcフォルダ 天気予報APIから東京の天気をJSON形式で取得します。 HTTPClientライブラリを用いることで、外部へHTTPリクエストを送ることができます。 ・Weather.rb: API接続からデータ加工処理を行うクラス ・WeatherInfo.rb: Weather.rbの関数ファイル Weather.rb # API接続からデータ加工処理を行うクラス require 'httpclient' require 'resolv' require 'json' require './src/WeatherInfo' class Weather @@DESCRIPTION = "description" @@TEXT = "text" @@FORECASTS = "forecasts" @@TELOP = "telop" @@DATE = "date" @@TEMPERATURE = "temperature" @@CELSIUS = "celsius" @@MIN = "min" @@MAX = "max" @@TODAY = 0 @@TMRW = 1 # コンストラクタ def initialize() end # メイン処理メソッド def doProcess() # 拠点コード(東京) keyWord = '130010' # 天気API URL取得 url = 'https://weather.tsukumijima.net/api/forecast' return analysisWeather(connectionAPI(keyWord, url)) end # API接続部 # 戻り値:ハッシュ化されたレスポンス def connectionAPI(keyWord, url) # http接続クライアントの生成 client = HTTPClient.new # 指定した拠点のコードをリクエストに設定する。 query = { 'city' => keyWord } # APIリクエスト res = client.get(url, query) # ハッシュ化して返却 return JSON.parse(res.body) end # 天気情報をHashより解析する def analysisWeather(hash) info = WeatherInfo.new # 概要の取得 info.description=(convertNil(hash.dig(@@DESCRIPTION, @@TEXT))) # 今日の天気情報 info.todayTelop=(convertNil(hash.dig(@@FORECASTS, @@TODAY, @@TELOP))) info.today=(convertNil(hash.dig(@@FORECASTS, @@TODAY, @@DATE))) info.todayTempMin=(convertNil(hash.dig(@@FORECASTS, @@TODAY, @@TEMPERATURE, @@MIN, @@CELSIUS))) info.todayTempMax=(convertNil(hash.dig(@@FORECASTS, @@TODAY, @@TEMPERATURE, @@MAX, @@CELSIUS))) # 明日の天気情報 info.tmrwTelop=(convertNil(hash.dig(@@FORECASTS, @@TMRW, @@TELOP))) info.tmrw=(convertNil(hash.dig(@@FORECASTS, @@TMRW, @@DATE))) info.tmrwTempMin=(convertNil(hash.dig(@@FORECASTS, @@TMRW, @@TEMPERATURE, @@MIN, @@CELSIUS))) info.tmrwTempMax=(convertNil(hash.dig(@@FORECASTS, @@TMRW, @@TEMPERATURE, @@MAX, @@CELSIUS))) return info end # nil判定、nilの場合は「-」を返却 def convertNil(value) return value == nil ? "-" : value end end WeatherInfo.rb class WeatherInfo attr_accessor :description, :todayTelop, :today, :todayTempMin, :todayTempMax, :tmrwTelop, :tmrw, :tmrwTempMin, :tmrwTempMax :comment # コンストラクタ def initialize() end # 概要 def description() @description end # 概要 def description=(value) @description = value end # 本日テロップ def todayTelop() @todayTelop end # 本日テロップ def todayTelop=(value) @todayTelop = value end # 本日最低気温 def todayTempMin() @todayTempMin end # 本日最低気温 def todayTempMin=(value) @todayTempMin = value end # 本日最高気温 def todayTempMax() @todayTempMax end # 本日最高気温 def todayTempMax=(value) @todayTempMax = value end # 明日テロップ def tmrwTelop() @tmrwTelop end # 明日テロップ def tmrwTelop=(value) @tmrwTelop = value end # 明日最低気温 def tmrwTempMin() @tmrwTempMin end # 明日最低気温 def tmrwTempMin=(value) @tmrwTempMin = value end # 明日最高気温 def tmrwTempMax() @tmrwTempMax end # 明日最高気温 def tmrwTempMax=(value) @tmrwTempMax = value end # コメント def comment() @comment end # 明日最高気温 def comment=(value) @comment = value end end app.rb Webアプリのメインファイルです。 TwitterBotをHerokuで動かすためにSinatraでWebアプリにしています。 /send_tweetを実行でツイートされます。 app.rb require 'sinatra' require_relative 'tweet.rb' get '/' do 'under constraction' end get '/send_tweet' do 'Closed' end config.ru Sinatraということを認識させる記述を行っています。 config.ru require_relative 'app.rb' run Sinatra::Application imagesフォルダ 添付する画像フォルダ HerokuSchedulerで定期的に動かす Herokuにデプロイし、Schedulerアドオンを使って毎日7:30に動くようにします。 コマンドの欄は、bundle exec ruby tweet.rb 参考記事 またHerokuにデプロイ後、Heroku側でも環境変数の設定が必要とのことで設定します。 参考記事 さいごに 以上が、天気情報を毎日ツイートするTwitterBotの制作過程でした。 TwitterBotの制作記事はたくさんあるので、参考記事には困りませんでした。 WebAPIを使うのは初めてでしたが、特に問題もなく導入できたので良かったと思います。 参考 ・TwitterBot ・天気情報取得方法 ・HTTPClient ・Heroku 環境変数
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RubyでTwitterBotを作って天気予報をツイートする

はじめに WebAPIとRubyの勉強を目的に、TwitterBotを作りました。 WebアプリとしてHerokuにデプロイし、HerokuSchedulerを用いて、毎日7:30に東京の天気を画像付きでツイートします。 背景 女児向けアニメの『アイカツ!』という作品内で、主人公の大空あかりちゃんがお天気キャスターとして活躍するのですが、それをBotとして作った次第です。 朝に東京の天気もわかるし、推しの画像も見られるので一石二鳥です。 TwitterBotの導入 下記の記事を参考にしました。 ・導入記事1 ・導入記事2 インストールしたGem source 'https://rubygems.org' ruby '2.5.1' gem 'sinatra' gem 'twitter' //TwitterAPI通信用 gem 'httpclient' //WebAPI通信用 gem 'dotenv' //環境変数設定用 TwitterBotのコード ディレクトリの構成 (一部抜粋) ~twitter_bot |- images        // ツイートに添付する画像フォルダ |- otenki0.jpg |- ... |- otenki22.jpg // 23枚ほど用意 |- src // 天気情報を取得するフォルダ |- Weather.rb // API接続からデータ加工処理を行うファイル |- WeatherInfo.rb // Weather.rbの関数ファイル |- .env // TwitterAPIのコンシューマーキーやアクセストークンを格納 |- app.rb // Webアプリのメインファイル |- config.ru // Webアプリの設定ファイル |- Gemfile // 使用するgemを管理するファイル |- tweet.rb       // ツイート処理ファイル tweet.rb ツイート処理ファイルです。 画像を添付してツイートするのに加えて、srcフォルダから天気情報を取得しています。 .envに書いたキーとトークンをENV['hoge_hoge']で呼び出しています。 tweet.rb require 'date' require 'twitter' require 'dotenv/load' require "./src/Weather" require "./src/WeatherInfo" class Tweet def initialize # 投稿内容の初期化 @text = "" # クライアントの生成 @client = Twitter::REST::Client.new do |config| config.consumer_key = ENV['CONSUMER_KEY'] config.consumer_secret = ENV['CONSUMER_SECRET'] config.access_token = ENV['ACCESS_TOKEN'] config.access_token_secret = ENV['ACCESS_TOKEN_SECRET'] end end # Tweetの投稿処理呼び出し def send_tweet create_text update end # ツイート本文の生成 def create_text # 天気情報取得 weatherobj = Weather.new info = weatherobj.doProcess() # ツイート部分 @text = "みなさんおはようございます!時刻は7時30分!\n" @text += "今日のお空はどんな空~❓\n大空お天気の時間です✨\n" @text += "今日の都心部は#{info.todayTelop()}、最高気温は#{info.todayTempMax()}℃です!\n" @text += "それでは通勤・通学気をつけて、行ってらっしゃ~い!" end private # Tweet投稿処理 def update begin # 画像添付部 images = [] range = 0..22 range.each{|i| images << File.new("./images/otenki#{i}.jpg") } @client.update_with_media(@text,images.sample) rescue => e p e # エラー時はログを出力 end end end # ツイートを実行 if __FILE__ == $0 Tweet.new.send_tweet end srcフォルダ 天気予報APIから東京の天気をJSON形式で取得します。 HTTPClientライブラリを用いることで、外部へHTTPリクエストを送ることができます。 ・Weather.rb: API接続からデータ加工処理を行うクラス ・WeatherInfo.rb: Weather.rbの関数ファイル Weather.rb # API接続からデータ加工処理を行うクラス require 'httpclient' require 'resolv' require 'json' require './src/WeatherInfo' class Weather @@DESCRIPTION = "description" @@TEXT = "text" @@FORECASTS = "forecasts" @@TELOP = "telop" @@DATE = "date" @@TEMPERATURE = "temperature" @@CELSIUS = "celsius" @@MIN = "min" @@MAX = "max" @@TODAY = 0 @@TMRW = 1 # コンストラクタ def initialize() end # メイン処理メソッド def doProcess() # 拠点コード(東京) keyWord = '130010' # 天気API URL取得 url = 'https://weather.tsukumijima.net/api/forecast' return analysisWeather(connectionAPI(keyWord, url)) end # API接続部 # 戻り値:ハッシュ化されたレスポンス def connectionAPI(keyWord, url) # http接続クライアントの生成 client = HTTPClient.new # 指定した拠点のコードをリクエストに設定する。 query = { 'city' => keyWord } # APIリクエスト res = client.get(url, query) # ハッシュ化して返却 return JSON.parse(res.body) end # 天気情報をHashより解析する def analysisWeather(hash) info = WeatherInfo.new # 概要の取得 info.description=(convertNil(hash.dig(@@DESCRIPTION, @@TEXT))) # 今日の天気情報 info.todayTelop=(convertNil(hash.dig(@@FORECASTS, @@TODAY, @@TELOP))) info.today=(convertNil(hash.dig(@@FORECASTS, @@TODAY, @@DATE))) info.todayTempMin=(convertNil(hash.dig(@@FORECASTS, @@TODAY, @@TEMPERATURE, @@MIN, @@CELSIUS))) info.todayTempMax=(convertNil(hash.dig(@@FORECASTS, @@TODAY, @@TEMPERATURE, @@MAX, @@CELSIUS))) # 明日の天気情報 info.tmrwTelop=(convertNil(hash.dig(@@FORECASTS, @@TMRW, @@TELOP))) info.tmrw=(convertNil(hash.dig(@@FORECASTS, @@TMRW, @@DATE))) info.tmrwTempMin=(convertNil(hash.dig(@@FORECASTS, @@TMRW, @@TEMPERATURE, @@MIN, @@CELSIUS))) info.tmrwTempMax=(convertNil(hash.dig(@@FORECASTS, @@TMRW, @@TEMPERATURE, @@MAX, @@CELSIUS))) return info end # nil判定、nilの場合は「-」を返却 def convertNil(value) return value == nil ? "-" : value end end WeatherInfo.rb class WeatherInfo attr_accessor :description, :todayTelop, :today, :todayTempMin, :todayTempMax, :tmrwTelop, :tmrw, :tmrwTempMin, :tmrwTempMax :comment # コンストラクタ def initialize() end # 概要 def description() @description end # 概要 def description=(value) @description = value end # 本日テロップ def todayTelop() @todayTelop end # 本日テロップ def todayTelop=(value) @todayTelop = value end # 本日最低気温 def todayTempMin() @todayTempMin end # 本日最低気温 def todayTempMin=(value) @todayTempMin = value end # 本日最高気温 def todayTempMax() @todayTempMax end # 本日最高気温 def todayTempMax=(value) @todayTempMax = value end # 明日テロップ def tmrwTelop() @tmrwTelop end # 明日テロップ def tmrwTelop=(value) @tmrwTelop = value end # 明日最低気温 def tmrwTempMin() @tmrwTempMin end # 明日最低気温 def tmrwTempMin=(value) @tmrwTempMin = value end # 明日最高気温 def tmrwTempMax() @tmrwTempMax end # 明日最高気温 def tmrwTempMax=(value) @tmrwTempMax = value end # コメント def comment() @comment end # 明日最高気温 def comment=(value) @comment = value end end app.rb Webアプリのメインファイルです。 TwitterBotをHerokuで動かすためにSinatraでWebアプリにしています。 /send_tweetを実行でツイートされます。 app.rb require 'sinatra' require_relative 'tweet.rb' get '/' do 'under constraction' end get '/send_tweet' do 'Closed' end config.ru Sinatraということを認識させる記述を行っています。 config.ru require_relative 'app.rb' run Sinatra::Application imagesフォルダ 添付する画像フォルダ HerokuSchedulerで定期的に動かす Herokuにデプロイし、Schedulerアドオンを使って毎日7:30に動くようにします。 コマンドの欄は、bundle exec ruby tweet.rb 参考記事 またHerokuにデプロイ後、Heroku側でも環境変数の設定が必要とのことで設定します。 参考記事 さいごに 以上が、天気情報を毎日ツイートするTwitterBotの制作過程でした。 TwitterBotの制作記事はたくさんあるので、参考記事には困りませんでした。 WebAPIを使うのは初めてでしたが、特に問題もなく導入できたので良かったと思います。 参考 ・TwitterBot ・天気情報取得方法 ・HTTPClient ・Heroku 環境変数
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【RSpec】モデル単体テスト実行時の「MissingAttributeError」を解決する

はじめに RSpecを用いたテスト時のエラーでつまずいたので、備忘録としてまとめます。 環境 Ruby 2.6.5 Rails 6.0.3.6 状況 Twitterのような投稿アプリを作成中です。FactoryBotを用いて、投稿に関するPostモデルの単体テストを実行したところ、「MissingAttributeError」が出ました。以下に詳細を記します。 FactoryBot posts.rb FactoryBot.define do factory :post do # 省略 association :user    # 省略 end end association :userは、users.rbのFactoryBotとアソシエーションがあることを意味しています。つまり、Postのインスタンスが生成したと同時に、関連するUserのインスタンスも生成されます。 exampleを書き出し、試しにテストを実行するとエラー発生 ActiveModel::MissingAttributeError: can't write unknown attribute `user_id` 「user_idという属性は知らないよ」という内容です。 postsテーブルを確認 テーブルのカラムを確認していくと、user_idカラムがありませんでした。 マイグレーションファイルを確認 t.references :user の記述を忘れており、テーブル作成時にuser_idカラムが作られなかったことがわかりました。 user_idカラムを追加し、エラー解決 カラムの追加方法は、他の方の分かりやすい記事がありますので、ご参考ください。 私はこちらを参考にさせていただきました。 https://qiita.com/kurawo___D/items/e3694f7a870a1cc4738e まとめ postはuserに紐付いているため、FactoryBotでassociation :userと記述し、紐付くuserデータを同時生成しようとしました。 しかし、postテーブルにuser_idカラムがなかったため、紐付かせることができず、エラーが出てしましました。 最後に 初めての投稿で分かりづらい部分が多々あるかと思いますが、どなたかの参考になれば幸いです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Ruby】lengthメソッドについて

lengthメソッド とは ・文字列の長さ(空白も数えます)、配列の要素の数を調べることができるメソッド。 ※Integer型では使えません!!!!! a = "123" a.class => String a.length => 3 b = "1 2 3" b.class => String b.length => 5 c = [1, 2, 3] c.class => Array c.length => 3
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWSで自動デプロイした時、ArgumentError: Index name 'index_boards_on_user_id' on table 'boards' already existsが出た際の解決法

開発環境 AWSサーバー使用 自動デプロイツールcapistrano使用 状況 ローカル環境で、データベースを一度削除し、作り直して、'bundle exec cap production deploy'コマンドを実行した際に、以下のようなエラーが出た。 Caused by: DEBUG [7ec9cb7c] ArgumentError: Index name 'index_boards_on_user_id' on table 'boards' already exists DEBUG [7ec9cb7c] /var/www/forum-lastapp/shared/bundle/ruby/2.6.0/gems/activerecord-6.0.3.6/lib/active_record/connection_adapters/abstract/schema_statements.rb:1197:in `add_index_options' /var/www/forum-lastapp/shared/bundle/ruby/2.6.0/gems/activerecord-6.0.3.6/lib/active_record/connection_adapters/mysql/schema_creation.rb:65:in `index_in_create' /var/www/forum-lastapp/shared/bundle/ruby/2.6.0/gems/activerecord-6.0.3.6/lib/active_record/connection_adapters/abstract/schema_creation.rb:49:in `block in visit_TableDefinition' /var/www/forum-lastapp/shared/bundle/ruby/2.6.0/gems/activerecord-6.0.3.6/lib/active_record/connection_adapters/abstract/schema_creation.rb:49:in `map' /var/www/forum-lastapp/shared/bundle/ruby/2.6.0/gems/activerecord-6.0.3.6/lib/active_record/connection_adapters/abstract/schema_creation.rb:49:in `visit_TableDefinition' /var/www/forum-lastapp/shared/bundle/ruby/2.6.0/gems/activerecord-6.0.3.6/lib/active_record/connection_adapters/abstract/schema_creation.rb:14:in `accept' /var/www/forum-lastapp/shared/bundle/ruby/2.6.0/gems/activerecord-6.0.3.6/lib/active_record/connection_adapters/abstract/schema_statements.rb:315:in `create_table' /var/www/forum-lastapp/shared/bundle/ruby/2.6.0/gems/activerecord-6.0.3.6/lib/active_record/connection_adapters/mysql/schema_statements.rb:81:in `create_table' /var/www/forum-lastapp/shared/bundle/ruby/2.6.0/gems/activerecord-6.0.3.6/lib/active_record/migration.rb:890:in `block in method_missing' /var/www/forum-lastapp/shared/bundle/ruby/2.6.0/gems/activerecord-6.0.3.6/lib/active_record/migration.rb:858:in `block in say_with_time' /var/www/forum-lastapp/shared/bundle/ruby/2.6.0/gems/activerecord-6.0.3.6/lib/active_record/migration.rb:858:in `say_with_time' 以下省略 解決法 ローカルでデータベース関連の内容を修正した場合の対処が必要となる。 本番環境で以下二つのコマンドを実行する % rails db:drop RAILS_ENV=production DISABLE_DATABASE_ENVIRONMENT_CHECK=1 % rails db:create RAILS_ENV=production その後、一度プロセスをkillした上で「bundle exec cap production deploy」を実行すると、エラーは解決された。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsコードリーデイング② validatesメソッドを深掘りしてみた 【備忘録】

はじめに https://qiita.com/engineer_ikuzou/items/e78fe4d5b4977bbe86c2 こちらの続編です。 前提 名前の長さ30文字制限をかけています。 u = User.new(name: 'sample name') app/models/user.rb class User < ApplicationRecord binding.pry validates :name, length: { maximum: 30 } end validatesメソッド2行目から見ていきます。 rails/activemodel/lib/active_model/validations/validates.rb def validates(*attributes) defaults = attributes.extract_options!.dup validations = defaults.slice!(*_validates_default_keys) defaultsには前回確認した値がは格納されています。 未確認の引数_validates_default_keysが出てきたので、確認してみると、ハッシュで条件文が格納されていました。 https://railsguides.jp/active_record_validations.html 条件付きvalidationの箇所と、そうでない部分を分けるための処理のようです。 [4] pry(User)> defaults => {:length=>{:maximum=>30}} [5] pry(User)> _validates_default_keys => [:if, :unless, :on, :allow_blank, :allow_nil, :strict] validations: dafaultsでsliceの対象外のhashを格納 dafaults: sliceの対象となったifなどがあれば格納 上記のように格納される処理でした。 学び① sliceメソッド 引数で指定されたキーとその値だけを含む Hash を返します。 A=[ a:100, b:300, c:500 ]という配列に対して、A.slice(:a)は{:a=>100}を返します。 https://docs.ruby-lang.org/ja/latest/method/Hash/i/slice.html また、全く意識していなかったのですが破壊的メソッド自体も定義されており、その中でsliceを使用しているのですね。 少し考えたらわかることだと思うのですが、意識できていませんでした。 また破壊的メソッドはそれ自体を更新する一方で、返り値としては更新されなかった部分を返してくれるのですね。 From: rails/activesupport/lib/active_support/core_ext/hash/slice.rb:11 Hash#slice!: 10: def slice!(*keys) => 11: omit = slice(*self.keys - keys) 12: hash = slice(*keys) 13: hash.default = default 14: hash.default_proc = default_proc if default_proc 15: replace(hash) 16: omit 17: end 10: omitに、引数keysを除くhashがあれば避けておき、返り値とする。 15: hashはこちらで更新。 validatesメソッド3,4行目 rails/activemodel/lib/active_model/validations/validates.rb raise ArgumentError, "You need to supply at least one attribute" if attributes.empty? raise ArgumentError, "You need to supply at least one validation" if validations.empty? [:if, :unless, :on, :allow_blank, :allow_nil, :strict]に該当するもののみvalidatesの引数にある場合は、エラーを起こします。 条件付きvalidatesの場合、条件true時のvalidationを記載する必要があるため、記載があるかどうかチェックしています。 validatesメソッド 5,6行目 rails/activemodel/lib/active_model/validations/validates.rb validations.each do |key, options| key = "#{key.to_s.camelize}Validator" validations => {:length=>{:maximum=>30}} なのでkeyは:length, optionsは{:maximum=>30}です。 キャメルケースへの変換が行われ、keyは、LengthValidatorとなります。 学び② camelizeメソッド デフォルトでアッパーキャメルケースに変換します。 https://www.rubydoc.info/gems/activesupport/String:camelize validatesメソッド 7~11行目 rails/activemodel/lib/active_model/validations/validates.rb begin validator = key.include?("::") ? key.constantize : const_get(key) rescue NameError raise ArgumentError, "Unknown validator: '#{key}'" end validatorが既にモジュールの形で表記されているかで条件式が分かれています。 今回は、モジュール形式ではないので、const_getメソッドでActiveRecord::Validations::LengthValidatorが返ります。 学び③ constantizeメソッド 引数の文字列で指定した名前で定数を探す。 https://railsdoc.com/page/constantize 学び④ const_getメソッド name で指定される名前の定数の値を取り出します。 https://docs.ruby-lang.org/ja/latest/method/Module/i/const_get.html  終わりに 今回はここまでです。 メソッドもそれぞれどのような挙動をしているのか確認することもできるのですが、そこの深掘りはしませんでした。あくまでvalidatesメソッドの深掘りです。全てを読み込むと永遠に終わりのない勉強なので、どこをどこまで深掘りするか、学習の目的をしっかり意識することが大事だと思いました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails] Rspecでテストコードの実装②

 はじめに テストコードの実装①をみていない方はこちらからご覧ください。 基本的なテストコードの書き方 まずは、どのような形でテストコードを書いていくのか確認です。 Userモデルにはnameカラムとemailカラムがあるとします。 spec/models/user_spec.rb require 'rails_helper' RSpec.describe User, type: :model do describe 'ユーザー新規登録' do context '登録ができるとき' do    it '全ての項目が正しく入力されている時' do  #その処理を記述    end   end context '登録ができないとき' do    it 'nameが空では登録できない' do # nameが空では登録できないテストコードを記述します  end  it 'emailが空では登録できない' do # emailが空では登録できないテストコードを記述します  end    end end end ひとつづつ確認しましょう。 describe describeとは、テストコードのグループ分けを行うメソッドです。 「どの機能に対してのテストを行うか」をdescribeでグループ分けします。 describeにつづくdo~endの間に、さらにdescribeメソッドを記述することで、入れ子構造をとることもできます。 context 特定の条件を分けたい場合に用います。contextはどのような「状況」を確認したいのかを書きます。 it describeメソッド同様に、グループ分けを行うメソッドです。 itの場合はより詳細に、「describeメソッドに記述した機能において、どのような状況のテストを行うか」を明記します。contextよりもさらに詳細なものを記述します。 example exampleとは、itで分けたグループのことを指します。 以上のような形で、どの機能のどんな状況のときにテストコードが実行されるのか、わかりやすい形でグループ分けしてやります。 テストコードの具体的な実装(正常系) それではグループ分けしたのちに、処理を実際に記述していきます。 spec/models/user_spec.rb require 'rails_helper' RSpec.describe User, type: :model do describe 'ユーザー新規登録' do before do @user=User.new(name:"tochi",email:"abc@abc") end   context '登録ができるとき' do    it '全ての項目が正しく入力されている時' do  expect(@user).to be_valid    end   end #中略 end end こちらも1つづ解説します。 before do~end これはコントローラーでいうbefore_actionと同様で、予め処理を走らす前に行いたい処理を記述します。今回は@userというインスタンスを作成しておきます。 expect 検証で得られた挙動が想定通りなのかを確認する構文のことです。 expect().to matcher()という基本的な形をもとにに、テストの内容に応じて引数やmatcherを随時変えて記述します。のちに出てくるinclude等がこれにあたります。 matcher(参考) matcherは、「expectの引数」と「想定した挙動」が一致しているかどうかを判断します。 be_valid インスタンスが正しく保存されるかどうか判断します 以上を踏まえると、以下の一文は、@userがちゃんとDBに保存されたかどうかを確認します。 expect(@user).to be_valid テストコードの具体的な実装(異常系) だんだん慣れてきたところで、異常系についてもさらっと触れておきましょう。 spec/models/user_spec.rb require 'rails_helper' RSpec.describe User, type: :model do describe 'ユーザー新規登録' do #省略 context '登録ができないとき' do    it 'nameが空では登録できない' do @user.name="" @uer.valid? expect(@user.errors.full_messages).to include("名前を入力してください")  end  it 'emailが空では登録できない' do # 自分で考えてみよう!  end    end end end 新しく出てきたものがあるので解説します。 valid? valid?は、バリデーションを実行させて、エラーがあるかどうかを判断するメソッドです。 エラーがない場合はtrueを、ある場合はfalseを返します。 これを実施しないと、errorsが発生しないため、エラー文を引き出すことができません。 errors errorsは、インスタンスにエラーを示す情報がある場合、その内容を返すメソッド full_messages full_messagesは、エラーの内容から、エラーメッセージを配列として取り出すメソッド。だいたい、errorsとセットで使います。 include includeは、「expectの引数」に「includeの引数」が含まれていることを確認するマッチャです。 以上より、以下の処理は、nameカラムが空だったとき、エラーがあるかどうか確認して、発生したエラー文に「名前を入力してください」と出るかどうかを確認しているということになります。 @user.name="" @uer.valid? expect(@user.errors.full_messages).to include("名前を入力してください") ちなみに、エラー文は日本語になっていないことがあるかと思いますので、どんなエラー文が出るかはbinding.pryで止めて確認してください。 終わりに お疲れ様でした。今回、簡単にモデルの単体テストについてまとめてみました。いかがでしたか。 やはり、実際に手を動かしてやってみることが一番いいと思いますので、是非やってみてください
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

railsで通知機能とその未読管理機能を開発

やりたいこと rails 通知機能 通知の種類は色々増える可能性あり 通知の未読管理 通知一覧を開いた時点で開封済みとする。もし未読のものがあればラベルをつける 仕様 全体の流れ notificationテーブルを作成し、そこで通知情報と未読管理を行う indexを開いたらopen_timeがnilのものを取得し、現在時刻を入れる headerのアイコンを未読(open_timeがnil)ありだったらバッジをつけ、なしだったらバッジをつけない 通知のscheme scheme.rb create_table "notifications", force: :cascade do |t| t.text "url" t.integer "to_user_id" t.integer "from_user_id" t.integer "notification_type" t.datetime "open_time" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false end open_timeに開いた時刻を入れてここがnilなら未読とする 通知内容は最初contentカラムを設けてそのまま文言を入れるか迷ったが時間が経ってユーザー情報などが変わった際に変更が効かないのと中の情報に何が入っているのか将来分からなくなるのが嫌だったので変数を取得するように urlで飛び先を指定する。もし特殊なメソッドを使いたい場合はroutesでメソッドを指定する 一覧を開いたらopen_timeに時間を入れる app/controllers/notifications_controller.rb unread_notifications = @notifications.where(open_time: nil) unread_notifications.each do |unread_notification| unread_notification.open_time = Date.today.to_time unread_notification.save end 上記繰り返し処理で現在時刻を入れていく headerにて未読がある場合とアイコンを出しわけ app/controllers/application_controller.rb @notification_count = Notification.where(to_user_id: current_user.id,open_time: nil).count 上記で未読数を取得 app/views/layouts/_header.html.erb <div class="mr-4"> <% if @notification_count > 0 %> <%= image_tag('notification_icon_unread.png',class: "header_icon align-middle") %> <% else %> <%= image_tag('notification_icon.png',class: "header_icon align-middle") %> <% end %> </div> 0より上だったらバッジなし
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CapybaraによるSystem specで使用したコマンド振り返り

この記事について 私が作成したテストを元に、使用したCapybaraのメソッドを解説します。 Capybaraの機能一覧を知りたい場合は下記を参考にすると良いと思います Capybaraチートシート rubydoc Module: Capybara::DSL 環境 Ruby 2.6.5 Rails 6.0.3.5 devise Capybara Capybaraとは? 統合テストを行う際に用いられるWeb上の操作を簡単に実行できるDSLを提供してくれるGemです。 実装内容 context 'コメントを削除できるとき' do it 'ログインしておりコメントを投稿したユーザーだと削除できる' do visit new_user_session_path #1 fill_in 'user_email', with: comment_user.email #2 fill_in 'user_password', with: comment_user.password find('input[name="commit"]').click #3 expect(current_path).to eq(root_path) click_link 'テストタイトル', href: article_path(article) #4 expect(current_path).to eq(article_path(article)) expect(page).to have_link '削除', href: article_comment_path(article, comment) page.accept_confirm('本当に削除しますか?') do #5 click_link '削除', href: article_comment_path(article, comment) end expect(current_path).to eq(article_path(comment.article)) expect(page).to have_content('コメントを削除しました') end end #1 visit visit以降に設定したパスへ移動し、この際のリクエストは必ずGETになります。 今回はgemのdeviseを導入して作成されたログインページへアクセスしています。 #2 fill_in これはtext_fieldやtext_areaなどの入力を必要とする要素に指定した値を挿入するメソッドです 今回の例では'user_email'という属性値を持った要素に、事前に準備したユーザのEメールアドレスを指定しています。 with:はStringであれば良いので以下の様に直接打ち込んでも問題ありません。 fill_in 'user_email', with: 'test@test.mail' #3 find, click まずfindですがこれは指定した要素を見つけるメソッドです そしてclickは文字通りクリックを行うメソッドです findで要素を探して、clickでクリックするという動作になります #4 click_link これは指定した要素を持つリンクをクリックするメソッドです。 今回はテストタイトルという要素を持ったリンク要素をクリックしています。 linkと指定しているだけあってリンクでなければクリックできません。 リンクかボタンか判断に困ったらclick_onを使うのも手です。 これはどちらの要素であってもクリックを行なってくれます。 #5 accept_confirm 大トリです。これはHTML5のHTMLに独自にデータを持たせるデータ属性の一つであるdata-confirm属性の判定に用います。 まずdata-confirm属性ですが、これはaタグやformタグのsubmitボタンの属性に追加することでダイアログを表示してくれるものです accept_confirmですがこれはダイアログを認可つまりOKという判定を行なってくれるメソッドです。 又引数にダイアログに表示される文字を入力する事で、data-confirmで指定した文言と、テストで判定したい文言を照らし合わせて判定してくれます。 記述は以下の様にaccept_confirmに引数を渡し、又ブロック変数でdata-confirmを発生させる要素を指定します。 page.accept_confirm('本当に削除しますか?') do #5 click_link '削除', href: article_comment_path(article, comment) # <-data-confirmを持った要素 end まとめ 便利なGemですがその分覚えることも増えるため、自分のアプリ作成での計画性に合わせて学習していきたいところです。 テストも時間をかけ過ぎて他が疎かになったら、本末転倒ですからね。勿論テストは重要な部分ではありますが。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

railsでorするとき、atter = nil時に不要な sqlを節約する (手元では400倍早くなった)

この記事は備忘録です DB(例) (id, first_name, last_name, email) 1 しのぶ 胡蝶 aaa@aa.com 2 杏寿郎 煉獄 bbb@bb.com 3 義勇 冨岡 ccc@cc.com 4 小芭内 伊黒 ddd@dd.com 5 天元 宇髄 eee@ee.com 6 蜜璃 甘露寺 fff@ff.com モデル(例) # User.rb # id :int # first_name :string # last_name :string # email :string first_name_attr = "しのぶ" last_name_attr = nil email_attr = "fff@ff.com" User.where(first_name: first_name_attr) .or( User.where(last_name: last_name_attr) ) .or( User.where(email: email_attr) ) この場合、last_nameのところ、いらないっすよね〜でもこれって、sql的に SELECT COUNT(*) FROM `users` WHERE (`users`.`first_name` = 'しのぶ' OR `users`.`last_name` IS NULL ... ) となるんですけど、orなのに IS NULL って処理いらないなって。(whereでandとかになるならもちろんいる) そして、この処理が結構重いので、railsの仕組みを使って軽くしよう、というのが今回の目的です。 (※ まあこれは実践的ではないかもしれないのでメンバーと要相談ということでお願いいたします。。) 結論 nilだと IS NULL の処理が走ってしまうので[]にする。 last_name = (last_name == nil ? [] : last_name) nilの場合は[]に変換することで SELECT COUNT(*) FROM `users` WHERE (`users`.`first_name` = 'しのぶ' OR 1=0 ... ) なって処理を飛ばすことができ、大幅に早くなります。 もし開発する上で、表示するとき遅いし、一旦仮で早くしたいとかいう事情がある場合には結構有効かなと思います。 ただし、上記にもあるように、 「実践的ではないかも。。」「ちょっと気持ち悪いな〜」 みたいな意見があり、この実装は見送られておりますのでご注意を。 おそらく、シンプルにifで分岐する方がいいのかなという意見です。 ただし、sqlは一つに纏まるような記述が必要そうです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ruby on Rails ヘルスケア ITの普及に向けて サービス開発備忘録 RSpec テスト~model spec

ずっと、サボっていましたが、技術力向上のためには、メモがてら、書き記すことが必要と思い、再開です。 言語化してアウトプットすることで、メタ的に認知し、結果、理解度が上がるんだろうか? model specである。 こちらを参考にさせていただいた。 (https://qiita.com/Jwataru/items/2cac90cc9bca4f76bb11) 13)が通らない patient.email = "Foo@ExAMPle.CoM"としているが、 下記のように出る。 1) Patient emailを小文字に変換後の値と大文字を混ぜて登録されたアドレスは 13) 同じ Failure/Error: expect(patient.reload.email).to eq "foo@example.com" expected: "foo@example.com" got: "test11@example.com" (compared using ==) # ./spec/models/patient_spec.rb:120:in `block (3 levels) in <top (required)>' FactoryBotのemailとなっている。 上書きされていないのか? 15)が通らない 2) Patient passwordとpassward_confirmationの一致 passwordとpassward_confirmationは 15) 不一致 Failure/Error: expect.(patient.errors[:password_confirmation]).to include("の入力が一致しません") ArgumentError: You must pass either an argument or a block to `expect`. # ./spec/models/patient_spec.rb:136:in `block (4 levels) in <top (required)>'``` patient_spec.rb require 'rails_helper' RSpec.describe Patient, type: :model do # FactoryBot patient let(:patient) { create(:patient) } describe '存在性' do context 'patientは' do it '1) 有効である' do expect(patient).to be_valid end end context 'nameが空白は' do it '2) 無効' do patient.patient_name = " " expect(patient).to be_invalid end end context "emailが空白は" do it '3) 無効' do patient.email = " " expect(patient).to be_invalid end end context "password空白は" do it '4) 無効' do patient.password = patient.password_confirmation = " " * 6 expect(patient).to be_invalid end end end describe '長さ' do context "name17文字以上は" do it '5) 無効' do patient.patient_name = "a" * 17 expect(patient).to be_invalid end end context "emailが256字以上" do it '6) 無効' do patient.email = "a" * 244 + "@example.com" expect(patient).to be_invalid end end context "emailが255字以下は" do it '7) 無効' do patient.email = "a" * 243 + "@example.com" expect(patient).to be_valid end end context "password5字以下は" do it '8) 無効' do patient.password = patient.password_confirmation = "a" * 5 expect(patient).to be_invalid end end context "password 6字以上は" do it '9) 有効' do patient.password = patient.password_confirmation = "a" * 6 expect(patient).to be_valid end end end # describe 'フォーマット' do # # context "emailは規定の表記でなければ" do # it '無効' do # invalid_addresses = %wpatient@example,com foo@bar..com user_at_foo.orgpatient.name@example. foo@bar_baz.com foo@bar+baz.com] # invalid_addresses.each do |invalid_address| # patient.email = invalid_address # expect(patient).to be_invalid,"#{invalid_address.inspect}が無効ではありません" # end # end # end # # context "emailは規定の表記であれば" do # it '有効' do # valid_addresses = %wpatient@example.com USER@foo.COM A_US-ER@foo.bar.org first.last@foo.jp alice+bob@baz.cn] # valid_addresses.each do |valid_address| # patient.email = valid_address # expect(patient).to be_valid, "#{valid_address.inspect} が有効ではありません" # end # end # end # # end describe '一意性' do context "emailの重複は" do it '12) 無効' do duplicate_user = patient.dup duplicate_user.email = patient.email.upcase patient.save! expect(duplicate_user).to be_invalid end end end context "emailを小文字に変換後の値と大文字を混ぜて登録されたアドレスは" do it '13) 同じ' do patient.email = "Foo@ExAMPle.CoM" patient.save! #全て小文字のemailと等しいかのテスト expect(patient.reload.email).to eq "foo@example.com" end end describe 'passwordとpassward_confirmationの一致' do context 'passwordとpassward_confirmationは' do it "14) 一致" do expect(patient).to be_valid end it "15) 不一致" do patient.password ="password" patient.password_confirmation = "different" patient.valid? expect.(patient.errors[:password_confirmation]).to include("の入力が一致しません") end end end end factory/patients.rb FactoryBot.define do factory :patient do #factory :testuser, class: User do のようにクラスを明示すればモデル名以外のデータも作れます。 patient_name { "山田花子" } sequence(:email) { |n| "TEST#{n}@example.com" } password { "abcder" } password_confirmation { "abcder" } birth_date { "1967-01-01" } sex { "woman" } end end model/patients.rb class Patient < ApplicationRecord # 以下コメントアウト 登録時は患者がclinicに所属していないから(import時にclinic_idが付与される) # belongs_to :clinic has_many :clinics, through: :clinic_patients has_many :clinic_patients has_many :tests #viewに男女を表示 enum sex: { man: 1, woman: 2 } default_scope { where(leave_at: nil) } #devise設定 devise :database_authenticatable, :registerable, :validatable, :recoverable, :rememberable, :confirmable, :lockable, :timeoutable, :trackable # validation validates :patient_name, { presence: true, length: { maximum: 16 } } # format: { # with: /\A[A-Za-z0-9]+\z/, # message: 'は小文字英数字で入力してください' # } VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i validates :email, { presence: true, confirmation: true, length: { maximum: 255 }, format: { with: VALID_EMAIL_REGEX } } validates :sex, { presence: true } validates :birth_date, { presence: true } end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Ruby on Rails】お気に入り済みを一覧表示する

目的 お気に入り済みの商品を一覧表示する機能を実装する 前提条件 既にお気に入り機能を実装済みであること itemsテーブル(商品情報)、usersテーブル(ユーザー情報)、bookmarksテーブル(お気に入り情報)が存在すること。 各テーブルとアソシエーションが組まれていること 手順 1.ルーティングのネスト 2.Usersコントローラーを定義 3.お気に入り済みをビューファイルに表示 1.ルーティングのネスト ユーザーがお気に入りをした商品を表示するために、usersコントローラーにbookmarksコントローラーをネスト(入れ子構造)します。 routes.rb resources :users, only: [:show] do get :bookmarks, on: :collection end 2.Usersコントローラーを定義 Bookmarkモデルからwhereメソッドとpluckメソッドを使い現在ログイン中のuser_idとitem_idを引き出してbookmarksに代入します。 Itemモデルの引数に先ほど定義したbookmarksを指定して、インスタンス変数@bookmark_listに代入します。 users_controller.rb def show @user = User.find(params[:id]) @items = @user.items bookmarks = Bookmark.where(user_id: current_user.id).pluck(:item_id) @bookmark_list = Item.find(bookmarks) pluckの補足情報 https://railsdoc.com/page/model_pluck 3.お気に入り済みをビューファイルに表示 ユーザーのマイページにusersコントローラーで定義した@bookmark_listを使用して、お気に入りした商品を表示していきます。 if文とpresent?メソッドを使い、@bookmark_listにデータが保存されている場合に実行する様にします。 今回は商品の名前と画像を表示するように記述しました。 users/show.html.erb <% if @bookmark_list.present? %> <% @bookmark_list.each do |item| %> <%= item.name %> <%= image_tag item.image%> <% end %> <% end %> さいごに お気に入り一覧表示機能を実装出来たので投稿しました。 最後まで読んで頂きありがとうございます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails] Deviseのユーザー登録後失敗後のルーティングが/usersになる件の対処

概要 現在運営しているカフェサークルのサイトをrailsで作成しているのですがdeviseの新規登録が失敗したときのルーティングがusers/sign_in > /users になってしまい、リロードした際にページが見つからないエラーが出ている状態でした。 解決策 deviseのscopeで/usersを新規登録のルーティングに設定してあげればよい。 具体的には以下のようにroutes.rbに設定を追加する。 routes.rb devise_scope :user do # TIPS: ユーザー登録しっぱいのリダイレクトのエラーを防ぐ https://github.com/heartcombo/devise/blob/master/app/controllers/devise/registrations_controller.rb get '/users', to: 'devise/registrations#new' end これで、deviseが/usersを新規登録のルーティングと認識してエラーが起こらなくなる。 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

railsでYou must have ImageMagick or GraphicsMagick installedを解決する

railsでruby:X.X.X-alpineってdocker image使っててYou must have ImageMagick or GraphicsMagick installedってエラーに遭遇した時は、次のコマンドで解決 $ docker exec {container name} apk add imagemagick apkってのがalpine linuxのpackage managerなんだけど知らなくて時間食った。yumが使えなくてずっと泣いてた
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】DBにデータを取りに行ったときに、ActiveRecord::RecordNotFound と怒られた時の対処法

症状 Rails6.0.3でcontrollerからDBに対して、findでデータを取りに行ったとき、下記エラーメッセージが出てしまいました。 humans_controller#show def show classroom_id = Classroom.find_by(id: params[:classroomId]) human = Human.find_by(id: params[:id],classroom_id: classroom_id) render json: { human: human   }, status: :ok end findでエラー app/controllers/api/v1/foods_controller.rb:26:in `show' Started GET "/api/v1/classroom/1/human/2" for ::1 at 2021-05-02 00:16:30 +0900 (0.1ms) SELECT sqlite_version(*) Processing by Api::V1::HumansController#show as HTML Parameters: {"classroom_id"=>"1", "id"=>"2"} Restaurant Load (0.3ms) SELECT "classrooms".* FROM "classrooms" WHERE "classroom"."id" = ? LIMIT ? [["id", nil], ["LIMIT", 1]] ↳ app/controllers/api/v1/humans_controller.rb:26:in `show' Completed 404 Not Found in 174ms (ActiveRecord: 1.9ms | Allocations: 2232) ActiveRecord::RecordNotFound (Couldn't find Classroom with 'id'={:id=>nil}): 下記記事で同様のエラーメッセージを発見。 【Rails】find・find_by・whereについてまとめてみた そこには主キーが見つからない場合、上記のActiveRecord::RecordNotFoundと怒られる模様。 解決策 findをfind_byに変更したら、解決しました。 findだと、主キーに対してのみ条件づけするので、find(id)と書けます。 が、find(id: id)とは書けないため、idにnilが入ってしまい、結果として、主キーに該当するデータがないと怒られていたようです。 find #できる id = 1 human = human.find(id) #できない human = human.find(id: 1) find_byだと、主キー以外にも条件づけできるので、カラム名を指定する必要があります。 なので、下記のように条件づけるカラム名も一緒にしてできます。 find_by #できる human = human.find(id: 1) #できる human = human.find_by(name: "太郎") 参考 【Rails】find・find_by・whereについてまとめてみた https://qiita.com/nakayuu07/items/3d5e2f8784b6f18186f2
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む