20190527のRubyに関する記事は26件です。

Rubyで数独を解いてみた(しらみつぶし法)

はじめに

Ruby勉強のために、昔好きだった数独を深さ優先探索などを利用して解きました。回答のし易さを優先し、回答速度は犠牲にしてます。

数独のルール

① 9列あるタテのどの列にも1から9の数字が1つずつ入ります。
② 9行あるヨコのどの行にも1から9の数字が1つずつ入ります。
③ 太線で囲まれた3×3のブロック内にも1から9の数字が1つずつ入ります。

スクリーンショット 2019-05-21 23.22.24.png

引用 : 数独 ナンプレ の解き方

用語の説明

使う用語を統一しておきます。

  • グリッド : セル全体を
  • セル : 一つ一つのマス
  • row : 行
  • column : 行
  • 正方形(square) : 3 × 3 のブロック
  • セルの番号は、以下の図に対応させている

スクリーンショット 2019-05-23 23.38.14.png

引用 : 盤面・セル・候補数字の表記法

問題

今回は、下記の問題を解くことにします。

スクリーンショット 2019-05-21 23.41.42.png

出典:「数独通信 Vol.26」 P.4

image.png

次に、プログラムで解く下準備を説明します。

問題のフォーマット

問題の入力を簡単にするために、グリッド表示されている数独の問題は以下の形式に変換しておきます。

problem.txt
.8.....1. 1..2..9.. ..7..4..3 3...1..9. ...7.2... .6..8...4 9..4..1.. ..4..3..5 .2.....8.

問題と回答プログラムは別ファイルで管理

同階層に問題と回答プログラムは別ファイルで管理するようにします。

  • index.rb(回答プログラム)
  • problem.txt(問題)

なので、problem.txtには、図(出典:「数独通信 Vol.26」 P.4)に示した問題を変換したものを入れておきます。

problem.txt
.8.....1. 1..2..9.. ..7..4..3 3...1..9. ...7.2... .6..8...4 9..4..1.. ..4..3..5 .2.....8.

index.rbには

$ ruby index.rb problem.txt

とコマンドを打てば、別ファイル(problem.txt)の内容を、(繰り返し呼びだせる)ARGFオブジェクトとして呼び出せるようにしました。

ARGF : スクリプトに指定した引数 (Object::ARGV を参照) をファイル名とみなして、 それらのファイルを連結した 1 つの仮想ファイルを表すオブジェクト
object ARGF (Ruby 2.6.0)

index.rb
ARGF.each do |line|
 # 処理
end

引数(line)には、problem.txtに入っている1つ1つの数字や.をオブジェクト(Enumerable::Enumerator オブジェクト)として格納され処理させるようにしました。

解き方の方針

値が入ってないセルに入り得る値を入れてみて、しらみつぶしに検証していく方法で解きました。
(入るはずのない値は入れないようにしています)

基本的な手順

  • (1) 空のセルを選ぶ
  • (2) そのセルに値を一つ入れる
    • そのセルに入ることができる候補の値が少なく、かつセル番号が若い順に、値を入れていく
  • (3) 数独のルールに従っているか検証
    • 従ってる → (1)に戻る
    • 従っていない → 別の値を試す

実際の全処理

実際の全処理は下記の用に書きました。

index.rb
ARGF.each do |line|
  line.chomp!print_grid(solve(make_grid(line.gsub(/\s/, ''))))
end

上に書いた各処理が連結されています。順を追って解説しますが、全てのプログラムを書いていきます。

index.rb
# 
# param [Array] grid 1から9の値か、nilが81個並んだ配列
# return [Array] 改行とスペース消した値を9×9のgrid
#
def print_grid(grid, pad="\n")
  print (0..8).map{ |i|
    grid[9*i, 9].map{ |v|
      (v || ".") }.join("") }.join(pad), "\n"
end

# 
# param [String] string 1から9の値か、nilが81個並んだ一元配列
# return [Array] .をnilに変換
#
def make_grid(string)
  string.split(//).map{ |c| c == "." ? nil : c.to_i }
end

# 
# param [Array] grid 
# param [Integer] cn セル番号
# return [Array] 該当する行に存在する値を配列としてを返す
#
def row(grid, cn)
  grid[9 * (cn / 9), 9]
end

# 
# param [Array] grid 
# param [Integer] cn セル番号
# return [Array] 該当する列に存在する値を配列としてを返す
#
def column(grid, cn)
  (0..8).map{ |k| grid[9 * k + cn % 9] }
end

# 
# param [Array] grid 
# param [Integer] cn セル番号
# return [Array] 該当する正方形に存在する値を配列としてを返す
#
def square(grid, cn)
  (0..8).map{ |k| grid[9*(3*(cn/9/3)+(k/3))+3*(cn%9/3)+(k%3)]}
end

# 
# param [Array] grid 1から9の値か、`nil`が81個並んだ配列
# return [Array] nil になっている位置をインデックス番号にして配列として返す
#
def empty_cell_numbers(grid)
  (0..80).reject{ |p| grid[p] }
end

# 
# param [Array] grid 
# param [Integer] cell_number 
# return [Array] 該当するセル番号の行、列、正方形に存在しない値を配列としてを返す
#
def possible_numbers(grid, cell_number)
  (1..9).to_a - fixed_numbers(grid, cell_number)
end

# 
# param [Array] grid 
# param [Integer] cell_number 
# return [Array] 該当するセル番号の行、列、正方形に存在する値を配列としてを返す
#
def fixed_numbers(grid, cell_number)
  (
    row(grid, cell_number).compact |
    column(grid, cell_number).compact |
    square(grid, cell_number).compact
  )
end

# 
# param [Array] grid
# return [Array] grid 回答
#
def solve(grid)
  empty_cell_numbers = empty_cell_numbers(grid)

  candidates = empty_cell_numbers.map{ |cell_number|
    [cell_number, possible_numbers(grid, cell_number)]
  }

  ordered_candidates = candidates.sort_by{ |cell| cell[1].length }

  if ordered_candidates.empty?
    print "\e[31m"  # bash出力を赤色に
    puts 'complete↓'
    print "\e[0m"   # bash出力の色を元に戻す
    grid
  else

    p ordered_candidates[0]

    cell_number, candidate_values = ordered_candidates[0]

    candidate_values.each do |value|
      grid[cell_number] = value

      sleep(0.02)
      print_grid(grid)
      puts "残り#{grid.count(nil)}個"
      puts '---------'
      return grid if solve(grid)
    end

    p "grid[#{cell_number}] => #{grid[cell_number]} 取り消し"
    grid[cell_number] = nil  # 全て失敗したら未確定に戻す
    return false
  end
end

# 
# return [ARGF] 数独を表示
#
ARGF.each do |line|
  line.chomp!print_grid(solve(make_grid(line.gsub(/\s/, ''))))
end

各メソッドの説明

問題が解かれる手順を説明しつつ、各メソッドの説明をします。基本的には、下記のメソッド内で別ファイルからデータを受け取って、ある処理をする中で、数独の問題を解いています。

ARGF.each do |line|
  line.chomp!print_grid(solve(make_grid(line.gsub(/\s/, ''))))
end

一つづつメソッドの概要を説明すると、

  • line.chomp!
    • line(example) => ".8.....1. 1..2..9.. ..7..4..3 3...1..9. ...7.2... .6..8...4 9..4..1.. ..4..3..5 .2.....8."
    • chomp : 文字列の末尾の改行文字を取り除いた新しい文字列を返す
  • print_grid()
    • param [Array] grid 1から9の値か、nilが81個並んだ一元配列
    • return [Array] 改行とスペース消した値を9×9のgrid
  • solve()
    • param [Array] 1から9の値か、nilが81個並んだ一元配列
      • empty_cell_numbers()
        • param [Array] grid 1から9の値か、nilが81個並んだ一元配列
        • return [Array] nilになっている位置をインデックス番号にして配列として返す
      • 変数 candidates に入るデータ
        • index[0] : インデックス番号
        • index[1] : セルに入ることができる値の候補
        • => 配列 [index, [候補となる値(複数も可)]]で返す
      • 変数 ordered_candidates に入るデータ
        • 候補となる値の数が少ない順に並べる
        • => 配列 [index, [候補となる値(複数も可)]]で返す
      • 条件文 ordered_candidates.empty?
        • TRUE
          • 配列の中身が空(全てのセルに1から9の値が入っている)ならgridを返す
        • FALSE
          • 配列に要素が在る場合、指定したindex番号のセルに、候補となる値を格納してメソッドsolve()を再帰的に呼び出す
  • make_grid()
    • param [String] 1から9の値か、nilが81個並んだ一元配列
    • return [Array] .をnilに変換
  • line.gsub(/\s/, '')
    • 引数lineにあるスペースの削除

変数 candidates に関わる処理

変数 candidates に入るデータには、数独のルールに従っているかの条件をクリアしている値が、候補となる値として格納されます。

関わらない処理を...で省略しました。

# 
# param [Array] grid 
# param [Integer] cn セル番号
# return [Array] 該当する行に存在する値を配列としてを返す
#
def row(grid, cn)
  grid[9 * (cn / 9), 9]
end

# 
# param [Array] grid 
# param [Integer] cn セル番号
# return [Array] 該当する列に存在する値を配列としてを返す
#
def column(grid, cn)
  (0..8).map{ |k| grid[9 * k + cn % 9] }
end

# 
# param [Array] grid 
# param [Integer] cn セル番号
# return [Array] 該当する正方形に存在する値を配列としてを返す
#
def square(grid, cn)
  (0..8).map{ |k| grid[9*(3*(cn/9/3)+(k/3))+3*(cn%9/3)+(k%3)]}
end

# 
# param [Array] grid 1から9の値か、`nil`が81個並んだ配列
# return [Array] nil になっている位置をインデックス番号にして配列として返す
#
def empty_cell_numbers(grid)
  (0..80).reject{ |p| grid[p] }
end

# 
# param [Array] grid 
# param [Integer] cell_number 
# return [Array] 該当するセル番号の行、列、正方形に存在しない値を配列としてを返す
#
def possible_numbers(grid, cell_number)
  (1..9).to_a - fixed_numbers(grid, cell_number)
end

# 
# param [Array] grid 
# param [Integer] cell_number 
# return [Array] 該当するセル番号の行、列、正方形に存在する値を配列としてを返す
#
def fixed_numbers(grid, cell_number)
  (
    row(grid, cell_number).compact |
    column(grid, cell_number).compact |
    square(grid, cell_number).compact
  )
end

def solve(grid)
...

  candidates = empty_cell_numbers.map{ |cell_number|
    [cell_number, possible_numbers(grid, cell_number)]
  }

...
end

メソッド row の処理内容

該当する行に存在する値を配列としてを返します。

例えば、セル番号0の行は以下の配列を返します。

=> [nil, 8, nil, nil, nil, nil, nil, 1, nil]

「Rubyで数独を解いてみた(しらみつぶし法)」を編集_-_Qiita.png

メソッド column の処理内容

該当する列に存在する値を配列としてを返します。

例えば、セル番号0の列は以下の配列を返します。

=> [nil, 1, nil, 3, nil, nil, 9, nil, nil]

「Rubyで数独を解いてみた(しらみつぶし法)」を編集_-_Qiita.png

メソッド square の処理内容

該当する正方形に存在する値を配列としてを返します。

例えば、セル番号0の正方形は以下の配列を返します。

=> [nil, 8, nil, 1, nil, nil, nil, nil, 7]

「Rubyで数独を解いてみた(しらみつぶし法)」を編集_-_Qiita.png

メソッド possible_numbers の処理内容

該当するセル番号の行、列、正方形に存在しない値を配列としてを返します。

例えば、セル番号0の場合は以下の配列を返します。

=> [2, 4, 5, 6]

「Rubyで数独を解いてみた(しらみつぶし法)」を編集_-_Qiita.png

メソッド fixed_numbers の処理内容

該当するセル番号の行、列、正方形に存在する値を配列としてを返します。

例えば、セル番号0の場合は以下の配列を返します。

=> [8, 1, 3, 9, 7]

上記のような、様々なメソッドを経て、数独の条件に合う値を選定しています。

答え合わせ

0.2秒間隔でセルに値を入力していく映像が以下になります。

871b937cfe482126e0dfdb413f91402e.gif

数独のチェックツール(ナンプレ解答プログラム)を使って、問題がないか確認できました。

スクリーンショット 2019-05-26 23.52.43.png

パフォーマンス

ruby index.rb problem.txt

0.36 real # コマンドを実行するためにかかった時間
0.20 user
0.07 sys

勉強になったこと

  • 配列処理系のメソッドの挙動の理解(each, select, reject, map, compact
  • アルゴリズム(深さ優先探索)

次回やること

  • 数独の基本解法(ブロッケン, レッツミー, マスミ,いずれにしても理論,予約 など)のプログラムを実装し解いてパフォーマンスを改善させたい
    • しらみつぶし法のデメリットは、問題によってはとても処理が長くなる場合がある
  • 最終的には、部分解答して、最適な基本解法を選択する、いわゆる「解き筋」をモデル化して解きたい

参考

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

Railsで基本情報技術者試験の過去問題サイトを作る(4:親子関係、参照編)

はじめに

ゆる〜く学ぶ。みんなのWeb勉強コミュニティー。 「にゅ〜ぶる会」を運用中です。
https://newburu.github.io/

そこで、何か教育用のコンテンツが欲しいなぁ〜と思い立ち、今回の企画をスタートしました!

Railsで基本情報技術者試験の過去問題サイトを作ります!

最終目標

  • 問題・回答の登録は、Scaffoldで簡易でOK
  • APIを用意して、ランダムに問題を抽出する機能を追加する
  • TwitterBOT、LINEBOT、SlackBOTが出来たら良いな

履歴

1:構築編
  https://qiita.com/newburu/items/ed59f47ac645b19620f6
2:日本語化(i18n)編
  https://qiita.com/newburu/items/4f12fdb61bf6cd601545/
3:親子関係、登録編
  https://qiita.com/newburu/items/f2a20289be5ec1fc1b77
4:親子関係、参照編
  本ページ

今回やる事

  • 登録した親子関係の情報を参照系の画面に表示します

※レイアウトをやろうと思いましたが、こちらの方が優先なので、予定を変更させて頂きました。

登録した親子関係の情報を参照系の画面に表示します

1. まずは、現状を確認しましょう。

登録画面
登録画面

問題一覧画面
問題一覧画面

問題参照画面
問題参照画面

2. 問題参照画面のViewを直します。

今回やる事は、ここ。
問題参照画面

問題参照画面に、回答一覧を追加するよ!

元ソース

app/views/questions/show.html.slim
p#notice = notice

p
  strong = "#{Question.human_attribute_name(:category1)}:"
  = @question.category1
p
  strong = "#{Question.human_attribute_name(:category2)}:"
  = @question.category2
p
  strong = "#{Question.human_attribute_name(:category3)}:"
  = @question.category3
p
  strong = "#{Question.human_attribute_name(:msg)}:"
  = @question.msg

// ここに回答一覧を足していきます。

=> link_to t('btn.edit'), edit_question_path(@question)
'|
=< link_to t('btn.back'), questions_path

修正後ソース

app/views/questions/show.html.slim
p#notice = notice

p
  strong = "#{Question.human_attribute_name(:category1)}:"
  = @question.category1
p
  strong = "#{Question.human_attribute_name(:category2)}:"
  = @question.category2
p
  strong = "#{Question.human_attribute_name(:category3)}:"
  = @question.category3
p
  strong = "#{Question.human_attribute_name(:msg)}:"
  = @question.msg

// ここに回答一覧を足します。
p
  strong = "#{Question.human_attribute_name(:answers)}:"
  // 問題の子供として、has_manyしているanswersでループ(each)します。そして、その値(回答モデル)はanswerに渡します。
  - @question.answers.each do |answer|
    p
      strong = "#{Answer.human_attribute_name(:msg)}:"
      = answer.msg
    p
      strong = "#{Answer.human_attribute_name(:correct)}:"
      = answer.correct

=> link_to t('btn.edit'), edit_question_path(@question)
'|
=< link_to t('btn.back'), questions_path

完成した画面

追加されましたね!
問題参照画面

今回はここまで

ありがとうございました!
画面周りは、まだまだ綺麗にする要素はいっぱいあるけど、
次回は、API化していこうかな。

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

rubyでブラック・ジャックできるんです!!

今日はこちらの記事を参考にしてトランプのブラック・ジャックを作ってみました。

仕様
・初期カードは52枚。引く際にカードの重複は無いようにする
・プレイヤーとディーラーの2人対戦。プレイヤーは実行者、ディーラーは自動的に実行・
・実行開始時、プレイヤーとディーラーはそれぞれ、カードを2枚引く。引いたカードは画面に表示する。ただし、ディーラーの2枚目のカードは分からないようにする
・その後、先にプレイヤーがカードを引く。プレイヤーが21を超えていたらバースト、その時点でゲーム終了
・プレイヤーは、カードを引くたびに、次のカードを引くか選択できる
・プレイヤーが引き終えたら、その後ディーラーは、自分の手札が17以上になるまで引き続ける
・プレイヤーとディーラーが引き終えたら勝負。より21に近い方の勝ち
・JとQとKは10として扱う
・Aはとりあえず「1」としてだけ扱う。「11」にはしない
・ダブルダウンなし、スプリットなし、サレンダーなし、その他特殊そうなルールなし

コード

view.rb
require './black_jack/player'
require './black_jack/card'
require './black_jack/controller'

p '今からブラックジャックを始めます'
p 'ディーラーとプレイヤーに2枚ずつカードを配ります。'

@dealer = create_player
@player = create_player

p "ディーラーの#{@dealer.hand_index(0)}"
p "あなたの#{@player.hand_index(0)}"
p "あなたの#{@player.hand_index(1)}"

2.upto(15) do |i|
  p 'カードを引きますか?(y/n)'
  input = gets.chomp

  if input == 'y'
    @player.draw
    p "あなたの引いたカードは#{@player.hand[i][0]}#{@player.hand[i][1]}です"
  else
    break
  end

  if @player.card_sum > 21
    p '合計が21を超えました!!!'
    p 'プレイヤーの負けです!!!'
    exit
  end
end

p 'では、次にディーラーが引いていきたいと思います。'

while @dealer.card_sum < 17
  @dealer.draw
end

p '両者引き終わりました。'
p '勝者は....'

p @player.battle(@dealer)

p 'プレイヤーのカード'
p @player.card_index

p 'ディーラーのカード'
p @dealer.card_index
login.rb
require './black_jack/player'
require './black_jack/card'

def create_player
  @player = Player.new
  @player.draw
  @player.draw
  @player
end
card.rb
class Card
  MARK = ['spade', 'heart', 'diamond', 'club'].freeze
  NUMBER = [
    1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13
  ].freeze

  attr_reader :mark, :number

  def initialize(mark,number)
    @mark = mark
    @number = number
  end
end
player.rb
class Player
  attr_reader :hand

  # 引いたカードを入れる配列
  def initialize
    @hand = []
  end

  # カードを引く処理。既に持っているカードを引いたらやり直し
  def draw
    @hand.uniq!
    card = Card.new(Card::MARK.sample, Card::NUMBER.sample)
    @hand << [card.mark, card.number]
    unless @hand.count == @hand.uniq.count
      draw
    end
    @hand
  end

  # 手札の内容
  def hand_index(i)
    "#{i + 1}枚目は#{self.hand[i][0]}#{self.hand[i][1]}です"
  end

  # プレイヤーのカードの合計がディーラーのカードの合計と比較する
  def battle(other)
    result = (card_sum - 21).abs <=> (other.card_sum - 21).abs
    if result == -1
      'プレイヤーです!!!'
    elsif result == 0
      '引き分けです!!!'
    else
      'ディーラーです!!!'
    end
  end

  # カードの合計を出す。
  def card_sum
    sum = 0
    @hand.each do |num|
      if num[1] > 10
        sum += 10
      else
        sum += num[1]
      end
    end
    sum
  end

  # 手札の一覧を返す
  def card_index
    hand.map { |num| "#{num[0]}#{num[1]}" }.sort.join(',')
  end
end

完成動画
実際にやってみると以下のような動画になります(リンクですいません)。

まとめ
今回は処理と出力で別のファイルにすることを特に意識しました。
実際に出力されるのはview.rbに、処理はその他のファイルに書きました。
ただ、create_playerメソッドの為だけにlogic.rbを作る必要はなかったかな〜と反省中です。
本当はplayerのインスタンスが作成された時点でdrawメソッドを2回実行させたかったけど、どうすればいいか分かりませんでした汗
後、view.rbに少しだけ処理が残ってしまいました。
こちらも今後リファクタリング 必須ですね。

ブラック・ジャックはこちらの記事にも書いてある通り、基本的な文法が理解出来ていれば出来るので、ぜひやってみてください!!!!

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

【Rails】nestされたcontrollerへのform_withの書き方

namespaceでnestされたcontroller(Admin::Userなど)のアクション宛てformを
form_withで記述したい場合の雛形になります。

書き方

例えば、newのviewからAmin::Postcreateアクションへ送りたい場合、

new.html.erb
<%= form_with scope: :post, url: admin_posts_path, local: true do |f| %>
  <%= f.text_field :title %>
<% end %>

scope:url:により、htmlレベルでは以下のようになります。

<form action="/admin/posts" method="post" >
  <input type="text" name="post[title]">
</form>

◆ちなみに、scope: :postをなくすと

new.html.erb
<%= form_with url: admin_posts_path, local: true do |f| %>
  <%= f.text_field :title %>
<% end %>

<form action="/admin/posts" method="post" >
  <input type="text" name="title">
</form>

controller側でストロングパラメーターを入れていれば↑は弾かれますね。

admin/posts_controller.rb
  private

  # ストロングパラメーター
  def post_params
    params.require(:post).permit(:title, :description)
  end

最後に

…例を見直すと、controllerのpostmethodのpostという2つのpostが出てくるので、ややこしい構成になってた。。。
(最近の松本人志が怒るやつや・・・)

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

テストで気づいたこと

テストをしていて気づいたこと

ユーザーがURLを入力し間違えるなど、
意図しない動作をしてくる可能性は大いにある。

最初から仕様として、盛り込んでおくほうが親切だ。


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

WSL上のUbuntu環境で chromedriver を使ってRSpecするとエラーが発生するので、その解決方法。

はじめに

WSLでUbutu環境を使いchromedriverでRSpecテストをしたらエラーが発生しましたが、無事に解決できたのでその方法をまとめます。

Railsで有名な参考書「現場で使える Ruby on Rails 5速習実践ガイド」をやっていてぶつかったエラーなので、参考になる方が多いのではと思いQiitaで記事にまとめようと思いました。

おそらく、Windows環境で参考書通りにWSLとUbuntuを使っている人だと、ほぼ確実にぶつかるエラーなので参考になれば嬉しいです。

環境とバージョン

  • WSL
  • Ubuntu 18.04.2 LTS
  • Ruby 2.5.1
  • Rails 5.2.3
  • RSpec-rails 3.8.2

エラー内容と解決方法

RSpecを記述した後、テストを通すとエラーが発生しました。(現場Rails205p)

エラー内容としては「こちらの記事(teratail)」と全く同じものです。

テスト内容の記述やspec_helper.rbrails_helper.rbのソースコードもこちらの記事と同じなので、必要に応じて参考にしてみてください。

エラー内容
 Failures:

 1) タスク管理機能 一覧表示機能 ユーザーAがログインしているとき ユーザーAが作成したタスクが表示される
    Got 0 failures and 2 other errors:

 1.1) Failure/Error: visit login_path

      Selenium::WebDriver::Error::WebDriverError:
        unable to connect to chromedriver 127.0.0.1:9515



      # ./spec/system/tasks_spec.rb:12:in `block (4 levels) in <top (required)>'

 1.2) Failure/Error: raise Error::WebDriverError, cannot_connect_error_text

      Selenium::WebDriver::Error::WebDriverError:
        unable to connect to chromedriver 127.0.0.1:9515

エラーを読むと、どうやらwebdriverにエラーが発生しててchromedriverに接続できないみたいです。

Windows側とは別に、Ubuntu側にもchromeをインストールしないといけないみたいなので、こちらの記事「ubuntuにchromeを簡単にインストールしよう」を参考にchromeをインストールしました。

ちなみに、Ubuntu上で以下のコマンドを入力するとchromeがインストールされているか分かります。

$ google-chrome -version
Google Chrome 74.0.3729.169

さて、Ubuntu上にもchromeがインストールできたということで、もう一度テストを実行すると次は以下のエラーが...。

エラー内容
Failures:

  1) タスク管理機能 一覧表示機能 ユーザーAがログインしているとき ユーザーAが作成したタスクが表示される
     Got 0 failures and 2 other errors:

     1.1) Failure/Error: visit login_path

          Net::ReadTimeout:
            Net::ReadTimeout



          # ./spec/system/tasks_spec.rb:12:in `block (4 levels) in <top (required)>'

     1.2) Failure/Error: @io.to_io.wait_readable(@read_timeout) or raise Net::ReadTimeout

          Net::ReadTimeout:
            Net::ReadTimeout

どうやら、次はchromeに繋がらずにタイムアウトしてしまったようですね。

こちらの記事(WSL Ubuntu 上で chromedriver を使った System Spec を動かす)によれば、

ChromeがインストールされているのはWSLの外のWindows環境。 なのでパスが通っていないし、バイナリも不一致となってしまう。

そのため cannot find Chrome binary のようなエラーが出るなどして System Spec を実行できない。

以下の2種類の対策方法がありそうだ。

A. chromedriver-helper を使わずに、自分で chromedriver を設定する
B. chromedriver-helper を利用しつつ、ChromeをWSL環境にインストールする
対策A. chromedriver-helper を使わずに、自分で chromedriver を設定する

とのこと。WSLでUbuntuを使うとパスが通ってないのでいろいろと設定が追加で必要なようです。

二つの対策方法があるみたいなので、記事を参考に僕もAの「自分でchromedriverを設定する」ことにしました。

結果、無事にテストを通すことができました。
めでたし、めでたし:clap:

参考記事まとめ

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

Herokuデプロイ後のエラー

Cloud9上では、問題のなかったページ移行の動作ですが、
Herokuにデプロイ後、Heroku上で確認すると以下のエラーメッセージが出てきてしまいます。
(例えば、loginページからログインし、tasks(タスク一覧ページ)に行こうとすると以下のエラーになってしまう。):point_down:
We're sorry, but something went wrong.
If you are the application owner check the logs for more information.

:point_up:行った対処
☆ マイグレーションの再実行
 ① rails db:migrate:reset
 意味 : 一度DBを削除して、作成し直し、もう一度マイグレーションを実行。

 結果 : 一部動作はクリアしたが、まだエラーが生じる。

注: マイグレーションファイルの内容を大幅に変えていたので、Heroku側のデータベースは一旦リセットする。
 ① heroku pg:reset DATABASE
 ②  To proceed, type xxxxxxxxxxxx or re-run this command with --confirm xxxxxxxxxxxx

アプリ名を入力(xxxxxxxxxxxxの箇所)
 ③ heroku run rails db:migrate
 意味 : テーブルを再作成する。

:frowning2:これでも改善できなかったら、、、
 ① heroku restart
 意味 : herokuのサーバも再起動。

これで何とかエラーは消滅しました。
これを解決するのに2日ほどかかりました。
初心者には結構キツイですね、、

 

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

ホーム以外でホームの Rake タスクを実行

ホームディレクトリーに Rakefile を置いて,いろいろな作業ができるようにしている,とする。

別のディレクトリーにいるときに,そのタスクを実行するにはどうするか,というのがこの記事のテーマ。

-f オプションで指定

rake コマンドには,Rake ファイルを指定する -f オプションがあるので,

rake -f ~/Rakefile foo

のようにすればいい。
でもちょっと打つのが面倒じゃない? もう少し簡略化できないの?

-f オプションにはディレクトリーを渡すことはできないが,ワイルドカードは使えるので,

rake -f ~/* foo

と書けばよいようだ。

おまけ

Windows の場合

Windows では ~ がホームディレクトリーを表してはくれないので,この手は使えない。残念。
これなんか,rake コマンド側で対処してくれたらいいのにねー。
もちろん Windows では ~ という名前のディレクトリーを作ることができてしまうので,原理的には曖昧性が生じるわけだけど,「-f オプションの ~ はホーム」ってルールにしちゃってもいいんじゃない?

Rake ファイルのファイル名

Rake ファイルのファイル名は

  • Rakefile
  • Rakefile.rb

のどっちでもいいし,大文字・小文字も気にしなくていい。

Rake タスク中のパスの扱い方

Rake ファイルの場所がカレントディレクトリーと一致しているとは限らない,ということは常に頭に入れておきたい。

-f を使わなくても不一致は起こりうる。
カレントディレクトリーに Rake ファイルが存在しない場合,ディレクトリー階層を上にたどって Rake ファイルを探すからだ。

となると,Rake ファイル中で,たとえば

require "./my_library"

みたいな記述をしてしまってはダメ,ということになる。
この . はカレントディレクトリーを表しているからね。
Rake ファイルと同じ場所にある my_library.rb を読み込ませたいなら

require_relative "my_library"

とすればいい。

また,ファイルの読み書きなどでも

IO.read("foo.txt")

みたいなことはやめて,__dir__ メソッドを使って Rake ファイルの置かれたディレクトリーのパスを取得し,そこからの相対パスでファイルを指定するようにしたい。

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

モデルを継承して値を取得したい

IBeacon(子) < ShopTerminal(親)

モデル間の継承をしたいとする

継承とは

簡単に言うと、親のモデルに定義されている内容は全部子モデルでも使えるよ〜ってこと

値の取得

で、今回親のモデルの内容を子モデルを使って取得したい と言うこと

コントローラでいつものように値を取得したいけどなぜか
beacon = IBeacon.find_by_uuid(params[:uuid])でbeaconの値が[ ]に。
beacon = ShopTerminal.find_by_uuid(params[:uuid])だとちゃんと値はいってるのに。。。

beacons_controller.rb
class Api::BeaconsController < Api::AbstractSystemController

  def show
    return render_error('Please set uuid', 400) unless params[:uuid].present?
    beacon = IBeacon.find_by_uuid(params[:uuid])
    return render_error('beacon not found', 400) unless beacon.present?
    render json: beacon, serializer: IBeaconSerializer, root: nil
  end
end

解決策

親モデルのShopTerminalにtypeカラム(String型)を追加

そして追加されたtypeカラムの中にIBeaconと書くことで上記のコード通り、子モデルでの値の取得ができるようになった!!

今回やったことはSTIと言うらしい

typeという語はテーブルでSTI(Single Table Inheritance)を指定するために予約されている予約語で、この場合以外に使ったらエラーになるので気をつける。

STI(単一テーブル継承)はACtiveRecoredがサポートしている機能です。

参考: Rails4でSTI(単一継承テーブル)を行う

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

【初学者向け】メールアドレスの正規表現について一番細かく解説する

はじめに

Railsチュートリアル などで学習していると、以下のような正規表現が使われているのを見たことがあると思います。

/\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i

これは、アプリのユーザ登録やログイン時に入力するメールアドレスが正しい形式になっているかを検証する時に用いる正規表現で、筆者が受講していたプログラミングスクールの教材でも同じものが登場していました。

しかし、ほとんどの教材ではこの正規表現について細かく解説されていません。(主観)
例えばRailsチュートリアルには以下のように書かれていますし、

この正規表現を理解するために、お手頃なサイズに分割して表 6.1にまとめました。

某スクールの教材では、「(今は)理解する必要はありません」と書かれています。
(これらの教材での学習のメインはRailsなので、正規表現について細かく解説されていないことが悪いと言いたいわけではありません!)

筆者も最初は、お手頃なサイズに分割された解説を見て理解した気でいましたが、改めて見てみると、細かいところまでしっかり理解していないことに気づきました。
もちろん、正規表現に関する書籍を読んだりして完全に理解すればよい話なのですが、とりあえずメールアドレスの正規表現だけでも理解しようと思い学習してみると、意外と一癖あるということが分かりました。(後述する\-とか)

そこで、(僭越ながら)このメールアドレスの正規表現について、お手頃なサイズをさらに分割して、一番細かく解説していきます。
今はまだ理解しなくていいやと思っている駆け出しエンジニアや、チュートリアルなどの解説だけを見て理解した気になってしまっている駆け出しエンジニアに届けばなーと思っています。

※正規表現の基本的な記法は同じですが、使用する言語によって細かいところが若干異なります。今回はRubyにおける書き方で進めていきます。

解説

お手頃なサイズ

まずは先述の「お手頃なサイズに分割」された解説を下に示します。Railsチュートリアル から引用。

正規表現 意味
/\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i (完全な正規表現)
/ 正規表現の開始を示す
\A 文字列の先頭
[\w+\-.]+ 英数字、アンダースコア (_)、プラス (+)、ハイフン (-)、ドット (.) のいずれかを少なくとも1文字以上繰り返す
@ アットマーク
[a-z\d\-.]+ 英小文字、数字、ハイフン、ドットのいずれかを少なくとも1文字以上繰り返す
\. ドット
[a-z]+ 英小文字を少なくとも1文字以上繰り返す
\z 文字列の末尾
/ 正規表現の終わりを示す
i 大文字小文字を無視するオプション

細かく解説

/

正規表現の開始を示す。
(言語によって記述は異なる。Rubyではこれ。)

\A

文字列の先頭。
以下の記事にとっっっっても分かりやすく説明が書かれています。
Railsの正規表現でよく使われる \A \z って何??

[\w+\-.]+

\w

aからzまでの英小文字、AからZまでの英大文字、0から9までの数字、アンダースコア(_)のどれか1文字。

+

文字としてのプラス+
[ ]の外では意味を持つ(後述)が、中では意味を持たないただの文字。

\-

文字としてのハイフン-
[ ]の中では意味を持ってしまう(後述)ため、\を前に付けることで意味を持たなくして(エスケープ)ただの文字扱いにする。

.

文字としてのドット.
[ ]の外では意味を持つ(後述)が、中では意味を持たないただの文字。

[ ]

[ ]の中のどれか1文字。

+

直前のパターンの1回以上の繰り返し。[ ]の外なので意味を持つ。
今回の直前のパターンは、[ ]の中身。

つまり

\w 英数字、アンダースコア (_)、
+ プラス (+)、
\- ハイフン (-)、
. ドット (.)
[ ] のいずれか
+ を少なくとも1文字以上繰り返す

メールアドレス○○○@△△△.***○○○の部分に当たります。(例:hoge, h-o.g+e, Ho_Ge)

@

文字としての@

メールアドレス○○○@△△△.***@の部分に当たります。

[a-z\d\-.]+

a-z

aからzまでの英小文字のどれか1文字。ここでの-は範囲の意味を持つ。
[ ]の中の-は意味を持つため、エスケープの方法として先述の\があったが、
[-abc][abc-]のように、最初か最後に-を置くことで、その-に意味を持たなくさせることも可能。
(cf. 正規表現リファレンス(CoffeeScript))

\d

0から9までの数字のどれか1文字。

\-

文字としてのハイフン-。(既出)

.

文字としてのドット.。(既出)

[ ]

[ ]の中のどれか1文字。(既出)

+

直前のパターンの1回以上の繰り返し。(既出)

つまり

a-z 英小文字、
\d 数字、
\- ハイフン、
. ドット
[ ] のいずれか
+ を少なくとも1文字以上繰り返す

メールアドレス○○○@△△△.***△△△の部分に当たります。(例:gmail, docomo.ne)

\.

文字としてのドット.
[ ]の外では「任意の1文字」という意味を持つため、エスケープのために\を付ける。

メールアドレス○○○@△△△.***.の部分に当たります。

[a-z]+

a-z

aからzまでの英小文字のどれか1文字。(既出)

[ ]

[ ]の中のどれか1文字。(既出)

+

直前のパターンの1回以上の繰り返し。(既出)

つまり

a-z 英小文字
[ ] (のいずれか)
+ を少なくとも1文字以上繰り返す

メールアドレス○○○@△△△.******の部分に当たります。(例:com, jp)

\z

文字列の末尾。
大文字の\Zだと意味が変わってしまうので注意。
(cf. Railsの正規表現でよく使われる \A \z って何??)

/

正規表現の終わりを示す。

i

大文字と小文字を区別しない。
(cf. 大文字と小文字を区別せずにマッチを行う(/i修飾子))

まとめ

ふぃー。お疲れ様でした。
このように、細かく分割してみると、お手頃なサイズで理解していたつもりがほとんど理解できていなかったことに気づくと思います。

では、改めてメールアドレスの正規表現を見てみましょう。

/\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i

もうどういう意味かすぐに分かりますね!

しかし、正規表現の用途はメールアドレスだけではありません。
正規表現にはもっともっといろいろな記法があって、いろんな表現ができるので、気になった方はどんどん調べてみてください!
伊藤淳一さん(@jnchito)のQiita記事がとてもわかりやすくオススメです!
初心者歓迎!手と目で覚える正規表現入門・その1「さまざまな形式の電話番号を検索しよう」

最後に

今回の正規表現に限った話ではなく、様々な教材や書籍において、そこではそれほど重要ではない内容について細かく解説されていないのは当然のことなので、少しでも「?」と思ったらすぐに調べてみる癖をつけたいものです…(戒め)

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

RubyのバージョンがHeroku未対応だった

はじめに

Rails初心者です。
主にBundlerの扱いにつまずいたのでメモします。
誤った点がありましたら、ご指摘いただけると勉強になります。

補足

Rubyの2.7.0は2019年5月の段階ではまだリリースされていませんでした。
最新バージョンは2.6.3でしたので修正しました。

つまづいたところ

Rubyのバージョン2.7.0を使ってHerokuにデプロイしたところ、
エラーが発生しました。

調べてみると、
Heroku側がまだこのバージョンに対応していないとのことでした。

スクリーンショット 2019-05-26 17.46.55.png

https://devcenter.heroku.com/articles/ruby-support#ruby-versions

対応しているのが2.6.3までですね。
対応したバージョンが使えるように、Rubyのバージョンを変えていきます。

ちなみに下の「MRI」のところには例として、

・Ruby2.6.14.4を入れたら、2.4.6になるよ
・2.7.6.2を入れたら、2.5.5だよ
・3.0.3を入れたら、2.6.3だよ

と書かれています。

Rubyのバージョンを変更します。
試しに今回は、2.6.0をインストールしてみます。

解決策

①使うRubyのバージョンをインストール

こちらが参考になります。

rbenvでrubyのバージョンを切り替える

②bundlerインストール

$ gem install bundler

③Gemfileに記載

source 'https://rubygems.org'

ruby '2.6.0'

#省略

④Gemfile.lockファイルがあれば消す

Gemfile.lockbundle installする度に新しく作られるので、
ファイルごと消しておきます。

⑤インストール

$ bundle install

⑥コミット

新しいGemfile.lockが作成されたのを確認し、
コミット→プッシュ

$ git commit
$ git push heroku

無事、Herokuに反映されました!

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

[Ruby]レーシバ自身を変更するとは?

概要

配列からnilである要素を取り除いた新しい要素を返すメソッドcompactですが、compact!となにが違うのかとおもいました。

リファレンスには「!」がつくほうはレシーバ自身を変更するメソッドとありました。

では、「レシーバ自身を変更する」とはどういうことなのでしょうか

レシーバとは

レシーバとはメソッドを実行するオブジェクトのことです。
下記だと、.の左側のarrayです。

array.compact
array.compact!

レシーバ自身を変更するとは

レシーバとはメソッドを実行するオブジェクトのことでしたので、「レシーバ自身を変更する」とは、このオブジェクトが変更されるということになります。

object_idをみてみます。

sample.rb
fruits = ["apple", nil, "orange", nil, nil, "banana"]

puts fruits.object_id
fruits =  fruits.compact
puts fruits.object_id
ターミナル
70311962445680
70311962445540

まずは「!」がないほうですが、idが違います。
つまり、配列fruitsから、compactメソッドによりnilを除いた新しい配列fruitsができたことになります。

次は「!」をつけます。

sample.rb
fruits = ["apple", nil, "orange", nil, nil, "banana"]

puts fruits.object_id
fruits =  fruits.compact!
puts fruits.object_id
ターミナル
70332086281520
70332086281520

idが同じです。
つまり、配列fruits自身が変更されたことになります。

これがレシーバ自身(今回でいうとfruits)を変更するの正体でした。

まとめ

レシーバ自身を変更とは破壊的メソッドということでした。

今回は「!」をつけるつけないで、参照元から変更するかしないかを選択できます。
「!」をつけ参照元から変更するメリットは、メモリの節約ってところでしょうか。

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

Array#injectでArray#sumを代用

Array#sumはRuby2.4から、それより前のバージョンでArray#sumっぽいことがしたい時はArray#injectで代用可能。

[1,2,3,4,5].inject(:+)

参考

http://rurema.clear-code.com/query:sum/
http://rurema.clear-code.com/2.3.0/method/Enumerable/i/inject.html

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

RailsにオープンソースECのSpreeを導入と注意点(2019年度版)

Railsに導入できるオープンソースのECというとSpreeかSolidusがあります。今回導入したので自分の備忘録代わりにメモ残します。随時追記予定です。

目的&目標

現在動作しているRailsのアプリにEC機能を搭載しようとしてます。
サービス側と管理側で別アプリになっているのでECとしての機能を同様に分けて実装します。

サービス側
プラントベース商品専門オンラインショップ:Vegewel Marché.png

管理側
Spree 管理  注文.png

前提条件

以下は諸々Spreeの設定にあたってベストと思われる状態にあえて揃えました(2019/4現在)。

Ruby 2.5.0
Rails 5.2.1
MySQL 5.7.25

※上記全てAWSにて用意

Spreeのインストール

本家サイトも参照すること
https://github.com/spree/spree

Gemfileに以下を設定

gem 'spree', '~> 3.7.0'
gem 'spree_auth_devise', '~> 3.5'
gem 'spree_gateway', '~> 3.4'

bundle installrake db:migrate したら以下のジェネレーターを実行して必要な初期設定を行います。

rails g spree:install --user_class=Spree::User
rails g spree:auth:install
rails g spree_gateway:install

config/routes.rbにマウント先を設定します。

mount Spree::Core::Engine, at: '/'

ここの/のところにディレクトリ名を入れればそこにマウントされます。

各種設定

ImageMagickの設定

brew install imagemagick@6
PKG_CONFIG_PATH=/usr/local/opt/imagemagick@6/lib/pkgconfig gem install rmagick -v '2.16.0’
(Gemfileのrmagickと合わせる)

ロゴの設定

config/initializers/spree.rb に以下を設定

Spree.config do |config|
  config.logo = "https://myapp.com/assets/logo_thumb.png"
end

ユーザ認証機能の統一

config/initializers/spree.rb に対象のモデルを設定

Spree.config do |config|
  Spree.user_class = "User"
end

lib/spree/authentication_helpers.rb に以下を記載。
ちなみに自分のアプリへのRoutingにはmain_appという接頭ワードをつける必要あり。実質自分のアプリへの誘導などを指定しているのでヘルパーの内容をよく確認すること。

module Spree
  module CurrentUserHelpers
    def self.included(receiver)
      receiver.send :helper_method, :spree_current_user
    end

    def spree_current_user
      current_user
    end
  end

  module AuthenticationHelpers
    def self.included(receiver)
      receiver.send :helper_method, :spree_login_path
      receiver.send :helper_method, :spree_signup_path
      receiver.send :helper_method, :spree_logout_path
    end

    def spree_login_path
      main_app.login_path
    end

    def spree_signup_path
      main_app.signup_path
    end

    def spree_logout_path
      main_app.logout_path
    end
  end
end

ApplicationController.include Spree::AuthenticationHelpers
ApplicationController.include Spree::CurrentUserHelpers

Spree::Api::BaseController.include Spree::CurrentUserHelpers

管理画面用ディレクトリの設定

おなじみspree.rbに以下を追加

config.admin_path = "/your_admin"

多言語設定

Gemfileに以下を記載

gem 'spree_i18n', github: 'spree-contrib/spree_i18n'
gem 'spree_globalize', github: 'spree-contrib/spree_globalize'

config/initializers/spree.rb に以下を設定

Spree.config do |config|
  Spree::Config[:currency] = 'JPY'
  SpreeI18n::Config.available_locales = [:en, :ja]
  SpreeGlobalize::Config.supported_locales = [:en, :ja]
end

決済にStripe導入

日本で決済機能を導入するならこれ一択です。
こちらからアカウント登録して、
https://stripe.com/ja-us
管理画面の設定>支払い方法からSpree::Gateway::StripeGatewayを選択してテスト用のPKとSKを設定すればOKです。本番用はPKとSKの本番用があるのでそちらに切り替えればOK。

トライして難しかったこと

独自に決済機能の実装

実はStripeよりPayJPの方が決済手数料が安い(特にStartup向けには)ので使いたかったのだけど、既存のGatewayを拡張してインプリメントしないといけないこともあり断念。
(CTOえふしんさん曰くStripeから拡張して開発しているぽいのでStripeGatewayから拡張するのが吉かも)

注意点

SpreeのSlackコミュニティは頼りにならない

Laravelとか他のオープンソースコミュニティでも盛んですがSpreeにもSlackコミュニティがあります。
http://slack.spreecommerce.com/
しかし意外と質問投げても返ってこない(わかりやすいものは回答してくれる可能性あり)ので頼りにしないことが注意です。

各種Extensionは意外と使えない

Extensionディレクトリ
https://github.com/spree-contrib
Reviewを追加するものとInvoice作成するものを使おうとしましたが既存のアプリを破壊してしまうようだったので利用を停止しました。基本的にGemfileに追加してジェネレータを動かす+migration動かすというのが通常のインプリメントなのですが既存アプリに影響ないかは慎重に確認することをオススメします。
特にmigrationファイルにはUserモデルやReviewモデルに何か追加するようなものもあり確認しないと危険です。

モデル、コントローラーは都度オーバライドする

ほとんどの機能はGemのソースのものをそのまま利用することになりますが、独自の挙動を入れたい場合はオーバーライドします。
vender/bundle配下にSpreeのGemが色々入っています(Spree_frontendとかSpree_coreとか)のでここから該当のソースを見つけて自分のアプリのapp配下に同じディレクトリ構造(spreeディレクトリをcontroller, model配下に作成してオーバーライド)にしてコピーしたソースを修正します。

わからなくなったら動くデモを参照する

素のRailsアプリにSpree入れたサンプルがあるので動作確認や何が正しいのかを見たければこちらを参考にすること。
https://spreecommerce-demo.herokuapp.com/

add-to-cartボタンが押せない

Spreeと関係ないページにカートに追加ボタン実装とかするとボタンが押せないことがあります。Spreeのテンプレートとかパスが通ってないことが原因なのですがおそらく以下で解決するかと思います。
対象のViewファイルに以下を足します(Slimの例)。

  = render 'spree/shared/paths'
  = javascript_include_tag 'spree/frontend/all'
  = render 'spree/shared/translations'

カート決済で住所入力から先に進まない

管理画面でゾーン、支払い方法、配送方法などちゃんと設定しないと内部エラーになります。なので管理画面の設定を見てみてください。

引き続きの課題

多言語の処理を含めたルーティング

Extensionで最も使われている&活性化しているものというとI18nがあると思います。
https://github.com/spree-contrib/spree_i18n
これを導入すると
https://your_site.com/somedirectory/(en|ja)/product
(somedirectoryにマウントしている前提)
というURL表記になります。これを直すのは結構難しいけどルーティング設定はもう少し柔軟に直したいところです。

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

ニートのプログラミング未経験者がRailsとVueでTodoアプリを作ってみた

はじめに

Vuejs と Rails API を使って Todo アプリを作りました。
まずは、ローカル環境で動かし
最終的に Heroku へデプロイするところまで書きました。

最初に作ったものを載せておきます。

デモ

https://vue-rails-api-todo.herokuapp.com/

vue.gif

コード

https://github.com/youbeer/vue-rails-api-todo

ディレクトリ構成

frontend ディレクトリに Vue のファイルをまとめてあります

vue-rails-api-todo/
├── app
│   ├── channels
│   ├── controllers
│   ├── jobs
│   ├── mailers
│   ├── models
│   └── views
├── bin
├── config
├── db
├── docs
├── frontend
│   ├── dist
│   ├── node_modules
│   ├── public
│   └── src
├── lib
├── log
├── public
├── storage
├── test
├── tmp
└── vendor

対象読者(こんな方に読んでいただけたら)

Rails と Vue のチュートリアルを勉強してなにか作ってみたい方

事前準備

Rails と VueCLI3 のインストールを行なってください

自分の環境です

Mac MoJava
ruby 2.6.1
Rails 5.2.3
Vue 3.7.0

【Rails】サーバーサイドの作成

Rails プロジェクトを API モードで作る

terminal
rails new vue-rails-api-todo --api

Gemfile を修正

Gemfile
source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby '2.6.1'
gem 'bootsnap', '>= 1.1.0', require: false
gem 'puma', '~> 3.11'
gem 'rack-cors'
gem 'rails', '~> 5.2.3'
gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby]

group :development, :test do
  gem 'byebug', platforms: %i[mri mingw x64_mingw]
  gem 'sqlite3'
end

group :development do
  gem 'listen', '>= 3.0.5', '< 3.2'
  gem 'pry-byebug'
  gem 'pry-doc'
  gem 'pry-rails'
  gem 'pry-stack_explorer'
  gem 'spring'
  gem 'spring-watcher-listen', '~> 2.0.0'
end

group :production do
  gem 'pg'
end

Gem をインストール

terminal
bundle install

Model を作る

フィールドは2つだけです

  1. title: タスクの内容
  2. completed: 完了・未完了
terminal
rails g model Todo title:string completed:boolean

migration ファイルの修正

Not Null 制約 と デフォルト値を追記してます

db/migrate/20190525063511_create_tasks.rb
class CreateTodos < ActiveRecord::Migration[5.2]
  def change
    create_table :todos do |t|
      t.string :title, null: false
      t.boolean :completed, default: false, null: false

      t.timestamps
    end
  end
end

マイグレーション

terminal
rails g model Task title:string completed:boolean

Model にバリデーションを追加

app/models/todo.rb
class Todo < ApplicationRecord
  validates :title, presence: true
end

ルーティングの修正

resources :todos, except: :show
以外に2つルーティングを追加しました

  1. patch 'check_all', to: 'todos#check_all': タスクの完了・未完了
  2. delete 'delete_completed', to: 'todos#delete_completed': 完了タスクを全削除
config/routes.rb
Rails.application.routes.draw do
  root 'api/v1/todos#index'

  namespace :api do
    namespace :v1, format: :json do
      patch 'check_all', to: 'todos#check_all'
      delete 'delete_completed', to: 'todos#delete_completed'
      resources :todos, except: :show
    end
  end
end

ルーティングは詳細は、こんな感じです

terminal
                   Prefix Verb   URI Pattern                                                                              Controller#Action
                     root GET    /                                                                                        api/v1/todos#index
         api_v1_check_all PATCH  /api/v1/check_all(.:format)                                                              api/v1/todos#check_all
  api_v1_delete_completed DELETE /api/v1/delete_completed(.:format)                                                       api/v1/todos#delete_completed
             api_v1_todos GET    /api/v1/todos(.:format)                                                                  api/v1/todos#index
                          POST   /api/v1/todos(.:format)                                                                  api/v1/todos#create
              api_v1_todo PATCH  /api/v1/todos/:id(.:format)                                                              api/v1/todos#update
                          PUT    /api/v1/todos/:id(.:format)                                                              api/v1/todos#update
                          DELETE /api/v1/todos/:id(.:format)                                                              api/v1/todos#destroy
       rails_service_blob GET    /rails/active_storage/blobs/:signed_id/*filename(.:format)                               active_storage/blobs#show
rails_blob_representation GET    /rails/active_storage/representations/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations#show
       rails_disk_service GET    /rails/active_storage/disk/:encoded_key/*filename(.:format)                              active_storage/disk#show
update_rails_disk_service PUT    /rails/active_storage/disk/:encoded_token(.:format)                                      active_storage/disk#update
     rails_direct_uploads POST   /rails/active_storage/direct_uploads(.:format)                                           active_storage/direct_uploads#create

controller の用意

terminal
rails g controller api::v1::todos

controller を修正

app/controllers/api/v1/todos_controller.rb
class Api::V1::TodosController < ApplicationController
  before_action :set_todo, only: %i[show update destroy]

  # GET api/vi/todos/
  def index
    @todos = Todo.all.order(created_at: :asc)
    render json: @todos
  end

  # Post api/vi/todos
  def create
    @todo = Todo.new(todo_params)
    if @todo.save
      render json: @todo
    else
      render json: { status: 'error', data: @todo.errors }
    end
  end

  # Put api/vi/todos/:id
  def update
    if @todo.update(todo_params)
      render json: @todo
    else
      render json: { status: 'error', data: @todo.errors }
    end
  end

  # Delete api/vi/todos/:id
  def destroy
    @todo.destroy
    render json: @todo
  end

  # Delete api/vi/delete_completed
  def delete_completed
    todo = Todo.where(completed: true).delete_all
    render json: todo
  end

  # Put api/vi/check_all
  def check_all
    todo = Todo.update_all(completed: params['checked'])
    render json: todo
  end

  private

  def todo_params
    params.require(:todo).permit(:title, :completed)
  end

  def set_todo
    @todo = Todo.find(params[:id])
  end
end

cors の設定ファイルを修正

Vue 側からのアクセスを許可するため追記

origins 'http://localhost:8080'
config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins 'http://localhost:8080'

    resource '*',
             headers: :any,
             methods: %i[get post put patch delete options head]
  end
end

seed ファイルを修正

テストデータ作成用

db/seeds.rb
10.times do |i|
  Todo.create(title: "title No#{i + 1}", completed: i.even?)
end

postman で確認してみる

terminal
rails s
rails db:seed

Postman をインストールされていない方は、こちらからインストールしてください

  1. プルダウンから GET を選択し http://localhost:3000/api/v1/todos を入力
  2. Send をクリック
  3. テストデータの json が返ってくることを確認

時間がある方はその他のアクションも試してみてください
やり方は、ここでは割愛します

Screen Shot 2019-05-25 at 20.54.02.png

【Vue】フロントエンドの作成

プロジェクトを作成

Rails プロジェクトの直下に frontend という名前で Vue プロジェクトを作成します

terminal
vue create frontend

いくつか質問がでてくるので

  • Manually select features を選択肢し
  • Vuex を追加してください

その他はお好みでどうぞ

terminal
? Please pick a preset:
default (babel, eslint)
❯ Manually select features
terminal
? Check the features needed for your project:
 ◉ Babel
 ◯ TypeScript
 ◯ Progressive Web App (PWA) Support
 ◯ Router
❯◉ Vuex
 ◯ CSS Pre-processors
 ◉ Linter / Formatter
 ◯ Unit Testing
 ◯ E2E Testing

インストールが完了したら fronend へ移動しサーバを起動してみましょう

terminal
cd frontend && yarn serve

ブラウザから http://localhost:8080/ へアクセスし
こんな画面が表示されたら成功です。

Screen Shot 2019-05-25 at 16.03.28.png

Bootstrap と axios を追加

  • BootstrapVue: Vue 用の Bootstrap モジュール
  • axios: 今時の ajax モジュール
terminal
yarn add bootstrap-vue bootstrap axios

bootstrap の設定を追加

bootstrap を使うため main.js に追記

import BootstrapVue from "bootstrap-vue";
import "bootstrap/dist/css/bootstrap.css";
import "bootstrap-vue/dist/bootstrap-vue.css";

Vue.use(BootstrapVue);
frontend/src/main.js
import Vue from "vue";
import App from "./App.vue";
import store from "./store";
import BootstrapVue from "bootstrap-vue";
import "bootstrap/dist/css/bootstrap.css";
import "bootstrap-vue/dist/bootstrap-vue.css";

Vue.use(BootstrapVue);
Vue.config.productionTip = false;

new Vue({
  store,
  render: h => h(App)
}).$mount("#app");

store を編集

frontend/src/store.js
import Vue from "vue";
import Vuex from "vuex";
import axios from "axios";

Vue.use(Vuex);

const http = axios.create({
  baseURL:
    process.env.NODE_ENV === "development" ? "http://localhost:3000/" : "/",
  headers: {
    "Content-Type": "application/json",
    "X-Requested-With": "XMLHttpRequest"
  },
  responseType: "json"
});

export default new Vuex.Store({
  state: {
    filter: "all",
    todos: []
  },
  getters: {
    remaining(state) {
      return state.todos.filter(todo => !todo.completed).length;
    },
    completedAll(state, getters) {
      return getters.remaining === 0;
    },
    todosFiltered(state) {
      if (state.filter === "all") {
        return state.todos;
      } else if (state.filter === "active") {
        return state.todos.filter(todo => !todo.completed);
      } else if (state.filter === "completed") {
        return state.todos.filter(todo => todo.completed);
      }
      return state.todos;
    },
    showClearCompletedButton(state) {
      return state.todos.filter(todo => todo.completed).length > 0;
    }
  },
  mutations: {
    addTodo(state, todo) {
      state.todos.push({
        id: todo.id,
        title: todo.title,
        completed: false,
        editing: false
      });
    },
    clearCompleted(state) {
      state.todos = state.todos.filter(todo => !todo.completed);
    },
    updateFilter(state, filter) {
      state.filter = filter;
    },
    checkAll(state, checked) {
      state.todos.forEach(todo => {
        todo.completed = checked;
      });
    },
    deleteTodo(state, id) {
      const index = state.todos.findIndex(todo => todo.id === id);
      state.todos.splice(index, 1);
    },
    updateTodo(state, todo) {
      const index = state.todos.findIndex(item => item.id === todo.id);

      state.todos.splice(index, 1, {
        id: todo.id,
        title: todo.title,
        completed: todo.completed,
        editing: todo.editing
      });
    },
    retrieveTodos(state, todos) {
      state.todos = todos;
    }
  },
  actions: {
    retrieveTodos({ commit }) {
      http
        .get("/api/v1/todos")
        .then(response => {
          commit("retrieveTodos", response.data);
        })
        .catch(error => {
          console.log(error);
        });
    },
    addTodo({ commit }, todo) {
      http
        .post("/api/v1/todos", {
          title: todo.title,
          completed: false
        })
        .then(response => {
          commit("addTodo", response.data);
        })
        .catch(error => {
          console.log(error);
        });
    },
    clearCompleted({ commit }) {
      http
        .delete("/api/v1/delete_completed")
        .then(response => {
          commit("clearCompleted", response.data);
        })
        .catch(error => {
          console.log(error);
        });
    },
    checkAll({ commit }, checked) {
      http
        .patch("/api/v1/check_all", {
          checked
        })
        .then(() => {
          commit("checkAll", checked);
        })
        .catch(error => {
          console.log(error);
        });
    },
    deleteTodo({ commit }, id) {
      http
        .delete(`/api/v1/todos/${id}`)
        .then(response => {
          commit("deleteTodo", response.data.id);
        })
        .catch(error => {
          console.log(error);
        });
    },
    updateTodo({ commit }, todo) {
      http
        .patch(`/api/v1/todos/${todo.id}`, {
          title: todo.title,
          completed: todo.completed
        })
        .then(response => {
          commit("updateTodo", response.data);
        })
        .catch(error => {
          console.log(error);
        });
    }
  }
});

store は大きく5つのブロックに分かれています

axios のデフォルト通信設定

  • baseURL: API 取得のための URL
  • header:リクエスト時のヘッダの値
  • responseType:レスポンスの形式
import axios from "axios";

Vue.use(Vuex);

const http = axios.create({
  baseURL:
    process.env.NODE_ENV === "development" ? "http://localhost:3000/" : "/",
  headers: {
    "Content-Type": "application/json",
    "X-Requested-With": "XMLHttpRequest"
  },
  responseType: "json"
});

state

アプリの状態管理をするための単一オブジェクトです

ステート | Vuex

  • todo: タスクの配列
  • filter:全て ・ 完了 ・ 未完了 のフィルタ
state: {
  filter: "all",
  todos: []
},

getters

component でいう computed にあたります

ゲッター | Vuex

  • remaining: タスク完了の件数
  • showClearCompletedButton: クリアボタンを 表示 ・ 非表示 の切替用
getters: {
    remaining(state) {
      return state.todos.filter(todo => !todo.completed).length;
    },

    /*************** 省略 ***************/

    showClearCompletedButton(state) {
      return state.todos.filter(todo => todo.completed).length > 0;
    }
}

mutaition

state を変更するためのメソッド群です
action を経由して state を更新するために使っています

ミューテーション | Vuex

  • addTodo: 新しいタスクを追加しています
  • retrieveTodos: ページに最初にアクセスしたとき、タスク一覧を作成しています
mutations: {
  addTodo(state, todo) {
    state.todos.push({
      id: todo.id,
      title: todo.title,
      completed: false,
      editing: false
    });
  },

  /*************** 省略 ***************/

  retrieveTodos(state, todos) {
    state.todos = todos;
  }
},

actions

非同期処理を行うためのメソッド群です
axios を使って Rails API を取得するために使っています

アクション | Vuex

  • retrieveTodos: Rails の API からタスク一覧を取得しています
  • updateTodo: Rails の API からタスクの更新結果を取得しています
 actions: {
  retrieveTodos({ commit }) {
    http
      .get("/api/v1/todos")
      .then(response => {
        commit("retrieveTodos", response.data);
      })
      .catch(error => {
        console.log(error);
      });
  },

  /*************** 省略 ***************/

  updateTodo({ commit }, todo) {
    http
      .patch(`/api/v1/todos/${todo.id}`, {
        title: todo.title,
        completed: todo.completed
      })
      .then(response => {
        commit("updateTodo", response.data);
      })
      .catch(error => {
        console.log(error);
      });
  }
}

App を編集

frontend/src/App.vue
<template>
  <div id="app" class="container">
    <img alt="Vue logo" src="./assets/logo.png" class="logo" />
    <h1>VueTODO</h1>
    <todo-list></todo-list>
  </div>
</template>

<script>
import TodoList from "./components/TodoList.vue";

export default {
  name: "App",
  components: {
    TodoList
  }
};
</script>

<style lang="scss" scoped>
.logo {
  margin: 0 auto;
  display: block;
}
</style>

メインとなる TodoList コンポーネントを呼び出しています

import TodoList from "./components/TodoList.vue";

export default {
  name: "App",
  components: {
    TodoList
  }
};

TodoList コンポーネントを作成

新しいタスクの追加 と 子コンポーネントを束ねています

frontend/src/components/TodoList.vue
<template>
  <div>
    <b-container class="bv-example-row">
      <b-row>
        <b-col cols="12">
          <b-form @submit.prevent="addTodo">
            <b-form-group label="New todo" label-for="new-todo">
              <b-form-input
                id="new-todo"
                v-model="newTodo"
                placeholder="What needs to be done?"
              ></b-form-input>
            </b-form-group>
          </b-form>

          <b-list-group>
            <transition-group name="fade">
              <TodoItem
                v-for="(todo, index) in todosFiltered"
                :key="todo.id"
                :todo="todo"
                :index="index"
                class="todo-item"
                :check-all="completedAll"
              />
            </transition-group>
          </b-list-group>

          <b-list-group class="mt-4">
            <b-list-group-item
              class="flex-wrap d-flex justify-content-around align-items-center"
            >
              <TodoCheckAll />
              <TodoItemsRemaining />
            </b-list-group-item>
            <b-list-group-item
              class="flex-wrap d-flex justify-content-around align-items-center"
            >
              <TodoFiltered />
              <TodoClearCompleted />
            </b-list-group-item>
          </b-list-group>
        </b-col>
      </b-row>
    </b-container>
  </div>
</template>

<script>
import TodoItem from "@/components/TodoItem";
import TodoItemsRemaining from "@/components/TodoItemsRemaining";
import TodoCheckAll from "@/components/TodoCheckAll";
import TodoFiltered from "@/components/TodoFiltered";
import TodoClearCompleted from "@/components/TodoClearCompleted";
import { mapGetters } from "vuex";

export default {
  name: "TodoList",
  components: {
    TodoItem,
    TodoItemsRemaining,
    TodoCheckAll,
    TodoFiltered,
    TodoClearCompleted
  },
  data() {
    return {
      newTodo: ""
    };
  },
  computed: {
    ...mapGetters(["completedAll", "todosFiltered"])
  },
  created() {
    this.$store.dispatch("retrieveTodos");
  },
  methods: {
    addTodo() {
      if (this.newTodo.trim()) {
        this.$store.dispatch("addTodo", {
          id: this.idForTodo,
          title: this.newTodo
        });
      }
      this.newTodo = "";
    }
  }
};
</script>

<style lang="scss">
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.5s;
}
.fade-enter,
.fade-leave-to {
  opacity: 0;
}
</style>

TodoItem コンポーネントを作成

親コンポーネントの TodoList から props を受け取り
個々のタスクの表示させています

frontend/src/components/TodoItem.vue
<template>
  <b-list-group-item
    class="flex-wrap d-flex justify-content-around align-items-center todo-item"
  >
    <b-col cols="2">
      <b-form-checkbox v-model="completed" @input="doneEdit"></b-form-checkbox>
    </b-col>

    <b-col cols="8">
      <label
        v-if="!editing"
        :class="{ completed: completed }"
        @dblclick="editing = true"
        >{{ title }}</label
      >
      <b-form-input
        v-else
        v-model="title"
        v-focus
        type="text"
        @blur="doneEdit"
        @keyup.enter="doneEdit"
        @keyup.escape="cancelEdit"
      />
    </b-col>

    <b-col cols="2">
      <button
        type="button"
        class="close"
        aria-label="Close"
        @click="deleteTodo(todo.id)"
      >
        <span aria-hidden="true">&times;</span>
      </button>
    </b-col>
  </b-list-group-item>
</template>
<script>
import { mapActions } from "vuex";
export default {
  name: "TodoItem",
  directives: {
    focus: {
      inserted: function(el) {
        el.focus();
      }
    }
  },
  props: {
    todo: {
      type: Object,
      required: true
    },
    index: {
      type: Number,
      required: true
    },
    checkAll: {
      type: Boolean,
      required: true
    }
  },
  data() {
    return {
      id: this.todo.id,
      title: this.todo.title,
      completed: this.todo.completed,
      editing: false
    };
  },
  watch: {
    checkAll() {
      this.completed = this.checkAll ? true : this.todo.completed;
    }
  },
  methods: {
    ...mapActions(["deleteTodo", "updateTodo"]),
    doneEdit() {
      this.editing = false;

      this.updateTodo({
        id: this.id,
        title: this.title,
        completed: this.completed,
        editing: this.editing
      });
    },
    cancelEdit() {
      this.title = this.todo.title;
      this.editing = false;
    }
  }
};
</script>

<style lang="scss" scoped>
.todo-item {
  animation-duration: 0.3s;
}

.completed {
  text-decoration: line-through;
  color: grey;
}
</style>

TodoItemsRemaining コンポーネントを作成

残りのタスク件数を表示させています

frontend/src/components/TodoItemsRemaining.vue
<template>
  <b-col cols="6">
    <span class="text-danger">{{ remaining }}</span>
    {{ remaining | pluralize("item") }} left
  </b-col>
</template>

<script>
import { mapGetters } from "vuex";
export default {
  name: "TodoItemsRemaining",
  filters: {
    pluralize: (n, w) => (n === 1 ? w : w + "s")
  },
  computed: {
    ...mapGetters(["remaining"])
  }
};
</script>

TodoFiltered コンポーネントを作成

All(全て) ・ Active(未完了) ・ Completed(完了)
の値によってタスクにフィルタをかけています

frontend/src/components/TodoFiltered.vue
<template>
  <b-col cols="6">
    <b-form-radio-group
      v-model="selected"
      :options="options"
      buttons
      button-variant="outline-primary"
      name="radio-btn-outline"
      @change="updateFilter"
    ></b-form-radio-group>
  </b-col>
</template>

<script>
import { mapState, mapMutations } from "vuex";
export default {
  name: "TodoFiltered",
  data() {
    return {
      selected: "all",
      options: [
        { text: "All", value: "all" },
        { text: "Active", value: "active" },
        { text: "Completed", value: "completed" }
      ]
    };
  },
  computed: {
    ...mapState(["filter"])
  },
  methods: {
    ...mapMutations(["updateFilter"])
  }
};
</script>

TodoClearCompleted コンポーネントを作成

完了したタスクの一括クリアボタンを表示させています

frontend/src/components/TodoClearCompleted.vue
<template>
  <b-col cols="6">
    <div>
      <b-button
        v-if="showClearCompletedButton"
        variant="outline-primary"
        @click="clearCompleted"
        >Clear Completed</b-button
      >
    </div>
  </b-col>
</template>

<script>
import { mapGetters, mapActions } from "vuex";
export default {
  name: "TodoClearCompleted",
  computed: {
    ...mapGetters(["showClearCompletedButton"])
  },
  methods: {
    ...mapActions(["clearCompleted"])
  }
};
</script>

TodoCheckAll コンポーネントを作成

タスクを一括で完了 ・ 未完了に切り替えるための
チェックボックスを表示させています

frontend/src/components/TodoCheckAll.vue
<template>
  <b-col cols="6">
    <b-form-checkbox :checked="completedAll" @change="checkAll"
      >Check All</b-form-checkbox
    >
  </b-col>
</template>

<script>
import { mapGetters, mapActions } from "vuex";
export default {
  name: "TodoCheckAll",
  computed: {
    ...mapGetters(["completedAll"])
  },
  methods: {
    ...mapActions(["checkAll"])
  }
};
</script>

ブラウザで確認

Rail のサーバを起動

terminal
rails s

Vue のサーバを起動

terminal
cd frontend
yarn serve

localhost:8080 にアクセスしてこんな画面が表示されたら成功です

Screen Shot 2019-05-27 at 11.00.41.png

Heroku へデプロイしてみる

事前準備

Heroku のアカウントがない場合はこちらから作成してください

デプロイには heroku toolbelt が必要なのでこちらからインストールしてください

mac の場合は Homebrew でインストール可能です

terminal
brew install heroku

プロジェクトを commit

プロジェクト直下へ移動し commit を行なってください

terminal
git init
git add .
git commit -m "init"

vue.config.js を作成

frontend ディレクトリの直下に vue.config.js ファイルを作成し
build ファイルの出力先をプロジェクト直下の public ディレクトリへ変更します

frontend/vue.config.js
module.exports = {
  outputDir: "../public"
};

Vue を build

terminal
yarn build

プロジェクト直下の public ディレクトリに
build されたファイルが作成されていることを確認してください

terminal
public/
├── css
│   ├── app.27d4506b.css
│   └── chunk-vendors.19588e8d.css
├── favicon.ico
├── img
│   └── logo.82b9c7a5.png
├── index.html
└── js
    ├── app.6635e2d3.js
    ├── app.6635e2d3.js.map
    ├── chunk-vendors.4ad97586.js
    └── chunk-vendors.4ad97586.js.map

Heroku にログイン

terminal
heroku login

上のコマンドを実行するとブラウザに切替わるのでボタンを押してログインしてください

Screen Shot 2019-05-27 at 11.15.45.png

Heroku にアプリを作成

アプリ名を入力すると URL にアプリ名が反映されます
https://アプリ名.herokuapp.com/
省略すると Heroku 側で自動的に割り振られます

terminal
heroku create アプリ名

Heroku のリポジトリへ push

terminal
git push heroku master

データベースの migration と テストデータを追加

terminal
heroku run rails db:migrate
heroku run rails db:seed

ブラウザで確認

terminal
heroku open

おわりに

最後まで読んでいただきありがとうございました。
おかしな部分がありましたら、ご指摘お願いします。

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

[初心者] CSSフレームワークで調子に乗ったヤツの話

はじめに

最近ようやく簡単なアプリが作れるようになって、「フロントをいじり倒したい!」という欲が出てきました。(笑)
そこで先輩エンジニアから伝授されたのが、CSSフレームワークです。
今回はそれについて書いていこうと思います。

まずCSSフレームワークって何だ?

CSSフレームワークとは、あらかじめデザインやレイアウトが用意されていて、クラス名を指定するだけで簡単にCSSを反映させることができるとても便利なフレームワークです。
HTMLを組んで、CSSで肉付けして・・・のような地道な作業を簡略化させてくれて、なおかつ初心者では作れないようなものが簡単に使えるようになる優れものです!(笑)

調子に乗った末・・・

これを知った僕は、案の定「すごい!楽!最高じゃん!」という感じで早速使いまくりました。
私が使用したフレームワークは「Materialize.css」です。
そして完成した、今の自分的には「めっちゃいいじゃん!」と思うアプリを他の人に見てもらうと、

「Materiallize感すごいね(笑)」
「これMateriallize使ってるでしょ(笑)」
illustrain05-otokonoko03.png
・・・・・・・。

なんでバレるんだよ!!!!!!!!!!!!!!!!!!!!!!!
というより使っちゃダメなのかよ!!!!!!!!!!!!!!!!!!!

となりました。(笑)
一体なぜ周りはこのような反応になったのでしょう・・・?

フレームワークの落とし穴

これだけ便利なものがあれば、みんな使いますよね。(笑)
だからこそ、
「あー、これね」となってしまうようです。
確かに既存のデザインをみんなで使うという仕組みから考えれば、そうなっても仕方ないなと思います。
使うにしても、ベースのコードに自分で何かを書き加えたりして工夫することが大切なんですね・・・
作業スピードは早くなっても、楽している分このような落とし穴があると痛感させられました。(笑)

おわりに

正直、プログラミングを勉強する前は、「システムさえ作れればデザインとかはサクっといけるでしょ!」と勝手に考えていました。
しかし勉強している中でやっと、エンジニアが「フロントエンド」と「バックエンド」に分けられている意味がわかるようになりました。
本当にまだまだ知識不足です。
今はまだ駆け出しで、基礎を学んでいる最中ですが、早く自分の思い描くようなアプリが作れるように日々頑張っていこうと思います!

ありがとうございました^^

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

Rails Tutorial(2週目)-7-

3つの環境

Railsにはテスト環境 (test)、開発環境 (development)、そして本番環境 (production) の3つの環境がデフォルトで装備されています。Rails consoleのデフォルトの環境はdevelopmentです。

$rails console testでtest環境のコンソール立ち上げ
$rails server --environment productionrails serverを開発環境で立ち上げ
$rails db:migrate RAILS_ENV=prodction で本番データーベースの作成

Sassのmixinとextend

mixin

何度も使うスタイルを定義することが出来、引数が使える。呼び出し時には@includeが必要

@mixin box_sizing {
  -moz-box-sizing:    border-box;
  -webkit-box-sizing: border-box;
  box-sizing:         border-box;
}
.
.
.
/* miscellaneous */

.debug_dump {
  clear: both;
  float: left;
  width: 100%;
  margin-top: 45px;
  @include box_sizing;
}

extend

既に定義しているスタイルを継承する。クラスの継承に近い感覚。
コンパイル後、セレクタがグループ化される。(以下「Sass(SCSS)のmixin, extendなどまとめ」より引用(https://qiita.com/one-a/items/2758511326c09200fded)

.box {
  margin-top: 15px;
  padding: 10px;
  background-color: #ccc;

  p {
    line-height: 1.3;
  }
}

.contentsBox {
  @extend .box;
  background-color: #eee;
}

次のようにCSSファイルに変換される

.box, .contentsBox {
  margin-top: 15px;
  padding: 10px;
  background-color: #ccc;
}

.box p, .contentsBox p {
  line-height: 1.3;
}

.contentsBox {
  background-color: #eee;
}

.box, .contentsBoxのように同一の要素がグループ化されているのがわかる。
Usersリソース
route.rbにresources :usersと記述することでRESTfulなUsersリソースで必要となる全てのアクションが利用できるようになる。また、この行に対応するURLやアクション、名前付きルートは以下のようになる。

HTTPリクエスト URL アクション 名前付きルート 用途
GET /users index users_path すべてのユーザーを一覧するページ
GET /users/1 show user_path(user) 特定のユーザーを表示するページ
GET /users/new new new_user_path ユーザーを新規作成するページ (ユーザー登録)
POST /users create users_path ユーザーを作成するアクション
GET /users/1/edit edit edit_user_path(user) id=1のユーザーを編集するページ
PATCH /users/1 update user_path(user) ユーザーを更新するアクション
DELETE /users/1 destroy user_path(user) ユーザーを削除するアクション

byebug

byebugというgemをインストールしていると、
メソッド内にdebuggerと記述するだけで、対話的にデバッグの処理を行う事ができる。

Gravatar

Gravatarは無料のサービスで、プロフィール写真をアップロードして、指定したメールアドレスと関連付けることができます。その結果、 Gravatarはプロフィール写真をアップロードするときの面倒な作業や写真が欠けるトラブル、また、画像の置き場所の悩みを解決します。というのも、ユーザーのメールアドレスを組み込んだGravatar専用の画像パスを構成するだけで、対応するGravatarの画像が自動的に表示されるからです

app/helpers/users_helper.rb
module UsersHelper

  # 引数で与えられたユーザーのGravatar画像を返す
  def gravatar_for(user)
    gravatar_id = Digest::MD5::hexdigest(user.email.downcase)
    gravatar_url = "https://secure.gravatar.com/avatar/#{gravatar_id}"
    image_tag(gravatar_url, alt: user.name, class: "gravatar")
  end
end

ユーザー登録フォームの作成

Railsでform_forヘルパーメソッドを使います。このメソッドはActive Recordのオブジェクトを取り込み、そのオブジェクトの属性を使ってフォームを構築します。

<div class="col-md-6 col-md-offset-3">
    <%= form_for(@user) do |f| %>
      <%= f.label :name %>
      <%= f.text_field :name %>

      <%= f.label :email %>
      <%= f.email_field :email %>

      <%= f.label :password %>
      <%= f.password_field :password %>

      <%= f.label :password_confirmation, "Confirmation" %>
      <%= f.password_field :password_confirmation %>

      <%= f.submit "Create my account", class: "btn btn-primary" %>
    <% end %>

form_for(@user) do |f|となっている部分は、form_forヘルパが、変数を一つ持つブロックを引数に取り、そのブロック変数はHTMLのフォーム要素に対応するメソッドが自身に呼び出されると、form_forの引数となっているユーザーの属性を設定するために特別に設計されたHTMLを返す。

HTMLに変換後のコードを見てみると
<input id="user_name" name="user[name]" - - - />
.
.
.
<input id="user_password" name="user[password]" - - - />

inputには属性として代表的なものに、 name属性,value属性、type属性などがある。

name属性

入力コントロールの名前を指定する文字列です。この名前は

要素の elements オブジェクトで保持され、フォームデータが送信される時に、コントロールの値と共に送信されます。

Railsにおいては、Railsはnameの値を使って、初期化したハッシュを (params変数経由で) 構成します。このハッシュは、入力された値に基づいてユーザーを作成するときに使われます。

つまり、user = {password: ##, email: ## ....}というuserを表すハッシュが存在し、params内に格納されるが、name属性を指定するとそのうち、passwordをキーとするuserハッシュの要素(入力された内容)がparamsに格納される。

この結果、name,email,password,password_confirmationで構成されるuserという名前のハッシュが、paramsというハッシュの中に格納される。

formタグ

<form action="/users" class="new_user" id="new_user" method="post">
 action属性はフォーム経由で送信された情報を処理するプログラムの URI。method属性は、HTTPメソッドの指定。

本格的なユーザー登録

paramsの送信先のcreateアクション内で
@user = User.new(params[:user])とすることで、フォーム内容に対応したユーザの作成が可能だが、paramsハッシュ全体を初期化してUser.newにわたすのは危険

以前のバージョンのRailsでは、モデル層でattr_accessibleメソッドを使うことで上のような危険を防止していましたが、Rails 4.0ではコントローラ層でStrong Parametersというテクニックを使うことが推奨されています。Strong Parametersを使うことで、必須のパラメータと許可されたパラメータを指定することができます。さらに、上のようにparamsハッシュをまるごと渡すとエラーが発生するので、Railsはデフォルトでマスアサインメントの脆弱性から守られるようになりました。

params.require(:user).permit(:name, :email, :password, :password_confirmation)
上のコードはparamsハッシュのうち、:user属性を必須として、そのうち名前、メールアドレス、パスワード、パスワードの確認の属性をそれぞれ許可し、それ以外を許可しないようにしてある。
このコードの戻り値は、許可された属性のみが含まれたparamsのハッシュです (:user属性がない場合はエラーになります)。

これらのパラメータを利用しやすくするために、user_paramsという外部メソッドを使うのが慣習化している。

エラーメッセージ

newページでエラーメッセージのパーシャルを出力する
<%= render 'shared/error_messages' %>
Rails全般の慣習として、複数のビューで使われるパーシャルは専用のディレクトリ「shared」によく置かれる

app/views/shared/_error_messages.html.erb
<% if @user.errors.any? %>
  <div id="error_explanation">
    <div class="alert alert-danger">
      The form contains <%= pluralize(@user.errors.count, "error") %>.
    </div>
    <ul>
    <% @user.errors.full_messages.each do |msg| %>
      <li><%= msg %></li>
    <% end %>
    </ul>
  </div>
<% end %>

pluralizeヘルパ

pluralizeの最初の引数に整数が与えられると、それに基づいて2番目の引数の英単語を複数形に変更したものを返します。
>> helper.pluralize(1, "error")
=> "1 error"

Railsは、無効な内容の送信によって元のページに戻されると、CSSクラスfield_with_errorsを持ったdivタグでエラー箇所を自動的に囲んでくれます。

エラーメッセージの重複を防ぐ

allow_ nil: true で防げる

ユーザー登録成功

def create
@user = User.new(user_params)
if @user.save
redirect_to @user
else
render 'new'
end
end

redirect_to @user
redirect_to user_url(@user)と等価(Railsが自動で変換してくれる)

flash

登録完了後に表示されるページにメッセージを表示し (この場合は新規ユーザーへのウェルカムメッセージ)、2度目以降にはそのページにメッセージを表示しないようにするというものです。

flashはハッシュのように扱う特殊な変数
flash変数に代入したメッセージは、リダイレクトした直後のページで表示できるようになります。

(例)flash[:success} = "welcome"

SSL

ローカルのサーバからネットワークに流れる前に、大事な情報を暗号化する技術。
ユーザーから送られた情報を暗号化することで、セキュリティ上の欠陥をなくす。

SSLの有効化

production.rbという本番環境の設定ファイルにsslを使用するように記述する

config/environments/production.rb
Rails.application.configure do
  .
  .
  .
  # Force all access to the app over SSL, use Strict-Transport-Security,
  # and use secure cookies.
  config.force_ssl = true
  .
  .
  .
end

WEBサーバーをWEBrickからPumaに変える

WEBrickは簡単にセットアップできたり動せることが特長ですが、著しいトラフィックを扱うことには適していない。

・puma gemをGemfileに追加する(rails5ではデフォルトの設定で使える)
・設定を書き込んでいく

config/puma.rb
workers Integer(ENV['WEB_CONCURRENCY'] || 2)
threads_count = Integer(ENV['RAILS_MAX_THREADS'] || 5)
threads threads_count, threads_count

preload_app!

rackup      DefaultRackup
port        ENV['PORT']     || 3000
environment ENV['RACK_ENV'] || 'development'

on_worker_boot do
  # Worker specific setup for Rails 4.1+
  # See: https://devcenter.heroku.com/articles/
  # deploying-rails-applications-with-the-puma-web-server#on-worker-boot
  ActiveRecord::Base.establish_connection
end

・Procfileと呼ばれる、Heroku上でPumaのプロセスを走らせる設定ファイルを作成

web: bundle exec puma -C config/puma.rb

本番環境へのデプロイ


$ rails test
$ git add -A
$ git commit -m "Use SSL and the Puma webserver in production"
$ git push
$ git push heroku
$ heroku run rails db:migrate

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

gem install bundler 時のエラー対処法(Mac)

こんにちは, Rubyほぼ初心者の自分が環境構築を行ってる際にぶち当たったエラーに対する対処法をお伝えします.

今回の環境構築は次のような順番で行いました.今回エラーがでたのはこのうちの3の部分です.

環境構築手順
1. rbenvのインストール
2. rbenv経由でrubyをインストール
3. bundlerをインストール

bundlerのインストールは次のコマンドで行います.

(base) ****-MacBook-air:~ user$ gem install bundler

ところがこの操作に対して次のようなエラーが出てきてしまいました.

ERROR:  While executing gem ... (Gem::FilePermissionError)
    You don't have write permissions for the /Library/Ruby/Gems/2.3.0 directory.

You don't have write permissionswrite permissionというのは専門用語で書き込み許可という意味だそうです.
つまり書き込みの許可がいるみたいです.
つまり,この操作はuserの権限を越えたことをやろうとしている??
ここでgem,rubyのPATHを確認してみます.

(base) ****-MacBook-air:training user$ which gem
/usr/bin/gem
(base) ****-MacBook-air:training user$ which ruby
/usr/bin/ruby

usrディレクトリはuserディレクトリのさらに上の階層であり,userの権限ではアクセスできない部分です.したがって,今回は,userの身分でありながら,上の階層のディレクトリにアクセスしようととしたことが原因であったとわかりました.
そもそもこのような事が起きたのは, gem install bundler をした際に,rbenv側のrubyやgemではなく, システム側(usr/bin)のruby,gemを使ってることが原因でした.
なので以下のコマンドでPATHを設定し直します.

(base) ****-MacBook-air:training user$ export RBENV_ROOT="${HOME}/.rbenv"
(base) ****-MacBook-air:training user$ if [ -d "${RBENV_ROOT}" ]; then    export PATH="${RBENV_ROOT}/bin:${PATH}";    eval "$(rbenv init -)";  fi

完了したら,もう一度rubyとgemのPATHを確認します.

(base) ****-MacBook-air:training user$ which ruby
/Users/user/.rbenv/shims/ruby
(base) ****-MacBook-air:training user$ which gem
/Users/user/.rbenv/shims/gem

これで現在,rubyとgemはrbenvのものを参照してることがわかります.

このあと

(base) ****-MacBook-air:~ user$ gem install bundler

をもう一度試してみたところうまくいきました!

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

Rails6 のちょい足しな新機能を試す24(unfreeze_time 編)

はじめに

Rails 6 に追加されそうな新機能を試す第24段。 今回のちょい足し機能は、 unfreeze_time 編です。
Rails 6.0 では、 travel_back の alias として、 unfreeze_time が追加されました。

Ruby 2.6.3, Rails 6.0.0.rc1 で確認しました。Rails 6.0.0.rc1 は gem install rails --prerelease でインストールできます。

せっかくなので、RSpec をインストールして試してみたいと思います。

$  rails --version
Rails 6.0.0.rc1

Rails プロジェクトを作る

rspec を使うので、 -T オプションをつけます。

$ rails new rails6_0_0rc1 -T
$ cd rails6_0_0rc1

db:create をしておく

$ bin/rails db:create

Gemfile に rspec-rails を追加する

Gemfile に rspec-rails を追加します。 test 環境前提です。

Gemfile
...
group :test do
  gem 'rspec-rails'
end
...

bundle を実行

$ bundle

RSpec の初期設定をする

$ rails g rspec:install

spec で unfreeze_time を使えるようにする

spec で unfreeze_time を使えるように ActiveSupport::Testing::TimeHelpers を include します。

spec/spec_helpers.rb
RSpec.configure do |config|
  ...
  require 'active_support/testing/time_helpers'
  config.include ActiveSupport::Testing::TimeHelpers
  ...
end

unfreeze_time を使った spec を書く

本来は、model とか何かしらのクラスやメソッドをテストするための spec なのですが、今回は spec だけで完結させることにしました。

before 内で freeze_time を実行し、 after 内で unfreeze_time を実行する場合と、実行しない場合を書いてみました。

spec/lib/unfreeze_time_spec.rb
require 'rails_helper'

RSpec.describe 'try to use unfreeze_time' do
  context 'when using freeze_time and unfreeze_time' do
    before :each do
      freeze_time
    end
    after :each do
      unfreeze_time
    end
    it 'time freezed' do
      t1 = Time.zone.now
      sleep 1
      expect(Time.zone.now).to eq t1
    end
  end
  context 'when without freeze_time and unfreeze_time' do
    it 'time not freezed' do
      t1 = Time.zone.now
      sleep 1
      expect(Time.zone.now).to be > t1
    end
  end
end

spec を実行する

spec を実行するとオールグリーンになります。

$ bundle exec rspec -fd

try to use unfreeze_time
  when using freeze_time and unfreeze_time
    time freezed
  when without freeze_time and unfreeze_time
    time not freezed

Finished in 2.01 seconds (files took 2.01 seconds to load)
2 examples, 0 failures

alias であることを確認する

オマケですが、 unfreeze_timetravel_back の別名であることを確認しておきます。

$ irb -ractive_support/testing/time_helpers
irb(main):001:0> ActiveSupport::Testing::TimeHelpers.instance_method(:unfreeze_time).original_name
=> :travel_back

試したソース

試したソースは以下にあります。
https://github.com/suketa/rails6_0_0rc1/tree/try024_unfreeze_time

参考情報

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

What is Ruby on Rails Used for: Tips from Back-End Developers

what is ruby on rails used for

When you consider developing a new project, choosing the right technology stack is vital. For Back-End development, you have various options: Python, Java, PHP, Ruby, and many more.

The task of choosing between Ruby on Rails and other frameworks becomes easier if you know the pros and cons of this technology and what Ruby on Rails is used for.

Ruby on Rails has a few beneficial arguments that developers can't ignore. As soon as all of them are combined, they decrease the development time and make the process more efficient.

This back-end framework has a number of advantages for projects:

  • Extensive ecosystem
    In comparison with many other frameworks, its ecosystem is what makes Ruby on Rails superior. RubyGems, a Ruby community’s gem hosting service, provides access to thousands of many gems, which can take the form of add-ons, libraries, or software snippets. Gems are ready-made solutions for different problems that streamline the development process.

  • Ruby on Rails MVC
    Another integral part of the Ruby on Rails framework is MVC. It means Model-View-Controller format. The approach divides the app work into three subsystems, each of which is responsible for a set of actions:

  • Models handle data and business logic
    Controllers handle the user interface and application
    Views handle graphical user interface objects and presentation
    Ruby on Rails MVC lets parallel development and allows programmers to speed up the engineering process three times. Ruby on Rails gives ready-to-use baskets for separation of the app business logic, in such a way a Ruby on Rails web development company can save time through its utilization.

  • Consistency and clean code
    The implementation of many features can be simplified by the fact that Ruby on Rails developers can utilize the ready-to-use parts of code. In such a way, the application code is clean and has high readability. As you have less code to read and sort through, all future updates are fast and seamless. This makes Ruby on Rails development time and cost efficient.

  • DRY
    DRY (Don’t Repeat Yourself) is one more of the principals Ruby on Rails is built on. If you have a repetitive task, in Ruby on Rails development, you can reuse them an unlimited number of times.

  • High scalability
    One more advantage is its scalability. An app built on RoR can be scaled to process thousands of requests per second sent by multiple users. This means that Ruby on Rails is a great solution for apps that are actively growing their audience.

  • Security
    Its security is one more benefit. Ruby on Rails has some security-centric features built in that make applications safe from SQL-injections and XSS attacks. Besides, there are a lot of gems that address other security threats.

  • Time and cost efficiency
    All of the features already mentioned make Ruby on Rails time and cost efficient.

  • RAD
    Rapid application development (RAD) is one more sphere Ruby on Rails is used for, which streamlines the process of change accommodation.

  • Self-documentation
    As mentioned above, Ruby code is highly readable and self-documenting (self-describing). It makes the development process quicker because the development team doesn’t have to write out separate documentation. New members in development teams should not have problems with understanding the concept and participating in existing projects.

  • Test environment
    This back-end framework has three default environments: production, development, and testing. The whole development cycle is optimized and you can test a product that is being developed at every stage. As a result, there are fewer bugs and errors that you should be aware of and debug. This is important to consider when you determine what is Ruby on Rails is used for.

Here https://mlsdev.com/blog/what-is-ruby-on-rails-used-for you will find out more about when Ruby on Rails is best applied, what projects can be built with it, and which companies successfully use it already.

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

Ubuntu18.04にRuby、Rails、Postgresqlを簡単にインストールする手順とコマンド

Ubuntu18にRuby周り一式をスムーズに入れる手順。

# インストールする環境
Ubuntu 18.04
Ruby 2.5.1
Rails 5.2.3
PostgreSQL 9.5.16
git version 2.17.1

をスムーズにインストールします。
所要時間は30分ぐらい。
ひたすら(今は意味は考えず)このままコマンドを打つ。

インストール手順コマンド

Rubyをインストール

$ curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash -
$ curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
$ echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list

$ sudo apt-get update
$ sudo apt-get install git-core curl zlib1g-dev build-essential libssl-dev libreadline-dev libyaml-dev libsqlite3-dev sqlite3 libxml2-dev libxslt1-dev libcurl4-openssl-dev software-properties-common libffi-dev nodejs yarn

rbenv インストール

$ cd
$ git clone https://github.com/rbenv/rbenv.git ~/.rbenv
$ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc
$ echo 'eval "$(rbenv init -)"' >> ~/.bashrc
$ exec $SHELL

$ git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
$ echo 'export PATH="$HOME/.rbenv/plugins/ruby-build/bin:$PATH"' >> ~/.bashrc
$ exec $SHELL

$ rbenv install 2.5.1
$ rbenv global 2.5.1
$ ruby -v
ruby 2.5.1

Gemインストール

$ gem install bundler

Railsインストール

$ curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash -
$ sudo apt-get install -y nodejs
$ gem install rails -v 5.2.3
$ rbenv rehash
$ rails -v
Rails 5.2.3

Postgresqlインストール

$ sudo sh -c "echo 'deb http://apt.postgresql.org/pub/repos/apt/ xenial-pgdg main' > /etc/apt/sources.list.d/pgdg.list"
$ wget --quiet -O - http://apt.postgresql.org/pub/repos/apt/ACCC4CF8.asc | sudo apt-key add -
$ sudo apt-get update
$ sudo apt-get install postgresql-common
$ sudo apt-get install postgresql-9.5 libpq-dev

Postgresqlのユーザー作成

$ sudo -u postgres createuser vagrant -s

この時パスワードは設定しませんでした。
パスワードを設定したい場合は、

$ sudo -u postgres psql
$ postgres=# \password vagrant

この様にします。

Git設定(飛ばしても良い)

$ git config --global color.ui true
$ git config --global user.name "YOUR NAME"
$ git config --global user.email "YOUR@EMAIL.com"
$ ssh-keygen -t rsa -b 4096 -C "YOUR@EMAIL.com"

ここまでインストールされたか確認

$ ruby -v
ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-linux-gnu]
$ rails -v
Rails 5.2.3
$ psql --version
psql (PostgreSQL) 9.5.16
$ git --version
git version 2.17.1

最後にアプリ作成できるかチェック

$ rails new testapp -d postgresql
$ cd testapp
$ rails db:create
$ rails g controller home index
$ rails db:migrate
$ rails s -b 192.168.xx.xx
# xxはあなたのサーバー番号
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

初学者がRubyでクイズをつくってみた

プログラミングを学習し始めて数日の超初心者が、遊びでつくったものを投稿してみたいと思います。
どなたかの役に立つかどうかは、謎ですが、とりあえず書いてみます。

学んだこと

条件分岐

if文を使います。クイズに正解の(条件式を満たす)場合、不正解の場合と分けて別の内容を出力させられます。

ループ

while文を使います。不正解だと、延々と回答させられ続ける事ができます。

クイズつくってみた

以上のことを勉強しましたので、クイズをつくってみました。

q1.rb
while true do
  puts "The second Rome?"
  input = gets.chomp
  answer = "Constantinople"
  if input == answer then
    puts "正解です。"
    exit
  else
    puts "ばーか"
  end
end

問題文「The second Rome?」がまず出力され(putsメソッド)、次の行のgetsメソッドは実行時に入力する内容を文字列として返すので、

  1. ここで正解を入力すると「正解です」
  2. それ以外だと「ばーか」

と返されます。ここでgetsメソッドは、入力した内容の最後に改行を入れてたものを返り値とするので、改行を外すためgets.chompとします。chompメソッドがないと、何をしても「ばーか」と言われます。

数値が答えになる問題

上は文字列(Constantinople)が等しいかどうかを比較していたわけですが、数値を回答するクイズもやってみました。

q2.rb
while true do
  puts "Fall-of-Constantinople year?"
  input = gets.to_i
  answer = 1453
  if input == answer then
    puts "正解です。"
    exit
  else
    puts "ばーかばーか"
  end
end

getsメソッドは文字列を返すので、to_iメソッドで数値に直します。これがないと、やはり、自分で書いたプログラムに、何を入力しても罵倒されることになります。

わかったこと

プログラムに罵倒されると楽しい

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

初学者がRubyでクイズをつくってみた

プログラミングを学習し始めて数日の超初心者が、遊びでつくったものを投稿してみたいと思います。
どなたかの役に立つかどうかは、謎ですが、とりあえず書いてみます。

学んだこと

条件分岐

if文を使います。クイズに正解の(条件式を満たす)場合、不正解の場合と分けて別の内容を出力させられます。

ループ

while文を使います。不正解だと、延々と回答させられ続ける事ができます。

クイズつくってみた

以上のことを勉強しましたので、クイズをつくってみました。

q1.rb
while true do
  puts "The second Rome?"
  input = gets.chomp
  answer = "Constantinople"
  if input == answer then
    puts "正解です。"
    exit
  else
    puts "ばーか"
  end
end

問題文「The second Rome?」がまず出力され(putsメソッド)、次の行のgetsメソッドは実行時に入力する内容を文字列として返すので、

  1. ここで正解を入力すると「正解です」
  2. それ以外だと「ばーか」

と返されます。ここでgetsメソッドは、入力した内容の最後に改行を入れてたものを返り値とするので、改行を外すためgets.chompとします。chompメソッドがないと、何をしても「ばーか」と言われます。

数値が答えになる問題

上は文字列(Constantinople)が等しいかどうかを比較していたわけですが、数値を回答するクイズもやってみました。

q2.rb
while true do
  puts "Fall-of-Constantinople year?"
  input = gets.to_i
  answer = 1453
  if input == answer then
    puts "正解です。"
    exit
  else
    puts "ばーかばーか"
  end
end

getsメソッドは文字列を返すので、to_iメソッドで数値に直します。これがないと、やはり、自分で書いたプログラムに、何を入力しても罵倒されることになります。

わかったこと

プログラムに罵倒されると楽しい

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

任意の桁数で0詰め2進数

10進数 -> 2進数の表記変換はto_sメソッドを用いれば簡単にできるが、同時にフォーマットもしたい場合はformatを利用するのがよい。

irb(main):028:0> ("%04b" % 2)
=> "0010"

参考

http://rurema.clear-code.com/2.6.0/method/Kernel/m/format.html

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

気象庁の予報区等GISデータをベクトルタイルにしたい

気象庁の予報区等GISデータをベクトルタイルにしたいという記事の作成を HackMD で進めています。

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

プログラミング未経験者が働きながら半年でRubyをそこそこ習得した際にやったこと

はじめに

プログラミング未経験者が半年間でとりくんだことと、その結果を書きます。期間は2018年10月〜2019年4月までです。

まず最初にことわっておくと、この投稿は、「プログラミングができるようになる」ためのノウハウを紹介するものではありません。プログラミング未経験者が「プログラミング言語」を半年間学習した際に行ったこと、また、その結果を紹介します。私と同じように仕事を続けながら、スキルアップの一貫として言語の学習に取り組みたい方の励みになれば、と思います。

蛇足かもしれませんが、「プログラミングできる」と言う表現は、「Git(バージョン管理ツール)をちゃんと使えるか」といったエンジニアの基本動作的なことや、「抽象的な要望を実装に落とし込めるか」等といったことを包括している気がします。学習開始時点でここ(「プログラミングできる」)を目標に設定するのは避けました。

半年やってみた実感としては、プログラミング言語をそこそこ習得するだけなら仕事をしながら、かつ、独学1でも事足りると思います。

1.導入

1.1.初期値

以下が学習開始時の私のステータスです。

  • ネットワークエンジニア 2018年10月時点で経験2年半
  • プログラミング言語は未経験

お仕事ではプログラミングをする機会はありません。何をやっているかというと、文字通りネットワークの設計や構築、運用、保守ですね。もっと抽象化するとデータの通り道を整備している、とも言えるかと思います。技術的にはTCP/IPというものと馴染みが深いかと思います。

1.2.動機

私の場合、そもそも何故プログラミングをやろうと思ったかと言うと…

  • 適応力を高めたい

「20代の内に技術をスタックしてゆくための土台を広げておきたい」という意図があります。以下の記事が参考になりました。
ネットワークエンジニアが習得すべきプログラミングスキル
Learn to become a Backend Developer
2018年の最先端バックエンドエンジニアになろう
※2つ目の記事は翻訳している方がいらっしゃいますのでそちらもリンクを貼っています

1.3.言語の選択

言語はRubyを選択しました。選択基準は以下の通り。

  • メジャーな言語であること
  • Scriptingであること
  • 本屋で立ち読みした印象

Rubyだけを一生やっていくつもりはないので、ノリで決めています。最初からScriptingを1つ、Functionalを1つやるつもりだったので、そんなに深く考えていません。2トレンドはどんどん変わってゆくかと思うので、メジャーであれば何でもよいと思います。

2.目標を立てる

ここがとっても大事です。冒頭でも申し上げましたが、個人的には『プログラミングできるようになる』は止めるべきだと思います。そもそも、どういう状態を持って『プログラミングできる』とみなすかを初心者が定義することは難しいかと思います3。なんだか、長期戦になりそうな気配が漂っています。そういった抽象度の高い目標は挫折しやすいかと思いますのでやめておきましょう。ありがたいことにRubyには資格がありますので、この取得を目標にしました。

2.1.Rubyの資格を取る

Rubyの資格にはSilverとGoldがあるようです。この2つの取得を目標に設定します。

予定
  10月:学習開始
  11月:Silver取得
  1月 :Gold取得
結果
  10月:学習開始
  11月:Silver取得
  1月 :Gold落ちる
  4月 :Gold取得

3.Rubyの資格 Silver取得

3.1.学習方法

Silverはそんなに難しくないと思います。基本的には通勤時間を充てました。だいたい下記のような順番で学習しました。

一通り学習したら、教科書の問題を再度解き、全問正解した時点で受験に臨みます。

3.2.結果

2018年11月に8割で合格しました。
教科書に掲載されているメソッドの使い方や、出力結果をしっかり覚えておけば難しい試験ではないかと思います。Silverに関しては公式教科書が優秀です。

4.Rubyの資格 Gold取得

4.1.学習方法

Silverの合格で味をしめたので、全く同じ方法で挑みます。

4.2.結果

2019年の1月末 落ちる。結果は66/100点 合格ボーダーに9点足りず…

4.3.敗因

メソッド探索について理解不足でした。自信が無い判断を下したところは、クラス定義とメソッド探索に関わる問題だったと思います。

4.4.リトライ

ここで先人の声に耳を傾けます。どうやらメタプログラミング Rubyという本が効くようです。

早速、メタプログラミングRubyを購入し、2~5章を繰り返し読んでゆきました。Goldの核心はクラス定義やメソッド探索の理解だと思います。2章と5章を重点的に学習しましょう。特に5章の以下の記述とそれを図式化したものはRubyのクラス定義やメソッド探索の理解に大変役立ちました。

オブジェクトの特異クラスのスーパークラスは、オブジェクトのクラスである。クラスの特異クラスのスーパークラスはクラスのスーパークラスの特異クラスである。5

また、理解できないものはirbで試してみます。実はSilverのときは環境構築だけして手を動かしていませんでした。Silverの資格をとるだけなら所謂写経は必須ではないかと思います。私の場合は怠惰もあり、写経はあまりやりませんでした。時間がある方は写経をした方が定着しますし良いと思います。メタプログラミング Rubyを3周ほどしたら、再度教科書の問題を解き理解度を確認。再受験します。

4.5.結果

2019年4月初旬に8割で合格しました。メタプログラミングRubyはとっても効果的です。

4.6.資格をとった所感

この資格は多分良い資格だと思います。クラスやモジュールの関係性がクリアに捉えられるようになり、APIドキュメントやGemのソースコードを見た際に、読解力が上がっていることを実感できます。半年前では考えられませんでした。半年間、資格を取ることを目標に勉強をしてきましたが、「プログラミング言語」の学習の成果としてはまぁまぁなものだと言えるかと思います。

5.まとめ

以上、プログラミング言語の学習を半年間続けてきました。半年間、働きながら、特別な学習環境6を用意しなくてもプログラミング言語をそこそこ習得することは可能でした。おそらく、エンジニアである以上、「ここまで勉強したら終わり。ほかの技術は勉強しない」という決断を下すことはできないでしょう。絶えず何かしらの技術にキャッチアップしてゆく必要があるかと思います。

ありがたいことに、ドットインストールやProgate、Udmeyなどのサービスが用意されており、そこを入り口にすることができます。ググればいたるところに自分の欲しい情報が存在します。素晴らしいですね。やらない理由がない、ということをこの度は感じました。

PS:「資格持ってるだけ」はなんかかっこ悪いので、GWの10連休にRailsでブログを作りました。デプロイ大変だった。


  1. これは推測ですが、プログラミングを独学って結構当たり前な気がします 

  2. Functionalな言語でオススメがあれば是非、ご教示ください 

  3. そもそも経験者でも一様の答えが出る問いでは無い気がします 

  4. Edtechは本当に素晴らしい。私は現在UdemyのRailsコースに取り組んでいますが、倍速再生機能、講師へのQA機能、コースの多さがとってもよいですね。Udmeyをやる場合、英語に強ければ、英語のコースも選択肢にいれると良いと思います。選択肢が圧倒的にひろがります。 

  5. Paolo Perrotta(著), 角 征典(訳) (2015)『メタプログラミングRuby 第2版』p.131, オライリージャパン. 

  6. 所謂プログラミングスクールは学習内容がパッケージ化されており、素晴らしいですね 

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