20201117のRubyに関する記事は25件です。

いらないコメントを一括削除する方法

これを覚えると時間の節約になるのでおすすめです。

前提として、環境はMacでエディターはVSCodeです。(Atomでや他のエディターでも同様のコマンドで可能かと思います)

Railsでテストアプリを作っている時などによく出る長文のコメント結構邪魔ですよね。

スクリーンショット 2020-11-17 23.19.03.png

そんな時は 

  • command+Fで任意のページで検索して
# .*\n
  • このコマンドを打ちますと(今回はVScode使ってます)

スクリーンショット 2020-11-17 23.27.46.png

  • 結果はありませんとなってしまいましたが、正規表現を表す

スクリーンショット 2020-11-17 23.31.44.png
↑こちらのボタンを押すと25個ヒットしました!

スクリーンショット 2020-11-17 23.29.24.png

  • 全て置換すると

スクリーンショット 2020-11-17 23.33.52(2).png

スクリーンショット 2020-11-17 23.36.15.png

コメントが一気に消えます。

インデントを揃えるとだいぶ見やすくなりました!
スクリーンショット 2020-11-17 23.41.40.png

ちなみにバックスラッシュの打ち方は日本語キーボードですと

Option + ¥  = \

です。

是非試してみてくださいね。

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

LINEみたいに自分のメッセージと自分以外のメッセージの表示場所を変えたい

はじめに

 現在作成中のチャットアプリのメッセージ画面をLINEのように自分の送ったメッセージは右、自分以外のメッセージは左に表示させよと一日苦戦した。ググってもあまり記事が見つからなかったので、ここに記録しておく。

ポイントとして使ったもの

  • renderメソッド
  • if文
  • deviseのメソッド

コントローラー

messages_controller.rb
def index
    @class_room = ClassRoom.find(params[:class_room_id])
    @messages = @class_room.messages.includes(:user)
  end

@class_roomにチャットルームをparamsから呼び出し代入
@messagesにそのチャットルームのメッセージを全て入れる。
.includes(:user)はN+1問題を解消するため。

ビュー

※divタブはコードが読みにくくなるため、ここでは割愛。

<% if message.user.id == current_user.id %>
    <%= l message.created_at %>
    <%= message.content %>
<% else %>
    <%= message.user.last_name %>
    <%= l message.created_at %>
    <%= message.content %>
<% end %>

if文で、メッセージのユーザーIDと現在のログインしているユーザーが同じか照会し、同じであれば(つまり、自分の送ったメッセージ)上の処理が行われ、違えば、下の処理が行われる。
自分のメッセージに送り主の表示は必要ないので、上の処理にmessage.user.last_nameがない。

その他

 CSSを整える。

終わりに

 他のやり方として、コントローラーの時点で、自分のメッセージと自分以外のメッセージを別の変数に入れる方法も考えられる。

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

【Ruby】値渡しと参照渡し

「値渡し」「参照渡し」とは

関数やメソッドにおける引数の渡し方の種類を表す用語のこと。
Rubyだけではなくどのプログラミング言語でも共通する。

値渡し

  • 実引数の値をコピーして仮引数に渡す
  • コピーを渡すので、実引数には影響なし
  • Rubyは値渡し(他の言語も)

参照渡し

  • 実引数の変数があるメモリ上の場所(メモリ番地)をコピーして仮引数に渡す
  • 与えられたメモリ番地から実引数の値を辿って値を変更する
  • 仮引数に変更を加えると実引数も変更される

参照の値渡し?

  • 変数が参照(メモリ番地)を保持している場合、参照がコピーされる
  • 「オブジェクトの参照(メモリ番地)」をコピーして実引数に渡すので、同じオブジェクトを共有しているということになる
  • つまり、引数が指すオブジェクトを変更すると元の変数のオブジェクトも変更される
  • あたかも「仮引数を変更したら実引数も変更された」ように見えるだけで、実際は値渡し
  • 「変数を変更する」ことと「変数が指すオブジェクトを変更する」ことは別!

引用元

値渡しと参照渡しの違いを理解する

メモリ番地でイメージするととてもわかりやすかったです。
今日学んだことを可視化するためにとりあえず書いてみました。後程自分の中で噛み砕いてまとめます。

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

【Rubyによるデザインパターン】ストラテジーパターンのメモ

プログラムの設計力向上のため『Rubyによるデザインパターン』を読んでおり、気になるデザインパターンを、1つずつまとめていきます。

今回は、ストラテジーパターンについてまとめました。

ストラテジーパターンについて

委譲をベースにした、複数の処理のバリエーションを実装するためのデザインパターンです。

継承を使いサブクラスごとにバリエーションを実装する実装するテンプレートメソッドパターンと異なり、ストラテジーパターンはバリエーションごとにばらばらのクラスを生成します。

サンプルコード

趣味の筋トレにちなんでサンプルコードを書きます。
(テンプレートメソッドパターンの記事との類似した例を題材としています。)

ベンチプレスと懸垂はどちらもトレーニングの内容は同じものの、具体的な内容はそれぞれ異なっています。

そこで、トレーニングごとの内容を出力するプログラムを書きます。

ストラテジーパターン適用前はこのとおりです。

class Training
  def initialize(type)
    @type = type
  end

  def start
    prepare
    execute
    cleanup
  end

  def prepare
    if @type == :bench_press
      puts 'バーベルをセットします'
    end
  end

  def execute
    puts 'トレーニングをします'
  end

  def cleanup
    puts 'アルコール消毒します'
    if @type == :bench_press
      puts 'バーベルを戻します'
    end
  end
end

呼び出し時は引数にトレーニングの種類を渡します。
実行結果はこのとおりです。

bench_press = Training.new(:bench_press)
bench_press.start
# バーベルをセットします
# トレーニングをします
# アルコール消毒します
# バーベルを戻します

tinning = Training.new(:tinning)
tinning.start
# トレーニングをします
# アルコール消毒します
# バーベルを戻します

インスタンス変数 @type によって if 分で分岐をしています。トレーニングが2種類ならばよいかもしれませんが、トレーニング数が増えるたびにこの条件分岐は増え、1つのメソッドは長く複雑になってしまいます。

また、 @type が :tinning のときは、 prepare が呼び出されているにも関わらず何も実行されていません。
これは、@type が :bench_press と :tinning で同じインターフェース(つまり、パブリックメソッド)である start を共有しているからです。

次に、ストラテジーパターンを適用して書き換えた場合です。

class Training
  def initialize(training_menu)
    @training_menu = training_menu
  end

  def start
    @training_menu.start
  end
end

class BenchPress
  def start
    prepare
    execute
    cleanup
  end

  def prepare
    puts 'バーベルをセットします'
  end

  def execute
    puts 'トレーニングをします'
  end


  def cleanup
    puts 'アルコール消毒します'
    puts 'バーベルを戻します'
  end
end

class Tinning
  def start
    execute
    cleanup
  end

  def execute
    puts 'トレーニングをします'
  end

  def cleanup
    puts 'アルコール消毒します'
  end
end

呼び出し時はトレーニングのバリエーションごとのオブジェクトを生成し、 Training 初期化時に渡します。

bench_press = BenchPress.new
training = Training.new(bench_press)
training.start
# バーベルをセットします
# トレーニングをします
# アルコール消毒します
# バーベルを戻します

tinning = Tinning.new
training = Training.new(tinning)
training.start
# トレーニングをします
# アルコール消毒します
# バーベルを戻します

BenchPress と Tinning で同じ start というインターフェースを持ちつつも、処理の内容が異なることがおわかりいただけるかと思います。
このように、処理の内容のを個々のオブジェクトに委ねられるため変更に強くなります。

テンプレートメソッドパターンとの比較

ストラテジーパターンはテンプレートメソッドパターンに似ているのも特徴ですね。
テンプレートメソッドパターンは継承を使うため、基底クラスの処理内容を生かしてサブクラスを実装します。基底クラスの処理内容に変更がない場合、実装するコードは少なくできる反面、サブクラスは基底クラスの処理内容に依存しているため基底クラスは変更しづらくなります。

ストラテジーパターンは処理内容自体をオブジェクトに委譲できる点がメリットが大きいです。
テンプレートメソッドパターンとストラテジーパターンどちらを使うか迷った場合は、基本の処理内容が同じ場合はテンプレートメソッドパターン、処理内容のバリエーションが幅広い場合はストラテジーパターンが良さそうです。

まとめ

ストラテジーパターンはポリモーフィズムを使ったオブジェクト指向らしいデザインパターンです。

テンプレートメソッドパターンと合わせてストラテジーパターンも使いこなせるようになりたいです。

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

未経験のおっさんがプログラミングを学ぶまで

はいどうも!
某プログラミングスクールに通い始めた37歳のおっさんです。
プログラミングは完全に未経験。
そんなおっさんが転職するまでの軌跡(奇跡?)をぼちぼち綴っていこうと思います。

投稿は今日が初めてですがスクールは今日で9日目。
HTML&CSSをやってRubyに入り、Railsをやって試験を受けて、というところです。

簡単に私の経歴を
↪️
地方出身、東京の文系私立大学卒、東京で就職し営業職(toC & toB)でマネジメント経験あり。
インディーズですが学生時代にやっていたバンドでCDを1枚だけ出しています。
キャンプとジム通い、音楽掘りが趣味。
ジャンルはRock、JazzからEDMまで幅広く聴きます。
今はちょっと難しくなってしまいましたが海外旅行もそこそこ行きました。




なぜプログラミングを始めたのか、どうして転職を決意したのかなどは後々ゆっくりとupしていこうと思います。
とりあえずまだ始めたばかりですが感想としては
「めちゃくちゃやることがいっぱいある」
「勉強すること、覚えることが今までやってきたことの比じゃないレベルで多い」
この二つですね。
日々思ったこと、できなかったこと、疑問点などなど
自分が見返した時に
「あ〜こんなことあったな」とか、
「こんなことで悩んでたんだな」とかとか、
あとはこれを読んだ他の方に少しでも役に立てればなと思います。

そんな具合ですが本日は頭がパンクしているのでこれにて:wave_tone1:

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

【アウトプット】find_by メソッド

今回はfind_byについてアウトプットをしていく。

find_byはある特定のIDを取得すときに使われる。
3番目のIDのデータを取得するとか5番目とか

モデル名.find_by(カラム名: 値)と書くことでその値を持ったデータのデータベースから取得ができるようになる。

例)post = Post.find_by (id:3)
ちなみにpostはeachのときに使った変数

このようにして使われる。

変数paramsとよく使われる?らしいのでこれも併用して覚えるとGOOD

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

LeetCodeをRubyで解く #1 [ Top Interview Questions, easy ]

GAFA等のコーディング試験対策としてよく使われている「LeetCode」をRubyで解きました。
まとまったRubyの解答例がなかったので、Top Interview Questionsの難易度easyで検索した46問についてまとめていきます。

#1 Two Sum (Acceptance:45.9%)

問題

配列numsと整数targetが与えられ、
異なる2つの要素の和がtargetと同値なる配列要素のインデックスを出力する

Example 1:
Input: nums = [2,7,11,15], target = 9
Output: [0,1]
Output: Because nums[0] + nums[1] == 9, we return [0, 1].

Example 2:
Input: nums = [3,2,4], target = 6
Output: [1,2]

Example 3:
Input: nums = [3,3], target = 6
Output: [0,1]

まず自分で書いてみた例です。
結果はTime Limit Exceededとなりました…

結果: Time Limit Exceeded
def two_sum(nums, target)
  nums.each_with_index do |n1, idx1|
    pair = target - n1
    nums.each_with_index do |n2, idx2|
      if n2 == pair && idx1 != idx2
         return [idx1, idx2]
      end
    end
  end
end

一見合っていそうですが、以下の条件だったので、eachを2重にするという愚直な方法では時間超過になるようです…

Constraints:

2 <= nums.length <= 10^5
-10^9 <= nums[i] <= 10^9
-10^9 <= target <= 10^9
Only one valid answer exists.

投稿されていた解法を探してみました。一部Wrongとなりましたが、
Acceptedされた解答例は以下の通りです。

結果: Accepted
def two_sum(nums, target)
  hash = Hash.new
  nums.each_with_index do |num, i|
    return [hash[num], i] if hash.key?(num)

    hash[target - num] = i
  end.clear
end
結果: Accepted
def two_sum(nums, target)
  hash = {}
  nums.each_with_index do |number, index|
    hash[number] = index
  end

  nums.each_with_index do |n, i|
    complement = target - n
    if hash.has_key?(complement) && hash[complement] != i
      return [hash[complement], i]
    end
  end
end
結果: Accepted
def two_sum(nums, target)
  hash = {}
  result = nil
  nums.each_with_index do |num, i|
    value = target - num
    if (hash.has_key? value)
      result = [hash[value], i]
      break
    end
    hash[num] = i
  end
  result
end

#7 Reverse Integer (Acceptance:25.8%)

問題

与えられた32ビットの整数Xを逆の桁にする
ただし変換後に−2^31〜2^31 − 1の範囲外であれば0とする

Example 1:
Input: x = 123
Output: 321

Example 2:
Input: x = -123
Output: -321

Example 3:
Input: x = 120
Output: 21

Example 4:
Input: x = 0
Output: 0

Example 5:
Input: x = 1534236469
Output: 0
※9646324351 > 2^31 − 1のため

まず自分で書いてみた例です。
なんとかAcceptedとなりました…
Railsチュートリアル感がある書き方ですね…

結果: Accepted
def reverse(x)
  answer = x.to_s.reverse.to_i
  answer *= (-1) if x < 0
  if answer > 2 ** 31 - 1 || answer < 2 ** 31 * (-1)
    answer = 0 
  end
  return answer
end

その他、Acceptedされた解答例は以下の通りです.

結果: Accepted
def reverse(x)
  reversed_num = x < 0 ? ('-' + x.to_s.reverse).to_i : x.to_s.reverse.to_i
  (-2**31..(2**31 - 1)).include?(reversed_num) ? reversed_num : 0
end
結果: Accepted
def reverse(x)
    number = x.abs
    reversed = 0

    # the while loop is based on absolute value
    while number != 0
        # guard against overflow before multiplying
        return 0 if reversed >= 2**31/10 + 0.8 # 0.8 since the last digit: 2147483648

        last_digit = number % 10
        reversed = (reversed * 10) + last_digit
        number = number / 10
    end

    x.negative? ? -reversed : reversed
end

自分の回答と似ているけど、こっちのほうがスマート。

結果: Accepted
def reverse(x)
  s_i = x.to_s
  s_i = s_i.reverse.to_i
  s_i *= -1 if (x < 0)
  return 0 if s_i.bit_length > 31
  return s_i
end

#13 Roman to Integer (Acceptance:56.2%)

問題

ローマ数字を整数に変換する。

Symbol       Value
I             1
V             5
X             10
L             50
C             100
D             500
M             1000

例えば、XXVIIとなれば、27=10+10+5+1+1
ローマ数字は通常、左から右に最大から最小の順に書かれる。ただし、4の数字はIIIIではなく、IVと書かれる。同じ原則が、IXと書かれる9にも当てはまります。

V(5)とX(10)の前にI(1)を置くと4と9を表します。 
L(50)とC(100)の前にX(10)を置くと40と90を表します。 
D(500)とM(1000)の前にC(100)を置くと、400と900を表します。

Example 1:
Input: s = "III"
Output: 3

Example 2:
Input: s = "IV"
Output: 4

Example 3:
Input: s = "IX"
Output: 9

Example 4:
Input: s = "LVIII"
Output: 58
Explanation: L = 50, V= 5, III = 3.

Example 5:
Input: s = "MCMXCIV"
Output: 1994
Explanation: M = 1000, CM = 900, XC = 90 and IV = 4.

まず自分で書いてみた例です。 ちょっとカンニング気味でしたが、なんとかAcceptedとなりました。

結果: Accepted
def roman_to_int(s)
  hash = {'M' => 1000, 'D' => 500 , 'C' => 100, 'L' => 50, 'X' => 10, 'V' => 5, 'I' => 1}
  answer = 0
  for i in 0..s.length-1 do
    if !hash[s[i+1]].nil? && hash[s[i]] < hash[s[i+1]]
      answer -= hash[s[i]]
    else
      answer += hash[s[i]]
    end
  end  
  return answer
end

本問題はサブスク課金しないと解答が見れませんでしたが、
stackoverflow等を調べた中でAcceptedされた解答例は以下の通りです。

downcaseする必要はあるのか?(丁寧なだけ?)というポイントと、三項演算子を使っているか程度の違いです。

結果: Accepted
ROMAN_TO_INT =
  {
    i: 1,
    v: 5,
    x: 10,
    l: 50,
    c: 100,
    d: 500,
    m: 1000
  }
def roman_to_int roman
  value_map = roman.split('').map { |e| ROMAN_TO_INT[e.downcase.to_sym] }
  value_map.map.with_index do |e, idx| 
    unless value_map[idx + 1].nil?
    then
      value_map[idx + 1] > e ? -e : e
    else e
    end
  end.sum
end

これは正規表現を使うスマートで高難易度の解法ですね…

結果: Accepted
H = {"VI"=>" 4", "XI"=>" 9", "LX"=>" 40", "CX"=>" 90", "DC"=>" 400", "MC"=>" 900", "I"=>" 1", "V"=>" 5", "X"=>" 10", "L"=>" 50", "C"=>" 100", "D"=>" 500", "M"=>" 1000"}

def roman_to_int(s)
   s.reverse.gsub(Regexp.union(H.keys), H).split.sum(&:to_i)
end


#ちょっと解説
k = H.keys
  #=> ["VI", "XI", "LX", "CX", "DC", "MC", "I", "V", "X", "L", "C", "D", "M"]
r = Regexp.union(H.keys)
  #=> /VI|XI|LX|CX|DC|MC|I|V|X|L|C|D|M/
t = s.gsub(r, H)
  #=> " 1 5 10 10 100 100"
a = t.split
  #=> ["1", "5", "10", "10", "100", "100"]
a.sum(&:to_i)
  # => 226

#14 Longest Common Prefix (Acceptance:35.8%)

問題

文字列の配列の中から最も長い共通のプレフィックス文字列を見つける.
共通のプレフィックスがない場合は、空の文字列を返す。
Example 1:
Input: strs = ["flower","flow","flight"]
Output: "fl"

Example 2:
Input: strs = ["dog","racecar","car"]
Output: ""
Explanation: There is no common prefix among the input strings.

自分の回答。Acceptedもらえる!と意気込んでsubmitしたら、Wrong Answerでした。

結果: Wrong Answer
def longest_common_prefix(strs)
  unless strs[0].nil?
    base = strs[0].split('')
    answer = base.dup
    for i in 1..strs.length-1 do
      base.each do |b|
        if !strs[i].split('').include?(b)
          answer.delete(b)
        end
      end
    end
  end
  return answer.join unless answer.nil?
  return answer = ""
end

Wrongとなった入出力を見ると以下の通りで、連続した文字列でないものもひろってしまっていたためでした。(問題をよく見ていなかった…)

Wrong Answer  > Details 
Input    ["cir","car"]
Output    "cr"
Expected  "c"

本問題に関してRubyの解答は投稿されていませんでしたが、
調べた中でAcceptedされた解答例は以下の通りです。

結果: Accepted
 def longest_common_prefix(strs)
   strs.map {|str| str.split('') }.tap {|str_arrays|
     break str_arrays[0] ? str_arrays[0].zip(*str_arrays[1..-1]) : []
   }.reduce('') do |result, arr|
     if arr.uniq.size == 1
       result << arr[0]
     else
       return result
     end
     result
   end
 end
結果: Accepted
def longest_common_prefix(strs)
  loop do
    str_a = strs.shift
    str_b = strs.shift
    return str_a.to_s if str_b.nil?

    common = longest_common_prefix_between(str_a, str_b)
    strs.unshift(common)
  end
end


def longest_common_prefix_between(str_a, str_b)
  len = [str_a.length, str_b.length].min

  result_str = ""
  len.times do |i|
    if str_a[i] == str_b[i]
      result_str += str_a[i]
    else
      break
    end
  end

  result_str
end

#20 Valid Parentheses (Acceptance:39.3%)

問題

'('と')'、'{'と'}'、'['と']'が有効に入力されているかどうかを判定する。

次の場合に有効とする。
開いた括弧は、同じタイプの括弧で閉じる必要があり。
開いた括弧は正しい順序で閉じる必要があり。

Example 1:
Input: s = "()"
Output: true

Example 2:
Input: s = "()[]{}"
Output: true

Example 3:
Input: s = "(]"
Output: false

Example 4:
Input: s = "([)]"
Output: false

Example 5:
Input: s = "{[]}"
Output: true

難問でした。手も足も出ず、解答を探しました。
調べた中でAcceptedされた解答例は以下の通りです。

結果: Accepted
def is_valid(s)
    map = {')': '(', '}': '{', ']': '['}
    stack = []

    s.each_char do |i|
        if map.has_key?(i.to_sym)
            top_element = stack ? stack.pop : '#'
            return false unless map[i.to_sym] == top_element
        else
            stack.push(i)
        end
    end

    stack.empty?
end

似たような解答ですが、もう一つ。

結果: Accepted
def is_valid(s)
    return true if s == ""
    return false if s.length % 2 == 1
    stack = []
    map = {
        ']': '[',
        '}': '{',
        ')': '('
    }
    s.each_char do |bracket|
        if stack.last && map[bracket.to_sym]
            top = stack.empty? ? '#' : stack.pop
            return false if top != map[bracket.to_sym]
        else
            stack << bracket
        end
    end
    stack.length == 0
end

問題の訳し方や解法等で問題があれば、お手数をおかけしますがコメントいただけると幸いです。

あと41問続きます…

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

混乱しがちなアソシエーションのclass_nameやforeign_keyを理解する

Railsでお知らせ機能やフォロー機能を実装しているとclass_nameやforeign_keyが出てくると思います。これらを理解するのに時間がかかったので、備忘録もかねて投稿します。

今回はお知らせ機能を例に挙げる。
イメージしやすいようER図を載せる。(最低限のカラムしか書いてません。)

お知らせ機能ER図.png

お知らせ機能を実装していると@user.passive_notificationsのようにして自分に来ている通知のレコードをまとめて取得したいことがある。そういう場合、下のように書く。

user.rb
has_many :passive_notifications, class_name: "Notification", foreign_key: "visited_id"

少し複雑なので、簡単な例を挙げる。

Userモデルで下記のように定義した場合

よく使うhas_many :tweetsは実は色々省略されている。

user.rb
has_many :tweets

# 同じ意味
has_many :tweets, class_name: "Tweets", foreign_key: "user_id"

Railsで開発をするとhas_manyやbelongs_toを使ってモデルでリレーションを組むが、これを書くことによって、Userクラスへのticketsメソッドの定義が裏で行われている。例でいうと、ticketsメソッドの対象となるClassはTweetクラスで、ticketsテーブルのuser_idの値を参照してレコードを取得する。

Railsの規則によりモデルの複数形でメソッドを定義する場合、クラス名、外部キーを省略することができる。なぜならメソッド名により暗黙的にクラス名、外部キーが決定されるからである。

上記のようにメソッドを定義する(リレーションを組む)ことで、コントローラーなどで下のメソッドが使えるようになる。

user.rb
@user = User.find(params[:id])
@user.tweets

少し脱線したが、このことを頭に入れてお知らせ機能のリレーションをもう一度見てみる。

user.rb
has_many :passive_notifications, class_name: "Notification", foreign_key: "visited_id"

ここの記述では、Userモデルにpassive_notificationsメソッドを定義している。
メソッド名からはクラス名が不明なため、class_name: "Notification"として対象のクラスを明示している。また、外部キーを設定しない場合、notificationsテーブルからuser_idのカラムを探しにいってしまうため、foreign_key: "visited_id"として定義する。

これにより、@userのidとnotificationsテーブルのvisited_idとが一致したレコード、つまり@user宛の通知のレコードがまとめて取得できるようになる。

user.rb
@user = User.find(params[:id])
@user.passive_notifications #ユーザ宛の通知のレコードを全件取得

以上です。
まちがっていたらご指摘お願いします。

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

Railsのユーザ入力情報の保護機能

本投稿の目的

・Rails学習の議事録です。


学習に使った教材

Udemyの "はじめてのRuby on Rails入門-RubyとRailsを基礎から学びWebアプリケーションをネットに公開しよう" を教材として使用しました。


①Validates(バリデート)

・modelファイルに対して実施
・ユーザーが入力した値をdb保存する際の条件を指定できる
・Ex)空入力禁止,最大文字数10文字まで...

qiita.rb
def model
  validates :column, 条件: 値の指定
end

【解説】
○validates :column名
⇒条件を課したいcolumn名を記載

○条件: 値の指定
⇒実施したい制約に応じた記述法を記述
⇒以下に2つ例を記述する

・空入力禁止

qiita.rb
presence: true

・最大文字数10文字まで

qiita.rb
length: { maximum: 10 } 

②params

・配列を入れるための箱(ハッシュ)
・ユーザーが送信したデータを一時的にparamsに格納
・その中から,必要なハッシュにヒットする値を取り出す

【例: ユーザーが名前と年齢をフォームから送信した場合】
・フォームからparams へ次のように格納される

qiita.rb
params = [:name,'naoto', :age,24]

・名前を取得したい場合

qiita.rb
 params[:name]

・年齢を取得したい場合

qiita.rb
params[:age]

③strong_parameters

・指定したcolumn情報以外のフォーム受信値を無視するフィルター

【いつ役に立つ?】
・ECサイトを作成したと想定
・ユーザーがフォームでソースを操作
・ポイントcolumnを修正し残高を高めに変更
・こういった改ざん処理を止めるための設定

【使いかた】
・controller中に記述(フォーム送信後のインスタンス生成時)
・引数にこのメソッドを()で指定する
・フィルタされた値がdbへ格納

【例:question_paramsメソッド】

qiita.rb
params.require(:question).permit(:name, :title, :content)

【paramsには以下が格納された場合を想定】

qiita.rb
params = [
question={name: 'naoto',age: 24, content: '質問内容'},
answer={name: 'kanopyo',age: 27, content: '回答内容'}
]

【解説】
〇.requireについて
⇒モデル名のキーを指定

〇.permitについて
⇒.requierで指定したモデルのプロパティのキーを指定

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

logを確認しながらrakeタスクを実装して、対象appを指定して起動しよう(Heroku)

こんにちは、株式会社ベストティーチャーでサーバーサイドエンジニアとして働き出したばかりのタワラです。

業務ではじめて学んだTipsを紹介したいと思います。
今回はrakeタスクを作成してHerokuで動作確認をする手順です。

Rakeタスクを実装する手順

Tips1. ローカルでRakeタスクを作成する際には、Rails.loggerを活用する

こんな感じでコードを書いていたのですが、、、キャッシュを問い合わせるだけだから、これではログに何の情報も出ないな、動作確認はどうしよう、、、と思っているところ、、、

 namespace :hogehoge do
   desc "適当な説明をいれる"
   task :foobar => :environment do
     Rails.cache.fetch('hoge')
   end
 end

先輩「Rails.loggerを使うんだ!」
ボク「なるほど!」

 namespace :hogehoge do
   desc "適当な説明をいれる"
   task :foobar => :environment do
     Rails.logger.info('foobar started!')
     Rails.cache.fetch('hoge')
     Rails.logger.info('foobar finished!')
   end
 end

このように記述しておけば、例えばログに結果が出ない場合の処理でも、rakeタスクの作業確認ができます。こんなふうに↓

log/development.log
略

foobar started!
foobar finished!

略

きちんと動作していることが一目瞭然なのですごい便利!

Tips2. Heroku上で確認するときはアプリ名を指定する

続いて、Heroku上で動作確認をする必要があります、↓のコマンドでrakeタスクを走らせることはできますが、、、

heroku run rake hogehoge:foobar 

先輩「ちょっとお待ち!」
先輩「オプションでプロジェクト名を指定するのを忘れないで!」

heroku run rake hogehoge:foobar -a omosiro-project

先輩「きちんとrakeタスクを起動する対象のプロジェクトなのかを確認しよう!」
先輩「こういうことでバグが起きる可能性はゼロじゃないからね」
ボク「なるほど!」

終わりに

今回のTipsはこの2つ!

Tips1. ローカルでRakeタスクを作成する際には、Rails.loggerを活用する
Tips2. Heroku上で確認するときはアプリ名を指定する

業務ではやはり学ぶことが多いです。
本記事が業務駆け出しの方などの役に立てば幸いです。

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

Railsを使ったToDoリストの作成(3.Railsの基本概念)

概要

本記事は、初学者がRailsを使ってToDoリストを作成する過程を記したものです。
私と同じく初学者の方で、Railsのアウトプット段階でつまづいている方に向けて基礎の基礎を押さえた解説をしております。
抜け漏れや説明不足など多々あるとは思いますが、読んでくださった方にとって少しでも役に立つ記事であれば幸いです。

環境

  • Homebrew: 2.5.10 -> MacOSのパッケージ管理ツール
  • ruby: 2.6.5p114 -> Ruby
  • Rails: 6.0.3.4 -> Rails
  • node: 14.3.0 -> Node.js
  • yarn: 1.22.10 -> JSのパッケージ管理ツール
  • Bundler: 2.1.4 -> gemのバージョン管理ツール
iTerm
$ brew -v => Homebrew 2.5.10
$ ruby -v => ruby 2.6.5p114
$ rails -v => Rails 6.0.3.4
$ npm version => node: '14.3.0'
$ yarn -v => 1.22.10
$ Bundler -v => Bundler version 2.1.4

第3章 Railsの基本概念

第3章では、Railsで本格的にアプリ開発をする前にRailsにおいて重要な概念を説明していきます。

1 Railsを使ってWebサイトを表示する

まずは、RailsがどのようにWebサイトを表示しているのか説明します。
Webサイトは、クライアント(ブラウザ)がリクエストを送り、サーバ(ここではRails)がレスポンス(HTMLやCSSファイルをブラウザに送る)したものを、ブラウザが解釈することで表示されています。

ではRailsでは具体的にどのようなことが行われているか見ていきます。

①まず、ブラウザからGETリクエストが飛んできたらroutes.rbで処理をします。
routes.rbはURLを作る場所です。

config/routes.rb
Rails.application.routes.draw do
  root to: 'boards#index'
end

?‍♂️root(localhost:3000)を表示してくださいというリクエストが飛んできたらBoardsControlerのindexメソッドを実行してください

②次に、コントローラです。
コントローラはroutes.rbからブラウザからのリクエストを受け取り、モデルやビューなどと連携し結果をブラウザに返す役割を担っています。
routes.rbの「BoardsControlerのindexメソッドを実行してください」という情報をもとに、コントローラが処理を行います。

app/controllers/boards_controller.rb
class BoardsController < ApplicationController
    def index
      render 'boards/index' #この部分は省略可能
    end   
end

?‍♂️viewsのboardsのindex.html.hamlを表示してください

ちなみにコントローラには命名規則があります。

名前
コントローラ名 boards
コントローラのクラス名 BoardsController
コントローラのファイル名 boards_controller.rb

上記のように、コントローラ名を「boards」とした場合、コントローラのクラス名は「BoardsController」のようにコントローラ名の先頭を大文字にしてControllerを付けます。このクラスが記載されているファイル名は「コントローラ名_controller.rb」になります。

③最後に、viewsです、viewsにはブラウザに表示したいことを記述します。

app/views/boards/index.html.haml
%h1 Webサイトの表示

スクリーンショット 2020-11-17 12.58.51.png

上の画像のように表示されていれば完了です。
以上が、RailsがWebサイトを表示する仕組みです。

2 MVC

1 MVCとは

MVCとは、Webフレームワークで一般的に取り入れられているアプリケーションの設定を整理するための概念の一つです。
「Model」「Views」「Controller」の頭文字をとって名付けられています。
それぞれに役割があり、ControllerがModelからデータを取得してViewsに表示するという処理が行われています。

スクリーンショット 2020-11-17 16.13.44.png
(画像の引用元:https://snome.jp/framework/mvc-model/)

2 モデルの作成

実際に、操作の対象となる「モデル」を作成します。
モデルを作成するためにはターミナルにて$rails g model [モデル名(単数形)]のコマンドを実行します。

iTerm
$ rails g model Board
=>create db/migrate/20201117041911_create_boards.rb
  create app/models/board.rb

?‍♂️Boardモデルを作成してください
?Boardに対応するデータベースのテーブル(migrationファイル)を作成しました
?Boardに対応するRubyのクラスを作成しました

モデルの作成が完了していたら以下のようなファイルが作成されているはずです。
コメントアウトされている部分は現在のデータベースの構造(schema)のメモであり、第1章で'annotate'をインストールしたため表示されている。

app/models/board.rb
# == Schema Information
#
# Table name: boards
#  id          :bigint           not null, primary key
#  created_at  :datetime         not null
#  updated_at  :datetime         not null
#
class Board < ApplicationRecord
end

モデルが作成できたら、migrationファイルにcolumnを追加していきます。migrationファイルはデータベースのテーブルを作成するファイルです。
今回はボード(タスクのまとまり)の名前と説明をデータとして扱いたいので、'name'と'description'という2つのcolumnを追加します。
columnを追加する際はt.[データ型] :[column名]と入力します。

20201115023512_create_boards.rb
class CreateBoards < ActiveRecord::Migration[6.0]
  def change
    create_table :boards do |t|
      t.string :name, null: false
      t.text :description, null: false
      t.timestamps
    end
  end
end

null: falseオプション→このcolumnには絶対に値が入っていないといけないという指定ができます。今回はタスク名とタスクの説明は必須項目のため指定しています。

マイグレーションをデータベース(PostgreSQL)に適用するには、ターミナルにて$rails db:migrateを忘れずに実行しましょう。

3 ActiveRecord

ActoveRecordとは、Railsで採用されているORマッパーです。
ORマッパーとは、オブジェクトとデータベース間の関係を定義するだけで、データベースへのアクセスが行えるシステムのことです。
つまりRubyを使うことでSQLを書かなくてもデータベースにアクセスすることができるということです。

ActiveRecordはRailsにデフォルトでインストールされており、モデルの中で使うことができます。
では、なぜモデルの中で使うことができるのでしょうか?
以下2つのファイルを見てみましょう。

app/models/board.rb
class Board < ApplicationRecord
end
app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true
end

BoardクラスはApplicationRecordクラスを継承しています。
ApplicationRecordクラスはActiveRecord::Baseクラスを継承しています。
つまりBoardクラスがActiveRecord::Baseクラスを継承しているため、モデルの中でActiveRecordを使うことができます。

ActiveRecord::Baseクラスには様々なメソッドが定義されており、それらのメソッドを実行することにより、データベースにアクセスすることができます。

では具体的にどのようなメソッドが定義されているのか?
代表的なものを見ていきましょう。

◆レコードを取得する

Board.find(1)
#テーブルから引数に入っているidのレコードを取得する

Board.find_by({column: ''})
#テーブルからcolumn名でレコードを取得する
#Board.find_by(column名: '') ->  {}を省略しても良い

Board.first,second,third
#テーブルからidが一番若いレコードを取得する

Board.last
#テーブルからidが一番大きいレコードを取得する

Board.all
#テーブルから全てのレコードをを配列として取得する

Board.all.order(:id)
#レコードををid順に並び替えて取得する
#逆順に並べ替えて取得する時は(id: :desc)

Board.all.limit(3)
#テーブルから引数に入っている数だけidの若い順にレコードを取得する

Board.where('id > ?', 2)
#引数の条件に合ったレコードを全て取得する
#条件は文字列で渡す必要がある

Board.count
#データの件数を数える

◆データの作成・削除

Board.create({name: '', description: ''})
#引数の値を元にインスタンスを作成し、DBに保存する
#board = Board.new({name: 'new', description: 'new'}) -> 空の箱を作る
#board.save -> レコードをDBに保存する

Board.save
#createとは違って保存機能のみ

Board.update
#取得したレコードを更新し保存する
#Board.last -> 対象となるデータを取ってくる
#Board.last.update({column名: ''}) -> 更新する

Board.assign_attributes
#取得したレコードを更新するが保存はしない
#board = Board.last
#board.assign_attributes(title: 'assigned')
#board.save

Board.destroy
#取得したレコードを削除する
#board = Board.last -> 削除する対象のデータを取得する
#board.destroy`

では実際に、ターミナルでコンソールを立ち上げてデータを作成してみましょう。
ターミナルでコンソールを立ち上げるためには以下のコマンドを実行します。

iTerm
$ rails c

コンソールが立ち上がったら、createメソッドを実行しましょう。

iTerm
irb(main):001:0> Board.create(name: 'console-name1', description: 'console-description1')

   (0.2ms)  BEGIN
  Board Create (4.4ms)  INSERT INTO "boards" ("name", "description", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id"  [["name", "console-name1"], ["description", "console-description1"], ["created_at", "2020-11-17 05:38:06.272799"], ["updated_at", "2020-11-17 05:38:06.272799"]]
   (1.5ms)  COMMIT
=> #<Board id: 1, name: "console-name1", description: "console-description1", created_at: "2020-11-17 05:38:06", updated_at: "2020-11-17 05:38:06">

createメソッドを実行することにより、SQLが実行され、テーブルにデータが保存されます。
では、実際に保存されているのかActiveRecord::Baseクラスのallメソッドを使って確かめてみましょう。

iTerm
irb(main):002:0> Board.all
  Board Load (0.9ms)  SELECT "boards".* FROM "boards" LIMIT $1  [["LIMIT", 11]]
=> #<ActiveRecord::Relation [#<Board id: 1, name: "console-name1", description: "console-description1", created_at: "2020-11-17 05:38:06", updated_at: "2020-11-17 05:38:06">]>

下記のようにデータが作成されていることがわかると思います。

Board id: 1,
name: "console-name1",
description: "console-description1",
created_at: "2020-11-17 05:38:06",
updated_at: "2020-11-17 05:38:06">

以上でActiveRecordの説明は終わりです。
コンソールを終了したい時はexitと入力しましょう。

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

【Rails6.0】ポートフォリオに必須な「かんたんログイン」実装手順

概要

Railsでアプリを作成する際、deviseを用いてログイン機能を実装する方は多いかと思います。
中でも、ポートフォリオには「かんたんログインをつけよう!」と耳にした方も多いかと。

しかし、私の経験談として、実装には結構苦労しました
理由としては、シンプルに「devise関連で、知らなかったことをやる必要があったから」です。

なので今回の記事では、

  • deviseのコントローラをいじらなきゃいけないの?
  • ルーティングはどうやって設定する?

このあたりを整理しつつ、「かんたんログイン実装手順」をアウトプットしたいと思います。

なお、この記事はこちらの素晴らしい記事を参考にしています。
YouTubeでの詳しい解説もありますので、ぜひ御覧ください。

Qiita | 簡単ログイン・ゲストログイン機能の実装方法(ポートフォリオ用)


環境

  • macOS Catalina 10.15.6
  • ruby 2.6.5
  • Rails 6.0.3.4
  • MySQL : 5.6.47
  • devise : 4.7.3


実装の流れを確認

実装に入る前に、まずかんたんログインの流れをおさえましょう。

かんたんログインの流れ
1. かんたんログインボタンクリック
2-1. 任意のメアド(guest@gmail.com)がusersテーブルにない場合は他カラムを追加しユーザー作成
2-2. 任意のメアド(guest@gmail.com)がusersテーブルにある場合はそれを取得
3. 作成・取得したユーザーでログイン
4. 好きなページにリダイレクト(大体はトップページ)

つまり、必要なのは次の工程です。

必要な工程
1. ルーティングにかんたんログイン用コントローラのパスを設定
2. かんたんログイン用コントローラーを作成
3. リンクをビューに設置

この工程に沿って実装していきます。


実装 : ファイルを3つ編集すればOK

それでは、ルーティング→コントローラ→ビューの順番で実装していきます


1. かんたんログイン用ルーティングの設定

ルーティングファイルに以下の記述を追加して下さい。

routes.rb
devise_scope :user do
  post 'users/guest_sign_in', to: 'users/guest_sessions#new_guest'
end

これにより、次のルーティングが生成します。

Prefix Verb URI Pattern Controller#Action
users_guest_sign_in POST /users/guest_sign_in users/guest_sessions#new_guest

この内容を整理します。

  • post, users_guest_sign_in_pathで
  • app/controllers/users/guest_sessions_controller.rb
  • new_guestアクションを指定

というわけで、指定したディレクトリにコントローラを作成して、かんたんログインアクションを実装しましょう。


2. かんたんログイン用コントローラの作成

app/controllers/users/guest_sessions_controller.rbnew_guestアクションを実装していきます。

  • 名前空間とRailsの決まりごとに従ってクラス名を選択
  • DeviseのSessionsControllerを継承
  • guest@gmail.comの有無でユーザーの作成or取得を変更

以上の点に注意しましょう。

app/controllers/users/guest_sessions_controller.rb
class Users::SessionsController < Devise::SessionsController
  def new_guest
    user = User.find_or_create_by!(email: "guest@gmail.com") do |user|
      # ブロックで必要カラムを追加(自分の場合はnicknameを追加)
      user.nickname = "テストユーザー"
      user.password = SecureRandom.urlsafe_base64
      # user.confirmed_at = Time.now  # Confirmable を使用している場合は必要
    end
    # ログイン(deviseのメソッド)
    sign_in user
    # トップページへリダイレクト
    redirect_to root_path
  end  
end

find_or_create_by!は次のようなメソッドです。

  • find_by(カラム: 値)で該当するレコードを取得
  • 見つからない場合は、createで新規レコードを作成
  • ! は例外を発生させる記述 (! がない場合は、ログインされないままただリダイレクトされる)

ちなみに、今回は実装するコードについての示すのが目的なので、名前空間とパスワードの部分については以下の記事を見て補完して下さい (なお、Qiitaは僕が書いた記事です)。


3. ビューにリンクを設置

かんたんログインボタンを実装したいビューにリンクを設置しましょう。
僕のアプリの場合は、トップページに以下のコードを追加しました。

トップページ
<%= link_to 'ゲストログイン', users_guest_sign_in_path, method: :post %>

これで完了です!
(以下のGIFでは自分のアプリ用に詳細を変更しています)
Image from Gyazo


4. (補足) 一部をUserモデルに移植する

DBとやりとりしてレコードを取得・生成するのはモデルの仕事です。
なので次のように切り離すとベターかと思います。

app/controllers/users/guest_sessions_controller.rb
class Users::SessionsController < Devise::SessionsController
  def new_guest
    user = User.guest
    # ログイン(deviseのメソッド)
    sign_in user
    # トップページへリダイレクト
    redirect_to root_path
  end  
end
app/models/user.rb
class User < ApplicationRecord

# (略)

  def self.guest
    find_or_create_by!(email: "guest@gmail.com") do |user|
      user.nickname = "テストユーザー"
      user.password = SecureRandom.urlsafe_base64
    end
  end
end


まとめ

  • かんたんログイン実装には、以下のファイルを作成or編集すればOK
    • かんたんログインアクションへのルーティング
    • deviseコントローラを継承した「かんたんログイン用コントローラ」
    • ボタンを設置したいビュー

いろんな記事があると思いますが、基本はこちらに示したとおりなので、ぜひ参考にして下さい!

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

[Rails]カテゴリー機能

はじめに

アプリ開発において、ancestryというgemを用いてカテゴリー機能を加えたのでまとめました。

目次

  1. カテゴリー選択・保存
  2. カテゴリー検索表示
  3. カテゴリー検索結果表示

1. カテゴリー選択・保存

カテゴリー選択・保存

categoriesテーブルの作成

ancestryをインストールします。

gemfile
gem 'ancestry'

次にcategoryモデルを作成します。

ターミナル
rails g model category

has_ancestryを記述します。

app/models/category.rb
class Category < ApplicationRecord
  has_many :posts
  has_ancestry
end

以下のようにマイグレーションファイルに記述します。
indexについてはこちら

db/migrate/20XXXXXXXXXXXX_create_categories.rb
class CreateCategories < ActiveRecord::Migration[6.0]
  def change
    create_table :categories do |t|
      t.string :name,     index: true, null: false
      t.string :ancestry, index: true
      t.timestamps
    end
  end
end

googleスプレッドシートにカテゴリーを記述していきます。
Aの列がid、Bの列がname(カテゴリー名)、Cの列がancestry(親子孫を見分ける数値)となります。
データの保存方法は、ファイル → ダウンロード → カンマ区切りの値(.csv 現在のシート) の手順で保存できます。

カテゴリー記述例
カテゴリー記述例

ダウンロードしたcsvファイルはdbフォルダに配置します。

seeds.rbファイル内へ以下の通り記述します。

db/seeds.rb 
require "csv"

CSV.foreach('db/category.csv') do |row|
  Category.create(:id => row[0], :name => row[1], :ancestry => row[2])
end 

ターミナルでrails db:seedコマンドを実行するとcsvファイルを読み込み自動でDBのレコードが生成されます。
foreachの後に読み込みたいファイルの指定を行います。
その下の記述については、モデル名.create(カラム名 => 読み込みたい列)となります。
row[0] → Aの列がid
row[1] → Bの列がname(カテゴリー名)
row[2] → Cの列がancestry(親子孫を見分ける数値)

ルーティング

子、孫カテゴリーをjson形式でルーティングを設定します。

config/routes.rb
Rails.application.routes.draw do
  ~~
  resources :posts do
    collection do
      get 'top'
      get 'get_category_children', defaults: { format: 'json' }
      get 'get_category_grandchildren', defaults: { format: 'json' }
      get 'name_search'
    end
  ~~
end

コントローラー

postsコントローラーに親カテゴリーを定義します。
複数箇所で使用するためbefore_actionを使って定義します。

app/controllers/posts_controller.rb
def set_parents
  @parents = Category.where(ancestry: nil)
end

postsコントローラーに子、孫カテゴリーのメソッドを定義します。

app/controllers/posts_controller.rb
def get_category_children
  @category_children = Category.find("#{params[:parent_id]}").children
end

def get_category_grandchildren
  @category_grandchildren = Category.find("#{params[:child_id]}").children
end

json.jbuilderファイルを作成し、jsonデータへ変換します。

app/views/posts/get_category_children.json.jbuilder 
json.array! @category_children do |child|
  json.id child.id
  json.name child.name
end
app/views/posts/get_category_grandchildren.json.jbuilder 
json.array! @category_grandchildren do |grandchild|
  json.id grandchild.id
  json.name grandchild.name
end

ビュー

javascriptでカテゴリー選択時の動作を設定します。

:app/javascript/category_post.js
$(function(){
  function appendOption(category){
    var html = `<option value="${category.id}">${category.name}</option>`;
    return html;
  }
  function appendChildrenBox(insertHTML){
    var childSelectHtml = "";
    childSelectHtml = `<div class="category__child" id="children_wrapper">
                        <select id="child__category" name="post[category_id]" class="serect_field">
                          <option value="">---</option>
                          ${insertHTML}
                        </select>
                      </div>`;
    $('.append__category').append(childSelectHtml);
  }
  function appendGrandchildrenBox(insertHTML){
    var grandchildSelectHtml = "";
    grandchildSelectHtml = `<div class="category__child" id="grandchildren_wrapper">
                              <select id="grandchild__category" name="post[category_id]" class="serect_field">
                                <option value="">---</option>
                                ${insertHTML}
                                </select>
                            </div>`;
    $('.append__category').append(grandchildSelectHtml);
  }

  $('#item_category_id').on('change',function(){
    var parentId = document.getElementById('item_category_id').value;
    if (parentId != ""){
      $.ajax({
        url: '/posts/get_category_children/',
        type: 'GET',
        data: { parent_id: parentId },
        dataType: 'json'
      })
      .done(function(children){
        $('#children_wrapper').remove();
        $('#grandchildren_wrapper').remove();
        var insertHTML = '';
        children.forEach(function(child){
          insertHTML += appendOption(child);
        });
        appendChildrenBox(insertHTML);
        if (insertHTML == "") {
          $('#children_wrapper').remove();
        }
      })
      .fail(function(){
        alert('カテゴリー取得に失敗しました');
      })
    }else{
      $('#children_wrapper').remove();
      $('#grandchildren_wrapper').remove();
    }
  });
  $('.append__category').on('change','#child__category',function(){
    var childId = document.getElementById('child__category').value;
    if(childId != ""){
      $.ajax({
        url: '/posts/get_category_grandchildren',
        type: 'GET',
        data: { child_id: childId },
        dataType: 'json'
      })
      .done(function(grandchildren){
        $('#grandchildren_wrapper').remove();
        var insertHTML = '';
        grandchildren.forEach(function(grandchild){
          insertHTML += appendOption(grandchild);
        });
        appendGrandchildrenBox(insertHTML);
        if (insertHTML == "") {
          $('#grandchildren_wrapper').remove();
        }
      })
      .fail(function(){
        alert('カテゴリー取得に失敗しました');
      })
    }else{
      $('#grandchildren_wrapper').remove();
    }
  })
});

新規投稿ページにカテゴリーセレクトボックスを表示させます。

app/views/posts/new.html.erb
<div class="append__category">
  <div class="category">
    <div class="form__label">
      <div class="weight-bold-text lavel__name ">
        カテゴリー
      </div>
      <div class="lavel__Required">
        <%= f.collection_select :category_id, @parents, :id, :name,{ include_blank: "選択してください"},class:"serect_field", id:"item_category_id" %>
      </div>
    </div>
  </div>
</div>

2. カテゴリー検索表示

カテゴリー検索表示

コントローラー

app/controllers/posts_controller.rb
def top
  respond_to do |format|
    format.html
    format.json do
      if params[:parent_id]
        @childrens = Category.find(params[:parent_id]).children
      elsif params[:children_id]
        @grandChilds = Category.find(params[:children_id]).children
      elsif params[:gcchildren_id]
        @parents = Category.where(id: params[:gcchildren_id])
      end
    end
  end
end

ビュー

javascriptでどの親カテゴリーにの上にマウスがいるのか、それに属する子カテゴリーや孫カテゴリーを取得しています。

:app/javascript/category.js
$(document).ready(function () {
  // 親カテゴリーを表示
  $('#categoBtn').hover(function (e) {
    e.preventDefault();
    e.stopPropagation();
    $('#tree_menu').show();
    $('.categoryTree').show();
  }, function () {
    // あえて何も記述しない
  });

  // 非同期にてヘッダーのカテゴリーを表示
  function childBuild(children) {
    let child_category = `
                        <li class="category_child">
                          <a href="/posts/${children.id}/search"><input class="child_btn" type="button" value="${children.name}" name= "${children.id}">
                          </a>
                        </li>
                        `
    return child_category;
  }

  function gcBuild(children) {
    let gc_category = `
                        <li class="category_grandchild">
                          <a href="/posts/${children.id}/search"><input class="gc_btn" type="button" value="${children.name}" name= "${children.id}">
                          </a>
                        </li>
                        `
    return gc_category;
  }

  // 親カテゴリーを表示
  $('#categoBtn').hover(function (e) {
    e.preventDefault();
    e.stopPropagation();
    timeOut = setTimeout(function () {
      $('#tree_menu').show();
      $('.categoryTree').show();
    }, 500)
  }, function () {
    clearTimeout(timeOut)
  });

  // 子カテゴリーを表示
  $('.parent_btn').hover(function () {
    $('.parent_btn').css('color', '');
    $('.parent_btn').css('background-color', '');
    let categoryParent = $(this).attr('name');
    timeParent = setTimeout(function () {
      $.ajax({
          url: '/posts/top',
          type: 'GET',
          data: {
            parent_id: categoryParent
          },
          dataType: 'json'
        })
        .done(function (data) {
          $(".categoryTree-grandchild").hide();
          $(".category_child").remove();
          $(".category_grandchild").remove();
          $('.categoryTree-child').show();
          data.forEach(function (child) {
            let child_html = childBuild(child)
            $(".categoryTree-child").append(child_html);
          });
          $('#tree_menu').css('max-height', '490px');
        })
        .fail(function () {
          alert("カテゴリーを選択してください");
        });
    }, 400)
  }, function () {
    clearTimeout(timeParent);
  });

  // 孫カテゴリーを表示
  $(document).on({
    mouseenter: function () {
      $('.child_btn').css('color', '');
      $('.child_btn').css('background-color', '');
      let categoryChild = $(this).attr('name');
      timeChild = setTimeout(function () {
        $.ajax({
            url: '/posts/top',
            type: 'GET',
            data: {
              children_id: categoryChild
            },
            dataType: 'json'
          })
          .done(function (gc_data) {
            $(".category_grandchild").remove();
            $('.categoryTree-grandchild').show();
            gc_data.forEach(function (gc) {
              let gc_html = gcBuild(gc)
              $(".categoryTree-grandchild").append(gc_html);
              let parcol = $('.categoryTree').find(`input[name="${gc.root}"]`);
              $(parcol).css('color', 'white');
              $(parcol).css('background-color', '#b1e9eb');
            });
            $('#tree_menu').css('max-height', '490px');
          })
          .fail(function () {
            alert("カテゴリーを選択してください");
          });
      }, 400)
    },
    mouseleave: function () {
      clearTimeout(timeChild);
    }
  }, '.child_btn');

  // 孫カテゴリーを選択時
  $(document).on({
    mouseenter: function () {
      let categoryGc = $(this).attr('name');
      timeGc = setTimeout(function () {
        $.ajax({
            url: '/posts/top',
            type: 'GET',
            data: {
              gcchildren_id: categoryGc
            },
            dataType: 'json'
          })
          .done(function (gc_result) {
            let childcol = $('.categoryTree-child').find(`input[name="${gc_result[0].parent}"]`);
            $(childcol).css('color', 'white');
            $(childcol).css('background-color', '#b1e9eb');
            $('#tree_menu').css('max-height', '490px');
          })
          .fail(function () {
            alert("カテゴリーを選択してください");
          });
      }, 400)
    },
    mouseleave: function () {
      clearTimeout(timeGc);
    }
  }, '.gc_btn');


  // カテゴリー一覧ページのボタン
  $('#all_btn').hover(function (e) {
    e.preventDefault();
    e.stopPropagation();
    $(".categoryTree-grandchild").hide();
    $(".categoryTree-child").hide();
    $(".category_grandchild").remove();
    $(".category_child").remove();
  }, function () {
    // あえて何も記述しないことで親要素に外れた際のアクションだけを伝搬する
  });

  // カテゴリーを非表示(カテゴリーメニュから0.8秒以上カーソルを外したら消える)
  $(document).on({
    mouseleave: function (e) {
      e.stopPropagation();
      e.preventDefault();
      timeChosed = setTimeout(function () {
        $(".categoryTree-grandchild").hide();
        $(".categoryTree-child").hide();
        $(".categoryTree").hide();
        $(this).hide();
        $('.parent_btn').css('color', '');
        $('.parent_btn').css('background-color', '');
        $(".category_child").remove();
        $(".category_grandchild").remove();
      }, 800);
    },
    mouseenter: function () {
      timeChosed = setTimeout(function () {
        $(".categoryTree-grandchild").hide();
        $(".categoryTree-child").hide();
        $(".categoryTree").hide();
        $(this).hide();
        $('.parent_btn').css('color', '');
        $('.parent_btn').css('background-color', '');
        $(".category_child").remove();
        $(".category_grandchild").remove();
      }, 800);
      clearTimeout(timeChosed);
    }
  }, '#tree_menu');

  // カテゴリーボタンの処理
  $(document).on({
    mouseenter: function (e) {
      e.stopPropagation();
      e.preventDefault();
      timeOpened = setTimeout(function () {
        $('#tree_menu').show();
        $('.categoryTree').show();
      }, 500);
    },
    mouseleave: function (e) {
      e.stopPropagation();
      e.preventDefault();
      clearTimeout(timeOpened);
      $(".categoryTree-grandchild").hide();
      $(".categoryTree-child").hide();
      $(".categoryTree").hide();
      $("#tree_menu").hide();
      $(".category_child").remove();
      $(".category_grandchild").remove();
    }
  }, '.header__headerInner__nav__listsLeft__item');
});

トップ画面にカテゴリー選択ウィンドウをセットします。

app/views/posts/top.html.erb
  <div class="item-categories">
    <h2>
      カテゴリー一覧
    </h2>
    <%= link_to  posts_path, class: "category-button", id: 'categoBtn' do %>
      カテゴリーから探す
    <% end %>
    <div id="tree_menu">
      <ul class="categoryTree">
        <% @parents.each do |parent| %>
          <li class="category_parent">
            <%= link_to search_post_path(parent) do %>
              <input type="button" value="<%= parent.name %>" name="<%= parent.id %>" class="parent_btn">
            <% end %>
          </li>
        <% end %>
      </ul>
      <ul class="categoryTree-child">
      </ul>
      <ul class="categoryTree-grandchild">
      </ul>
    </div>
  </div>

3. カテゴリー検索結果表示

カテゴリいー検索結果表示

ルーティング

カテゴリーをidで区別するため、memberを用いてsearchアクションを定義しています。

config/routes.rb
resources :posts do
    ~~
    member do
      get 'search'
    end
   ~~
end

コントローラー

クリックしたカテゴリーが、親カテゴリー、子カテゴリー、孫カテゴリーのどれなのかで条件分岐しています。

app/controllers/posts_controller.rb
  def search
    @category = Category.find_by(id: params[:id])

    if @category.ancestry == nil
      category = Category.find_by(id: params[:id]).indirect_ids
      if category.empty?
        @posts = Post.where(category_id: @category.id).order(created_at: :desc)
      else
        @posts = []
        find_item(category)
      end

    elsif @category.ancestry.include?("/")
      @posts = Post.where(category_id: params[:id]).order(created_at: :desc)

    else
      category = Category.find_by(id: params[:id]).child_ids
      @posts = []
      find_item(category)
    end
  end

  def find_item(category)
    category.each do |id|
      post_array = Post.where(category_id: id).order(created_at: :desc)
      if post_array.present?
        post_array.each do |post|
          if post.present?
            @posts.push(post)
          end
        end
      end
    end
  end

ビュー

app/views/posts/search.html.erb
  <div class="item-categories">
    <h2>
      カテゴリー一覧
    </h2>
    <%= link_to  posts_path, class: "category-button", id: 'categoBtn' do %>
      カテゴリーから探す
    <% end %>
    <div id="tree_menu">
      <ul class="categoryTree">
        <% @parents.each do |parent| %>
          <li class="category_parent">
            <%= link_to search_post_path(parent) do %>
              <input type="button" value="<%= parent.name %>" name="<%= parent.id %>" class="parent_btn">
            <% end %>
          </li>
        <% end %>
      </ul>
      <ul class="categoryTree-child">
      </ul>
      <ul class="categoryTree-grandchild">
      </ul>
    </div>
  </div>

参考リンク

https://qiita.com/k_suke_ja/items/aee192b5174402b6e8ca
https://qiita.com/Sobue-Yuki/items/9c1b05a66ce6020ff8c1
https://qiita.com/dr_tensyo/items/88e8ddf0f5ce37040dc8
https://qiita.com/ATORA1992/items/bd824f5097caeee09678
https://qiita.com/misioro_missie/items/175af1f1678e76e59dea
https://qiita.com/Rubyist_SOTA/items/49383aa7f60c42141871

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

HTML.CSS.JavaScript.Ruby.Rails概要

HTML

 HTML(Hyper Text Markup Language)とはWEBページを作成する際に使用されるマークアップ言語である。
 WEBページのほとんどにHTMLが使用されている。
 HTMLはタグを使いコンピューターに命令を出す事により見出しを付けたり段落を付けたりと、WEBページのレイアウト、構成を形作ることができる。

CSS

CSS(Cascading Style Sheets)は先ほどのHTMLと組み合わせて使用する言語である。
CSSは文字の色やサイズ、レイアウトを変えたり、WEBページを装飾する言語である。
HTMLでもWEBページの装飾をすることは出来るが、CSSの役割なので分けて使う必要がある。

JavaScript

WEBサイトに動きをつけるためのプログラミング言語である。
具体的には文章や画像を拡大表示したり、より動的なWEBサイトを作ることができる。
サーバーを介さずにブラウザ上で動かすことができる。またこのようなプログラムをクライアントサイド・スクリプトという

Ruby

Rubyとは日本人であるまつもとゆきひろ氏によって作成されたオブジェクト指向スクリプト言語である。
WEBサイトやECサイトなどの製作、SNS開発など様々なことができる。有名なサイトではぐるなび食べログなど
Rubyは他の言語に比べシンプルなコードのため開発スピードが早く読みやすい

Rails

Ruby on RailsとはRubyを使用したフレームワークである。
フレームワークとは雛形のことで、一からプログラミングをしなくても枠組みが用意されているため開発時間を大幅に削減することができる。他にもSinatraやHANAMIなどがある

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

Rails5.1+puma ローカルのproduction環境でSSL接続する

Rails5.0から5.1にアップグレードし、ローカルでproductionの動作確認しようとしたら、何かとつまずいたので諸問題の解決方法をまとめておきます。

まず、 rails s -e production を叩いたところ、この接続にはセキュリティの問題があるのでWEBページを表示できません的なエラーが出ました。httpsにしないとダメなようです。

SSLで接続する

基本的なやり方は、こちらの記事を参照させていただきました。
Rails5 + pumaのローカル環境でSSL/HTTPSを有効にする

上記記事は、opensslで証明書発行してましたが、エラーが出たので証明書の発行をmkcertでやりました。
mkcertでの証明書の発行のやり方は、こちらの記事が参考になりました。
ローカル環境でSSLをオレオレ証明書で行っていて警告が出てる人に朗報

まず、上記記事に従ってmkcertで証明書を発行、appフォルダ以下の適当な場所にserver.key、server.crtファイルを置きます。

Rails5 + pumaのローカル環境でSSL/HTTPSを有効にする を参考にpuma.rbを設定。
bundle exec pumactl start -e production でサーバーを起動。

css/jsが表示されなくなった問題の解決

SSLで接続できたものの、 HTTP parse error, malformed request () というエラーが出て、application.js、application.cssが読み込まれませんでした。
jsとcssのサーバーがhttp://0.0.0.0:3000 になっていたので、asset_hostの設定を以下のように修正。ローカルと本番環境で変えられるよう、環境変数で定義しました。

config/environments/production.rb
config.action_controller.asset_host = "https://#{ENV['HOST_URI']}"

参考
- StackOverflow::Ruby on Railsの本番環境でpublic/assetsが参照できない

public/images以下のファイルの表示問題解決

上記の対応でcssとjsは出るようになったけれど、public/images以下に置いていた画像ファイルがまだ表示されませんでした。
Rails5.1からは、asset_pipelineでプリコンパイルしない静的なファイルは、ヘルパーで呼び出すときには以下の用に書きます。

= image_tag 'hogehoge.png', skip_pipeline: true

しかし、これが設定してあってもダメだったので、別の問題のようです。

対応1
config.public_file_server.enabledをtrueに

config/environments/production.rb
- config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
+ config.public_file_server.enabled = true

対応2
assets:precompileをやり直す。

# 思い切って一度全部消したければ、先にこちら
RAILS_ENV=production bundle exec rake assets:clobber

# assets:cleanは古いバージョンのファイルを消す。clobberをやってたらcleanは不要
RAILS_ENV=production bundle exec rake assets:precompile assets:clean

上記対応で、assetすべて表示されるようになりました。

参考

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

Railsを使ったToDoリストの作成(2.hamlの導入と使い方)

概要

本記事は、初学者がRailsを使ってToDoリストを作成する過程を記したものです。
私と同じく初学者の方で、Railsのアウトプット段階でつまづいている方に向けて基礎の基礎を押さえた解説をしております。
抜け漏れや説明不足など多々あるとは思いますが、読んでくれた方にとって少しでも役に立つ記事であれば幸いです。

環境

  • Homebrew: 2.5.10 -> MacOSのパッケージ管理ツール
  • ruby: 2.6.5p114 -> Ruby
  • Rails: 6.0.3.4 -> Rails
  • node: 14.3.0 -> Node.js
  • yarn: 1.22.10 -> JSのパッケージ管理ツール
  • Bundler: 2.1.4 -> gemのバージョン管理ツール
iTerm
$ brew -v => Homebrew 2.5.10
$ ruby -v => ruby 2.6.5p114
$ rails -v => Rails 6.0.3.4
$ npm version => node: '14.3.0'
$ yarn -v => 1.22.10
$ Bundler -v => Bundler version 2.1.4

第2章 hamlの導入

第2章では、Railsでアプリ開発をする時にhamlテンプレートを使えるように設定していきます。
Railsはデフォルトはerbテンプレートが使われていますが、erbテンプレートは使いづらい上に、現場レベルでもあまり使われていないみたいなので、今回の開発においてはhamlテンプレートを使っていきたいと思います。

1 hamlの導入

まずは、hamlテンプレートをRailsで使えるようにしていきます。
hamlitというgemとerb2hamlというgemをインストールします。

Gemfile
gem 'hamlit'

group :development do
  gem 'erb2haml'
end

?‍♂️hamlitというgemをインストールしてください
?‍♂️erb2hamlという変換ツールのgemをインストールしてください

ターミナルで$bundle installを行えばgemのインストールは完了です。

既存のerbファイルをhamlに置き換えるためにはターミナルにて以下のコマンドを実行する必要があります。

iterm
$ bundle exec rake haml:replace_erbs

⚠︎hamlitをインストールしてエラーが起こってしまった場合は$rails sでサーバを立ち上げ直しましょう


以上でRailsの開発においてhamlテンプレートを使うことができるようになりました。
次は、実際にhamlの書き方を見ていきます。

2 hamlの基本ルール①

① タグの書き方

erb
<header>
  <p></p>
</header>
haml
%header
  %p
  • hamlでは閉じタグが必要ない
  • 入れ子構造にする際はスペースキーを2回押すこと

② class/id/attributeの書き方

erb
<header class="header">
  <p id="id"></p>
    <a href="https://haml.com"></a>
</header>
haml
%header.header
  %p#id
    %a{href: "https://haml.com"}
  • classは.class名で指定する
  • idは#id名で指定する
  • attributeはハッシュで指定する

③divタグの書き方

erb
<div class="container"></div>
haml
.container
  • divタグを作成したい時はタグの表記は必要ない。hamlが勝手に察してdivタグを付けてくれる

3 hamlの基本ルール②

①Rubyのコードの埋め込み(画面に表示する場合)

erb
<%= board.title %>
haml
= board.title
  • <%=%>を表現する際は=を使う

②Rubyのコードの埋め込み(画面に表示しない場合)

erb
<% Board.all.each do |board| %>
  <%= board.title %>
<% end %>
haml
- Board.all.each do |board|
  = board.title
  • <%%>を表現する際は-を使う
  • <% end %>はhamlが勝手に付けてくれる

※もしhamlの書き方がわからなくなったら下記のサイトで確認しましょう。

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

Railsを使ったToDoリストの作成(2.hamlの導入と書き方)

概要

本記事は、初学者がRailsを使ってToDoリストを作成する過程を記したものです。
私と同じく初学者の方で、Railsのアウトプット段階でつまづいている方に向けて基礎の基礎を押さえた解説をしております。
抜け漏れや説明不足など多々あるとは思いますが、読んでくださった方にとって少しでも役に立つ記事であれば幸いです。

環境

  • Homebrew: 2.5.10 -> MacOSのパッケージ管理ツール
  • ruby: 2.6.5p114 -> Ruby
  • Rails: 6.0.3.4 -> Rails
  • node: 14.3.0 -> Node.js
  • yarn: 1.22.10 -> JSのパッケージ管理ツール
  • Bundler: 2.1.4 -> gemのバージョン管理ツール
iTerm
$ brew -v => Homebrew 2.5.10
$ ruby -v => ruby 2.6.5p114
$ rails -v => Rails 6.0.3.4
$ npm version => node: '14.3.0'
$ yarn -v => 1.22.10
$ Bundler -v => Bundler version 2.1.4

第2章 hamlの導入と書き方

第2章では、Railsでアプリ開発をする時にhamlテンプレートを使えるように設定していきます。
Railsはデフォルトはerbテンプレートが使われていますが、erbテンプレートは使いづらい上に、現場レベルでもあまり使われていない(らしい)ので、今回の開発においてはhamlテンプレートを使っていきたいと思います。

1 hamlの導入

まずは、hamlテンプレートをRailsで使えるようにしていきます。
hamlitというgemとerb2hamlという変換ツールのgemをインストールします。

Gemfile
gem 'hamlit'

group :development do
  gem 'erb2haml'
end

?‍♂️hamlitというgemをインストールしてください
?‍♂️erb2hamlというgemをインストールしてください

ターミナルで$bundle installを行えばgemのインストールは完了です。

既存のerbファイルをhamlに置き換えるためにはターミナルにて以下のコマンドを実行する必要があります。

iterm
$ bundle exec rake haml:replace_erbs

⚠︎hamlitをインストールしてエラーが起こってしまった場合は$rails sでサーバを立ち上げ直しましょう


以上でRailsの開発においてhamlテンプレートを使うことができるようになりました。
次は、実際にhamlの書き方を見ていきます。

2 hamlの基本ルール①

① タグの書き方

erb
<header>
  <p></p>
</header>
haml
%header
  %p
  • hamlでは閉じタグが必要ない
  • 入れ子構造にする際はスペースキーを2回押すこと

② class/id/attributeの書き方

erb
<header class="header">
  <p id="id"></p>
    <a href="https://haml.com"></a>
</header>
haml
%header.header
  %p#id
    %a{href: "https://haml.com"}
  • classは.class名で指定する
  • idは#id名で指定する
  • attributeはハッシュで指定する

③divタグの書き方

erb
<div class="container"></div>
haml
.container
  • divタグを作成したい時はタグの表記は必要ない。hamlが勝手に察してdivタグを付けてくれる

3 hamlの基本ルール②

①Rubyのコードの埋め込み(画面に表示する場合)

erb
<%= board.title %>
haml
= board.title
  • <%=%>を表現する際は=を使う

②Rubyのコードの埋め込み(画面に表示しない場合)

erb
<% Board.all.each do |board| %>
  <%= board.title %>
<% end %>
haml
- Board.all.each do |board|
  = board.title
  • <%%>を表現する際は-を使う
  • <% end %>はhamlが勝手に付けてくれる

※もしhamlの書き方がわからなくなったら下記のサイトで確認しましょう。

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

10の倍数との差を求める

今取り組んだものは
正の整数を入力して、それが10の倍数との差が2以下であるならばTrue、そうでないならFalseを出力するというものだった。

以下が自分の回答である。

def near_ten(num)
  basis = (num + 5) / 10 * 10
  difference = (num - basis).abs
  if difference <= 2
    puts "True"
  else
    puts "False"
  end
end

まずnumに一番近い10の倍数をbasisとする。

basisにふさわしい数字を計算しているのが2行目。

  basis = (num + 5) / 10 * 10

例として、15~24までの整数だと一番近い10の倍数は20であるから、20を基準にしたい。
この整数たちにそれぞれ5を加えると、20~29になる。
この整数たちは10の位が2なので、それぞれ/ 10 * 10を行うと20が出てきて、基準にしたい数字となる。

他の整数でも同じようにうまくいく。

これでbasisの部分は終了。

次に、元々の数字とこの基準との差を計算している。

num - basis

このままでは差がマイナスになることもあるので、絶対値を出すメソッドを使っている。

(num - basis).abs

あとはこれを代入

difference = (num - basis).abs

こうして求めた差であるdifferenceが2以下であれば良いからそれを判定して終了。

  if difference <= 2
    puts "True"
  else
    puts "False"
  end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails SecureRandom.alphanumericを使ってテストアカウントのパスワードを生成する

railsでアプリケーションを作成しています。
テストアカウントでのログイン機能を実装したときにエラーが発生したため、自分が行った解決方法を記録として残しておきます。

開発環境

Ruby 2.6.5
Rails 6.0.3.3

実装内容

以下のコードでテストログイン機能の実装を行いました。

コントローラー

class Users::SessionsController < Devise::SessionsController
  # ゲストユーザーとしてログイン
  def new_guest
    user = User.find_or_create_by!(nickname: 'ゲストユーザー', email: 'user@example.com') do |user|
    user.password = SecureRandom.alphanumeric
    end
    sign_in user
    redirect_to root_path, notice: 'ゲストユーザーとしてログインしました。'
  end
end

モデル

 #半角英数字のみを許可するバリデーションを設定
  PASSWORD_REGEX = /\A(?=.*?[a-z])(?=.*?[\d])[a-z\d]+\z/i.freeze
  validates_format_of :password, with: PASSWORD_REGEX, on: :create, message: 'には半角英字と半角数字の両方を含めて設定してください'

開発環境でもエラーが発生することなく、テストも通ったため、本番環境にデプロイをしたところ「Sorry, something went wrong.」が表示されてしまいました。

原因

SecureRandom.alphanumericで英字のみのパスワードが発行されていたため。
そもそもSecureRandomはレファレンスでは以下のように説明がされています。

安全な乱数発生器のためのインターフェースを提供するモジュールです。 HTTP のセッションキーなどに適しています。

そしてalphanumericはSecureRandomモジュールのメソッドの一種であり、ランダムな英数字を生成してくれます。しかし必ずしも英数字混合で生成されるわけではありません。

コンソール
pry(main)> SecureRandom.alphanumeric
=> "NNCMHbfUbHRQmbwW"

確率は高くはないですが、上記のように英字のみのパスワードが生成されてしまうことがあります。

解決策

class Users::SessionsController < Devise::SessionsController
  # ゲストユーザーとしてログイン
  def new_guest
    user = User.find_or_create_by!(nickname: 'ゲストユーザー', email: 'user@example.com') do |user|
    user.password = SecureRandom.alphanumeric(10) + [*'a'..'z'].sample(1).join + [*'0'..'9'].sample(1).join
    end
    sign_in user
    redirect_to root_path, notice: 'ゲストユーザーとしてログインしました。'
  end
end

力技になってしまいましたが、[*'a'..'z'].sample(1).join + [*'0'..'9'].sample(1).joinとすることでパスワードの最後に必ず英字+数字の2文字が入るようにして英字のみor数字のみのパスワードが生成されないようにしました。

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

Rubyによるデザインパターン テンプレートメソッドパターンのメモ

プログラムの設計力向上のため『Rubyによるデザインパターン』を読んでおり、気になるデザインパターンを、1つずつまとめていきます。

今回は、テンプレートメソッドパターンについてまとめました。

テンプレートメソッドパターンについて

基底クラスに不変の部分を記述し、変わる部分はサブクラスに定義するメソッドにカプセル化するパターンです。

変わるものと変わらないものを分離する、という設計の考えに基づいています。

サンプルコード

趣味の筋トレにちなんでサンプルコードを書きます。
ベンチプレスと懸垂はどちらもトレーニングの流れは同じものの、具体的な流れはそれぞれ異なっています。

そこで、トレーニングごとの内容を出力するプログラムを書きます。

class Training
  def initialize(type)
    @type = type
  end

  def start
    prepare
    execute
    cleanup
  end

  def prepare
    if @type == :bench_press
      puts 'ベンチプレスを始めます'
      puts 'バーベルをセットします'
    elsif @type == :tinning
      puts '懸垂を始めます'
      puts '踏み台に乗ります'
    end
  end

  def execute
    puts 'トレーニングをします'
  end

  def cleanup
    puts 'アルコール消毒します'
    if @type == :bench_press
      puts 'バーベルを戻します'
    elsif @type == :tinning
      puts '踏み台から降ります'
    end
  end
end

呼び出し時は引数にトレーニングの種類を渡します。
実行結果はこのとおりです。

bench_press = Training.new(:bench_press)
bench_press.start
# ベンチプレスを始めます
# バーベルをセットします
# トレーニングをします
# アルコール消毒します
# バーベルを戻します

tinning = Training.new(:tinning)
tinning.start
# 懸垂を始めます
# 踏み台に乗ります
# トレーニングをします
# アルコール消毒します
# 踏み台から降ります

インスタンス変数 @type によって if 分で分岐をしています。トレーニングが2種類ならばよいかもしれませんが、トレーニング数が増えるたびにこの条件分岐は増え、1つのメソッドは長く複雑になってしまいます。

次に Template Method パターンを適用して書き換えた場合です。

class Training
  def start
    prepare
    execute
    cleanup
  end

  def prepare
  end

  def execute
    puts 'トレーニングをします'
  end

  def cleanup
    puts 'アルコール消毒します'
  end
end

class BenchPress < Training
  def prepare
    puts 'ベンチプレスを始めます'
    puts 'バーベルをセットします'
  end

  def cleanup
    puts 'バーベルを戻します'
    super
  end
end

class Tinning < Training
  def prepare
    puts '懸垂を始めます'
    puts '踏み台に乗ります'
  end

  def cleanup
    super
    puts '踏み台から降ります'
  end
end

呼び出し時はサブクラスのインスタンスを生成しており、テンプレートメソッドパターン適用前はトレーニングの種目を引数で渡していましたが、それがなくなっています。
実行結果はこのとおりです。

bench_press = BenchPress.new
bench_press.start
# ベンチプレスを始めます
# バーベルをセットします
# トレーニングをします
# アルコール消毒します
# バーベルを戻します


tinning = Tinning.new
tinning.start
# 懸垂を始めます
# 踏み台に乗ります
# トレーニングをします
# アルコール消毒します
# 踏み台から降ります

基底クラス Training にはトレーニングの骨子だけを定義し、具体的なトレーニングの内容はサブクラスで定義しています。

新たにサブクラスを生成する必要になったときは、基底クラスは変えずにサブクラスだけを変えれば良くなり、変更に強くなりました。

まとめ

テンプレートメソッドパターンは、継承を使ったオブジェクト指向らしいデザインパターンでした。

個人的にはデザインパターンの中で一番使用頻度が高いのではと思いますので、使いこなせるようになりたいです。

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

【Rubyによるデザインパターン】テンプレートメソッドパターンのメモ

プログラムの設計力向上のため『Rubyによるデザインパターン』を読んでおり、気になるデザインパターンを、1つずつまとめます。

今回は、第一弾ですがテンプレートメソッドパターンについてまとめました。

テンプレートメソッドパターンについて

基底クラスに不変の部分を記述し、変わる部分はサブクラスに定義するメソッドにカプセル化するパターンです。

変わるものと変わらないものを分離する、という設計の考えに基づいています。

サンプルコード

趣味の筋トレにちなんでサンプルコードを書きます。
ベンチプレスと懸垂はどちらもトレーニングの流れは同じものの、具体的な流れはそれぞれ異なっています。

そこで、トレーニングごとの内容を出力するプログラムを書きます。

まずはテンプレートメソッドパターン適用前のコードです。

class Training
  def initialize(type)
    @type = type
  end

  def start
    prepare
    execute
    cleanup
  end

  def prepare
    if @type == :bench_press
      puts 'ベンチプレスを始めます'
      puts 'バーベルをセットします'
    elsif @type == :tinning
      puts '懸垂を始めます'
      puts '踏み台に乗ります'
    end
  end

  def execute
    puts 'トレーニングをします'
  end

  def cleanup
    puts 'アルコール消毒します'
    if @type == :bench_press
      puts 'バーベルを戻します'
    elsif @type == :tinning
      puts '踏み台から降ります'
    end
  end
end

呼び出し時は引数にトレーニングの種類を渡します。
実行結果はこのとおりです。

bench_press = Training.new(:bench_press)
bench_press.start
# ベンチプレスを始めます
# バーベルをセットします
# トレーニングをします
# アルコール消毒します
# バーベルを戻します

tinning = Training.new(:tinning)
tinning.start
# 懸垂を始めます
# 踏み台に乗ります
# トレーニングをします
# アルコール消毒します
# 踏み台から降ります

インスタンス変数 @type によって if 分で分岐をしています。トレーニングが2種類ならばよいかもしれませんが、トレーニング数が増えるたびにこの条件分岐は増え、1つのメソッドは長く複雑になってしまいます。

次に Template Method パターンを適用して書き換えた場合です。

class Training
  def start
    prepare
    execute
    cleanup
  end

  def prepare
  end

  def execute
    puts 'トレーニングをします'
  end

  def cleanup
    puts 'アルコール消毒します'
  end
end

class BenchPress < Training
  def prepare
    puts 'ベンチプレスを始めます'
    puts 'バーベルをセットします'
  end

  def cleanup
    puts 'バーベルを戻します'
    super
  end
end

class Tinning < Training
  def prepare
    puts '懸垂を始めます'
    puts '踏み台に乗ります'
  end

  def cleanup
    super
    puts '踏み台から降ります'
  end
end

呼び出し時はサブクラスのインスタンスを生成しており、テンプレートメソッドパターン適用前はトレーニングの種目を引数で渡していましたが、それがなくなっています。
実行結果はこのとおりです。

bench_press = BenchPress.new
bench_press.start
# ベンチプレスを始めます
# バーベルをセットします
# トレーニングをします
# アルコール消毒します
# バーベルを戻します


tinning = Tinning.new
tinning.start
# 懸垂を始めます
# 踏み台に乗ります
# トレーニングをします
# アルコール消毒します
# 踏み台から降ります

基底クラス Training にはトレーニングの骨子だけを定義し、具体的なトレーニングの内容はサブクラスで定義しています。

新たにサブクラスを生成する必要になったときは、基底クラスは変えずにサブクラスだけを変えれば良くなり、変更に強くなりました。

まとめ

テンプレートメソッドパターンは、継承を使ったオブジェクト指向らしいデザインパターンですね。

個人的にはデザインパターンの中で一番使用頻度が高いのではと思いますので、使いこなせるようになりたいです。

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

Ruby で URL を作るときのエスケープ処理

https://xxxx といった文字列のURLに、 クエリパラメータをつけて URL を作る場合、 クエリパラメータは 特別な形に変換する必要があります。

JavaScript でいう encodeURIComponent を実行してやる必要があります。
これにより +%2B に、 &%26 になります。

基本

Ruby では CGI.escape を使います。

require 'cgi'

CGI.escape('+ &') # => "%2B+%26"

使用例

CGI.escape を使って URL を組み立ててみます。
ハッシュを使うなどいろいろと改善できるところはありますが、ここではシンプルなパターンを記載します。

"https://samole.com?key={CGI.escape(value)}"

key に特殊な文字を使うことはまずないと考えて value のみ escape をしています。

key も escape する場合は、
https://github.com/rails/rails/blob/3-0-stable/activesupport/lib/active_support/core_ext/object/to_query.rb が参考になります。

その他参考

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

Rubyの継承と委譲

継承と委譲の使い分けについてあまりわかっていないのでまとめました。

継承

他のクラスから機能を引継ぎ、新たにクラスを作成することです。

Baseクラスからすべのたメソッドを引継ぎ、Fooクラスを作成する

class Base
  def message
    'ok'
  end
end

class Foo < Base; end

foo = Foo.new
puts foo.message
# => ok

委譲

特定の処理(責任)を他のクラスのメソッドに委ねることです。

BaseクラスのmessageメソッドをFooクラスにmessageメソッドを委ねる

class Base
  def message
    'ok'
  end
end

require 'forwardable'
class Foo
  extend Forwardable
  attr_reader :mess

  def initialize
    @mess = Base.new
  end
  def_delegator :mess, :message
end

foo = Foo.new
puts foo.message
# => ok

継承を使う場面

役割が同じ

駆け出しエンジニアも、経験が豊富なつよつよエンジニアも、プログラマーという役割は一緒です。
なので、Progurammerクラスを継承し、機能をそのまま引き継ぐようにします。

class Programmer
  def initialize(type, langages)
    @type = type
    @langages = Array(langages)
  end

  def type_is
    puts "私は#{type}です。"
  end

  def langage_is
    str = ''
    langages.each do |lang|
      str += "#{lang} "
    end
    puts "私は#{str}の言語を扱えます"
  end

  def klass_is
    str = case self.class.name
    when 'TsuyoTsuyo'
      '強強エンジニア'
    when 'Kakedashi'
      '駆け出しエンジニア'
    end

    puts "私は#{str}です。"
  end

  private
  attr_accessor :type, :langages
end

class TsuyoTsuyo < Programmer; end

class Kakedashi < Programmer; end

tsuyo = TsuyoTsuyo.new('Backend', %w[C C++ Python Go Ruby])
tsuyo.type_is
tsuyo.langage_is
tsuyo.klass_is

kake = Kakedashi.new('Frontend', %w[HTML CSS jQuery])
kake.type_is
kake.langage_is
kake.klass_is

委譲を使う場面

役割は異なるが一部の機能を使いたい

プログラマーとデザイナーでは役割が異なります。なのでこの場合は委譲を使い、type_isメソッドのみをProgrammerクラスより譲り受けるようにします。

require 'forwardable'
class Programmer
  def initialize(type, langages)
    @type = type
    @langages = Array(langages)
  end

  def type_is
    puts "私は#{type}です。"
  end

  private
  attr_accessor :type, :langages
end

class Designer
  extend Forwardable

  def initialize(type, skills)
    @programmer = Programmer.new(type, nil)
    @type = type
    @skills = Array(skills)
  end

  def_delegator :@programmer, :type_is

  private
  attr_accessor :type, :skills
end

class IkeIke < Designer; end

ikeike = IkeIke.new('WebDesigner', %w[HTML CSS Adobe JavaScript])
ikeike.type_is # このtype_isメソッドは`Programmer` classから譲り受けたメソッド

まとめ

ここまでお読みいただきありがとうございます。
自分のコードを見直す機会になればと思います。

自分は普段はRailsでwebサービスの開発を行っているが、委譲を使う場面があまりない気がします。
今までは何でもかんでも「継承!!」としていた自分がいたのも事実です。
継承関係がいいのか、委譲を使った方がいいのかの判断は難しいですが、今後は委譲を利用するという選択肢も含めた実装をしていこうと思いました。

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

C10K 問題を解決する Ruby のワンライナー

C10K 問題とは

i18n と L10N は,それぞれ internationalization と localization の略称だ。
internationalization は綴りが長いので,「i」と「n」の間に 18 文字あるというところから「i18n」という表記が生み出された。L10N も同様。
なお,「i18n」が小文字で「L10N」が大文字なのはタイプミスではない。
参考:国際化と地域化 - Wikipedia

では,「c」で始まり「k」で終わり,間に 10 文字ある英単語としてどんなものがあるだろうか。
これが C10K 問題である1

コード

まず,英単語が大量に集められたファイルがあるとよいのだが,幸い,Linux や macOS にはそういうファイルが存在する。
/usr/share/dict というディレクトリー内にたいていそういうファイルがあるので覗いてみてほしい。
私の macOS では /usr/share/dict/words が英単語リストだった。
1 行に 1 単語。これがただひたすら並んでいるだけのファイルだ。数えたら全部で 23 万語以上あった。

ここから正規表現で抜き出せばいい。Ruby なら 1 行で書ける。

puts IO.foreach("/usr/share/dict/words").grep(/^c.{10}k$/i)

IO.foreach にファイルパスを与えると,行とごとにブロックを評価するが,ブロックを与えない場合は Enumerator を返す。
それを Enumerable#grep でフィルタリングしてやるだけ。
grep に正規表現を与えると,マッチした文字列だけからなる配列ができる。

結果は以下のとおり。

concertstuck
countercheck
counterprick
counterstock
Czechoslovak

な,なんじゃこれは?
Czechoslovak は名詞で「チェコスロバキア人」,形容詞で「チェコスロバキア(人・語)の」だが,あとの単語は見たこともないものばかり。macOS 付属の『ウィズダム英和辞典』にも載っていない。

concertstuck はどうもドイツ語の Konzertstück と関係ありそうだ。
Konzertstück は音楽用語で「小協奏曲」などと訳されたり,外来語として「コンツェルトシュテュック」と呼ばれたりするようだが,音楽に疎いのでよく分からん。

残りの 3 語はすべて counter- が接頭辞になった複合語だ。

ネットで調べてもいまひとつよく分からないけど,「countercheck」は「対抗手段」とか「ダブルチェック」(二重に点検すること)というような意味があるっぽい。
「counterstock」はチケットなどの半券を指すぽい。
「counterprick」はなんだろうな?


  1. この記事はジョーク記事です。本当の C10K 問題はまったく違うものです。ごめんなさい。 

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

【Rails】カラムの追加・データ型/カラム名の変更

はじめに

カラムの追加・カラム名、テータ型の変更をする手順として代表的なのが、
1⃣rails db:migrate:statusで現状の確認
2⃣マイグレーションファイルの作成or追加コマンドをターミナルに入力(行いたい操作でコマンドは異なります)
3⃣マイグレーションファイルに変更内容を記述(追加の場合は内容の確認だけになります)
4⃣rails db:migrate
だと思います(違ってたらすみません)。もちろんこれも正解だと思います。(rails db:rollback使うこともあります)毎度コマンドを調べたり誤字があるとテーブルが謎に消えてしまうことがあるかと思います、、、

レコードの内容が消えていいような場合は楽な方法があるそうで、、(ご存じの方はすみません)

※レコードの内容が少なかったり消えても良い状態、seeds.rbで必要なデータを作成しているとき向けの内容です。

上記の楽な方法(複数同時変更も可能)

1⃣rails db:migrate:statusで現状の確認(問題なければ2⃣へ)
2⃣既に作成してあるマイグレーションファイルの変更したい部分を直接書き換えます(追加の場合は書き加えてください)
3⃣rails db:migrate:resetを実行して完了
schema.rbを確認してみてください!

rails db:migrate:resetを行うことで、既存のマイグレーションファイルを全て利用して再度テーブルを作成しなおします。なので、書き換えた部分や追記した部分も全て反映されます(誤字でerrorが出ても大体直ります)
ただし、場合によって使い分ける必要があるのでレコードを削除したくない場合は丁寧にコマンドを打ち込みましょう。

1⃣状態確認

rails db:migrate:status実行
コマンド実行時に出力された分だけマイグレーションファイルが存在して、この後の工程でそれらを一斉に再度migreateしなおすようなイメージになります。

ターミナル
vocstartsoft:~/environment/bookers2-task (master) $ rails db:migrate:status #ここで実行

database: /home/ec2-user/environment/bookers2-task/db/development.sqlite3

 Status   Migration ID    Migration Name
--------------------------------------------------
   up     20200830060820  Devise create users      #upなのかdownなのか状態が出てきます。
   up     20200830062142  Create books             #upは基本的にmigrate済みということになります
   up     20201101080413  Create book comments
  #以下省略

2⃣マイグレーションファイルに追記・変更

追記・変更したい内容の含まれるマイグレーションファイルをdb/migrateフォルダ内から持ってきます。
今回は投稿に使うtitleカラムbodyカラム共にデータ型がおかしいのでそこを修正します(booleanとかどんなミスだよ。)

db/migrate/20201115102020_create_books.rb
class CreateBooks < ActiveRecord::Migration[5.2]
  def change
    create_table :books do |t|
      t.integer :title          #←integerをstringにしたい
      t.boolean :body           #←booleanをtextにしたい
      t.integer :user_id
      t.timestamps
    end
  end
end

書き換えます(追加したい場合は追記してください)

db/migrate/20201115102020_create_books.rb
      #↑省略
      t.string :title          #integer→string
      t.text :body             #boolean→text
      #↓省略

3⃣rails db:migrate:resetを実行して一気にDBを削除→作成します

ターミナル
$ rails db:migrate:reset

ターミナルに普段のrails db:migrate時と同じような内容が出力されていればおそらく問題ありません。
schema.rbを確認してちゃんと変更・追加ができているか確認してみましょう。
くどいようですが、作成していたuser情報(名前とか)や投稿内容などは消えているはずなので、注意してください。

おまけ

rails db:resetとrails db:migrat:resetの違いについて

どちらのコマンドもDBを削除して作成しなおすコマンドですが、大きな違いがあるようです。
rails db:resetはDBをdropして、現在の scheme.rb をロードして DB を作り直します。db/migrate/~.rb は使われないようです。
rails db:migrate:resetはDBをdropした後、通常通りのマイグレート(db:migrate)が行われます。つまりdb/migrate/~.rb が古い順から全て実行されます。
最後に上記の方法以外のカラム追加記事はこちら
参考にもなったし、逆にrails db:migrate:resetできなかったときの内容の記事はこちら
長くなりましたが、個人的にDB周りは意外とややこしいエラーが多い気がします、この記事で解決すると嬉しいです!(改善されない方はすみません、、)

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