20200117のRubyに関する記事は16件です。

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を確認する。

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

【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が有利

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

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
end

rubyはover flowの扱いが特殊みたいで
生々しい実装になってしまいました😀😀😀

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

【Rails】SNSアプリのコメント削除機能の実装

参考 URL
https://qiita.com/takahisa_s/items/e6d9713eb9dc0f85f66d
参考も何も理解するには上記の記事を見るだけで十分なのですが、自分用の備忘録として今回の記事を作成いたします。

前提

私の書いたコメント作成機能の記事の続きです。
https://qiita.com/Zhongcun/items/42b15f045a363ec82af5

また、【削除できるコメントは自分が作成したコメントのみ】とします。

ルーティング

routes.rb
resources :microposts, only: [:create, :destroy, :show] do
  resources :comments, only: [:create, :destroy]
end

コントローラー

コメント削除ボタンに関わることなので、micropostのコントローラの記述もしておきます。

microposts_controller.rb
  def show
    @micropost = Micropost.find(params[:id])
    @comments = @micropost.comments.includes(:user)
    @comment = @micropost.comments.build # form_with 用
  end

省略されている部分に関しては先述の前回の記事を参考にお願いします。

comments_controller.rb
class 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|を基にしています。

何か間違っていることがあれば、コメントや編集リクエストをいただけると幸いです。

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

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

一応通りましたが、酷いコード😇

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

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

一応通りましたが、酷いコード😇

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

ログイン機能の実装(SNS認証機能の追加)

記事保留

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

【初心者向け】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

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

同じテーブル内に複数の外部キーを設定する方法!!

はじめに

某プログラミングスクールで、担当した実装を復習していきたいと思います。
今回は、出品・取引中・売却済みのこの3つをクリックした際に、それぞれにあった商品を
表示させる実装を行いました。

これを実装するにあたって、1つのテーブル内に複数の外部キーを設定する必要があり、
ここで詰まったため、記録として残していきます。

ちなみにこんな感じの実装をしていきます。
c0b6f5dcc3487ef71b3605eb6e99ffe0.gif

工程

今回は、工程を以下に分けて説明をしていきます。
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: true

foreign_key: trueのみで外部キーを設定できるのですが、
今回のように、同じテーブル内に複数の外部キーを設定する場合、
foreign_key: trueで定義してしまうと、
カラム名がテーブル名_idになってしまうため、
複数カラムを設定したいときにうまくいかないことがありました。

そのため、{to_table: :テーブル名}で今回使用するテーブルを直接指定する必要があるようです。

「参考記事」
Railsで同じモデルを参照する外部キーを2つ以上もつ方法

2.モデルの作成

product.rb
class 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.rb
class 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.rb
has_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.rb
 has_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.rb
has_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間のアソシエーションは終了です。

「参考記事」
【Rails】テーブル間の条件付きアソシエーションの設定【メルカリコピー作成記】

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.rb
  def 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での条件分岐設定
最後に、コントローラーで取得してきた値を繰り返し処理する記述を加えていきます。

c0b6f5dcc3487ef71b3605eb6e99ffe0.gif

上記の画像のように、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を繰り返すようにしています。

以上です。

最後に

長い行を読んでいただきありがとうございました。
所々、切り抜いて記事を書かせて頂いているため、間違っている箇所があった際には、
私の記述でエラーを起こしてしまい申し訳ありません。
また、間違っている箇所がありましたら、コメントをいただけると幸いです。
ご視聴ありがとうございました。

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

【第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.rb
class 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)
  end
app/controllers/sessions_controller.rb
def 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
  end

9.1.3 ユーザーを忘れる

ユーザーのログアウトのためにはユーザーを忘れる(DBから消す)必要があるので

app/models/user.rb
# ユーザーのログイン情報を破棄する
  def forget
    self.update_attribute(:remember_digest, nil)
  end

Cookie側からも消す

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
  end

9.1.4 2つの目立たないバグ

1つ目のバグは、この2つのタブで順にログアウトさせると、current_userがnilとなってlog_outメソッド内のforget(引数current_user → nilで)失敗してエラーになる(SS参照)。

スクリーンショット 2020-01-15 16.02.11.png

ユーザーがログイン中の場合にのみログアウト(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.rb
assert_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.rb
 test "authenticated? should return false for a user with nil digest" do
    assert_not @user.authenticated?('')
  end
app/models/user.rb
def 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'」はあり)

スクリーンショット 2020-01-16 12.57.14.png

 remember_me: '0'

ログインフォームの編集が終わったので、チェックボックスがオンのときにユーザーを記憶し、オフのときには記憶しないようにする。

app/controllers/sessions_controller.rb
if 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.rb
class 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
end
test/integration/users_login_test.rb
 test "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']
  end

raiseの追加

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:off

2つ開いて片方でログイン/ログアウトしてCookieクリア等の確認ができれば完了!

終わりに

最後まで見て頂きありがとうございました!

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

rspecの始め方

Gemfileに

gem 'rspec'
group :development, :test do
  gem 'rspec-rails'
end

コマンドラインで

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

Rails.application.credentials.xxxxx[:yyyy]が空になる時の対策

Rails.application.credentials.xxxxx[:yyyy]

Credentialsもしっかり設定しているし、rails cコマンドではちゃんと読み込める
しかも共同開発してる他のメンバーでは読み込める
でも私のhttp://localhost:3000/ 環境では中身がないとエラーが出る

原因

はっきりわかってないけれど、恐らく権限の問題

対策

すでにあるmaster.keyを消して、同じ文字列で作り直す

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

[Rails]serializeなcolumnのdefault値を空の配列にしたい。

やりたいこと

migration
class 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に設定してみる。

migration
class Users < ActiveRecord::Migration[5.1]
  def change
    create_table :users do |t|
      t.integer  :id
      t.string   :hoges, null: false, default: '--- []'
    end
  end
end

完璧。

殴り書きだから後日ちゃんとまとめる。まとめないかも。

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

【Rails】簡単にDatetimepickerを使う方法

はじめに

個人的にオススメなDatetimepickerについて、紹介しようと思います。
導入がめちゃくちゃ簡単です。

できあがり例

こんな感じで、日付と時間を選ぶと実装することができます。

image.png

個人的にこのdatetimepickerが優れていると感じる点は以下です。

  • はじめからカレンダーと時間が表示されていること
  • ボタンをクリックする回数が2回で入力ができること

他のdatetimepickerだと、日付をクリックしないと時間の欄が表示されないため作業量を一瞬で把握できません。
微妙な差ではありますが、ユーザーが少しでも楽な方を考えました。

使い方

まず、GemFileに以下のgemを記載して、bundle installをしてください。

GemFile
gem '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 をつけるとその部分に適用される。

まとめ

いかがでしょうか。導入は簡単でしたでしょうか。
不備などがあれば、申しつけください。

参考

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

【YAML】Railsのdatabase.ymlについてなんとなく分かった気になっていた記法・意味まとめ

はじめに

「そういえば、yamlって良く使うくせにちゃんと調べたことがないな」
と思い、特徴的な書き方をする機能3つに絞ってまとめました。

YAMLの公式はこちら

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 Railsapp/config/database.ymlを例に、他ではあまり見ない記法について説明していきます。

例:database.yml

コメントでどこで何を使っているのか書いています。
それぞれの説明は後述します。

app/config/database.yml
default: &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.yml
default: &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さんアドバイスありがとうございます:relaxed:
参考:プログラマーのための YAML 入門 (初級編)

<<: *default

<<:の右側にあるものを、該当部にまとめるという意味になります。

以下例をご覧ください。

実例

ここまで出てきた

  • アンカー
  • エイリアス
  • ハッシュのマージ

の知識を使うと、以下のdevelopmentで何を意味しているのかが説明できます。

app/config/database.yml
default: &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

以上です!

おわりに

最後まで読んで頂きありがとうございました:bow_tone1:

どなたかの参考になれば幸いです:relaxed:

参考にさせて頂いたサイト(いつもありがとうございます)

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

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"}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む