- 投稿日:2020-01-17T23:38:14+09:00
Railsで起きたRoutingErrorの対処について
RailsでRoutingErrorが起きた。
今回の対処法
至って問題はシンプルで、<%= %>内のpathに問題があった。
具体的には
example.html.erb<%= link_to "Delete", post_path(@post), method: :delete, data:{confirm: "Are you sure?"}, class:"button is-danger" %>としないといけなかったところで、
example.html.erb<%= link_to "Delete", edit_post_path(@post), method: :delete, data:{confirm: "Are you sure?"}, class:"button is-danger" %>としてしまっていた。
今後の対策
RoutingErrorが起きたら、まずPathを確認する。
- 投稿日:2020-01-17T23:14:44+09:00
【Tech::Expert】 WindowsでRuby開発 だいぶ 土台が出来てきました
WindowsでRuby開発 だいぶ 土台が出来てきました
・Ruby Installer
環境依存が結構出るかも
テキスト内のインストール方法よりも独自で考察しなければならない・Docker、Cloud9
上記同様に準備を自分でやらなければならない・Macをサーバーとしてシェルのみ使用
現在これが最適。
環境のインストールはMacでテキスト通りに行いSSHでアクセス。
エディタ・ブラウザ・ターミナル・メモ帳などは、全て使い慣れたWindows上で行える。実物 Mac、バーチャルマシン上のMac、両方試した結果、それぞれの感じは ↓
・Mac Book Pro
机の上に出しておくと場所を取るので、クローゼットに仕舞っておき、TeamViewerで遠隔操作
クラムシェルモードのように、蓋を閉じても電源が切れないよう設定
動作は快適だが、今後を考えると、VMの方が有利
端末自体が邪魔なので、Windows内に取り込みたい・VM上のMac (ハッキントッシュ)
環境の複製が自由自在
スナップショットが取れるので、色々試した後の復元が簡単
色々試してもAppleのサポート切れなど気にしなくて良い
サーバー化した際、Windowsとのフォルダ共有も簡単 (ディレイが少ない)
一切場所を取らない→ 現状、VMが有利
- 投稿日:2020-01-17T18:17:22+09:00
RubyでLeetCodeを解いてみた Reverse Integer
https://leetcode.com/problems/reverse-integer/
Given a 32-bit signed integer, reverse digits of an integer.
# @param {Integer} x # @return {Integer} def reverse(x) if x.positive? y = x.to_s.reverse.to_i else y = x.to_s.reverse.to_i*-1 end return 0 if ( y > 2147483646 || y < -2147483647) y endrubyはover flowの扱いが特殊みたいで
生々しい実装になってしまいました???
- 投稿日:2020-01-17T17:20:22+09:00
【Rails】SNSアプリのコメント削除機能の実装
参考 URL
https://qiita.com/takahisa_s/items/e6d9713eb9dc0f85f66d
参考も何も理解するには上記の記事を見るだけで十分なのですが、自分用の備忘録として今回の記事を作成いたします。前提
私の書いたコメント作成機能の記事の続きです。
https://qiita.com/Zhongcun/items/42b15f045a363ec82af5また、【削除できるコメントは自分が作成したコメントのみ】とします。
ルーティング
routes.rbresources :microposts, only: [:create, :destroy, :show] do resources :comments, only: [:create, :destroy] endコントローラー
コメント削除ボタンに関わることなので、micropostのコントローラの記述もしておきます。
microposts_controller.rbdef show @micropost = Micropost.find(params[:id]) @comments = @micropost.comments.includes(:user) @comment = @micropost.comments.build # form_with 用 end省略されている部分に関しては先述の前回の記事を参考にお願いします。
comments_controller.rbclass CommentsController < ApplicationController before_action :require_user_logged_in before_action :correct_user, only: [:destroy] def create @micropost = Micropost.find(params[:micropost_id]) @comment = @micropost.comments.build(comment_params) @comment.user_id = current_user.id 〜〜〜 省略 〜〜〜 end def destroy @comment.destroy flash[:success] = '投稿へのコメントを削除しました。' redirect_back(fallback_location: root_path) end private def comment_params params.require(:comment).permit(:content) end def correct_user @comment = current_user.comments.find_by(id: params[:id]) unless @comment redirect_to root_url end end end
def destroy
で@comment
を定義しなくていい理由は、createで定義した@comment
を使いまわしているためです。
redirect_back
とありますが、投稿の詳細ページでのみコメントを削除するため投稿詳細ページに戻るだけです。また
def correct_user
により、削除できるコメントは自分がしたコメントのみになります。コメント削除のルーティングの確認
$ rails routesコメント削除に必要なPrefixと渡すべき値を確認します。
micropost_comment DELETE /microposts/:micropost_id/comments/:id(.:format) comments#destroy削除するには
micropost_comment_path
を使い、
特定のmicropostとcommentのidを渡す必要があります。ビューファイル
投稿詳細ページ
このファイルの
@micropost
だけを心に留めておいてください。microposts/show.html.erb<ul class="list-unstyled"> <li class="media mb-3"> <img class="mr-2 rounded" src="<%= gravatar_url(@micropost.user, { size: 50 }) %>" alt=""> <div class="media-body"> <div> <%= link_to @micropost.user.name, user_path(@micropost.user) %> <span class="text-muted">posted at <%= @micropost.created_at %></span> </div> <div> <p><%= @micropost.content %></p> </div> <div class="btn-group"> <% if current_user == @micropost.user %> <%#=投稿詳細ページで投稿を削除するとid見つからないエラー発生 link_to "Delete", @micropost, method: :delete, data: { confirm: "You sure?" }, class: 'btn btn-danger btn-sm' %> <% end %> <%= render 'favorites/favorite_button', micropost: @micropost %> </div> </div> </li> </ul> <%# コメント入力フォームのパーシャル %> <%= render 'comments/form', micropost: @micropost %> <%# コメント一覧のパーシャル %> <%= render 'comments/comments', micropost: @micropost %>コメント一覧のパーシャル(ここにコメント削除ボタン実装)
<%# コメント一覧用 %> <ul class="list-unstyled"> <% @comments.each do |comment| %> <li class="media mb-3"> <img class="mr-2 rounded" src="<%= gravatar_url(comment.user, { size: 50 }) %>" alt=""> <div class="media-body"> <div> <%= link_to comment.user.name, user_path(comment.user) %> <span class="text-muted">posted at <%= comment.created_at %></span> </div> <div> <p><%= comment.content %></p> </div> <div class="btn-group"> <% if current_user == comment.user %> <%# ここにコメント削除ボタン %> <%= link_to "Delete", micropost_comment_path(@micropost, comment), method: :delete, data: { confirm: "You sure?" }, class: 'btn btn-danger btn-sm' %> <% end %> </div> </div> </li> <% end %> <%#= paginate comments %> </ul>削除するにはURLに
micropost_comment_path
を使い、
@micropost, comment
の2つを渡す必要があります。
@micropost
はコントローラのshowで定義されているもので、
comment
はコメント一覧パーシャルの
<% @comments.each do |comment| %>
の|comment|
を基にしています。何か間違っていることがあれば、コメントや編集リクエストをいただけると幸いです。
- 投稿日:2020-01-17T16:36:33+09:00
RubyでLeetCodeを解いてみた Two Sum
Given an array of integers, return indices of the two numbers such that they add up to a specific target.
You may assume that each input would have exactly one solution, and you may not use the same element twice.
Example: Given nums = [2, 7, 11, 15], target = 9, Because nums[0] + nums[1] = 2 + 7 = 9, return [0, 1].意外とむずいよね
# @param {Integer[]} nums # @param {Integer} target # @return {Integer[]} def two_sum(nums, target) nums.each_with_index do |num, idx| x = target - num if num == x y = nums.rindex(x) next if idx == y return [idx, y] end y = nums.index(x) next if y == nil return [idx, y] end end一応通りましたが、酷いコード?
- 投稿日:2020-01-17T16:36:33+09:00
RubyでLeetCodeを解いてみた Two Sum
https://leetcode.com/problems/two-sum/
Given an array of integers, return indices of the two numbers such that they add up to a specific target.
You may assume that each input would have exactly one solution, and you may not use the same element twice.
Example: Given nums = [2, 7, 11, 15], target = 9, Because nums[0] + nums[1] = 2 + 7 = 9, return [0, 1].意外とむずいよね
# @param {Integer[]} nums # @param {Integer} target # @return {Integer[]} def two_sum(nums, target) nums.each_with_index do |num, idx| x = target - num if num == x y = nums.rindex(x) next if idx == y return [idx, y] end y = nums.index(x) next if y == nil return [idx, y] end end一応通りましたが、酷いコード?
- 投稿日:2020-01-17T15:09:10+09:00
【初心者向け】Rubyの配列を使用して、オリジナル本登録アプリを作ってみた。
Rubyの配列を使用した学習をします。
配列が便利ということで、配列を使用して、スクールでおそらくやってない配列を使用した本登録アプリケーションを作ります。
条件
・本の名前を登録
・値段を登録
・booksという配列に、本の名前(title)と値段(price)をいれていく
・登録が済んだら、登録したデータを取り出すメソッドも作成する
・登録、一覧表示を繰り返し表示させられるよう、メソッドを書いていく今回は擬似コードなしで書いていきます。
books = [] def register(books) puts "本のタイトルを記載してください" book_title = gets.chomp puts "本のタイトルを#{book_title}で登録しました" puts "販売したい値段を記載してください" price = gets.to_i books.each do |book_title, price| puts "#{book_title}を#{price}円で登録しました" end book = [book_title, price] books << book end def book_list(books) puts "一覧から番号を選んでください" index = 1 books.each do |book| puts "#{index}:#{book[0]}" index += 1 end input = gets.to_i show(books[input-1]) end def show(books) puts "本のタイトルは#{books[0][0]}" puts "値段は#{books[0][1]}円" end while true do puts "番号を選択してください" puts "1:本の登録" puts "2:本の詳細" puts "3:終了する" case gets.to_i when 1 register(books) when 2 book_list(books) when 3 exit else "無効な値です" end end今回は登録したものが、そのまま本の詳細として表示される使用になっています。
あれ、しかも、動作確認すると、番号を選択してください 1:本の登録 2:本の詳細 3:終了する 2 一覧から番号を選んでください 1:qwe 1 本のタイトルはq 値段はw円になっている。値段どこいったw
ん、これはどうしたものか???2分くらいで改修できました。
books = [] def register(books) puts "本のタイトルを記載してください" book_title = gets.chomp puts "本のタイトルを#{book_title}で登録しました" puts "販売したい値段を記載してください" price = gets.to_i puts "#{book_title}を#{price}円で登録しました" book = [book_title, price] books << book end def book_list(books) puts "一覧から番号を選んでください" index = 1 books.each do |book| puts "#{index}:#{book[0]}" index += 1 end input = gets.to_i input = input - 1 puts "本のタイトルは#{books[input][0]}" puts "値段は#{books[input][1]}円" puts "--------------------------" end while true do puts "番号を選択してください" puts "1:本の登録" puts "2:本の詳細" puts "3:終了する" case gets.to_i when 1 register(books) when 2 book_list(books) when 3 exit else "無効な値です" end endしょーもない処理をしてしまっていたことに気づいた点
① registerで使ってたeach文いらんやん
② book_listメソッドでまとめて処理できるやん
③ メソッドをまとめたことによってinput変数で繰り返し処理できるやん
④ 動作確認すると、みずらい・・・・横棒いれよう!!念の為、動作確認
番号を選択してください 1:本の登録 2:本の詳細 3:終了する 1 本のタイトルを記載してください ビジョナリー・カンパニー 本のタイトルをビジョナリー・カンパニーで登録しました 販売したい値段を記載してください 3800 ビジョナリー・カンパニーを3800円で登録しました 番号を選択してください 1:本の登録 2:本の詳細 3:終了する 1 本のタイトルを記載してください ビジョナリー・カンパニー飛躍の法則 本のタイトルをビジョナリー・カンパニー飛躍の法則で登録しました 販売したい値段を記載してください 4000 ビジョナリー・カンパニー飛躍の法則を4000円で登録しました 番号を選択してください 1:本の登録 2:本の詳細 3:終了する 2 一覧から番号を選んでください 1:ビジョナリー・カンパニー 2:ビジョナリー・カンパニー飛躍の法則 2 本のタイトルはビジョナリー・カンパニー飛躍の法則 値段は4000円 -------------------------- 番号を選択してください 1:本の登録 2:本の詳細 3:終了する 2 一覧から番号を選んでください 1:ビジョナリー・カンパニー 2:ビジョナリー・カンパニー飛躍の法則 1 本のタイトルはビジョナリー・カンパニー 値段は3800円 --------------------------2つ登録してみました。
動きましたね。プログラミング楽しいですね。
ちなみに、知らない方いたらなんですが、ビジョナリー・カンパニー飛躍の法則、個人的におすすめ本です。
優良企業がなぜ優良企業なのか? 科学的(統計的)に分析した結果がわかります。ここは違う、ここはこうしたほうがセンスが良い等々ございましたらご指摘いただけますと幸いです。
最後までみていただき、ありがとうございますm(_ _)m
- 投稿日:2020-01-17T14:30:54+09:00
同じテーブル内に複数の外部キーを設定する方法!!
はじめに
某プログラミングスクールで、担当した実装を復習していきたいと思います。
今回は、出品・取引中・売却済みのこの3つをクリックした際に、それぞれにあった商品を
表示させる実装を行いました。これを実装するにあたって、1つのテーブル内に複数の外部キーを設定する必要があり、
ここで詰まったため、記録として残していきます。工程
今回は、工程を以下に分けて説明をしていきます。
1.実装の大まかな説明とマイグレーションファイルの作成
2.モデルの作成
3.コントローラーの作成
4.hamlでの条件分岐設定
の順で行っていきます。
少し、長いですががんばっていきましょう。解説
1.実装の大まかな説明とマイグレーションファイルの作成
はじめに出品中・取引中・売却済みを区別するために、
productというテーブル内に、seller_id・auction_id・buyer_idという
userと紐づく外部キーを3つ設定しました。そして、出品中の際には、productのレコードの中から
seller_id(出品者)にだけ値が入っているレコードをDBから引っ張って来ています。取引中の場合は、seller_id(出品者)とauction_id(取引者)がいる
productのレコードをDBから引っ張ってきています。売却済みの場合は、seller_id(出品者)とbuyer_id(買取者)がいる
productのレコードをDBから引っ張って来ることで、
それぞれを区別してDBから取得してきています。マイグレーションファイルはこんな感じです。
*今回の実装であれば、user側はテーブルを作成しidがあればOKだと思います。products.rb(マイグレーションファイル)class CreateProducts < ActiveRecord::Migration[5.2] def change create_table :products do |t| t.string :name, null: false t.references :seller, foreign_key: {to_table: :users} t.references :buyer, foreign_key: {to_table: :users} t.references :auction, foreign_key: {to_table: :users} t.timestamps end end end「詰まったポイント その1」
(1)foreign_key: {to_table: :users}
通常であれば、t.references :user, foreign_key: trueforeign_key: trueのみで外部キーを設定できるのですが、
今回のように、同じテーブル内に複数の外部キーを設定する場合、
foreign_key: trueで定義してしまうと、
カラム名がテーブル名_idになってしまうため、
複数カラムを設定したいときにうまくいかないことがありました。そのため、{to_table: :テーブル名}で今回使用するテーブルを直接指定する必要があるようです。
2.モデルの作成
product.rbclass Product < ApplicationRecord belongs_to :seller, class_name: "User", optional: true,foreign_key: "seller_id" belongs_to :buyer, class_name: "User", optional: true,foreign_key: "buyer_id" belongs_to :auction, class_name: "User", optional: true,foreign_key: "auction_id" end各、外部キーをuserとアソシエーションを組んでいます。
user.rbclass User < ApplicationRecord has_many :saling_items, -> { where("seller_id is not NULL && buyer_id is NULL") }, class_name: "Product" has_many :sold_items, -> { where("seller_id is not NULL && buyer_id is not NULL && auction_id is NULL") }, class_name: "Product" has_many :auction_items, -> { where("seller_id is not NULL && auction_id is not NULL && buyer_id is NULL") }, class_name: "Product" end次に、user.rbに焦点を当てて説明をしていきます。
user.rbhas_many :saling_items, -> { where("saler_id is not NULL && buyer_id is NULL && auction_id is NULL") }, class_name: "Product"この1文は、出品中のアイテムをproductのレコードから取得するための記述となっています。
今回でいう、出品中の商品とは言い換えると、「seller_id(出品者)はいるが、まだ、buyer_id(買取者)またはauction_id(取引者)はいないproductのレコード」
を取得すればいいという形となるため、
上記のwhereの記述で制限することで、:saling_itemsカラムには
出品中の商品のみが取得できるという感じです。user.rbhas_many :auction_items, -> { where("seller_id is not NULL && auction_id is not NULL && buyer_id is NULL") }, class_name: "Product"次に、取引中の商品の記述になります。
取引中は言い換えると「seller_id(出品者)とauction_id(取引者)のユーザーが存在し、buyer_id(買取者)はまだ存在していないproductレコード」
という形となるため、
上記のwhereでの制限となっています。user.rbhas_many :sold_items, -> { where("seller_id is not NULL && buyer_id is not NULL && auction_id is NULL") }, class_name: "Product"最後に、売却済みの商品の記述です。
売却済みは言い換えると「seller_id(出品者)とbuyer_id(買取者)は存在するが、auction_id(取引者)は存在していないproductレコード」
ということになるため、
上記のwhereでの制限となっています。これで、とりあえずはproductとuser間のアソシエーションは終了です。
3.コントローラーの作成
1.2の記述で、マイグレーションファイルとアソシエーションを組んだため、
コントローラーでその取得したデーターを取り出す記述を行っていきます。products.controller.rb(必要な箇所のみ記載)class ProductsController < ApplicationController before_action :set_current_user_products,only:[:p_transaction,:p_exhibiting,:p_soldout] before_action :set_user,only:[:p_transaction,:p_exhibiting,:p_soldout] def p_exhibiting #出品中のアクション end def p_transaction #取引中のアクション end def p_soldout #売却済みのアクション end private def set_current_user_products if user_signed_in? @products = current_user.products.includes(:seller,:buyer,:auction,:product_images) else redirect_to new_user_session_path end end def set_user @user = User.find(current_user.id) end end*product.conrollerで行っていますが、productとuserでネストをしている場合は、
user.controllerへ上記の記載をしても大丈夫だと思います。
*current_userを使用しているため、ログインしていない場合idがないため、
エラーが出てしまうことがあります。
その際は、DBへの直打ち等でユーザーを存在させる必要があると思います。(ここはあまり自信がないので、この方法でエラーをはいてしまったら、すみません。)【解説】
@user = User.find(current_user.id)この1行によって、まずはログインしているユーザーのレコードを
取得している形となっています。if user_signed_in? @products = current_user.products.includes(:seller,:buyer,:auction,:product_images) else redirect_to new_user_session_path endこの記述によって、ログインしているユーザーが所持しているproductレコードのみを取得していきます。
「詰まったポイント その2」
(1)上記で、指定のuserやprodutのレコードの取得はできた。
だが、そもそもproductテーブルに複数のカラムを指定したが、どうやって・どのタイミングで狙ったidへいれるのかがわかりませんでした。「解決策」
products.controller.rbdef new @product = Product.new @product.product_images.new end def create @product = Product.new(product_params) if @product.save redirect_to root_path else redirect_to new_product_path,data: { turbolinks: false } end end private def product_params params.require(:product).permit(:name product_images_attributes: [:image, :_destroy]).merge(seller_id: current_user.id) #productやご自身のカラムに合わせて変更してください。 endまず、seller_idとは、出品者がもつidなため、出品する段階のnew・createの段階で、
そのユーザーが持っているidをseller_idへいれることによって解決しました。@product.update(buyer_id: current_user.id)また、buyer_idに関しては、上記の一行を購入する画面でいれることによって実装しました。
4.hamlでの条件分岐設定
最後に、コントローラーで取得してきた値を繰り返し処理する記述を加えていきます。上記の画像のように、productに指定したレコードがある場合と、
ない場合で表示の仕方を変更する必要があるため、以下でif文による条件分岐を行っていきます。p_exhibiting.html.haml(一部のみ表示しています。)- if @user.saling_items.present? - @user.saling_items.each do |product| = link_to product_path(product),data: { turbolinks: false },class:"item_content" do .item_content__image = image_tag product.product_images[0].image.to_s,size:"58x48" .item_content__right .item_content__right--name =product.name .item_content__right__good .item_content__right__good--goods = icon("far","heart") 0 .item_content__right__good--comment = icon("far","comment-alt") 0 .item_content__right__good--exhibition 出品中 = icon('fas', 'angle-right', class: 'item_content__icon') - else .pmain__bottom = image_tag "", class: "pmain__bottom--img", size: "100x100" .pmain__bottom--text 出品中の商品がありません*今回は、長いため出品中のみの記載としています。
特に重要な部分を記載していきます。
- if @user.saling_items.present?この一行で、userのsaling_itemsがある場合は以下に記述した
- @user.saling_items.each do |product|のsaling_itemsを繰り返すようにしています。
以上です。
最後に
長い行を読んでいただきありがとうございました。
所々、切り抜いて記事を書かせて頂いているため、間違っている箇所があった際には、
私の記述でエラーを起こしてしまい申し訳ありません。
また、間違っている箇所がありましたら、コメントをいただけると幸いです。
ご視聴ありがとうございました。
- 投稿日:2020-01-17T11:41:05+09:00
【第9章】Railsチュートリアル 5.1(第4版) 発展的なログイン機能
はじめに
筆者は非IT業界から独学でRails学習中で、備忘録目的で執筆しています。
個人的に本章はこれまでの章より難易度が高かったです?(2周目確認・整理が必須と感じました、不足等あれば訂正致しますのでおっしゃっていただければ幸いです)
筆者は安川さん講義の動画版を模写するような形で学んだため、演習でなく全体を流す程度に参考にしてもらえたら嬉しいです。<参考>
Ruby on Rails チュートリアル 第9章 永続的セッション(cookies remember me 記憶トークン ハッシュ)を解説
個人的に非常に理解しやすい記事でした。
(Cookieの攻撃手法などは改めて追記したいほどです)9.1 Remember me 機能
Remember me とは
主にアカウント認証に用いられる機能の一つで、ユーザーがログイン時に入力したアカウント情報をサーバ側で持つ(UI視点での)機能。
ログインのときに「このユーザーIDとパスワードを情報を記録しますか?」のあれ。< 参考 >
認証におけるRemember Meの仕組み
Remember-Me認証とSpringSecurity今回のRemember me導入にはCookiesを用いる。
Cookies
Cookiesはユーザー(クライアント側)のブラウザにあるデータ保存領域。
<参考>
セッションとかクッキーとかよくわからないのでRailsチュートリアルでWebアプリケーション作りながら勉強してみた前記事8章の基本的なログイン機構のsessionメソッドではユーザーIDを保存できたが、この情報はブラウザを閉じると消えてしまう。
なので今回はセッションの永続化の第一歩として記憶トークン (remember token) を生成しcookiesメソッドによる永続的Cookiesの作成や、安全性の高い記憶ダイジェスト (remember digest) によるトークン認証にこの記憶トークンを活用し、セキュリティを考慮して以下の方針で永続的セッションを作成する。(公式より)< 手順 >
1. 記憶トークンはランダムな文字列を生成して用いる。
2. ブラウザのcookiesにトークンを保存するときは有効期限を設定する。
3. トークンはハッシュ値に変換してからDBに保存する。
4. ブラウザのcookiesに保存するユーザーIDは暗号化しておく。
5. 永続ユーザーIDを含むcookiesを受け取ったら、そのIDでDBを検索し、記憶トークンのcookiesがDB内のハッシュ値と一致することを確認する。
※ トークンとはパスワードの平文と同じような秘密情報(コンピューターが作成・管理する情報)のこと。
ブランチ作成&チェックアウトgit checkout -b advanced-login
まずはパスワードダイジェストと同じように記憶トークンを保存する場所remember_digestの作成$ rails generate migration add_remember_digest_to_users remember_digest:string
確認後、$ rails db:migrate
手順1の記憶トークンのためのランダム文字列を出す。urlsafeはURLで使える文字列をランダムで生成するもの。これでできた文字列をユーザーさんのブラウザに置かせてもらう。トークン生成用メソッドを追加する
app/models/user.rb# ランダムなトークンを返す def User.new_token SecureRandom.urlsafe_base64 end
rememberメソッドを追加する。このメソッドは、記憶トークンをユーザーと関連付け、記憶トークンに対応する記憶ダイジェストをDBに保存する。
ハッシュ変換するためhas_secureパスワードと同じように実装するが、attr_accessor(メソッドを定義するメソッド)を使って仮想のremember_token属性をUserクラスに追加する(rememberはよく使われるのでこれで下記意義メソッドも定義される)。参考
Rails Tutorialの知識から【ポートフォリオ】を作って勉強する話 #9 永続セッション, cookie編
クッキー(cookie)とは?初心者でも分かるように図解attr_accessor :remember_token || 下記の略 \\// \/ def remember=(taken) @remember = token end def remember @remember end今回はインスタンスメソッドなので、(ローカル変数にならないよう)
self
を使う。app/models/user.rbclass User < ApplicationRecord attr_accessor :remember_token #省略 # 永続セッションのためにユーザ-をデータベースに記憶する(クッキー認証のための準備 トークン残し) def remember # new_tokenを発行する self.remember_token = User.new_token # remember_digestの中にUser.digest(remember_token)を入れる # update_attributeで無駄にvalidationをかける必要がなくハッシュ化して入る self.update_attribute(:remember_digest,User.digest(remember_token)) endユーザのブラウザ内にあるキー?とremember_digestを使って認証させる必要がある。今回はauthenticateメソッドのレシーバに署名付きuser_id(cookie)を使う。これはcookieをブラウザに保存する前に安全に暗号化するためのもの(暗号化したuser_id)でブラウザに置いておく。
authenticateメソッド
引数を渡すと暗号化し、その文字列がパスワード(〇〇digest等)と一致するとUserオブジェクトを返し、間違っているとfalseを返すメソッド(公式第6章より)。
ユーザー(ブラウザ)保存のクッキーとremember_digestを一致させるのにauthenticateメソッドを使うが、このメソッドは@user(今回はemailでなく署名付きで暗号化されたuser_id ex.数字892350)を復号化して(数字892350 → user_id:5)をfind_byメソッドで呼び出してユーザーオブジェクトを引っ張って、その後authenticateメソッドで認証がはじまる。
引数にremember_tokenを渡せばDBの中身と確認できる(左selfが自分、右がDB)
# 渡されたトークンがダイジェストと一致したらtrueを返す def authenticated?(remember_token) BCrypt::Password.new(self.remember_digest).is_password?(remember_token) endapp/controllers/sessions_controller.rbdef create log_in user remember user #=> SessionsHelperの。 ログイン後にrememberでnew_token発行してDB保存 save to DB #=> (cokies[:token] クッキー追加必要なのでsessionsヘルパーに引数付きremember(user)を追加する) #略ユーザーを記憶する
app/helpers/sessions_helper.rb# ユーザーのセッションを永続的にする def remember(user) # => DB: remember_digest user.remember cookies.permanent.signed[:user_id] = user.id #=> coolies(クッキーに指定)、permanent(期限指定20年とする)、signed→暗号化 cookies.permanent[:remember_token] = user.remember_token endヘルパー内のcurrent_userメソッドに追加する。
app/helpers/sessions_helper.rb# 記憶トークンcookieに対応するユーザーを返す(新型) #=> ユーザオブジェクトが帰るか、nilが帰るか どちらか def current_user if (user_id = session[:user_id]) @current_user ||= User.find_by(id: user_id) elsif (user_id = cookies.signed[:user_id]) #=> signedで復号化 user = User.find_by(id: user_id) if user && user.authenticated?(cookies[:remember_token]) log_in user @current_user = user end end end9.1.3 ユーザーを忘れる
ユーザーのログアウトのためにはユーザーを忘れる(DBから消す)必要があるので
app/models/user.rb# ユーザーのログイン情報を破棄する def forget self.update_attribute(:remember_digest, nil) endCookie側からも消す
app/helpers/sessions_helper.rb# 永続的セッション(Cookie)を削除する def forget(user) user.forget cookies.delete(:user_id) cookies.delete(:remember_token) end # 現在のユーザーをログアウトする def log_out forget(current_user) session.delete(:user_id) @current_user = nil end9.1.4 2つの目立たないバグ
1つ目のバグは、この2つのタブで順にログアウトさせると、current_userがnilとなってlog_outメソッド内のforget(引数current_user → nilで)失敗してエラーになる(SS参照)。
ユーザーがログイン中の場合にのみログアウト(log_outメソッドを実行)させるため、sessionsコントローラを編集する。
app/controllers/sessions_controller.rb# DELETE /logout def destroy log_out if logged_in? # ログイン中の場合のみログアウト(log_outメソッドを実行)する redirect_to root_url end回帰バグを防ぐため統合テストにrootへリダイレクト後にdeleteを追記して確かめる。
test/integration/users_login_test.rbassert_redirected_to root_url # 2番目のウィンドウでログアウトをクリックするユーザーをシミュレートする delete logout_pathテストしてRED → GREEN。
もう一つのバグは、2種類のブラウザで以下のようなときに発生。
1.ChromeとFirefoxでログイン(両方にCookieが入る)
2.Chromeでログアウト
3.Firefoxでログアウトせず閉じる
4.Firefoxでホームにアクセスしようとすると、エラー発生。ChromeではログアウトしてるのでCookieの中身は削除され、(user.forgetメソッドによって)remember_digestもnilでBCryptは失敗するが、
FirefoxではCookieはあるが、remember_digestがnilなのでBCryptが失敗するだけでなく(デフォルトで)エラーを返してしまう。digestダイジェストが存在しない場合のauthenticated?のテストとauthenticated?メソッドにBCrypt前にreturnを追加。
test/models/user_test.rbtest "authenticated? should return false for a user with nil digest" do assert_not @user.authenticated?('') endapp/models/user.rbdef authenticated?(remember_token) return false if remember_digest.nil? #authenticated?を更新して、ダイジェストが存在しない場合に対応 BCrypt::Password.new(self.remember_digest).is_password?(remember_token) end備考: failuresとerrorsの違い
failures → 期待された値にならなかったときに出る。クライアントが欲しいシステムまたはコンポーネントの機能が性能要件を満足していない時などで発生。テスターが開発中に発見できる問題とされる。
errors → 期待の値とか関係なく異常・例外的なケースに出る。変数名のミス、間違ったログイン、誤ったループ条件などで発生する。
筆者がチュートリアル迷走中によく現れる<参考>
wrong、extra、error、bug、failure、faultの違い
9.2 [Remember me] チェックボックス
[remember me] チェックボックスをログインフォームview画面に追加する
app/views/sessions/new.html.erb<%= f.label :remember_me, class: "checkbox inline" do %> <%= f.check_box :remember_me %> <span>Remember me on this computer</span> <% end %>app/assets/stylesheets/custom.scss.checkbox { margin-top: -10px; margin-bottom: 10px; span { margin-left: 20px; font-weight: normal; } } #session_remember_me { width: auto; margin-left: 0; }チェックだけして「Log in」でエラーを表示すると、チェックなしの文字列「'0'」が選択されているのでok(「'1'」はあり)
remember_me: '0'ログインフォームの編集が終わったので、チェックボックスがオンのときにユーザーを記憶し、オフのときには記憶しないようにする。
app/controllers/sessions_controller.rbif user && user.authenticate(params[:session][:password]) log_in user # [remember me] チェックボックスの送信結果を処理する params[:session][:remember_me] == '1' ? remember(user) : forget(user)
9.3 [Remember me] のテスト
9.3.1 [Remember me] ボックスをテストする
単体テスト、統合テストとも同じメソッド名でテストヘルパーに入れる。
統合テストの特徴としては下記が注意どころ。
1.ActionDispatch::IntegrationTestクラスの中で定義
2.統合テストではsessionを直接取り扱えない為、代わりにSessionsリソースにpostリクエストを送信することで代用test/test_helper.rbclass ActiveSupport::TestCase 省略 # テストユーザーとしてログインする def log_in_as(user) session[:user_id] = user.id end end class ActionDispatch::IntegrationTest # テストユーザーとしてログインする def log_in_as(user, password: 'password', remember_me: '1') post login_path, params: { session: { email: user.email, password: password, remember_me: remember_me } } end endtest/integration/users_login_test.rbtest "login with remembering" do log_in_as(@user, remember_me: '1') assert_not_empty cookies['remember_token'] end test "login without remembering" do # クッキーを保存してログイン log_in_as(@user, remember_me: '1') delete logout_path # クッキーを削除してログイン log_in_as(@user, remember_me: '0') assert_empty cookies['remember_token'] endraiseの追加
raiseはコードブロック中に例外を発生させるもので、Kernelモジュールのインスタンスメソッドにあたる。
<参考>
begin~rescue~ensureとraiseを利用した例外処理の流れと捕捉について今回はテストしていなかったcurrent_use内にraiseを挿入し、もう一度テストがパスすれば、この部分がテストされていない(マズい)ことがわかる。
app/helpers/sessions_helper.rb# 記憶トークンcookieに対応するユーザーを返す def current_user if (user_id = session[:user_id]) @current_user ||= User.find_by(id: user_id) elsif (user_id = cookies.signed[:user_id]) raise # テストがパスすれば、この部分がテストされていない(マズい状況な)ことがわかる user = User.find_by(id: user_id)raiseで例外を入れてもテストが通過(error 0)してしまうので、別途ファイルにテストを追加する。
テスト内容としては以下の通り(公式より)
1. user変数(@user)を定義
2. 渡されたユーザー(@user)をrememberメソッドで記憶
3. current_userが、渡されたユーザー(@user)と同じであることを確認
4. ユーザーの記憶ダイジェストが記憶トークンと正しく対応していない場合に現在のユーザーがnilになるかどうかをチェックtest/helpers/sessions_helper_test.rb# 永続的セッションのテスト require 'test_helper' class SessionsHelperTest < ActionView::TestCase def setup @user = users(:michael) remember(@user) end test "current_user returns right user when session is nil" do assert_equal @user, current_user assert is_logged_in? end test "current_user returns nil when remember digest is wrong" do @user.update_attribute(:remember_digest, User.digest(User.new_token)) assert_nil current_user end end無事エラー確認できたのでok(raise外すとGREEN)
Gitコミット、herokuデプロイ(本番意識ならメンテナンスモード意識などが必要だが、今回は利用中のユーザー無視のためmasterへ)。
$ git add -A $ git commit -m "Finish ch09" $ git checkout master $ git merge advanced-login $ git push heroku master $ heroku run rails db:migrate備考: メンテナンスモード
herokuにデプロイしても、heroku上でマイグレーションを実行するまでの間は一時的にアクセスできない状態 (エラーページ) になるのでトラフィック( (一定時間に)通信回線やネットワーク上で送受信される信号やデータ量のこと )の多い本番サイトでは変更する前にメンテナンスモードをオンが推奨とされる。
$ heroku maintenance:on $ git push heroku $ heroku run rails db:migrate $ heroku maintenance:off2つ開いて片方でログイン/ログアウトしてCookieクリア等の確認ができれば完了!
終わりに
最後まで見て頂きありがとうございました!
- 投稿日:2020-01-17T11:04:45+09:00
rspecの始め方
Gemfileに
gem 'rspec' group :development, :test do gem 'rspec-rails' endコマンドラインで
$ bundle install $ bundle g rspec:install
- 投稿日:2020-01-17T09:41:20+09:00
Rails.application.credentials.xxxxx[:yyyy]が空になる時の対策
Rails.application.credentials.xxxxx[:yyyy]
Credentialsもしっかり設定しているし、rails cコマンドではちゃんと読み込める
しかも共同開発してる他のメンバーでは読み込める
でも私のhttp://localhost:3000/ 環境では中身がないとエラーが出る原因
はっきりわかってないけれど、恐らく権限の問題
対策
すでにあるmaster.keyを消して、同じ文字列で作り直す
- 投稿日:2020-01-17T09:37:03+09:00
[Rails]serializeなcolumnのdefault値を空の配列にしたい。
やりたいこと
migrationclass Users < ActiveRecord::Migration[5.1] def change create_table :users do |t| t.integer :id t.string :hoges, null: false, default: [] # これ end end endでもこれじゃ
== 20200109052420 Users: migrating ============================ -- create_table(:search_histories) rails aborted! StandardError: An error has occurred, all later migrations canceled: can't quote Array /Users/lyrical_school/project/disappearing_planet/db/migrate/20200109052420_create_users.rb:3:in `change' bin/rails:9:in `require' bin/rails:9:in `<main>'空配列を登録した時って何が保存されるんだっけ?
DB確認してみる。
--- []
が登録されてた。じゃあそれをdefaultに設定してみる。
migrationclass Users < ActiveRecord::Migration[5.1] def change create_table :users do |t| t.integer :id t.string :hoges, null: false, default: '--- []' end end end完璧。
殴り書きだから後日ちゃんとまとめる。まとめないかも。
- 投稿日:2020-01-17T08:40:54+09:00
【Rails】簡単にDatetimepickerを使う方法
はじめに
個人的にオススメなDatetimepickerについて、紹介しようと思います。
導入がめちゃくちゃ簡単です。できあがり例
こんな感じで、日付と時間を選ぶと実装することができます。
個人的にこのdatetimepickerが優れていると感じる点は以下です。
- はじめからカレンダーと時間が表示されていること
- ボタンをクリックする回数が2回で入力ができること
他のdatetimepickerだと、日付をクリックしないと時間の欄が表示されないため作業量を一瞬で把握できません。
微妙な差ではありますが、ユーザーが少しでも楽な方を考えました。使い方
まず、
GemFile
に以下のgem
を記載して、bundle install
をしてください。GemFilegem 'jquery-datetimepicker-rails'$ bundle install次に、
app/assets/stylesheets/application.css
に以下の1行を追加してください。app/assets/stylesheets/application.css*= require jquery.datetimepicker次に、
app/assets/javascripts/application.js
に以下の1行を追加してください。
(コメントはそのままで大丈夫です。)app/assets/javascripts/application.js//= require jquery.datetimepicker
下記の2行を追加することで、日本語対応のdatetimepickerを適用する準備が整いました。
(私の環境では日本語対応になりませんでした。どなたか分かる方がいれば教えていただきたい。。)application.js に 下記を書いていますが、各モデルのみに適用したい場合はcoffee scriptに書いても問題ありません。(ただし、その場合はcoffee scriptへの変換が必要。)
app/assets/javascripts/application.js// 日本語化対応用 $.datetimepicker.setLocale('ja'); // datetimepickerクラスにdatetimepickerを適用する。 $('.datetimepicker').datetimepicker();今回は一例として、postモデルのpost_datetimeカラムにdatetimepickerを適用しています。
ポイントは、class に datetimepicker をつけることです。そうすることでその部分に適用されます。
slimに馴染みのない方、ごめんなさい。。new.html.slim= form_for @post do |f| = label :post, :post_datetime = f.text_field :post_datetime, value: "", class: "datetimepicker", autocomplete: "off" // class に datetimepicker をつけるとその部分に適用される。まとめ
いかがでしょうか。導入は簡単でしたでしょうか。
不備などがあれば、申しつけください。参考
- GitHub(jquery-datetimepicker-rails)
- 投稿日:2020-01-17T02:11:34+09:00
【YAML】Railsのdatabase.ymlについてなんとなく分かった気になっていた記法・意味まとめ
はじめに
「そういえば、yamlって良く使うくせにちゃんと調べたことがないな」
と思い、特徴的な書き方をする機能3つに絞ってまとめました。Ruby on Railsで良く使う、
database.yaml
を例に説明します。環境
OS: macOS Catalina 10.15.1 Ruby: 2.6.5 Rails: 6.0.2.1そもそも
yaml
って何?YAML is a human friendly data serialization
standard for all programming languages.公式より抜粋しました。
「YAMLは全てのプログラミング言語で使える、人間に優しいデータの書き方だよ!」ということですね。
要するに、複雑なデータ構造をシンプルに表現するファイル形式です。
ここからRuby on Railsの
app/config/database.yml
を例に、他ではあまり見ない記法について説明していきます。例:
database.yml
コメントでどこで何を使っているのか書いています。
それぞれの説明は後述します。app/config/database.ymldefault: &default #アンカー adapter: mysql2 encoding: utf8 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: root password: password host: db development: <<: *default #エイリアス・ハッシュのマージ database: app_name_development test: <<: *default #エイリアス・ハッシュのマージ database: app_name_test production: <<: *default #エイリアス・ハッシュのマージ database: app_name_production username: app_name password: <%= ENV['APP_NAME_DATABASE_PASSWORD'] %>アンカー: 名前をつける機能
&default
これをアンカーと呼びます。
該当部に名前をつける機能です。実例
default: adapter: mysql2 encoding: utf8 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: root password: password host: db上記ハッシュ(連想配列)のことを
default
と呼ぶよという宣言をするなら
↓
アンカーを追記する。app/config/database.ymldefault: &default adapter: mysql2 encoding: utf8 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: root password: password host: dbアンカーを書いておくことにより、次のエイリアスが使えるようになります。
エイリアス: 名前をつけたものを呼び出す
*default
アンカーを付けたものを呼び出すときに使います。
これがどう使われているのかは以下ハッシュのマージを絡めて後述します。
ハッシュのマージ: 一つにまとめる
※2020/01/17追記 YAMLでの正式名称はハッシュではなくマッピングです。
ここではRuby on Railsを例にしているため、わかりやすくするためハッシュと表現しています。
(scivolaさんアドバイスありがとうございます)
参考:プログラマーのための YAML 入門 (初級編)<<: *default
<<:
の右側にあるものを、該当部にまとめるという意味になります。以下例をご覧ください。
実例
ここまで出てきた
- アンカー
- エイリアス
- ハッシュのマージ
の知識を使うと、以下の
development
で何を意味しているのかが説明できます。app/config/database.ymldefault: &default #ここで共通部分にアンカーで名前をつけておくと adapter: mysql2 encoding: utf8 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: root password: password host: db development: <<: *default #ここでdefaultとして呼び出し、スッキリ書ける database: app_name_development↓
つまり、developmentに以下が書かれているのと同じ意味になります。app/config/database.yml#...略 development: #ここからdefault adapter: mysql2 encoding: utf8 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: root password: password host: db #ここまでdefault database: app_name_development以上です!
おわりに
最後まで読んで頂きありがとうございました
どなたかの参考になれば幸いです
参考にさせて頂いたサイト(いつもありがとうございます)
- 投稿日:2020-01-17T00:05:25+09:00
each_with_objectの簡単なサンプルメモ
メモ
enum.each_with_object(object) {|item, memo| block }
each_with_indexメソッドは、要素を使って何らかのオブジェクトを操作するのに使います。要素の数だけブロックを繰り返し実行し、繰り返しごとにブロック引数itemには各要素を、memoには引数objectで指定したオブジェクトを入れます。戻り値は、objectのオブジェクトです。
https://ref.xaio.jp/ruby/classes/enumerable/each_with_objectサンプル
require "pp" arr = [1, 2, 3] result = arr.each_with_object([]) do |item, memo| p item # => 1... memo << 1 end pp result # => [1, 1, 1] result = arr.each_with_object([1, 2, 3]) do |item, memo| memo << 1 end pp result # => [1, 2, 3, 1, 1, 1] result = arr.each_with_object([1, 2, 3]) do |item, memo| memo << item + 1 end pp result # => [1, 2, 3, 2, 3, 4] ################################### # 以下Hash ################################### hash = { a: "A", b: "B", c: "C" } result = hash.each_with_object({}) do |item, memo| p item # => [:a, "A"]... p item.class # => Array end pp result result = hash.each_with_object({}) do |(k, v), memo| p k # => :a... p v # => "A"... end pp result # => {} result = hash.each_with_object({aaa: "AAA"}) do |(k, v), memo| memo["#{k}XXX"] = "#{v}YYY" end pp result # => {:aaa=>"AAA", "aXXX"=>"AYYY", "bXXX"=>"BYYY", "cXXX"=>"CYYY"}