- 投稿日:2021-05-03T22:40:31+09:00
Action_textのテキスト内で画像保存する方法
初めての投稿です。 railsのAction_textを使ってAWSのS3に画像を保存する方法を探していて、自分で忘れないようにここに残します。 環境 rails 6.0.0 開発しているアプリは単なるメモアプリですが、以下のように編集されたメモがJavaScriptで自動保存されるようにしています。 実装方法 まず、AWSでバケットを作成していることを前提としています。 保存したいバケットを選択→アクセス許可と進んでいくと下の方にCross-Origin Resource Sharing (CORS)というところがあります。 編集するをクリックして以下を記述します。 [ { "AllowedHeaders": [ "*" ], "AllowedMethods": [ "PUT", "POST", "DELETE" ], "AllowedOrigins": [ "*" ], "ExposeHeaders": [] } ] ...終わりです。笑 その後、localで確認します。 【更新:解決?】【Rails】ActionTextでアップロードした画像の保存先がローカルから変更できない? この方の追記を参考に画像をドラッグ&ドロップした後にAction_textの通常の画像添付機能を使ったら、その後はどちらの方法でも保存できるようになりました。 謎です... ですが、僕はできましたので是非やってみてください。笑
- 投稿日:2021-05-03T21:10:34+09:00
【rails】seed.rbを作成する
seed.rbを書いてみる。 category = Category.create!( category_name: "food" ) sale_status = SaleStatus.create!( sale_status_name: "sale" ) product_status = ProductStatus.create!( product_status_name: "on-sale" ) product = Product.create!( product_name: "sample_product", price: 1000, description: "サンプル製品です。", delete_flag: false, category_id: category.id, sale_status_id: sale_status.id, product_status_id: product_status.id ) Purchase.create!( purchase_price: 1000, purchase_quantity: 1, purchase_company: "架空社", product_id: product.id ) 雑ですけど、自分のメモように。 seedは、書く順番が大切。 親から子へ、上から下に書く。
- 投稿日:2021-05-03T20:38:25+09:00
RailsでGemを使用せずにハッシュタグ検索機能を実装
はじめに 2020年3月より某プログラミングスクールにてRailsを学び同8月よりRailsエンジニアとして勤務しております。 2020年の6月に学習の成果というところで、下記の記事を執筆させて頂いたところ、 想像以上の閲覧数、LGTMを頂きました。本当にありがとうございます。 しかしながら読み返してみると非常にわかりづらい、コードもぐちゃぐちゃ、今の自分でもよくわからないと感じまして、 今回機能を作り直して改めて作成の方法を皆さんと考えていければ良いなと思い、 ハッシュタグ機能の実装方法の記事を書きます。 ※上記記事と実装方法がかなり異なります。 この記事での目的 ・Gemを使わずにハッシュタグ機能を実装する。 ・Arrayのメソッドの使用に慣れる。 ・Hashの使用に慣れる。 この記事ではやらないこと ・modelでのhas_many、belongs_toの使用。 ・migration_fileでの明示的(referenceの使用など)な外部キーの設定。 ・Arrayメソッドの詳しい解説(こちらは調べてください。。) ・イケてるデザインの実装。 前提としてお伝えしておきたいこと 休みに概ね1日で実装をしたので高度な設計などはしてません。 一応こういう考え方で実装できますよ。というところをお伝えできればと思っております。 それではやっていきましょう。 実行環境 ・Ruby version 2.5.7 ・Rails version 5.2.4.4 ・DB sqlite3 完成図 投稿の際に#をつけて投稿することで、 #から始まる文字列がハッシュタグとして保存される。 また、投稿本文の中に存在している#から始まる文字列は表示されない。 ハッシュタグはリンクとなっており、クリックをすることでそのハッシュタグが埋め込まれた投稿一覧を取得できる。 ※みなさんが想像するハッシュタグ機能ですのでご安心を。 posts/new posts/index hashtags/show hashtags/index 作成するファイル一覧 Model ・post.rb #投稿保存用のモデル ・post_hashtag.rb #投稿とハッシュタグの中間テーブル用のモデル ・hashtag.rb #ハッシュタグ保存用のモデル 上記作成用のマイグレーションファイル Controller ・posts_controller.rb #投稿全般のアクションを行います。 ・hashtags_controller.rb #ハッシュタグに紐づいた投稿を表示、ハッシュタグの一覧を表示など。 外部メソッドファイル ・controllers/concerns/hashtag_methods.rb #自作メソッドが多いため切り分けて作成しました。 Views ・posts/index #投稿一覧 ・posts/new #投稿ページ ・posts/edit #投稿編集ページ ・posts/show #投稿詳細ページ ・hashtags/index #ハッシュタグ一覧ページ ・hasgtags/show #ハッシュタグに紐づいた投稿一覧ページ Routes routes.rbにて下記を記載 config/routes.rb resources :posts #postsコントローラー作成後記入 resources :hashtags, only: [:index, :show] #hashtagsコントローラー作成後記入 ①Modelの作成 post.rb(投稿テーブル) app/models/post.rb class Post < ApplicationRecord validates :title, presence: true validates :caption, presence: true attachment :image #gem refileを使用しているため記入。ハッシュタグ実装には関係ない。 end マイグレーションファイル(posts) db/migrate/create_posts.rb class CreatePosts < ActiveRecord::Migration[5.2] def change create_table :posts do |t| t.integer :user_id t.string :title #投稿のタイトルを入力 t.string :caption #投稿の内容を入力。ここにハッシュタグが入力される前提。 t.string :image_id #画像投稿用 t.timestamps end end end 投稿に一応user_idと画像を持たせたいのでこのような構造になっております。 特になくても大丈夫です。 後述しますが、必要になるのはcaptionカラムのみです。 hashtag.rb(ハッシュタグ保存テーブル) app/models/hashtag.rb class Hashtag < ApplicationRecord #特別な設定はしておりませんが念の為バリデーションはかけても良いと思います。 end マイグレーションファイル(hashtag) db/migrate/create_hashtags.rb class CreateHashtags < ActiveRecord::Migration[5.2] def change create_table :hashtags do |t| t.string :name #ハッシュタグが保存されるカラム t.timestamps end end end post_hashtag.rb(中間テーブル) app/models/post_hashtag.rb class PostHashtag < ApplicationRecord validates :post_id, presence: true validates :hashtag_id, presence: true end マイグレーションファイル(post_hashtag) db/migrate/create_post_hashtag.rb class CreatePostHashtags < ActiveRecord::Migration[5.2] def change create_table :post_hashtags do |t| t.integer :post_id t.integer :hashtag_id t.timestamps end end end 冒頭でも書きましたが、has_manyなどは一切使いません。 また、マイグレーションファイルで外部キーなどの細かい設定も一切せずに実装します。 中間テーブルにはpost_idとhashtag_idが保存されます。 多対多のリレーションになるので、中間テーブルは必須です。 一つの投稿に複数のハッシュタグが保存されますし、一つのハッシュタグには複数の投稿が紐づかなければなりません。 例えばPostモデルからHashtagを取得する場合 1、Postのidを判別する。 2、中間テーブルから、1のidが入っているレコードを検索、取得する。 3、2で取得したレコードに含まれるhashtag_idを取得する。 4、3で取得したhashtag_idからHashtagテーブルに対し検索をかけ、レコードを取得する。 このような流れになります。 逆にHashtagモデルからPostを取得する場合 1、Hashtagのidを判別する。 2、中間テーブルから、1のidが入っているレコードを検索、取得する。 3、2で取得したレコードに含まれるpost_idを取得する。 4、3で取得したpost_idからPostテーブルに対し検索をかけ、レコードを取得する。 ざっくりとした中間テーブルの使用方法です。 これより下記にて作成するメソッドでは上記のような流れを意識して作成をしております。 ②コントローラーの作成 posts_controller.rb(投稿) app/controllers/posts_controller.rb class PostsController < ApplicationController include HashtagMethods before_action :authenticate_user! def index @posts = Post.all @hashtags = Hashtag.all @post_hashtags = PostHashtag.all @post_objects = creating_structures(posts: @posts,post_hashtags: @post_hashtags,hashtags: @hashtags) end def new @newpost = Post.new end def show @post = Post.find(params[:id]) related_records = PostHashtag.where(post_id: @post.id).pluck(:hashtag_id) #=> [1,2,3] idのみを配列にして返す hashtags = Hashtag.all @hashtags = hashtags.select{|hashtag| related_records.include?(hashtag.id)} #hashtagテーブルより中間テーブルで取得したidのハッシュタグを取得。配列に。 @display_caption = @post.caption.gsub(/[##][\w\p{Han}ぁ-ヶヲ-゚ー]+/,"") #実際に表示するキャプション。ハッシュタグが文字列のまま表示されてしまうので、#から始まる文字列を""に変換したものをViewにて表示 end def edit @post = Post.find(params[:id]) end def create @newpost = Post.new(post_params) @newpost.user_id = current_user.id hashtag = extract_hashtag(@newpost.caption) @newpost.save! save_hashtag(hashtag,@newpost) redirect_to posts_path end def update @post = Post.find(params[:id]) strong_paramater = post_params post_params["image"] = @post.image_id if strong_paramater["image"].to_s.length <= 2 #ハッシュタグの実装には関係ないです。画像情報が空で渡ってきた場合は前に保存してある画像をセットするというものです。 delete_records_related_to_hashtag(params[:id]) #こちらのメソッドで中間テーブルとハッシュタグのレコードを削除 @post.update(post_params) hashtag = extract_hashtag(@post.caption) #投稿よりハッシュタグを取得 save_hashtag(hashtag,@post) #ハッシュタグの保存 redirect_to posts_path end def destroy post = Post.find_by(id: params[:id]) #削除対象のレコード post.destroy #投稿を削除 delete_records_related_to_hashtag(params[:id]) #中間テーブルとハッシュタグのレコードを削除 redirect_to posts_path end private def post_params params.require(:post).permit(:title, :caption,:image) end end 今回は保存と表示用に多くのメソッドを自作しているので外部ファイルにまとめてから、コントローラーで呼び出しました。 ファイルの最上部で読んでいる外部ファイルはこちらです。 app/controllers/concerns/hashtag_methods.rb module HashtagMethods extend ActiveSupport::Concern #--------------ハッシュタグ抽出処理 create update アクションの中で実行 ---------------- def extract_hashtag(caption) if caption.blank? #例外処理のため。引数が空で渡ってきた場合は処理をしない return end # 入力された文字列の中より、#で始まる文字列を配列にして返す return caption.scan(/[##][\w\p{Han}ぁ-ヶヲ-゚ー]+/) #=> ["#aaa","#bbb"] end #--------------ハッシュタグ保存処理 create update アクションの中で実行 ---------------- def save_hashtag(hashtag_array,post_instance) if hashtag_array.blank? #ハッシュタグを付けずに投稿された時、下のメソッドを実行させないようにする。 return end hashtag_array.uniq.map do |hashtag| # ハッシュタグは先頭の#を外し、小文字にして保存 tag = Hashtag.find_or_create_by(name: hashtag.downcase.delete('#')) #-------中間テーブルへの保存処理-------- post_hashtag = PostHashtag.new #中間テーブルのインスタンスを作成 post_hashtag.post_id = post_instance.id post_hashtag.hashtag_id = tag.id post_hashtag.save! end end #---------ハッシュタグの情報をPostオブジェクトに含めるメソッド------------ def creating_structures(posts: "",post_hashtags: "",hashtags: "") #引数として必要なのはPostのデータ、中間テーブルの全データ、ハッシュタグの全てのデータです。 #このメソッドはPostのActiveRecordインスタンスをハッシュに変換し、更に一つ一つのオブジェクトに対し、idに紐づくハッシュタグを配列として格納するメソッドです。 array = [] #最終戻り値用 posts.each do |post| hashtag = [] #中間テーブルのID情報から探したハッシュタグを格納するための配列 post_hash = post.attributes #ActiveRecordインスタンスをハッシュに変換 { key => val, key=> val} related_hashtag_records = post_hashtags.select{|ph| ph.post_id == post.id } #中間テーブルより投稿idが一致するレコードを取り出す related_hashtag_records.each do |record| hashtag << hashtags.detect{ |hashtag| hashtag.id == record.hashtag_id } #上記レコードをもとにハッシュタグを検索し、配列に格納 end post_hash["hashtags"] = hashtag #投稿一つ一つのデータに['hashtags']のkeyを追加、そこにハッシュタグのデータを格納する array << post_hash #=> [{"id"=>1, "title"=>"aaaa", "caption"=>"#aaaa", "created_at"=>Sun, 02 May 2021 15:13:42 UTC +00:00, "updated_at"=>Sun, 02 May 2021 15:13:42 UTC +00:00, "user_id"=>1, "image_id"=>"e347a197a5c2e6466db2d5b1673792c0a7b3a37460b1dea00f36b8b366a6", "hashtag"=>[#<Hashtag id: 1, name: "aaaa", created_at: "2021-05-02 15:13:42", updated_at: "2021-05-02 15:13:42">}] end return array end #---------ハッシュタグの情報をハッシュタグテーブルと中間テーブルから削除するメソッド------------ def delete_records_related_to_hashtag(post_id) relationship_records = PostHashtag.where(post_id: post_id) #中間テーブルのレコード if relationship_records #中間テーブルにレコードが保存されていれば relationship_records.each do |record| record.destroy #中間テーブルのレコードを削除する end end all_hashtags = Hashtag.all all_related_records = PostHashtag.all all_hashtags.each do |hashtag| #ハッシュタグに紐づくレコードが中間テーブルに保存されていなければ、ハッシュタグを削除する if all_related_records.none?{ |record| hashtag.id == record.hashtag_id } hashtag.destroy end end end end 一応いたるところにコメントを書いてはいるので、 適宜質問していただければと思います。 こちら外部ファイルを作成して、コントローラーの一番上にてincludeすることにより外部ファイルのメソッドが呼び出せるようになります。 各メソッドの目的をざっくりと下記にまとめます。 extract_hashtag(caption) 引数(caption)に入ってきた文字列から、先頭が#で始まる文字列を配列にして返すメソッドです。 save_hashtag(hashtag_array, post_instance) ハッシュタグをハッシュタグテーブルに保存する、そして中間テーブルへの保存処理を行っております。 引数で渡ってきたハッシュタグの配列を、重複しないようにハッシュタグテーブルに保存の後、 引数で渡ってきたpost_instance(Post.new)のようなオブジェクトのidをハッシュタグのidとセットにして中間テーブルに保存します。 ですので保存の処理は、 ①extract_hashtagにてハッシュタグを抽出。②save_hashtagの引数に①のハッシュタグと①の時に作っているであろうPost.newの値を渡す→保存のような流れになります。 creating_structures(posts: "",post_hashtags: "",hashtags: "") 投稿と一緒にハッシュタグを表示するために作成したメソッドです。 「構造体を作成する」という意味です。 indexやshowにて投稿を表示したとして、中間テーブルのそのまた向こうにいるハッシュタグをどう取得するかを考えると結構難しいと言いますか、なんか大変そうな気がします。 それこそさっき述べた下記の流れを1オブジェクトに対して毎回行わなければなりません。 1、Postのidを判別する。 2、中間テーブルから、1のidが入っているレコードを検索、取得する。 3、2で取得したレコードに含まれるhashtag_idを取得する。 4、3で取得したhashtag_idからHashtagテーブルに対し検索をかけ、レコードを取得する。 クエリもとんでもない量になりますし、なにより面倒ということで、Post.allの一つ一つのレコードに対してハッシュタグを入れ込んでしまう(合体させてしませえば)いいという考えから作成したメソッドになります。 ActiveRecordInstance(Post.find(params[:id])とかで返ってくるオブジェクト)を一度ハッシュにして、一つ一つのレコードというか、値に対して、新しくhashtagsというkeyを与えます。 そしてそのkeyの中に中間テーブル経由で取得したハッシュタグを入れ込み、最後は大きな配列にまとめて返します。 (post_hash["hashtags"] = hashtag) array << post_hash このarray << post_hashのコメントにあるように、hashtagsというkeyの中に配列を埋め込みます。 delete_records_related_to_hashtag(post_id) コメントにあるように投稿を削除した時に関連するレコードをまとめて削除するメソッドです。 モデルでよく記載されている dependent destoryを手作業でやったイメージです。 投稿を消す→中間テーブルのレコードを消す その後、ハッシュタグテーブルのレコード一つ一つが中間テーブルに保存されているかを調べます。 もし中間テーブルに保存されていなければ、それは関連する投稿がないということですのでハッシュタグも削除します。 showアクションにて使用している.pluckというのはActiveRecordインスタンスに使用するメソッドです。 こちらは引数にシンボルで与えた値(カラム名)を配列にして返すというメソッドです。 詳しくは下記記事にて 先に投稿関連のViewを app/views/posts/index.html.erb <h2>ハッシュタグが埋め込まれている投稿一覧</h2> <div style = "display: flex; flex-wrap: wrap;"> <% @post_objects.each_with_index do |post,i| %> <% display_caption = post["caption"].gsub(/[##][\w\p{Han}ぁ-ヶヲ-゚ー]+/,"") %> <div style="border: 1px solid black;margin: 2%;padding: 3%;width: 25%;"> <%= attachment_image_tag @posts[i],:image,style: 'width: 40%;' %> <div style="border-bottom: 1px solid"> <p style="color: red">title</p> <%= post["title"] %> </div> <div> <p style="color: red">caption</p> <p> <%= display_caption %><br> <% if post["hashtags"].present? %> <% post["hashtags"].each do |hashtag| %> <%= link_to "##{hashtag.name}", hashtag_path(hashtag.id) %> <% end %> <% end %> <p> </div> <%= link_to "詳細", post_path(id: post["id"]) %> <%= link_to "編集", edit_post_path(id: post["id"]) %> <%= link_to "削除", post_path(id: post["id"]),method: :delete %> </div> <% end %> </div> @post_objectsには先ほど外部メソッド(creating_structures)で作ったオブジェクトが格納されております。 こちらはハッシュの形でデータが渡ってきますのでtitleもpost.titleではなくpost["title"]という記載になるところに注意が必要です。 display_captionというのはcaptionの中に保存されてしまっているハッシュタグを非表示にしたものです。 gsubを使用し第一引数にマッチするものを第二引数(””)に変換しております。 またattatchment_image_tagを使用する際にはどうやらハッシュの情報ではダメらしく、やむを得ずハッシュに変換する前のActiveRecordのインスタンスを使用しております。 app/views/posts/show.html.erb <h2>投稿詳細</h2> <div style="border: 1px solid black;margin: 2% 0;padding: 3%;"> <%= attachment_image_tag @post,:image,style: 'width: 40%;' %> <div style="border-bottom: 1px solid"> <p style="color: red">title</p> <%= @post.title %> </div> <div> <p style="color: red">caption</p> <p> <%= @display_caption %><br> <% if @hashtags.present? %> <% @hashtags.each do |hashtag| %> <%= link_to "##{hashtag.name}", hashtag_path(hashtag.id) %> <% end %> <% end %> <p> </div> <div> <%= link_to "編集", edit_post_path(id: @post.id) %> <%= link_to "削除", post_path(id: @post.id),method: :delete %> </div> </div> 投稿詳細ページはindexと違い、ハッシュの形を使用しておりませんので従来のpost.titleという形で情報を出力しております。 app/views/posts/new.html.erb <h2>投稿ページです</h2> <%= form_with model:@newpost, local: true do |f| %> <div> <p>画像を選択</p> <%= f.attachment_field :image %> <p>タイトル入力</p> <%= f.text_field :title %> <br> <p>投稿内容入力</p> <%= f.text_area :caption, size:"55x12" %> <%= f.submit "新規投稿" ,class:'postimage-new-button' %> </div> <% end %> 手抜きですみません・・・ app/views/posts/edit.html.erb <h1>投稿の編集</h1> <%= form_with model:@post, local: true do |f| %> <div> <p>画像を選択</p> <%= f.attachment_field :image %> <p>タイトル入力</p> <%= f.text_field :title %> <br> <p>投稿内容入力</p> <%= f.text_area :caption, size:"55x12" %> <%= f.submit "更新" ,class:'postimage-new-button' %> </div> <% end %> こちらもこれといって特記事項はありません。 続いてhashtag_controller app/controllers/hashtags_controller.rb class HashtagsController < ApplicationController include HashtagMethods def index hashtags = Hashtag.all.select(:id,:name) #全てのハッシュタグを取得 hashtag_count = PostHashtag.all.group(:hashtag_id).count #中間テーブルのレコードをhashtag_id毎にグループ化し、数を取得 @hashtags = [] hashtags.each_with_index do |hashtag,i| hashtag = hashtag.attributes #ハッシュに変換 hashtag["count"] = hashtag_count[hashtag["id"]] #countというkeyを増やし、中間テーブルの数の情報を格納する @hashtags << hashtag #配列に格納 end if @hashtags.length > 1 @hashtags = @hashtags.sort{ |a,b| b["count"] <=> a["count"]} #表示はハッシュタグが使用されている投稿の多い順にする end end def show post_hashtags = PostHashtag.all relationship_records = post_hashtags.select{ |ph| ph.hashtag_id == params[:id].to_i}.map(&:post_id) #中間テーブルの全レコードより、該当ハッシュタグIDが含まれるものを取得→post_idを配列に格納 #=> [1,3] all_posts = Post.all @posts = all_posts.select{ |post| relationship_records.include?(post.id)} #中間テーブルの情報が含まれるPostのレコードを取得する @post_objects = creating_structures(posts: @posts,post_hashtags: post_hashtags ,hashtags: Hashtag.all) #取得したレコードをハッシュに変換し、ハッシュタグを一つ一つのハッシュに格納する。 end end こちらでもincludeにて外部ファイルを呼び出して、hashtag_methods.rbのメソッドを使用できるようにしております。 Views(hashtag) app/views/hashtags/index.html.erb <h2>ハッシュタグ一覧</h2> <% @hashtags.each do |hashtag| %> <% name = hashtag["name"] count = hashtag["count"] id = hashtag["id"] %> <p> <%= link_to "##{name}(#{count})", hashtag_path(id) %> </p> <% end %> 最低限の機能です。 紐づく投稿が何件なのかが()の中に表示されています。 冗長なコードになるのもあれなので、name、count、idという変数に情報を格納しています。 app/views/hashtags/show.html.erb <h2>ハッシュタグが埋め込まれている投稿一覧</h2> <div style = "display: flex; flex-wrap: wrap;"> <% @post_objects.each_with_index do |post,i| %> <% display_caption = post["caption"].gsub(/[##][\w\p{Han}ぁ-ヶヲ-゚ー]+/,"") %> <div style="border: 1px solid black;margin: 2%;padding: 3%;width: 25%;"> <%= attachment_image_tag @posts[i],:image,style: 'width: 40%;' %> <div style="border-bottom: 1px solid"> <p style="color: red">title</p> <%= post["title"] %> </div> <div> <p style="color: red">caption</p> <p> <%= display_caption %><br> <% if post["hashtags"].present? %> <% post["hashtags"].each do |hashtag| %> <%= link_to "##{hashtag.name}", hashtag_path(hashtag.id) %> <% end %> <% end %> <p> </div> <%= link_to "詳細", post_path(id: post["id"]) %> <%= link_to "編集", edit_post_path(id: post["id"]) %> <%= link_to "削除", post_path(id: post["id"]),method: :delete %> </div> <% end %> </div> posts/indexと同じコードです。 これにて完成です。 まとめ 表示をしたいオブジェクトは自分の使いやすい形で作成してしまおう!! 以上です! 終わりに 長々と読んでくださりありがとうございました。 僕が当初ハッシュタグ機能を実装した際にはRails(フレームワーク)のヘルパーを多く使用したことで、できたけどよく分からないというような状態になってしまったのもあり、今回は地道な実装の方法を取ってみました。 自由度の高いRubyですので、使いやすいデータを自分で作ることも容易いと感じます。 かなり高速で作ったので見つけていないバグがあったり、ここ無駄だよねってところは是非教えてください。 また質問もお待ちしております。 無事にハッシュタグが実装できることをお祈りしています。
- 投稿日:2021-05-03T20:32:24+09:00
【Rails】フォロー機能実装(routes、モデル編)
完成GIF フォローする、フォローを外すを表示 フォロー中と、フォロワーの数を表示 非同期通信で実装 フォロー機能実装には、多対多のリレーションとなる。 複雑な部分も多いのでゆっくりまとめていきます。 1.テーブル作成 フォロー機能では、User対Userとなる。 つまり、多対多です。 Relationshipテーブル カラム データ 説明 id integer PK follower_id integer フォローするユーザのid followed_id integer フォローされるユーザーのid ターミナル $ rails g model Relationship //必要なデータを入れよう $ rails db:migrate 2.Modelの編集 Relationshipモデルに以下の記述をして、Userとのリレーションを繋ぐ。 app/models/relationship.rb belongs_to :followed, class_name: "User" belongs_to :follower, class_name "User" //class_nameを付与する事で、Userを参照するようにする class_nameは一つのモデルに対して、二つのアソシエーションを組む際に使用します。 今回のケースで考えると、followerとfollowedが上記に当てはまります。 次にUserモデルの記述に移ります。 この辺りが少しややこしくなります。 app/models/user.rb class User < ApplicationRecord has_many :reverse_of_relationships, class_name: "Relationship", foreign_key: "followed_id", dependent: :destroy //フォローされる側のリレーション has_many :relationships, class_name: "Relationship", foreign_key: "follower_id", dependent: :destroy //フォローする側のリレーション has_many :followers, through: :reverse_of_relationships, source: :follower //自分をフォローしている人のデータ has_many :followings, through: :relationships, source: :followed //自分がフォローした人のデータ end foreign_key(外部キー)について has_many :reverse_of_relationships, class_name: "Relationship", foreign_key: "followed_id", dependent: :destroy has_many :relationships, class_name: "Relationship", foreign_key: "follower_id", dependent: :destroy class_nameをRelationshipとして、どこのテーブルと情報のやり取りをするかを指定。 foreign_key(外部キー)で、どこの情報と繋がるかを指定。 reverse_of_relationshipsはfollowed_id relationshipsはfollower_idとリレーション関係となる。 そもそもPrimary_key(主キー)とforeign_key(外部キー)って? 例) userモデル id(PK) name 1 碇 2 綾波 3 アスカ evaモデル id(PK) user_id(FK) unit 1 2 零号機 2 1 初号機 3 3 弐号機 userモデルとevaモデルにはそれぞれid(PK)がある。 二つのモデルを繋ぐためにevaモデルにはuser_id(FK)があり、関係性を結ぶことが出来る。 先程のフォロー機能でも、FKを指定することでどこの情報と繋ぎたいかを明確にすることができます。 throughとsource has_many :followers, through: :reverse_of_relationships, source: :follower has_many :followings, through: :relationships, source: :followed throughはあるリレーションを他テーブルを通じて実現するために使います。 先ほど作成したリレーションを通じて、sourceで欲しい情報があるカラムへと繋ぎます。 followers => reverse_of_relationships => follower フォロワーデータ一覧 followings => relationships => followed フォローデータ一覧 例えば、user.followerとすることで、userのfollowerデータを簡単に取得することができます。 これで、TwitterやFacebookのようなフォロー、フォロワー一覧を表示させることも出来る訳ですね。 3.routesを編集 resources :users,only:[:index,:show,:edit,:update] do resource :relationships, only:[:create,:destroy] get 'followings', to: 'relationships#followings', as: 'followings' get 'followers', to: 'relationships#followers', as: 'followers' end Relationshipsに:idは必要ないので、resourseで作成。 情報量も多いので、次の記事にControllerとViewの記述を紹介します。
- 投稿日:2021-05-03T20:27:41+09:00
Railsコードリーデイング③ route.rbのgetメソッドを深掘りしてみた 【備忘録】
はじめに MVCにおいてどのように内部処理が行われページ遷移に至るのかフレームワークを使用していると何も気にせずとも開発できてしまいます。 しかしこの基本的な動作の仕組みを理解することで、エンジニアとしての基本的な思考を理解できるのではと考えました。 今回は最も基本的なgetメソッドを見ていきます。 準備 以下のルーティングを作成 rails/config/route.rb Rails.application.routes.draw do binding.pry get 'users' => 'users#index' end 実行 getメソッド rails/actionpack/lib/action_dispatch/routing/mapper.rb module ActionDispatch module Routing class Mapper (省略) module HttpHelpers # Define a route that only recognizes HTTP GET. # For supported arguments, see match[rdoc-ref:Base#match] # # get 'bacon', to: 'food#bacon' def get(*args, &block) map_method(:get, args, &block) end args: {"users"=>"users#index"} block: [] 学び① 仮引数の* (*args) 仮引数に*をつけることで余りを全て配列として受け取る事ができる。これを可変長引数と呼ぶ。 https://docs.oracle.com/javase/jp/7/technotes/guides/language/varargs.html 引数の数に柔軟に対応したいときに使えそうです。 学び② ブロック ブロックは「処理の集合体」です。無名関数とかラムダ式とかクロージャのようなものです。 https://qiita.com/genya0407/items/1a34244cba6c3089a317 getに対し処理を追記した場合にblockに処理が入ると思われます。 { |e| e + 1 } こちらのような表記です。 map_methodメソッド rails/actionpack/lib/action_dispatch/routing/mapper.rb module HttpHelpers private def map_method(method, args, &block) options = args.extract_options! options[:via] = method match(*args, options, &block) self end end argsをpopしています。今回要素は1つだけなので、optionsにargsがそのまま代入されます。 map_methodメソッドは、整形してoptionsに getメソッド {"users"=>"users#index"} という2点の情報をまとめる機能を担っています。 rails/actionpack/lib/action_dispatch/routing/mapper.rb module Resources # Matches a URL pattern to one or more routes. # For more information, see match[rdoc-ref:Base#match]. # # match 'path' => 'controller#action', via: :patch # match 'path', to: 'controller#action', via: :post # match 'path', 'otherpath', on: :member, via: :get def match(path, *rest, &block) if rest.empty? && Hash === path options = path path, to = options.find { |name, _value| name.is_a?(String) } raise ArgumentError, "Route path not specified" if path.nil? case to when Symbol options[:action] = to when String if /#/.match?(to) options[:to] = to else options[:controller] = to end else options[:to] = to end options.delete(path) paths = [path] else options = rest.pop || {} paths = [path] + rest end if options.key?(:defaults) defaults(options.delete(:defaults)) { map_match(paths, options, &block) } else map_match(paths, options, &block) end end 学び③ === self === obj -> bool 指定された obj が self かそのサブクラスのインスタンスであるとき真を返します。また、obj が self をインクルードしたクラスか>そのサブクラスのインスタンスである場合にも真を返します。上記のいずれでもない場合に false を返します。 https://docs.ruby-lang.org/ja/latest/method/Module/i/=3d=3d=3d.html モジュール内で型を確認する場合に使えそうです。 obj.kind_of?(self)と同じ機能ということですが、===の方がスッキリかけます。ただobjとselfの並びが逆なことには注意が必要です。 学び④ findメソッドと多重代入の組み合わせ path, to = options.find { |name, _value| name.is_a?(String) } ハッシュのname(keyと呼ぶことの方が多いが)がStringの場合は、pathとtoに代入します。 シンボルで表記されていた場合は、代入されないということになります。 変数を_valueと先頭にアンダーバーを付けているのは何故でしょうか。 こちらは、先頭アンダーバーのもので絞り込むことによって探しやすいというメリットがあるようです。処理内の変数なのでこの表記にする理由はいまいちわかりませんでした。 https://anderson02.com/cs/cs-rules/cs-rules10/ 最後に 今日はここまでです。前処理の部分でこんなに学びがあるとはという感想です。 まだまだ、routingがどのように処理されているかに踏み込めていないため、この部分のコードリーディング継続する予定です。
- 投稿日:2021-05-03T19:43:47+09:00
[解決]Access to XMLHttpRequest at from origin has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
結論 RailsAPIとVue間でデータを共有しようとしたところエラー。API側のCORS設定がうまく行っていなかったことが原因だった。 詳しく エラーのコード Rails.application.config.middleware.insert_before 0, Rack::Cors do allow do origins '*' resource '*', headers: :any, methods: [:get, :post, :put, :patch, :delete, :options, :head] end end CORSが正常に機能したコード Rails.application.config.middleware.insert_before 0, Rack::Cors do allow do origins 'http://localhost:8080', 'http://localhost:3002/v1/blogs/' resource '*', headers: :any, methods: [:get, :post, :put, :patch, :delete, :options, :head] end end originを全て受け入れる(*)にすると、CORSのセキュリティ的にエラーが出るようになっているらしい。だから、クライアント側のoriginを明記すると、CORSが機能する。 参考 https://web.dev/cross-origin-resource-sharing/ https://qiita.com/att55/items/2154a8aad8bf1409db2b https://qiita.com/residenti/items/3a03e5e0268b354284b7
- 投稿日:2021-05-03T18:59:53+09:00
[AWS & Postgres & Rails]EC2にRailsアプリの配置
はじめに AWSのEC2でRailsアプリをデプロイしたのでメモとして残しておきます。 今回はEC2にRailsアプリの配置をしていきます 自分の手順書としてのメモですので画像はありません。あしからず。 前提としてEC2のサーバー環境構築が終わっている状態とします。 環境 nginx 1.12.2 PostgreSQL 11.5 node 12.22.1 yarn 1.22.5 rbenv 1.1.2 ruby 2.6.6 GitHub連携に必要な公開鍵作成 最初にgitに関する設定ファイルを生成します。 .gitconfigファイルを新たに作ります。 サーバー $ cd ~ $ vim .gitconfig 以下を記述していきます。 [user] name = your_name #gitに登録した自分の名前 email = hoge@gmail.com #git登録時の自分のメアド [alias] #お好きに a = add b = branch ch = checkout st = status [color] #色付け ui = true # githubの場合 [url "github:"] #pull、pushのための設定 InsteadOf = https://github.com/ InsteadOf = git@github.com: # bitbucketの場合 [url "bitbucket:"] InsteadOf = https://ユーザ名@bitbucket.org/ InsteadOf = git@bitbucket.org: shift+ZZで保存&終了 次はアプリを配置するディレクトリを作成します。 サーバー構築 $ cd / $ sudo mkdir var/www $ sudo mkdir var/www/rails $ sudo chown ユーザ名 var #ユーザに所有権を与える $ cd var/ $ sudo chown -R ユーザ名 www #このディレクトリ以下の所有権をユーザに与える gitの接続に必要な鍵を格納するディレクトリに移動します。 サーバー $ cd ~ $ chmod 700 .ssh #所有者に対して、読み取り、書き込み、実行の権限を与える $ cd .ssh サーバー $ ssh-keygen -t rsa 以下のメッセージが表示されるので、「aws_git_rsa」と入力 Enter file in which to save the key ():aws_git_rsa 何もせずにエンター Enter passphrase (empty for no passphrase): 何もせずにエンター Enter same passphrase again lsコマンドでaws_key_rsaとaws_key_rsa.pubが生成されたことを確認してください configファイルを生成します。 $ vi config ----------------------------- # githubの場合以下を追記 Host github github.com Hostname github.com User git IdentityFile ~/.ssh/aws_git_rsa # bitbucketの場合以下を追記 Host bitbucket Hostname bitbucket.org User git IdentityFIle ~/.ssh/aws_git_rsa ----------------------------- `Shift`+`ZZ`で保存&終了 公開鍵の中身を表示する $ cat aws_git_rsa.pub ssh-rsa~~~ catで表示させた公開鍵をgithubにアクセスして登録します。 公開鍵をGitHubにアップ https://github.com/settings/ssh 上記のページでGitHubで公開鍵の設定が可能です。 GitHubのページへ遷移した後、「New SSH Key」ボタンを選択します。 以下の通り項目に記載 1 . Title:「aws_git_rsa.pub」 #公開鍵名 2 . Key:「ssh-rsa~~~」 #catで確認した公開鍵の中身 「Add SSH Key」ボタンをクリックする。 GitHub側の設定が終われば、ターミナルから接続が可能か確認してみます。 サーバー ターミナルへ戻り設定ファイルの権限を変更(読み取りと書き込みの許可) $ chmod 600 config GitHubへの接続確認。途中の質問にはYesで。GitHubのユーザ名が出てくれば成功。 $ ssh -T git@github.com Githubからアプリをクローンする いよいよアプリをクローンする時がきました。 サーバー $ cd ~ $ cd /var/www/rails $ git clone git@github.com:~~~~~~ git cloneの後に続くURLはGitHubの下記の手順で取得します。 1 . githubからデプロイしたいアプリのリモートレポジトリへ移動 2 . 「code」を選択 3 . 「SSH」を選択して、表示されているURLをコピーする lsコマンドでアプリ名が記載されているディレクトリが存在すれば、クローンは成功です。 クローンしたアプリに移動し、必要なGemをインストールしましょう。 サーバー $ cd アプリ名 $ bundle install アプリのシークレットの設定 クローンが成功したら、シークレットを生成します。 まずcredentials.yml.encの編集を行っていきます。 configディレクトリに格納されているファイルを確認しましょう サーバー(/var/www/rails/アプリ名/) $ ls config これから、「credentials.yml.enc」の中身を編集していきます。 1.マスターキーを作成 「credentials.yml.enc」を編集する為には 「master.key」が必要です。 ローカル環境の同ディレクトリには格納されていますが サーバー環境の同ディレクトリには格納されていません。 「master.key」はGit管理しない様に設定されていて 本番サーバー上でプロジェクトのリポジトリをクローンしても、このファイルはついてこない様になっております。 そこで手動で「master.key」を生成します。 ローカル環境のクローン元のアプリディレクトリにて、以下の手順を実行してください。 ローカル(アプリ名/config) $ vim master.key 表示された英数字をメモします。 次にサーバー環境で以下の手順を実行します。 サーバー(/var/www/rails/アプリ名/) $ cd config $ vim master.key # ローカルのmaster.keyの値を入力 26286c42~~~ Shift+ZZで保存&終了 これで「credentials.yml.enc」を編集する準備ができました。 2.credentials.yml.encを編集 初めにローカル環境にてシークレットを生成します。 ローカル環境にて以下の手順を実行してください。 ローカル(アプリ名) $ rake secret jflaskfdk~~~~~~ 表示されたシークレットキーをコピーします。 次はローカル環境で生成した、シークレットキーを「credential.yml.enc」に記載します。 サーバー環境でクローンした、アプリで以下の手順を実行します サーバー環境(/var/www/rails/アプリ名/) $ EDITOR=vim bin/rails credentials:edit # aws: # access_key_id: 123 # secret_access_key: 345 # Used as the base secret for all MessageVerifiers in Rails, including the one protecting cookies. secret_key_base: 01078f3faf42140621e86753aabcb~~~~ #シークレットキーをコピー Shift+ZZで保存&終了 これでアプリのシークレットの設定は完了です Nginxの設定 Nginx(エンジンエックス)の設定ファイルを修正します。 以下の手順を実行します サーバー(/var/www/rails/アプリ名/) $ cd /etc/nginx/conf.d/ $ sudo vim アプリ名.conf # log directory error_log /var/www/rails/アプリ名/log/nginx.error.log; #自分のアプリケーション名に変更 access_log /var/www/rails/アプリ名/log/nginx.access.log; #自分のアプリケーション名に変更 upstream unicorn_server { server unix:/var/www/rails/アプリ名/tmp/sockets/.unicorn.sock fail_timeout=0; #自分のアプリケーション名に変更 } server { listen 80; client_max_body_size 4G; server_name ~~~.~~~.~~~.~~~; #アプリのElastic IPに変更 keepalive_timeout 5; # Location of our static files root /var/www/rails/アプリ名/public; #自分のアプリケーション名に変更 location ~ ^/assets/ { root /var/www/rails/アプリ名/public; #自分のアプリケーション名に変更 } location / { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; proxy_redirect off; if (!-f $request_filename) { proxy_pass http://unicorn_server; break; } } error_page 500 502 503 504 /500.html; location = /500.html { root /var/www/rails/アプリ名/public; #自分のアプリケーション名に変更 } } Shift+ZZで保存&終了 そして権限の変更をします。 サーバー(/var/www/rails/アプリ名/) $ cd /var/lib $ sudo chmod -R 775 nginx #nginx以下のファイルに所有者、所有グループに全ての権限を付与、その他に読み取りと実行を許可 Unicornの設定 Unicornはアプリケーションサーバーの一種です。 アプリ本体を格納するUnicornを設定していきましょう。 1.Unicornのインストール GemファイルにUnicornを追記 サーバー(/var/www/rails/アプリ名/) vim Gemfile ---------------------------- #以下を追記 group :production, :staging do gem 'unicorn' end ---------------------------- Shift+ZZで保存&終了 Unicornをインストール サーバー(/var/www/rails/アプリ名/) $ gem install bundler $ bundle install 2.Unicornの設定ファイルを作成 サーバー(/var/www/rails/アプリ名/) $ vim config/unicorn.conf.rb ------------------------------------ #以下を記述:合計1箇所変更点があります # set lets $worker = 2 $timeout = 30 $app_dir = "/var/www/rails/アプリ名" #自分のアプリケーション名 $listen = File.expand_path 'tmp/sockets/.unicorn.sock', $app_dir $pid = File.expand_path 'tmp/pids/unicorn.pid', $app_dir $std_log = File.expand_path 'log/unicorn.log', $app_dir # set config worker_processes $worker working_directory $app_dir stderr_path $std_log stdout_path $std_log timeout $timeout listen $listen pid $pid # loading booster preload_app true # before starting processes before_fork do |server, worker| defined?(ActiveRecord::Base) and ActiveRecord::Base.connection.disconnect! old_pid = "#{server.config[:pid]}.oldbin" if old_pid != server.pid begin Process.kill "QUIT", File.read(old_pid).to_i rescue Errno::ENOENT, Errno::ESRCH end end end # after finishing processes after_fork do |server, worker| defined?(ActiveRecord::Base) and ActiveRecord::Base.establish_connection end ------------------------------------ Shift+ZZで保存&終了 これでUnicornの設定も完了です PostgreSQLの設定 続いてはDBの設定をしていきます (アプリケーションのDBがPostgreで作成されている前提で話を進めていきます。) DBへアクセスする情報(ユーザー名やパスワード等)をdatabase.ymlへ直接記入するとセキュリティ的に問題があるので、環境変数を使用して情報を受け渡していきます。 今回はdotenv-railsを使用して変数管理を行っていきます。 1.dotenv-railsの導入 Gemfileを編集する サーバー(/var/www/rails/アプリ名/) $ vim Gemfile Gemfileに以下を追加します。 # 環境変数の管理をするもの gem 'dotenv-rails' Shift+ZZで保存&終了 インストール サーバー(/var/www/rails/アプリ名/) $ bundle install シークレットキーを作成します。 サーバー(/var/www/rails/アプリ名/) $ rake secret 表示されたシークレットキーはコピーします。 次にアプリケーションのルートディレクトリに.envファイルを作成し、 その中にシークレットキーの記述を記載してください。 サーバー(/var/www/rails/アプリ名/) $ touch .env $ vim .env --------------------------- #シークレットキーにコピーしたキーを貼り付けます。 SECRET_KEY_BASE={シークレットキー} Shift+ZZで保存&終了 ファイルの記述ができたら結果を確認します。 サーバー(/var/www/rails/アプリ名/) $ source .env $ echo $SECRET_KEY_BASE シークレットキーの値が返ってきたら設定完了です。 PostgreSQLの環境変数を設定 先ほどのenvファイルに、PostgreSQLの変数を設定していきます。 シークレットキーの下へ追記してください。 ※ 他にも環境変数を入れる場合はここに入れてください!筆者の場合、S3関係の環境変数を使いました。 サーバー(/var/www/rails/アプリ名/) $ vim .env ----------------------- DB_NAME=travelour_production #作成したDB名 DB_USERNAME=taro #ロール名(ユーザ名のこと) DB_PASSWORD=********* #パスワード Shift+ZZで保存&終了 ここで入力したロール名とパスワードは使うので覚えておきましょう。 変数の値を確認しましょう サーバー(/var/www/rails/アプリ名/) $ source .env $ echo $DB_NAME $ echo $DB_USERNAME $ echo $DB_PASSWORD 入力した情報が表示されれば完了です。 3.database.ymlを修正 環境変数を用いて、データベースにアクセスする設定を変更します。 サーバー環境(/var/www/rails/アプリ名/) $ vim config/database.yml #最下部の本番環境の記述のみ変更します production: <<: *default database: <%= ENV['DB_NAME'] %> username: <%= ENV['DB_USERNAME'] %> password: <%= ENV['DB_PASSWORD'] %> Shift+ZZで保存&終了 4.Postgresの起動 以下のコマンドでpostgresを起動します。 サーバー(/var/www/rails/アプリ名/) $ sudo service postgres start 最初はデータベースにパスワードがかかっているため強制的にパスできる様にします。 まず'pg_hba.confファイルを編集します。 #pg_hba.confファイルを探す $ sudo find / -print |grep pg_hba.conf /var/lib/pgsql/data/pg_hba.conf #見つけたファイルをvimで編集する $ sudo vim /var/lib/pgsql/data/pg_hba.conf ------------------------------------ 以下のように編集(peerをtrustに変更) # TYPE DATABASE USER ADDRESS METHOD # "local" is for Unix domain socket connections only local all all trust #ここをtrustにする # IPv4 local connections: host all all 127.0.0.1/32 ident # IPv6 local connections: host all all ::1/128 ident これでpostgresにログインできます。 postgersにログイン $ psql -U postgres ロールを作成 ※ロールの詳細はこちら taroと11111111はロール名とパスワードです。 postgres=# CREATE ROLE taro WITH PASSWORD '11111111'; CREATE ROLEと表示されれば成功です。 SELECT ROLNAME FROM pg_roles;でちゃんと表示されているか確認してみましょう postgres=# SELECT ROLNAME FROM pg_roles; rolname ーーーーーーーー postgres taro (2 行) ALTER ROLEコマンドでログインとデータベース作成、権限を与える postgres=# ALTER ROLE taro LOGIN; postgres=# ALTER ROLE taro WITH CREATEDB; exitでpostgresからログアウトする データベース作成とマイグレーションを実行 サーバー $ rake db:create RAILS_ENV=production $ rake db:migrate RAILS_ENV=production 本番環境をプリコンパイルする サーバー $ bundle exec rake assets:precompile RAILS_ENV=production Nginxを起動 サーバー $ start nginx.service Unicornを起動 サーバー $ bundle exec unicorn_rails -c /var/www/rails/アプリ名/config/unicorn.conf.rb -D -E production Uniconの起動を確認(プロセスのリストが3行程表示されればOK) サーバー $ ps -ef | grep unicorn | grep -v grep taro 24411 1 13 19:46 ? 00:00:02 unicorn_rails master -c /var/www/rails/travelour/config/unicorn.conf.rb -D -E production taro 24419 24411 0 19:46 ? 00:00:00 unicorn_rails worker[0] -c /var/www/rails/travelour/config/unicorn.conf.rb -D -E production taro 24420 24411 0 19:46 ? 00:00:00 unicorn_rails worker[1] -c /var/www/rails/travelour/config/unicorn.conf.rb -D -E production Nginxを再起動します。 サーバー $sudo nginx -s reload ブラウザにIPを入力してアクセスします。※IPアドレスはElastic IP http://IPアドレス/ Railsが動作すれば成功です。
- 投稿日:2021-05-03T18:22:52+09:00
rails 自動整形ツールRubocop
初心者なので、勉強用にのこしております。 Rubocop Rubocop(ルボコップ)はRubyの静的コード解析ツールです。「インデントが揃っていない」「余分な改行・スペースがある」などの指摘をRubyStyleGuideに基づいて行ってくれます。 Rubocopを導入することにより、レビューに掛かる時間を減らし、コードの品質を担保できるようになります。 Rubocopを導入しましょう Gemfileに記述 group :development do gem 'rubocop', require: false end 一番下ではないのに気をつけよう そして bundle install Rubocopの設定を記述するファイルを新規作成します。 touch .rubocop.yml .rubocop.ymlの中身を記述する。 たぶん、会社やチームのきまりで決まっているものがあるとおもわれる。 こちらは、例え AllCops: # 除外するディレクトリ(自動生成されたファイル) # デフォルト設定にある"vendor/**/*"が無効化されないように記述 Exclude: - "vendor/**/*" # rubocop config/default.yml - "db/**/*" - "config/**/*" - "bin/*" - "node_modules/**/*" - "Gemfile" # 1行あたりの文字数をチェックする Layout/LineLength: Max: 130 # 下記ファイルはチェックの対象から外す Exclude: - "Rakefile" - "spec/rails_helper.rb" - "spec/spec_helper.rb" # RSpecは1つのブロックあたりの行数が多くなるため、チェックの除外から外す # ブロック内の行数をチェックする Metrics/BlockLength: Exclude: - "spec/**/*" # Assignment: 変数への代入 # Branch: メソッド呼び出し # Condition: 条件文 # 上記項目をRubocopが計算して基準値を超えると警告を出す(上記頭文字をとって'Abc') Metrics/AbcSize: Max: 50 # メソッドの中身が複雑になっていないか、Rubocopが計算して基準値を超えると警告を出す Metrics/PerceivedComplexity: Max: 8 # 循環的複雑度が高すぎないかをチェック(ifやforなどを1メソッド内で使いすぎている) Metrics/CyclomaticComplexity: Max: 10 # メソッドの行数が多すぎないかをチェック Metrics/MethodLength: Max: 30 # ネストが深すぎないかをチェック(if文のネストもチェック) Metrics/BlockNesting: Max: 5 # クラスの行数をチェック(無効) Metrics/ClassLength: Enabled: false # 空メソッドの場合に、1行のスタイルにしない NG例:def style1; end Style/EmptyMethod: EnforcedStyle: expanded # クラス内にクラスが定義されていないかチェック(無効) Style/ClassAndModuleChildren: Enabled: false # 日本語でのコメントを許可 Style/AsciiComments: Enabled: false # クラスやモジュール定義前に、それらの説明書きがあるかをチェック(無効) Style/Documentation: Enabled: false # %i()構文を使用していないシンボルで構成される配列リテラルをチェック(無効) Style/SymbolArray: Enabled: false # 文字列に値が代入されて変わっていないかチェック(無効) Style/FrozenStringLiteralComment: Enabled: false # メソッドパラメータ名の最小文字数を設定 Naming/MethodParameterName: MinNameLength: 1 文字列 = シングルクォーテーション 式展開や文字列内にシングルクォーテーションがある場合はシングルクォーテーションを使います 変数名とメソッド名 = スネークケースで定義する というデフォルトの標準設定がしてあります。他にもあるそうです。 Rubocopを実行するにはターミナルで以下のコマンドを実行します。 # Rubocopを実行 % bundle exec rubocop チェック内容を自動で修正させることもできます。 # Rubocopを実行し、check内容を自動修正 % bundle exec rubocop -a 以上です。
- 投稿日:2021-05-03T18:12:57+09:00
DoorkeeperのAuthorization Code Flowが導入されている状態からPKCEを導入する手順
これは何? 前回こちら の記事でPKCEのフローをまとめました。 今回は実際にdoorkeeperというgemを使って実装していこうと思います。 doorkeeper gemでPKCEを導入する手順 以前 こちら の記事でdoorkeeper gemでOAuthプロバイダ機能を作成する記事を投稿しました。 今回は前回の状態からPKCEを導入する手順を記載します。 (※ doorkeeper gem のwikiにもPKCEについて記載があるのでご確認お願いします。) (https://github.com/doorkeeper-gem/doorkeeper/wiki/Using-PKCE-flow) 導入手順 # wikiに記載されているように以下のコマンドを実行します。 bundle exec rails generate doorkeeper:pkce # 実行すると以下のmigrationファイルが作成されます。 db/migrate/20210330224805_enable_pkce.rb migrationファイルの中身は以下のようになっており oauth_access_grantsテーブルへのカラム追加となっています。 # frozen_string_literal: true class EnablePkce < ActiveRecord::Migration[6.0] def change add_column :oauth_access_grants, :code_challenge, :string, null: true add_column :oauth_access_grants, :code_challenge_method, :string, null: true end end migrateを実行します。 rails db:migrate RAILS_ENV=development これで oauth_access_grants テーブルへ code_challenge、code_challenge_method を保存することができます。 続いてOAuth Applicationを作成していきます。 http://localhost:3000/oauth/applications へアクセスし[New Application]をクリックします。 アプリケーションの情報を入力して登録します。 今回Client側のアプリのURLは http://localhost:3001 としcallbackURLを http://localhost:3001/pkce_callback として準備しました。 画面に表示されている CLIENT_ID(UID)、CLIENT_SECRET、scopes をClient側のアプリへ設定しておきます。 ここでPKCEの仕組みを以下へ記載します。 1. code_verifier を使って code_challenge_method に合った code_challenge を生成し、code_challenge, code_challenge_method をパラメータへ指定して認可画面をリクエストします。 2. 認可サーバは code_challenge, code_challenge_method を保存し、Clientへ認可コードを返します。 3. Clientは認可コードと code_verifier を使ってアクセストークンを取得します。 4. 認可サーバで受け取った code_verifierを保存してある code_challenge_methodで変換して code_challengeのになるか検証します。 それではPKCEを使うためClientアプリケーションの OAuth2::Clientの部分を実装していきます。 以下は認可画面のURLを作成する処理のサンプルになります。 # 認可画面のURLを作成する処理 def pkce_authorization session[:code_verifier] = SecureRandom.alphanumeric(100) client = OAuth2::Client.new( ENV["CLIENT_ID_PKCE"], ENV["CLIENT_SECRET_PKCE"], site: ENV["SITE"], authorize_url: ENV["AUTHORIZE_URL"], token_url: ENV["TOKEN_URL"] ) code_challenge = Base64.urlsafe_encode64(OpenSSL::Digest::SHA256.digest(session[:code_verifier]), padding: false) authorize_url = client.auth_code.authorize_url( redirect_uri: ENV["CALLBACK_URI_PKCE"], scope: ENV["SCOPE"], code_challenge: code_challenge, code_challenge_method: "S256" ) redirect_to authorize_url end code_verifier の値は、[A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~" からなるランダムな文字列であり、最低43文字、最大128文字の長さが必要となります。 今回は code_verifier = SecureRandom.alphanumeric(100) のように100文字の英数字を設定します。ref:https://tools.ietf.org/html/rfc7636 code_challenge は code_challenge_method によって値が変わります。 code_challenge_method を plain にした場合 code_challenge = code_verifier となります。 code_challenge_method を S256 にした場合 code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier))) となります。 ref:https://tools.ietf.org/html/rfc7636 これをrubyで実装すると code_challenge = Base64.urlsafe_encode64(OpenSSL::Digest::SHA256.digest(session[:code_verifier]), padding: false) で実装できます。 code_challenge_method は 2種類あるのですが plainは code_verifierと code_challenge が同じ値のため特に plain を選ぶ理由がなければセキュリティリスクを考えて S256 を使用しましょう。 続いて認可コードが返ってきてからアクセストークンを取得する部分のサンプルが以下になります。 # 認可コードよりアクセストークンを取得 def pkce_callback client = OAuth2::Client.new( ENV["CLIENT_ID_PKCE"], ENV["CLIENT_SECRET_PKCE"], site: ENV["SITE"], authorize_url: ENV["AUTHORIZE_URL"], token_url: ENV["TOKEN_URL"] ) code_verifier = session[:code_verifier] session.delete(:code_verifier) @access_token = client.auth_code.get_token( params[:code], redirect_uri: ENV["CALLBACK_URI_PKCE"], code_verifier: code_verifier ) render :callback end 実際に認可画面から認可後、アクセストークンを取得できるか確認します。 認可画面 認可コード取得後、アクセストークン取得 念のため、認可サーバ側で code_challenge, code_challenge_method が保存されているか確認しておきます。 Doorkeeper::AccessGrant.all Doorkeeper::AccessGrant Load (0.3ms) SELECT "oauth_access_grants".* FROM "oauth_access_grants" LIMIT ? [["LIMIT", 11]] => #<ActiveRecord::Relation [#<Doorkeeper::AccessGrant id: 1, resource_owner_id: 1, application_id: 2, token: "ygCPJhHoenE3vfiY1StLqCVBRJWTZfWnZFdOPUcTn7M", expires_in: 600, redirect_uri: "http://localhost:3001/pkce_callback", created_at: "2021-05-03 07:32:25", revoked_at: "2021-05-03 07:32:25", scopes: "public write", code_challenge: "oDJceObZlNuTivuP6EocfJZt2ODCdyjOh5IyUXK_4Rw", code_challenge_method: "S256">]> 正常にcode_challenge, code_challenge_methodが保存されていることが確認できました! 最後に doorkeeper gemを使用していればPKCEの導入は非常に簡単にできました。 Authorization Code Flowとの共存も可能なため実際の業務でもPKCEは導入しやすいと思います!
- 投稿日:2021-05-03T17:16:04+09:00
[Rails] Rspecでテストコードの実装③
はじめに テストコードの実装②をみていない方はこちらからご覧ください。 コントローラの単体テスト 今回はコントローラの単体テストについてまとめていきます。コントローラのテストでは、あるアクションにリクエストを送ったとき、想定通りのレスポンスが生成されるかどうかを確かめることが主な役割です。RSpecの中でもRequest Specと呼ばれる手法を利用します。 なお、コントローラーのテストコードは、結合テストコードに記述する内容と、同じような責務を果たすことが多いです。よって、今回は簡単にまとめるのみといたします。 Request Spec(参考) RSpecが提供している、コントローラーのテストコードを書くために特化した手法です。RSpecの導入が完了していれば使用できます。 テストコードの具体的な実装 まずはファイルの生成をします。 rails g rspec:request tweets 次に生成されたファイルに以下のようにテスト項目を入力してください。(テスト項目は完璧ではありません、あくまで一例です) なお今回、すでにある投稿に対してテストを行いたいので、buildではなく createを使用しています。 spec/requests/tweets_spec.rb require 'rails_helper' describe TweetsController, type: :request do before do @tweet = FactoryBot.create(:tweet) end describe 'GET #index' do it 'indexアクションにリクエストすると正常にレスポンスが返ってくる' do get tweet_path(@tweet) expect(response.status).to eq 200 end it 'indexアクションにリクエストするとレスポンスに投稿済みのツイートのテキストが存在する' do get tweet_path(@tweet) expect(response.body).to include(@tweet.text) end #一部省略 end end 一つづつ見ていきます。 get get tweet_pathのように、どこのパスにリクエストを送るかを記述します。 response リクエストに対するレスポンスのこと。 このレスポンスで取得できる情報に、想定する内容が含まれているかを確認することで、テストコードを書くことができます。 response.status そのレスポンスのHTTPステータスコードを出力できます。 response.body response.bodyと記述すると、ブラウザに表示されるHTMLの情報を抜き出すことができます。 コントローラの単体テストでは、リクエストに対して、どんなレスポンスをするかの確認をしていることを意識しながら見るとわかりやすいのではないでしょうか おわりに お疲れ様です。 今回はかなり簡潔にコントローラの単体手薄とについてまとめましたので、「え、これだけ?」と不安になる方もいるでしょう。しかし心配無用です!! 先に書いた通り、結合テストでしっかり確認していくので問題はありません!!!! 次回をぜひ楽しみにしていてください! つづき Rspecでテストコードの実装④
- 投稿日:2021-05-03T16:44:44+09:00
Reactとの連携 7 完成
app/javascript/pack/***jsx import React from 'react' import ReactDOM from 'react-dom' import PropTypes from 'prop-types' var target_dom = null; document.addEventListener('DOMContentLoaded', () => { target_dom = document.querySelector('#data'); const url = new URL(location.href); let f = url.searchParams.get("name"); if (f == null){ f = ''; } getData(f); }); function getData(f){ let url = "http://localhost:3000/data/ajax"; fetch(url) .then( res => res.json(), (error) => { const el = ( <p>ERROR!!</p> ); ReactDOM.render(el, target_dom); } ) .then( (result) => { console.log(result); let arr =[]; for(let n in result.rss.channel.item){ let data = result.rss.channel.item[n]; arr.push( <tr> <th>{data.title}</th> <td class="small">{data.pubDate}</td> </tr> ); } const el = ( <table class="table mt-4"> <thead class="thead-dark"> <tr><th><a href={data.link}>{data.title}</a></th> <th>Date</th></tr> </thead> <tbody>{arr}</tbody> </table> ); ReactDOM.render(el, target_dom); }, (error) => { console.log(error); const el = ( <p>ERROR!!</p> ); ReactDOM.render(el, target_dom); } ); }
- 投稿日:2021-05-03T16:22:27+09:00
No template for interactive requestエラーの解消法
No template for interactive requestが発生しエラーの解消がわからなくなった 『購入画面へ進む』を押すと上記のエラーが発生しcontroller,show,index関係を見直したが不明。。。 メンターへ相談し、解消法を聞いたところapp/controller/order_controller.rb部分が単数形になっている事、 内部のclass OrderController < ApplicationControllerのorderが単数形になっている部分の指摘を受けた。 改めてapp/controller/orders_controller.rbに変更と class OrdersControllerへ変更 その後、rails routesをして再度ルーティングの確認をするとURLが変わっている部分があるのでshow.html.erb 部分を修正しcontrol c,rails sと再起動したところ解消できた。 コントローラ名は複数形にすることが命名規則なので、今後も注意をします。
- 投稿日:2021-05-03T16:20:14+09:00
エラーメッセージを表示させるためには?
エラーメッセージを表示させるには? _error_messages.html.erbなどがある場合は下記の様にrenderを使用して呼び出す html.erb <%= render 'shared/error_messages', model: @order %> 学習時のメモとして残しておきました。
- 投稿日:2021-05-03T16:19:14+09:00
ログイン状態でも他のユーザーの編集をできないようにする
ログイン状態でも他のユーザーの編集が出来てしまう状態をログインしていてユーザーIDが一致していないと編集出来ない状態にする。 controller.rb def edit @item = Item.find(params[:id]) redirect_to root_path unless current_user.id == @item.user_id end 上記を記載していないと直接URLを打ち込む事で編集できてしまう。それを制御するためにはunlessを用いてcurrent_user.id == @item.user_idの時でないと編集できないようにした。 今日もまた良い学習になりました。
- 投稿日:2021-05-03T16:16:32+09:00
プルリクエストのコンフリクト解消方法
プルリクエスト後にコンフリクトが発生している ●解消方法● ❶リモートのプルリクエストでコンフリクトが起きている ❷ローカルでgit merge masterを実行することで、ローカルにも同じコンフリクトを起こす ❸ローカル上でコンフリクトを解消(緑と青のどちらかを残す)し、コミットプッシュすれば同じコンフリクトが起きているリモートも解消される コンフリクト解消も勉強になりました〜!
- 投稿日:2021-05-03T16:14:57+09:00
単体テストコードエラー
単体テストコード エラーコード ⇒expected [ ] to include "Item must exist" 上記の場合はバリデーションが定義されていないことを表す ⇒expected [Item must exist] to include "" 上記の場合は、includeが定義されていない場合を表す 自分がミスした部分をメモ替わりに投稿します。
- 投稿日:2021-05-03T16:13:12+09:00
新規投稿、ログイン、ログアウト実装時 pathの書き方注意点
新規投稿、ログイン、ログアウト等ルートパスを記載する際の注意点を記載します!(めっちゃ初歩です) app/views/shared/_header.html.erb <%# CSS・・・assets/stylesheets/shared/header.css %> <header class='top-page-header'> <div class='search-bar-contents'> <%= link_to image_tag("furima-logo-color.png", class:"furima-icon"), "/" %> <form class="search-form" action="#"> <input class='input-box' placeholder='キーワードから探す'> <button class="search-button"> <%= image_tag "search.png", class:"search-icon" %> </button> </form> </div> <div class='nav'> <ul class='lists-left'> <li><%= link_to 'カテゴリー', "#", class: "category-list" %></li> <li><%= link_to 'ブランド', "#", class: "brand-list" %></li> </ul> <ul class='lists-right'> <%# deviseを導入できたら、ログインの有無で表示が変わるように分岐しましょう%> <% if user_signed_in? %> <%= link_to current_user.nickname, "#{@user.name}", class: "user-nickname" %> <%= link_to 'ログアウト', "destroy_user_session_path" , method: :delete, class: "logout" %> <% else %> <%= link_to 'ログイン', "user_session_path", class: "login" %> <%= link_to '新規登録', "new_user_registration_path", class: "sign-up" %> <% end %> <%# //deviseを導入できたら、ログインの有無で表示が変わるように分岐しましょう%> </ul> </div> </header> 上記だとpathにそのままアクセスしてしまう。 例)新規登録ボタンを押すと ⇒new_user_registration_pathへ直接アクセスする これの解決方法として""を外すこと!たったこれだけ!! app/views/shared/_header.html.erb app/views/shared/_header.html.erb <%# CSS・・・assets/stylesheets/shared/header.css %> <header class='top-page-header'> <div class='search-bar-contents'> <%= link_to image_tag("furima-logo-color.png", class:"furima-icon"), "/" %> <form class="search-form" action="#"> <input class='input-box' placeholder='キーワードから探す'> <button class="search-button"> <%= image_tag "search.png", class:"search-icon" %> </button> </form> </div> <div class='nav'> <ul class='lists-left'> <li><%= link_to 'カテゴリー', "#", class: "category-list" %></li> <li><%= link_to 'ブランド', "#", class: "brand-list" %></li> </ul> <ul class='lists-right'> <%# deviseを導入できたら、ログインの有無で表示が変わるように分岐しましょう%> <% if user_signed_in? %> <%= link_to current_user.nickname, "#{@user.name}", class: "user-nickname" %> <%= link_to 'ログアウト', destroy_user_session_path , method: :delete, class: "logout" %> <% else %> <%= link_to 'ログイン', user_session_path, class: "login" %> <%= link_to '新規登録', new_user_registration_path, class: "sign-up" %> <% end %> <%# //deviseを導入できたら、ログインの有無で表示が変わるように分岐しましょう%> </ul> </div> </header> ※リンク先には""を付けないように注意が必要!
- 投稿日:2021-05-03T16:10:52+09:00
単体テストコードエラー解消 Failure/Error: expect(@user.errors.full_messages).to include("First name can't be blank")
◉単体テストコード発生 Failure/Error: expect(@user.errors.full_messages).to include("First name can't be blank") ◯furimaアプリ実装中に単体テストコードを実装 エラーメッセージが発生したのでその時の対処と共にアウトプット spec/models/user_spec.rb require 'rails_helper' RSpec.describe User, type: :model do describe '#create' do before do @user = FactoryBot.build(:user) end it 'passwordとpassword_confirmationが5文字以下では登録できない' do @user.password = '00000' @user.password_confirmation = '00000' @user.valid? expect(@user.errors.full_messages).to include('Password is too short (minimum is 6 characters)') end it 'nicknameが空では登録できない' do @user.nickname = '' @user.valid? expect(@user.errors.full_messages).to include ("Nickname can't be blank") end it 'emailが空では登録できない' do @user.email = '' @user.valid? expect(@user.errors.full_messages).to include("Email can't be blank") end it 'emailは@が含まれていなければ登録できない' do @user.email = 'sample.com' @user.valid? expect(@user.errors.full_messages).to include("Email is invalid") end it 'passwordが空では登録できない' do @user.password = '' @user.valid? expect(@user.errors.full_messages).to include("Password can't be blank") end it 'first_nameが空では登録できない' do @user.first_name = '' @user.valid? expect(@user.errors.full_messages).to include("First_name can't be blank") # ここでエラーが発生した end エラーメッセージは↓ 1) User#create first_nameが空では登録できない Failure/Error: expect(@user.errors.full_messages).to include("First name can't be blank") expected [] to include "First name can't be blank" # ./spec/models/user_spec.rb:38:in `block (3 levels) in <top (required)>' よく見ていくと to include後の("First name can't be blank")とエラーが記載されている。 もしかしたら、_がいらないのか?と仮定し削除して再度bundle exec rspec spec/models/user_spec.rb 実行。 User #create passwordとpassword_confirmationが5文字以下では登録できない nicknameが空では登録できない emailが空では登録できない emailは@が含まれていなければ登録できない passwordが空では登録できない first_nameが空では登録できない last_nameが空では登録できない first_name_kanaが空では登録できない last_name_kanaが空では登録できない birthdayが空では登録できない passwordが存在してもpassword_confirmationが空では登録できない 重複したemailが存在する場合登録できない passwordが5文字以下では登録できない Finished in 0.1892 seconds (files took 1.01 seconds to load) 13 examples, 0 failures なんとか解消!! _の部分は忘れがちなので注意して覚えておこう!
- 投稿日:2021-05-03T16:09:39+09:00
単体テストコードエラー解消方法
テストコードエラーが続いて解消できなかった時のエラーコードと解消方法!!! #create ユーザー新規登録 新規登録がうまくいくとき 全ての値が正しく入力されていれば保存できること 新規登録がうまくいかないとき passwordとpassword_confirmationが5文字以下では登録できない nicknameが空では登録できない emailが空では登録できない emailは@が含まれていなければ登録できない passwordが空では登録できない passwordが英語のみでは登録できないこと passwordが数字のみでは登録できないこと (FAILED - 1) passwordが全角では登録できないこと (FAILED - 2) first_nameが空では登録できない first_nameが漢字・平仮名・カタカナ以外では登録できないこと last_nameが空では登録できない last_nameが漢字・平仮名・カタカナ以外では登録できないこと first_name_kanaが空では登録できない first_name_kanaが全角カタカナ以外では登録できないこと (FAILED - 3) last_name_kanaが空では登録できない last_name_kanaが全角カタカナ以外では登録できないこと (FAILED - 4) birthdayが空では登録できない passwordが存在してもpassword_confirmationが空では登録できない 重複したemailが存在する場合登録できない Failures: 1) User#create ユーザー新規登録 新規登録がうまくいかないとき passwordが数字のみでは登録できないこと Failure/Error: expect(@user.errors.full_messages).to include("Password cannot be registered only with numbers") expected ["Password confirmation doesn't match Password"] to include "Password cannot be registered only with numbers" # ./spec/models/user_spec.rb:49:in `block (5 levels) in <top (required)>' 2) User#create ユーザー新規登録 新規登録がうまくいかないとき passwordが全角では登録できないこと Failure/Error: expect(@user.errors.full_messages).to include("Password is invalid") expected ["Password confirmation doesn't match Password", "Password is invalid. Input half-width characters."] to include "Password is invalid" # ./spec/models/user_spec.rb:54:in `block (5 levels) in <top (required)>' 3) User#create ユーザー新規登録 新規登録がうまくいかないとき first_name_kanaが全角カタカナ以外では登録できないこと Failure/Error: expect(@user.errors.full_messages).to include("first name kana is invalid. Input full_width katakana characters.") expected ["First name kana is invalid. Input full-width katakana characters."] to include "first name kana is invalid. Input full_width katakana characters." # ./spec/models/user_spec.rb:80:in `block (5 levels) in <top (required)>' 4) User#create ユーザー新規登録 新規登録がうまくいかないとき last_name_kanaが全角カタカナ以外では登録できないこと Failure/Error: expect(@user.errors.full_messages).to include("last name kana is invalid. Input full_width katakana characters.") expected ["Last name kana is invalid. Input full-width katakana characters."] to include "last name kana is invalid. Input full_width katakana characters." # ./spec/models/user_spec.rb:90:in `block (5 levels) in <top (required)>' Finished in 0.285 seconds (files took 1.07 seconds to load) 20 examples, 4 failures Failed examples: rspec ./spec/models/user_spec.rb:46 # User#create ユーザー新規登録 新規登録がうまくいかないとき passwordが数字のみでは登録できないこと rspec ./spec/models/user_spec.rb:51 # User#create ユーザー新規登録 新規登録がうまくいかないとき passwordが全角では登録できないこと rspec ./spec/models/user_spec.rb:77 # User#create ユーザー新規登録 新規登録がうまくいかないとき first_name_kanaが全角カタカナ以外では登録できないこと rspec ./spec/models/user_spec.rb:87 # User#create ユーザー新規登録 新規登録がうまくいかないとき last_name_kanaが全角カタカナ以外では登録できないこと エラーの原因は、「expectの部分で取得されたメッセージ と includeの部分に記述しているメッセージ が異なるため」 解決方法は エラーコードのここに注目↓↓↓ expected ["Last name kana is invalid. Input full-width katakana characters."] to include "last name kana is invalid. Input full_width katakana characters." expectedの後の[]に答え書いてある!!!! こちらを記載することで解消されました!!!! ※ちなみに、「.」を忘れてもエラーになるので注意!!!
- 投稿日:2021-05-03T16:03:37+09:00
Unknown validator: '~' (ArgumentError)
問題 rails s をしてもローカルでの立ち上げができない 解決方法 初めてみるエラーで調べてみてもよくわからず・・・ と言うのも'FnumericalityValidator'と言う誤記が原因でエラーが起きておりこんな単語はないからである・・・! バリデーションの記述をformatからnumericalityに変更する際に"f"が残ってしまいエラーになっていたよう! バリデーションの記述でローカルの立ち上げ自体ができなくなるなんて・・・ 立ち上がってからエラーがでるんじゃななくて、そもそも立ち上がらない場合もあるのだなと学び!!!
- 投稿日:2021-05-03T15:26:05+09:00
Reactとの連携 6 YahooNewsを表示してみる
以下に書き換えます。 app/controllers/data_controller.rb class DataController < ApplicationController def index end def ajax url = 'https://news.yahoo.co.jp/pickup/rss.xml'#1 YahooNewのURL uri = URI.parse(url) response = Net::HTTP.get_response(uri) render plain:Hash::from_xml(response.body).to_json end end ・Rubyの外部サーバーにアクセスする機能「Net::HTTP(アドレス)」クラス ・URI.parseを使うと、URLの文字列をURIオブジェクトへと生成いたします。 require 'uri' uri = URI.parse("https://www.google.co.jp/") p uri.scheme p uri.host p uri.port p uri.path uri.scheme => "https" uri.host => "www.google.co.jp" uri.port => "443" uri.path => "/" 実行するとこのようになります。これはhttpの構成要素を以下のように分解して出力しています。 ・ Net::HTTPResponse 指定した対象に GET リクエストを送り、そのレスポンスを Net::HTTPResponse として返します。 対象の指定方法は URI で指定するか、 (host, path, port) で指定するかのいずれかです。 uri データの取得対象を URI で指定します。 host 接続先のホストを文字列で指定します。 path データの存在するパスを文字列で指定します。 port 接続するポートを整数で指定します。 lcalhost:3000/data/ajaxにアクセス 取得できました。
- 投稿日:2021-05-03T15:14:36+09:00
Railsのみで日付からデータを表示する方法
記事の概要 Webサービス上で日付を入力して「送信」のようなボタンを押すと、その日付に応じた結果が表示されることはよくあるものと思います。これをRailsで実装する手順について解説していきたいと思います。 バージョン ・ruby 2.6.5 ・rails 6.0.3.4 ・mysql 14.14 体重や体脂肪をデータベースに保存してそれを表示するアプリケーションを例にとって考えてみます。 前提として、事前にデータベースに日付・体重・体脂肪・体調の情報が保存されているものとします。 まず、記録を保存する機能として、Body_Record というモデルを用意しています。 app>models>body_record.rb class BodyRecord < ApplicationRecord belongs_to :user, optional: true with_options presence: true do validates :date #日付必須 validates :body_weight #体重必須 validates :fat #体脂肪率必須 end 続いてビューは以下の通りです。 ビューファイルの中身は以下のようになっています(CSSは割愛します) app>views>body_records>index.html.erb <h3 class="input-nav">体重・体脂肪記録を確認する</h1> <% unless @body_record.empty? %> <div class="action-box"> <%= form_tag(search_body_records_path, method: :post) do %> <p>見たい日付を選択してください<input type="date" name="body_record[date]" id="body_record_date"> <input type="submit" value="確認する", class="btn btn-success"></p> <% end %> </div> #テーブル形式で表示するための記述ここから <div class="item-box"> <% @body_record.each do |latest_bodyrecord| %> <div class="latest-box"> <table class="latest-body-data"> <tbody> <tr> <th class="latest-data">日付</th> <td class="latest-value"><%= latest_bodyrecord.date %></td> </tr> <tr> <th class="latest-data">体重</th> <td class="latest-value"><%= latest_bodyrecord.body_weight %>kg</td> </tr> <tr> <th class="latest-data">体脂肪率</th> <td class="latest-value"><%= latest_bodyrecord.fat %>%</td> </tr> <tr> <th class="latest-data">体調</th> <td class="latest-value"><%= latest_bodyrecord.todays_condition %></td> </tr> </tbody> </table> </div> #編集・削除ボタンここから <div class="edit-delete-box"> <li class="edit-box"> <%= link_to "編集する", edit_body_record_path(latest_bodyrecord.id), method: :get, class: "btn btn-info" %> </li> <li class="delete-box"> <%= link_to "削除する", "/body_records/#{latest_bodyrecord.id}", method: :delete, class: "btn btn-danger" %> </li> </div> #編集・削除ボタンここまで <% end %> </div> #テーブル形式で表示するための記述ここまで #データが入っていない際の条件分岐 <% else %> <div class="empty"> <p>記録されたデータはありません</p> </div> <% end %> コントローラー側の処理は以下の通りです。 app>controllers>body_records_controller.rb #〜省略 def index @body_records = BodyRecord.all @body_record = BodyRecord.order(date: :desc).limit(1) end def search @body_records = BodyRecord.all @body_record = BodyRecord.new(body_params) if @body_record.date.present? @body_record = BodyRecord.where('date = ?', "#{@body_record.date}}") else @body_record = BodyRecord.none end render :index end #省略〜 ルーティングは以下の通りです。 config>route.rb resources :body_records do collection do post :search end 処理の流れ まず、コントローラー側のindexアクション内の@body_record = BodyRecord.order(date: :desc).limit(1)の記述により、index.html.erbにアクセスした際は、最新の日付のデータが表示されます。 その後、日付を選択して確認するボタンを押すと、searchアクションが動き、データベースの中から選択された日付のデータを取得して、render :indexの記述によりindex.html.erbを再表示する仕組みになっています。 さらに、もし選択された日付にデータが保存されていなかった場合、 <% unless @body_record.empty? %> 省略 <% else %> <div class="empty"> <p>記録されたデータはありません</p> </div> <% end %> の記述により、「記録されたデータはありません」という表示が出るように条件分岐をしています。 終わりに 今回ご紹介した内容は、Railsで実装しましたが、Ajaxで非同期通信を用いて実装するほうが望ましいのではないかな?と考えています。これからアプリケーションをアップデートする中で、より良い方法や手法をどんどん試していこうと考えます。 間違っていたり、おかしなところがありましたら修正いたしますので、遠慮なくコメントいただければと思います。 最後までお読みいただき、ありがとうございました。 参考記事 【Rails入門】form_tagの使い方まとめ | 侍エンジニアブログhttps://www.sejuku.net/blog/29083 Railsで検索機能を実装する方法を現役エンジニアが解説【初心者向け】https://techacademy.jp/magazine/22330 【Rails】パラメーター値の取得(paramsメソッドの使い方https://qiita.com/yuki_0920/items/9f450a51c74407645a3b
- 投稿日:2021-05-03T14:41:06+09:00
Ruby(Rails)でGoogle Calendar API を本番環境(Heroku)で叩く
概要 Google Calendar APIは認証まわりが非常に難しく魔境と言われています。 プログラミング初心者や公式ドキュメントを読むことが苦手な人であれば大半は挫折することでしょう。 実際のところ、私もトライしてみてかなり苦戦しました。 おそらく、次使う時は極力GAS(Google Apps Script)で実装すると思います。(認証&連携が楽なので) しかし、意地でもRubyを使ってGoogle Calendarを叩きたいという人は少なからずいると思いますし、今後も同じところで躓くひとが多いと思うので備忘録として書き残しておきたいと思います。 前提 Ruby(2.2以上) Railsを使用することを想定(オブジェクト指向で設計) 本番環境:Heroku(無料でタスクスケジューラが使えるため) 公式ドキュメント まずは公式ドキュメントをコピペしてじっくり眺めつつ、わからないところは仕様を調べましょう。 quickstart.rb require "google/apis/calendar_v3" require "googleauth" require "googleauth/stores/file_token_store" require "date" require "fileutils" OOB_URI = "urn:ietf:wg:oauth:2.0:oob".freeze APPLICATION_NAME = "Google Calendar API Ruby Quickstart".freeze CREDENTIALS_PATH = "credentials.json".freeze # The file token.yaml stores the user's access and refresh tokens, and is # created automatically when the authorization flow completes for the first # time. TOKEN_PATH = "token.yaml".freeze SCOPE = Google::Apis::CalendarV3::AUTH_CALENDAR_READONLY ## # Ensure valid credentials, either by restoring from the saved credentials # files or intitiating an OAuth2 authorization. If authorization is required, # the user's default browser will be launched to approve the request. # # @return [Google::Auth::UserRefreshCredentials] OAuth2 credentials def authorize client_id = Google::Auth::ClientId.from_file CREDENTIALS_PATH token_store = Google::Auth::Stores::FileTokenStore.new file: TOKEN_PATH authorizer = Google::Auth::UserAuthorizer.new client_id, SCOPE, token_store user_id = "default" credentials = authorizer.get_credentials user_id if credentials.nil? url = authorizer.get_authorization_url base_url: OOB_URI puts "Open the following URL in the browser and enter the " \ "resulting code after authorization:\n" + url code = gets credentials = authorizer.get_and_store_credentials_from_code( user_id: user_id, code: code, base_url: OOB_URI ) end credentials end # Initialize the API service = Google::Apis::CalendarV3::CalendarService.new service.client_options.application_name = APPLICATION_NAME service.authorization = authorize # Fetch the next 10 events for the user calendar_id = "primary" response = service.list_events(calendar_id, max_results: 10, single_events: true, order_by: "startTime", time_min: DateTime.now.rfc3339) puts "Upcoming events:" puts "No upcoming events found" if response.items.empty? response.items.each do |event| start = event.start.date || event.start.date_time puts "- #{event.summary} (#{start})" end 無駄を省いてモデルに切り出す calendar.rb require "google/apis/calendar_v3" require "googleauth" require "googleauth/stores/file_token_store" require "date" require "fileutils" class Calendar # The file token.yaml stores the user's access and refresh tokens, and is # created automatically when the authorization flow completes for the first # time. ## # Ensure valid credentials, either by restoring from the saved credentials # files or intitiating an OAuth2 authorization. If authorization is required, # the user's default browser will be launched to approve the request. # # @return [Google::Auth::UserRefreshCredentials] OAuth2 credentials def authorize # 環境変数の定義 uri = ENV["OOB_URI"] user_id = ENV["MAIL"] secret_hash = { "web" => { "client_id" => ENV["CLIENT_ID"], "project_id" => ENV["PROJECT_ID"], "auth_uri" => ENV["AUTH_URI"], "token_uri" => ENV["TOKEN_URI"], "auth_provider_x509_cert_url" => ENV["PROVIDER_URI"], "client_secret" => ENV["CLIENT_SECRET"], "redirect_uris" => [ENV["REDIRECT_URIS"]], "javascript_origins" => [ENV["JAVASCRIPT_ORIGINS"]] } } # herokuの環境的に環境変数から読み込んだほうが良い client_id = Google::Auth::ClientId.from_hash secret_hash token_store = Google::Auth::Stores::FileTokenStore.new file: "token.yaml" authorizer = Google::Auth::UserAuthorizer.new client_id, Google::Apis::CalendarV3::AUTH_CALENDAR_READONLY, token_store credentials = authorizer.get_credentials user_id if !credentials url = authorizer.get_authorization_url base_url: uri puts "Open the following URL in the browser and enter the " \ "resulting code after authorization:\n" + url code = ENV["CODE"] credentials = authorizer.get_and_store_credentials_from_code( user_id: user_id, code: code, base_url: uri ) end credentials end # Initialize the API def initialize @service = Google::Apis::CalendarV3::CalendarService.new @service.client_options.application_name = ENV["APPLICATION_NAME"] @service.authorization = authorize end def fetch_events calendar_id = ENV["CALENDAR_ID"] now = DateTime.now + 1 response = @service.list_events(calendar_id, max_results: 5, single_events: true, order_by: "startTime", time_min: DateTime.new(now.year,now.month,now.day,0,0,0), time_max: DateTime.new(now.year,now.month,now.day,23,59,59) ) end end フレームワークを使わずにRubyだけで書くくらいの小規模なコードであれば個人的にGASを使うことを推奨しているので、今回は大規模なシステムにGoogle Calendarを導入することを想定してRailsを使っていると想定しています。 余談ですが、GASでGoogle Calendarから引っ張った情報をRailsに送るという手法もあります。(こっちのほうが楽な人は楽かも) ではひとつずつ見ていきましょう。 initializeメソッド(準備) calendar.rb # Initialize the API def initialize @service = Google::Apis::CalendarV3::CalendarService.new @service.client_options.application_name = ENV["APPLICATION_NAME"] @service.authorization = authorize end calendarクラスでインスタンスが生成されるとまず、initializeメソッドが呼び出されます。 3行目でauthorizeメソッドが呼び出され認証がはじまります。 変数は全て環境変数(ENV)に格納し、githubにプッシュして漏れるリスクをなくしています。 dotenv-railsというgemをインストールすれば.envファイルで一括管理できるのでおすすめです。 間違えて大事なキーをgithubにプッシュしないように気をつけましょう。 dotenv-railsの参考記事 続いて鬼門である認証まわりを見ていきましょう。 authorizeメソッド(認証) calendar.rb def authorize # 環境変数の定義 uri = ENV["OOB_URI"] user_id = ENV["MAIL"] secret_hash = { "web" => { "client_id" => ENV["CLIENT_ID"], "project_id" => ENV["PROJECT_ID"], "auth_uri" => ENV["AUTH_URI"], "token_uri" => ENV["TOKEN_URI"], "auth_provider_x509_cert_url" => ENV["PROVIDER_URI"], "client_secret" => ENV["CLIENT_SECRET"], "redirect_uris" => [ENV["REDIRECT_URIS"]], "javascript_origins" => [ENV["JAVASCRIPT_ORIGINS"]] } } # herokuの環境的に環境変数から読み込んだほうが良い client_id = Google::Auth::ClientId.from_hash secret_hash token_store = Google::Auth::Stores::FileTokenStore.new file: "token.yaml" authorizer = Google::Auth::UserAuthorizer.new client_id, Google::Apis::CalendarV3::AUTH_CALENDAR_READONLY, token_store credentials = authorizer.get_credentials user_id if !credentials url = authorizer.get_authorization_url base_url: uri puts "Open the following URL in the browser and enter the " \ "resulting code after authorization:\n" + url code = ENV["CODE"] credentials = authorizer.get_and_store_credentials_from_code( user_id: user_id, code: code, base_url: uri ) end credentials end 公式ドキュメントと違うところはsecret_hashという変数に全て格納し、Google::Auth::ClientId.from_hash secret_hash でclient_idを入手している点です。 公式ではGoogleDeveloperからダウンロードしたjsonファイルを読み込む仕様でしたがherokuでjsonファイルを読み込むのは少々面倒なので環境変数で読み込むことは出来ないか検討し、公式のソースコードを読み漁り、hashから読み込む関数があったので書き換えました。 secret_hashの中身はすべてダウンロードしたjsonファイルの中に書いてある変数です。 uriはGoogleDeveloperにリダイレクトURLを登録したものにします。 jsonファイルから読み込む仕様で問題ない場合はこのような書き換えは必要ないです。 実際にこの関数(authorize)を実施しようとするとcredentialsがnilなのでターミナルに認証のURLが表示され、そこのURLにcodeが格納されているためコピペして環境変数(CODE)に格納します。 jsonファイルのダウンロードやredirect_uriについて詳しく知りたい方は↓におすすめの記事を載せておきます。 おすすめ記事 fetch_eventsメソッド(イベント情報の取得) calendar.rb def fetch_events calendar_id = ENV["CALENDAR_ID"] now = DateTime.now + 1 response = @service.list_events(calendar_id, max_results: 5, single_events: true, order_by: "startTime", time_min: DateTime.new(now.year,now.month,now.day,0,0,0), time_max: DateTime.new(now.year,now.month,now.day,23,59,59) ) end 認証がうまく行けばあとはイベント情報を取得するだけです。 こちらもほとんど公式ドキュメントのまんまですが公式の説明が適当すぎて分かりにくかったので軽く解説します。 calendar_idはGoogle Calendarの設定画面から取得します。 時刻は基本USタイムゾーンで返却されるので返却された時刻をJST(日本時間)に変換する必要があります。 DateTimeはrailsのゾーン設定に依存するので極力TimeWithZoneを使うのをおすすめします。 時間の扱い方にあまり慣れていない人は伊藤さんの素晴らしい記事があるので一読することをおすすめします。 RubyとRailsにおけるTime, Date, DateTime, TimeWithZoneの違い max_result : 取得したい最大のイベント数 order_by : 取得するイベントの並び順 time_min : 取得したいイベントの開始時刻の最小値(最も近い時間帯) time_max : 取得したいイベントの終了時刻の最大値(最も遠い時間帯) single_events : これはよくわかんないけどtrueにしたほうが良い(みんなそうしてる) データが取得できたら欲しいデータだけ変数に格納し、データを整形してあげましょう。 私自身ここから先のコードは別のモデルに切り出しました。 responseの値をデバック(ターミナルに吐き出して)ひとつひとつどんな情報が格納されているか観察するのをおすすめします。(response.itemsでイベントの配列が取得) 多くの人が使うであろう値をここに列挙しておきます 題名、タイトル(summary) 開始時刻(start) 終了時刻(end) 場所(location) 場所が設定されてない時や時刻が終日のときはしっかり例外処理をすることをおすすめします。 データの取得、整形 message.rb class Message def organize_from_calendar club_calendar = Calendar.new response = club_calendar.fetch_events if response.items.empty? result = '予定無し' else event =response.items.first start_time = event.start.date_time.in_time_zone('Tokyo').strftime("%H:%M") end_time = event.end.date_time.in_time_zone('Tokyo').strftime("%H:%M") location = event.location title = event.summary #要件は満たせるけど可読性が微妙 #locationがnilではない場合は○○での「で」を追加 location += "で" if location result = "明日は#{start_time}から#{end_time}まで#{location}#{title}があります。\n欠席or遅刻者は背番号+(スペース)遅刻or欠席+(スペース)理由の形式でご回答ください。\n(例)21番 欠席 授業があるため" end end end せっかくなので私が実際に書いたコードを載せておきます。 例外処理はbegin rescueを使って書いても問題ないです。 最後に 公式ドキュメントにはquickstart.rbとか書いてあるのに全然すぐにスタート出来ない&公式ドキュメントは不親切で笑えてきます。 こういうときこそ、公式のドキュメントやgithubのソースコード、英語記事を読む力が問われてきます。 私自身かなり良い訓練になりました。 この記事が少しでも皆さんのお役に立てれば幸いです。
- 投稿日:2021-05-03T13:58:43+09:00
Reactとの連携 その5 Ajax利用の基本
Reactのスクリプトを作る。 javascript:app>javascript>packs>dummy_data.jsx import React from 'react' import ReactDOM from 'react-dom' import PropTypes from 'prop-types' var target_dom = null; #① document.addEventListener('DOMContentLoaded', () => {#② target_dom = document.querySelector('#data');#③ const url = new URL(location.href); #③ let f = url.searchParams.get("name"); #④ if (f == null){ f = ''; } getData(f); }); function getData(f){ let url = "http://localhost:3000/data/ajax"; if (f != ''){ url += '?name=' + f; } fetch(url) .then( res => res.json(), (error) => { const el = ( <p>ERROR!!</p> ); ReactDOM.render(el, target_dom); } ) .then( (result) => { let arr =[]; for(let n in result){ #⑤ let val = result[n]; arr.push(<li class="list-group-item"> {val.id}:{val.name} ({val.mail})</li>); } const el = ( <ul class="list-group">{arr}</ul> ); ReactDOM.render(el, target_dom); }, (error) => { const el = ( <p>ERROR!!</p> ); ReactDOM.render(el, target_dom); } ); } 受け取ったデータをjavascriptのデータとして取り出す。 ① 変数を宣言するvar var 変数名 = 値 ②発火ポイントを決定:addEventListener ③指定されたセレクターまたはセレクターのグループに一致する、文書内の最初の Element を返す:querySelector locationとは locationとは、現在表示されているウェブページのURLを抽出したり、別のページへ遷移する場合などに便利なオブジェクト URlを操作 const url = new URL(location) で初期化 url.searchParams.set("hoge", "fuga") などで設定。URLに含まれるGETクエリ引数にアクセス プロパティ名 内容 location.href 指定したURLに画面遷移する location.protocol 現在のプロトコル情報(http:など)を取得する location.host プロトコル情報を除外したURLを取得する(port情報あり) location.hostname プロトコル情報を除外したURLを取得する(port情報なし) location.port ポート番号を取得・設定する location.pathname URLでパスの部分を取得・設定する location.search URL内のクエリ情報を抽出して取得する location.hash URL内のハッシュ情報を抽出して取得する location.origin プロトコルやポートを含めたURLを取得する JavaScript で URL のクエリパラメータを操作する方法 append( ) 指定されたキーと値のペアを新しい検索パラメーターとして追加します。 delete( ) 指定された検索パラメーターとその値を、検索パラメーターのリストから削除します。 entries( ) このオブジェクトに含まれる全てのキーと値のペアを列挙するための iterator を返します。 get( ) 指定された検索パラメーターに対応する最初の値を返します。 getAll( ) 指定された検索パラメーターに対応する全ての値を返します。 has( ) 指定された検索パラメーターが存在するかを表す Boolean 値を返します。 keys( ) このオブジェクトに含まれる全てのキーと値のペアのキーを列挙する iterator を返します。 set( ) 指定された検索パラメーターに対応する値を設定します。複数の値が存在していた場合、それらは削除されます。 sort( ) 全てのキーと値のペアを、キーを基準にソートします。 toString( ) URL で使用するのに適したクエリー文字列を返します。 values( ) このオブジェクトに含まれる全てのキーと値のペアの値を列挙する iterator を返します。 const constとは、一般的なJavaScriptの定義の宣言。varと違い、値の上書き・同一名での宣言を禁止。 const el = document.querySelector('p'); console.log(el)); // → <p>Hellow World!!</p> <例>メソッドが実行されると、定数 el に p エレメントが代入されます const 定数 = メソッド(); ↓ const 定数 = 戻り値; メソッド(や関数)が戻り値に置き換わることを「戻り値が返ってくる」といいます。 メソッドの立場からすると「戻り値を返す」わけです。そのため、“返り値” という表現をする人もいます。 getData() 計算後のデータを返します。 基本的なfatchメソッドの書き方 JSONファイルを取得するまでは下記のような流れが基本。functionを使った場合とアロー関数を使った場合の2通りがあります。 //通常のバージョン fetch('URL', { method: 'GET' }).then(function (response) { return response.json(); }).then(function (myjson) { consol.log(myjson); }).catch(function (error) { console.log(error); }); //アロー関数の場合 fetch("URL", { method: 'GET' }).then((response) => { return response.json(); }).then((myjson) => { console.log(myjson); }).catch((err) => { console.error(err); }); fetchメソッドを呼び出して、第1引数にURL、第2引数に実はオプションが付けられます。「method」プロパティに「GET」としていますが、他にも「POST」があります。ただ「GET」はデフォルトの状態なので、省略することができます。 fetchメソッドは非同期処理が可能で戻り値をPromiseオブジェクトとして返します。*詳しくは「javascript promise」などと検索すれば出てきます。 jsonメソッドでJSON形式にして返す fecthメソッドは Promiseの為、Promiseオブジェクトにあるthenメソッドで繋げて処理ができます。 fetchメソッド実行した後はその結果をResponse オブジェクトとして受け取れます。その為thenメソッドでResponseオブジェクトを受け取っています。変数は「response」としていますが、「res」とかでも大丈夫です。 Responseオブジェクトにはデータを読み取る関数としてjsonメソッドがあります。jsonメソッドを使用することでResponseオブジェクトとして受け取ったデータをJSON形式として返す事ができます。 JSONファイルをJavascriptJavascriptで扱う場合はparseメソッドでパースするのですが、どうやらResponseオブジェクトのjsonメソッドでは内部的にパースされてるようです。今回はJSONファイルだったのでjsonメソッドを使用しましたが、テキストを読み込みたい場合はtextメソッドもあります。 for - in文 一般的な構文としては次のようになります。 for( var 変数 in オブジェクト ) { //ここに繰り返し処理を書く } 変数宣言 let / const / var (1)再宣言 var、let、constの中で再宣言が可能なのはvarのみです。 //一度宣言した後に、、、 var techacademy = '宣言1回目'; //再度同じ変数名で宣言しなおせる var techacademy = '宣言2回目' let、constで同じことをしようとするとエラーが出るため、再宣言を予防することができます。 (2)スコープの違い スコープとは、簡単にいうと「変数の有効範囲」です。宣言した変数はコード内のどこでも使えるわけではなく、このスコープによって使える範囲が定められています。 varは関数スコープ varはlet、constに比べて広いスコープを持っています。 名前の通り、ある関数内でvarを用いて宣言した変数は、その関数をどこからでも呼び出すことが可能です。 let, constはブロックスコープ ブロックとは、{ }で括られた処理のことです。 let、constを使うと、同じ関数内であってもブロックの外側からは変数を呼び出せません。 (#⑤)val = result[n]で配列の要素を取り出して、先ほどのダミーデータをval.id val.name val.mailで取り出している。
- 投稿日:2021-05-03T12:00:35+09:00
Reactとの連携(4)
rails g model data name:text mail:text dataの複数形で単数形はdatumです。 モデルを用意してバリデーションを設定します。 内容はnameが空でないこととメールがからでないこと class Datum < ApplicationRecord validates :name, presence: true validates :mail, presence: true end テーブル設定してマイグレーションします。 2021050293420_create_data.rb class CreateData < ActiveRecord::Migration[6.0] def change create_table :data do |t| t.text :name t.text :mail t.timestamps end end end ダミーデータを3件ほど入力します。 db/seed.rb Datum.create(id:1, name:'Taro', mail:'taro@yamada') Datum.create(id:2, name:'Hanako', mail:'hanako@flower') Datum.create(id:3, name:'Sachiko', mail:'sachiko@happy') terminal rails db:seed 実行します。 terminal rails g controller data index ajax コントローラを作ります。 app/controllers/data_controller.rb class DataController < ApplicationController def index end def ajax if params[:name] then data = Datum.where 'name like ?', "%" + params[:name] + '%' else data = Datum.all end render plain:data.to_json end end jsonデータとしてとり出してrender plainでテキストとして送信します。
- 投稿日:2021-05-03T11:31:16+09:00
rails日本語化
このドキュメントの目的 rails全体を日本語化する 下記はdeviseのみ deviseを日本語化する https://qiita.com/you8/items/921e0dd1210eb0d158df デフォルトの言語を日本語化 config/initializers/locale.rb Rails.application.config.i18n.default_locale = :ja 多言語ファイルの導入 Gemfile gem 'rails-i18n' modelなどの日本語化 config/application.rb config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}').to_s] 上記の記述でファイルで分けた場合に読み込まれるようにする ファイル構造は上記
- 投稿日:2021-05-03T11:14:40+09:00
解決!Datetime型と文字列の比較でハマった話 & 開発環境と本番環境のdbは揃えておいた方がいいねって話
1. 環境 Ruby : 2.6.6 Rails : 6.0.3.6 db : sqLite 3 (開発環境) | postgreSQL(本番環境) 2. 背景 コロナ禍で、昼食時に会社の食堂に入る人数をコントロールする簡単な席予約アプリを作っている時にハマった話です。 views で、予約したい日付(date)と、時間帯(slot)を選ばせて、controllersのcreateアクションに送る、その際の条件として下記の4つの条件を設定。 1) 過去の日付を予約させない 2) 土日を予約させない 3) 同じ人が同じ日に予約をさせない(昼食は一日一回であろうと想定) 4) 同じ日の同じスロットに入る人数の調整 (現在はテストとして1人以上予約が入ることを阻止) 1),2)は問題なし。しかし3),4)が開発環境ではうまく動作するのに、Heroku上では動作しない。つまり同じ人が同じ日に予約できてしまい、同じ日の同じスロットに入る人数の調整ができない (テストとして1人以上予約が入ってしまう)という現象が発生。 素晴らしいメンターさんの指導で解決に至ったという話です。 3. 結論 (1) timeカラムの設定が datetime 型なので、格納されているレコードは当然datetime型。 一方viewsから送信された値は、"2020-12-11" などという文字列となる。 datetime型と文字列では比較ができない。 (2) よって viewsから送信された値"2020-12-11"を、Timeクラスのzoneメソッド(time zone をつける)とparseメソッド(文字列から日付への変換)を使用してdatetime型に変換する。 @booking_date = Date.parse(params[:booking][:date]) ↓ @booking_date = Time.zone.parse(params[:booking][:date]) (3) それでは、なぜ development環境下では動作していたものが production環境下では動作しなかったのか? どうやらこれは、development および test 環境下では sqlite3 を使用、production環境下ではPostgresQL を使用していたことに起因するようだ。development 環境下で動作したのは、sqlite3が文字列と datetime の比較をやってくれていたからで、PostgresQLはそれら型が違うものの比較はしてくれないというのが原因だった。 ・・・ということで解決! 学びとしては、development, test, production の各環境で使用するdbは同じものを使用するべきであるということ。あとはデータの型に注意ということ。 app/views/bookings/new.html.erb <div class="wrapper"> <h2>Book your table here.</h2> <%= form_with(model: @booking, local: true) do |f| %> <div class="form-group"> <%= f.label :date %> <%= f.date_field :date, class: 'form-control' %> #これで日付を飛ばすと文字列データに・・・ </div> <div class="form-group"> <%= f.label :slot %><br> <%= f.select :slot, [["11:30~11:45", "11:30~11:45"],["11:45~12:00", "11:45~12:00"], ["12:00~12:15", "12:00~12:15"],["12:15~12:30", "12:15~12:30"], ["12:30~12:45", "12:30~12:45"],["12:45~13:00", "12:45~13:00"], ["13:00~13:15", "13:00~13:15"],["13:15~13:30", "13:15~13:30"]], include_blank: "select slot" %> </div> <div class="form-group"> <%= f.submit "Book my table",class: "button-create" %> </div> <% end %> app/controllers/bookings_controller.rb def create @user = current_user @booking_date = Date.parse(params[:booking][:date]) #どうも↑がおかしい! #のちに@booking_date = Time.zone.parse(params[:booking][:date])に変更して解決! @booking_slot = params[:booking][:slot] # 予約ができる日は本日以降の未来とする if @booking_date < Date.today flash[:danger]= "Head for the future!" redirect_to new_booking_path # 日曜(0)か土曜(6)は予約をさせない。 Date.strptime(@booking_date).wday == 6 elsif @booking_date.wday == 0 || Date.strptime(@booking_date).wday == 6 flash[:danger]= "Dont' be a slave of your job!" redirect_to new_booking_path else #同じ人が同じ日に予約を入れる事を防ぐ…昼食は一日一回であろうと想定 if Booking.where(user_id: @user.id).where(date: @booking_date).any? flash[:danger]= "Double booking in the the day! Please check." redirect_to root_path #同じ日の同じスロットに入る人数の調整…現在はテストとして1人以上予約が入ることを阻止 elsif Booking.where(date:@booking_date).where(slot:@booking_slot).count >= 1 flash[:danger]= "That slot is fully occupied! Please try other slot." redirect_to new_booking_path else app/db/schema.rb ActiveRecord::Schema.define(version: 2020_10_30_034620) do create_table "bookings", force: :cascade do |t| t.datetime "date" #dateカラムにdatetime型を設定 t.string "slot" t.integer "user_id", null: false t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.index ["user_id"], name: "index_bookings_on_user_id" end end
- 投稿日:2021-05-03T09:47:25+09:00
【Rails】javascriptのfetchを使って非同期通信(POST)を実装してみた
初めに railsにデータを保存する際(post)にfetchを使って非同期通信を実現できるのではないかと思い、学習した内容をもとに纏めてみました。 ※内容に間違いなどがある場合はご指摘をよろしくお願いします。 前回の記事:https://qiita.com/redrabbit1104/items/1ce9f665a0fcd1d99bb2 https://qiita.com/redrabbit1104/items/b8b61a72f849fa3e8881 https://qiita.com/redrabbit1104/items/02bc16cf5abd4ed10ec1 https://qiita.com/redrabbit1104/items/c131c46897bfdf86e08b https://qiita.com/redrabbit1104/items/806ef9849f4b0962ceaa やりたいこと railsの投稿formから入力した内容をfetchを使ってrequest(post)を送り、保存されたデータのresponseをブラウザに表示させること。 前提 Macでrailsアプリの新規作成及びdatabaseの作成などが終わっている。 作業手順 ①tableを準備(modelを作成) ②routing ③view ④controller ⑤javascriptを用意する ⑥結果 tableを準備(modelを作成) まずはデータを保存するためのtableを用意します。テーブル名はtestとしました。 ❯ rails g model test invoke active_record create db/migrate/20210502224447_create_tests.rb create app/models/test.rb invoke rspec create spec/models/test_spec.rb invoke factory_bot create spec/factories/tests.rb 生成された20210502224447_create_tests.rbファイルを開き、カラム名がnameでstring型のとてもシンプルなカラムを作ります。 class CreateTests < ActiveRecord::Migration[6.0] def change create_table :tests do |t| t.string :name t.timestamps end end end migrateファイルが出来上がりましたので、migrationしテーブルを生成します。 ❯ rails db:migrate == 20210502224447 CreateTests: migrating ====================================== -- create_table(:tests) -> 0.0399s == 20210502224447 CreateTests: migrated (0.0400s) ============================= routing railsを起動したらトップページに入力formを表示させたいので、その記述をします。また、testモデルは画面に表示するためのページ(view)と保存するためのcontroller側の記述が必要なため、resourcesをindexとcreateだけにします。 Rails.application.routes.draw do root "tests#index" resources :tests, only: [:index, :create] end view 画面に表示するためのページを作成します。この時にrails g controller testというコマンドでcontrollerファイルも一緒に生成します。 ❯ rails g controller tests create app/controllers/tests_controller.rb invoke erb create app/views/tests invoke rspec create spec/requests/tests_spec.rb invoke helper create app/helpers/tests_helper.rb invoke rspec create spec/helpers/tests_helper_spec.rb invoke assets invoke scss create app/assets/stylesheets/tests.scss 生成されたapp/views/testsフォルダにindex.html.erbファイルを作り、以下のように記述します。 #responseとして戻ってきた内容を描画するところ <div id="list"> </div> #testテーブルに保存されたnameカラムを全て表示 <% @tests.each do |test| %> <%= test.name %> <br> <% end%> #testテーブルにnameを投稿するための入力フォーム <%=form_with url: "/tests" , method: :post do |form| %> <%= form.text_field :test %> <%= form.submit '投稿する' %> <% end %> fetchで非同期通信をして戻ってきたresponseを描画するところと、テーブルに保存されたデータを表示させる場所、そしてtestテーブルにnameデータを保存するための入力フォームを用意しました。 controller indexアクションにはtestテーブルに保存されているnameを全て取得し、@testsに格納します。また、createアクションにはフォームから入力されたnameを保存し、json形式でresponseを返す処理を記述します。 class TestsController < ApplicationController def index @tests = Test.all.order(id: "DESC") #テストテーブルのデータを全て取得し、@testsに格納 end def create name = Test.create(name: params[:name]) #フォームから入力したnameをtestテーブルに保存。その結果を変数testに格納。 render json:{ test: name } #変数testに格納されているnameデータをjson形式でresponseとして返す。 end end javascriptを用意する app/javascriptの直下にtest.jsを作成します。また、app/javascript/packsのapplication.jsファイルを開き、先ほど作成したtest.jsを読み込むようにします。 require("@rails/ujs").start(); require("turbolinks").start(); require("@rails/activestorage").start(); require("channels"); require("../test"); //追記 test.jsには以下のように記述します。test関数を用意してフォームの投稿するボタンをクリックするとfetchで非同期通信が行われます。responseはjson形式で戻ってきて、受け取ったデータをlist要素の後に追加します。 //test関数の定義 function test() { const submit = document.querySelector(".submit_test"); if (!submit) { return false; } submit.addEventListener("click", (event) => { const formData = new FormData(document.querySelector(".test_form")); const url = "/tests"; const post_options = { method: "post", body: formData, }; fetch(url, post_options) .then((response) => { return response.json(); }) .then((item) => { const list = document.getElementById("list"); const formText = document.querySelector(".input_name"); const HTML = ` <div>${item.test.name}</div> `; list.insertAdjacentHTML("afterend", HTML); formText.value = " "; }); event.preventDefault(); }); } window.addEventListener("load", test); ここで注意したいのは、responseのitemはpromiseであり、その中にtestというオブジェクトが格納されています。 testというオブジェクトにはデータを保存する際のパラメーターがハッシュの形で入っています。nameカラムのデータに接続するにはitem.nameではなく、item.test.nameにする必要があります。 結果 railsを立ち上げ、投稿してみます。投稿した内容がページの更新をしなくても表示されるようになりました。
- 投稿日:2021-05-03T07:40:43+09:00
【Rails6 × React】devise_auth_tokenでのSNS認証時に初回ログインの場合のみ特定の処理を促す
環境 API Rails6.0 フロント React 目的 Omniauthを用いたSNS認証でのユーザー登録で初回ログイン時にSNS上のアカウント情報から一部変更したり追加情報の入力を促したりしたい。 devise_auth_tokenを用いているが、deviseでも基本は同じように設計していいと思う。 実装方法 ①usersテーブルにTrackable系のカラムを追加する class AddColumnsToUsers < ActiveRecord::Migration[6.0] def change add_column :users, :sign_in_count, :integer, default: 0, null: false add_column :users, :current_sign_in_at, :datetime add_column :users, :last_sign_in_at, :datetime add_column :users, :current_sign_in_ip, :string add_column :users, :last_sign_in_ip, :string end end これらのカラムを追加。上から順に サインイン回数 現在のログインをした日時 最後にログインした日時 現在のログイン元IPアドレス 最後のログインしたときのログイン元IPアドレス の情報を持っている ②models/user.rbにてtrackableを設定 class User < ActiveRecord::Base # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable devise :database_authenticatable, :registerable, :trackable, #←これ :recoverable, :rememberable, :validatable, :omniauthable, omniauth_providers: [:twitter] モデルにも設定が必要。逆に言うとこれだけでサインイン時に勝手にカラムに値が入っていく。 ③omniauthでの認証時にユーザーが初回ログインかどうかを判定する def render_data_or_redirect(message, data, user_data = {}) # 初回ログイン時にはcookieに情報をセット。Oauth認証後ユーザー情報の編集ページに飛ばす cookies[:first_session] = { value: true, path: root_path, expres: 10.minutes } if @resource.sign_in_count == 0 devise_auth_tokenではomniauth_callbacksコントローラーでユーザーが認証できた場合に render_data_or_redirectメソッドが呼ばれる。このメソッドでアプリ側のページに返す直前で 保存したユーザーのsign_in_countが0の場合にCookie上にfirst_sessionという値を仕込む。期限は10分とかにしとく ④フロント側でCookieから初回ログインのtmpデータを取得する class App extends React.Component { componentDidMount() { const cookies = new Cookies(); // ユーザー認証が済んでいる場合 if (authToken) { if (cookies.get('first_session')) { // 実際にはユーザー情報編集ページに飛ばす処理を入れる。次のブランチで alert('初回ログイン') } // 中略 return authToken } else { return null } } 今回はルートコンポーネントのAppでログイン済みかどうかの判定をするので、 そこにcookieの中にfirst_sessionが存在しているかどうかの判定を行う条件式を実装。 なお、Cookieを読み込むライブラリとしては事前にuniversal-cookieを導入している。 npm install universal-cookie ⑤2回目以降のログイン時にCookieのtmpデータを削除する tmpデータの有効期限が10分とかにしてるので、10分以内に2回ログインされることも想定して2回目以降のログインでは Cookieのデータを削除するようにした。 # 2回目以降のログイン時にはfirst_sessionのクッキーデータを削除 cookies.delete(:first_session, path: root_path) if cookies[:first_session] 挙動 ここまでの流れで挙動は以下のようになる。 感想 思ったよりかんたんにできた。 Trackable系のカラムちゃんと使ったことなかったのでこういう使い方ができるのか、と感動した。 Cookieを使って条件分岐しているのでセキュリティ的にどうなの?とかはあるけど一旦置いておく。
- 投稿日:2021-05-03T00:21:36+09:00
Paypal,Stripe決済後のサンキューメールを全自動かつ無料で送れるアプリを作った話。
初めて投稿させていただきます、Takaoです。 素人なので間違っている点もあると思いますが、温かい目で見ていただけると嬉しいです。 今回は僕が作成したPayPal,Stripe決済後に自動でサンキューメールを送信するアプリケーションについて書きたいと思います。 以下のリンクからアクセスできます。 PalPalMailer 僕がPaypalやStripeで商品を販売しようとした際に、決済後のサンキューメールを送るアプリをいろいろ探したのですがなかなかしっくりくるのが見つかりませんでした。(というか値段が高い涙)。 僕は商品ごとに送信するテンプレートが選択でき、ステップメール機能もあるアプリが欲しかったのですが、なかなか見つからず(MyASPというサービスを使えばできるようだが月額3000円以上と値段が少し高い)仕方なく自分でこのPalPalMailerを作成しました。 PalPalMailerは無料で決済後のメールを送ることができ、さらに商品ごとに違うテンプレートを送信したり、違うシナリオのステップメールを送ることもできます。 送信したメールが届いたかや開かれたかなどメールのアクションを追跡する機能も付いています。 自分でほしいと思った機能はほとんどつけられたと思います。 アプリの紹介はこれくらいにしてこのアプリに使用した技術について書きたいと思います。 使用した技術 PalPalMailerはとにかくお金をかけずに作成することにこだわりました。 フレームワーク:Ruby on Rails データベース:mongodb atlas プラットフォーム:Heroku メール送信システム:Sendgrid 決済システム:Stripe フレームワーク まずフレームワークはRuby on Railsを選択しました。最初はRails APIでバックエンド、フロントをReactで構成しようと考えていたのですが、認証周りのセキュリティでどうするのが安全なのかわからず結局Rails単体で構成しました(jwtを使った認証があるそうなのですがlocal storageに保存すべきか否かや、Cookieを使用すべきかで意見が分かれる。)。どうするのが正解なのかわかる方がいたら教えていただけると嬉しいです。 認証はdeviseを使用し、権限管理にはcancancanを使用しました。 実装にあたってこちらの記事を参考にさせていただきました。 [Rails] deviseの使い方(rails6版) [Rails5]cancancanってなんぞ? データベース データベースはMongoDB Atlasを選択しました。理由は単純でHerokuのデータベースのpostgreSQLやMySQLは20000行までしか無料でないので無料で512MBのM0 Sandboxが使えるMongodb Atlasを選択しました。HerokuでMongodb Atlasを利用するうえでこちらのサイトが参考になりました。 HerokuでMongoDB Atlasを使う方法 RailsでMongoDBを操作するためにMongoidというgemを使ったのですが若干RailsのActiveRecordと挙動が違って少し苦労しました。いろいろ詰まった時はこちらのサイトを参考にさせていただきました。 Mongoidの挙動の違い Mongoid プラットフォーム 言わずと知れたPaaSのHerokuを利用しました。利用方法についてはほかの方が詳しく書かれているので他の方の記事を参考にしてください。無料で扱うためにクレジットカードを登録して1000時間のdyno時間を使用しています。dynoのスリープを防ぐためにUptimeRobotを使用しています。 UptimeRobotについては以下の記事がとても参考になりました。 UptimeRobot メール送信システム メール送信システムにはSendgridを選択しました。最初はGmailを使用する予定だったのですが、SMTPやAPIでは1日当たり100通までしか送信できないので無料で月間で12000通送信できるSendegridを選択しました。実装に当たってはこちらのサイトが参考になりました。 Rails Sendgrid API これは利用していて気付いたのですがSendgridはアメリカに本社があり、日本語で書かれたサイトは日本の代行業者(構造計画研究所)が運営しているそうで、日本の代行業者でアカウントを作成するのとアメリカの本社のサイトでアカウントを作成するのとでは送信数や利用できるサービスなどに大きく差があります。料金プランなどを見ていて結構混乱したので利用しようとしている方は気を付けてください。 決済システム PalPalMailerを作成するうえで最も苦戦したのは決済システムの実装でした。色々調べましたがStripeが公式ドキュメントも充実しており、導入しやすそうだったので使用することにしました。しかし公式のドキュメントはSinatraで書かれているため公式のドキュメントはあまり参考にならず、こちらの記事を参考にしました。 Rails Stripe実装 StripeはRailsのドキュメントこそ少ないですが仕様や利用法などはきれいにまとめられていて調べて解決することがほとんどだったので今後も使用したいと思います。 最後に PalPalMailerについてはサイトのフォームからお問い合わせお願いいたします。 あと間違っている点があったら教えていただけると嬉しいです。