20191211のRailsに関する記事は26件です。

Rails新規アプリケーション作成(haml版)①大枠作り

書いてあること

1.新規アプリ作成(バージョン指定をしてrails newをする)
2.Git管理下に置いてリモートリポジトリへ
・.gitignoreについて
3.haml実装
4.コントローラー・ビュー・ルーティングの作成
5.Sassの導入(リセットcss"YUI3"導入)
6.ビューをマークアップ(大枠作成)
7.参考ページ
・参考ソース
8.終わりに

1.新規アプリ作成

Ruby on Railsのバージョンを変更

ターミナル
$ cd ~  # ホームディレクトリに移動
$ gem install rails --version="5.0.7.2"
#バージョン指定
ターミナル
$ rbenv rehash

バージョン指定して新規作成

cdでアプリを作りたいディレクトリを指定して移動

ターミナル
$ rails _5.2.3_ new 新規アプリ名 -d mysql

cdで今作ったディレクトリへ移動

ターミナル
$ rails db:create

2.Git管理下に置いてリモートリポジトリへ

【参照】
アプリ作成〜SNSグループ作成と連携

  • gitの管理下におく
ターミナル
/git管理下に置きたいディレクトリへcdで移動し下記実行/
$git init 
$git add .   /.は全部という意味/
$git status /ちゃんと移動したか確認/
  • ローカルリポジトリに追加(コミットするよ)
ターミナル
$git commit -m "initial commit"
  • GitHubDesktopの[CurrentRepository]でaddする image.png

image.png

次の画面はこれ
image.png

image.png

リモートリポジトリ作成完了!

.gitignore
コミットしたくないファイル・ディレクトリを指定できる設定ファイル。画像投稿機能などがあるものは画像が保管されるpublic/uploadsディレクトリを.gitignoreに追加しておくと、画像がGithubにコミットされない

.gitignore
# 末尾に次の記述を追加
public/uploads/*

3.haml実装

・gem導入

Gemfile
gem "haml-rails", ">= 1.0", '<= 2.0.1'
# 最下部に記載
ターミナル
$ bundle

導入完了

・hamlに変換

ターミナル
$ rails haml:erb2haml
途中でターミナルに出てきたら
Would you like to delete the original .erb files? (This is not recommended unless you are under version control.) (y/n)
#yを入力してエンター

hamlへ変換完了

4.コントローラー・ビュー・ルーティングの作成

rails gの前にやっておくこと
不要なファイルを生成しないように記述しておく

config/application.rb
# 省略
(例)
module ChatSpace
  class Application < Rails::Application
    config.generators do |g|
      g.stylesheets false #←このfalseで作成しないファイルを指定できます
      g.javascripts false
      g.helper false
      g.test_framework false
    end
  end
end

コントローラ作成

ターミナル
$ rails g controller コントローラ名(例:popcorn)

コントローラにindexアクション作るよ

popcorn_controller
def index
end

app/views/コントローラ名と同じ名前のディレクトリ/index.html.hamlを作成

(例)app/views/popcorn/index.html.haml
hello haml!(おすきに入力)

ルーティングを設定(root_pathにアクセスした場合indexに飛ぶ設定)

routes.rb
root to: 'popcorn#index'
#'コントローラ#indexのビューへ飛んでね'の意味

5.Sassの導入(リセットcss"YUI3"導入)

application.cssは削除し、application.scssを作成

application.scss
@import "scssファイル名";
#importしたいファイル名の頭は_(アンダーバー)始まり。

_reset.scssをapplication.scssと同じディレクトリ内に作成

YUI3導入
YUI3
Source code(zip)をダウンロードしてファイルを開き全てコピーして_reset.scssに貼り付け

application.scss
@import "reset";

6.ビューをマークアップ(大枠作成)

紙にボックスの配置を書いて命名する(後でどこに何の名前のクラスを配置したか見返しやすく、sassを当てたりイベント発火させるクラスがわかりやすくなって便利)

7.参考ページ

参考ソース

_reset.scss
/*
    TODO will need to remove settings on HTML since we can't namespace it.
    TODO with the prefix, should I group by selector or property for weight savings?
*/
html{
    color:#000;
    background:#FFF;
}
/*
    TODO remove settings on BODY since we can't namespace it.
*/
/*
    TODO test putting a class on HEAD.
        - Fails on FF.
*/
body,
div,
dl,
dt,
dd,
ul,
ol,
li,
h1,
h2,
h3,
h4,
h5,
h6,
pre,
code,
form,
fieldset,
legend,
input,
textarea,
p,
blockquote,
th,
td {
    margin:0;
    padding:0;
}
table {
    border-collapse:collapse;
    border-spacing:0;
}
fieldset,
img {
    border:0;
}
/*
    TODO think about hanlding inheritence differently, maybe letting IE6 fail a bit...
*/
address,
caption,
cite,
code,
dfn,
em,
strong,
th,
var {
    font-style:normal;
    font-weight:normal;
}

ol,
ul {
    list-style:none;
}

caption,
th {
    text-align:left;
}
h1,
h2,
h3,
h4,
h5,
h6 {
    font-size:100%;
    font-weight:normal;
}
q:before,
q:after {
    content:'';
}
abbr,
acronym {
    border:0;
    font-variant:normal;
}
/* to preserve line-height and selector appearance */
sup {
    vertical-align:text-top;
}
sub {
    vertical-align:text-bottom;
}
input,
textarea,
select {
    font-family:inherit;
    font-size:inherit;
    font-weight:inherit;
    *font-size:100%; /*to enable resizing for IE*/
}
/*because legend doesn't inherit in IE */
legend {
    color:#000;
}

8.終わりに

①ってタイトルつけたけど果たして②はできるのかな・・・?(②にはページ遷移をまとめたいが。)
MVCの流れを追いつつ変数(インスタンス変数)の定義の仕方や記述の仕方が甘いので、そこをまとめたいと思うのですが、整理しながらだと思ったようにかけないですね(あれもこれも詰め込みたくなるので)
今週の目標はメッセージ送信機能実装かインスタンス変数を定義して7つのアクションごとにページ遷移の記述方法がかけたらと思います。jsもスクレイピングも復習したい。。。

上手な頭の中身の整理方法があったらぜひ教えてくださいませφ(・

記述ミスや説明不備がございましたらご指摘いただければ幸いです。

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

コスモを感じた初めてのRSpec

自己紹介

さっそくだが、君は、小宇宙(コスモ)を感じたことがあるか!?

はじめまして。営業からRuby on RailsのWebエンジニアになったたっきー(半年)と言います。
僕は文系出身で開発は全くの未経験ですが、一念発起してJOBチェンジした者です。
とても優しく指導して下さっている先輩たちのもと、コツコツと勉強を進めているのですが、やはり分からない事も多く、先輩たちの時間を頂くことが多い恐縮な日々を過ごしています。。

とりあえずRubyを触って半年経ちました。
formの動きとか基礎的なものはようやく動かせるようになりましたが、まだまだ半人前。
そんなちょっとやそっと触っただけで、できる甘い世界ではないですね。

家はシェアハウスに住んでいて住人達から黒魔術ってタイトルのQiita記事をネタにされてブラックマジシャンって呼ばれてます。
今回は、会社で自分の書いたコードのテストを書いてねって言われた10月を振り返ります。

スキル

HTMLとCSSは最低限作りたいイメージのものは作れるようになった。
JSはまだまだテンプレートとか調べながらじゃないと、全然できません。
rubyは分岐やら正規表現やら初歩中の初歩は理解したけど、綺麗には書いていない(らしい)
railsはチュートリアル二周半回って、イメージはつくけどまだお手本ないと辛い感じ。

テスト元のコントローラー

細かくは書けないですが、ユーザーに設定してもらうユーザーに設定してもらうparamsを削除・作成・編集(RENAME)するものを作りました。
アクティブレコードで対象のモデルからデータを引っ張ってきて、idで合わせている書き方をしています。

文法

作法は難しい。
これまで書いていたRails的なdef...endみたいな単純な書き方ではないからだ。
それでも任されたからに書き始めないといけない・・・

STEP1 RSpecのファイルを作る。該当のcontrollerの名前に合致するようにspecファイルを作成してください。
$ touchでコマンドで書いてもよし、該当フォルダで右クリックしてファイルを作ってもよし、まあ良しなにやってくれ。

STEP2 ファイルの中身をまずはdescribeを書いてく

例えば、GET関連のテストを書きたい時には一番最初にこのように書くんだ。

categories_controller_spec.rb
describe'⚪︎◯#show'do
  コメント
  it "●●" do
    expect(××).to eq □□
  end
end

※実際にどのように書いていくかは伊藤さんの記事がとても丁寧に紹介されているよ。
https://qiita.com/jnchito/items/42193d066bd61c740612

RSpecという小宇宙

テストを書く意義はテストにアサインされる身になるとよくわかる。
何十何百ってパターンを回さないといけないようなテストを人力なんかでやっちゃいけない。
勿論、必要なケースもあるだろうが、あまり生産性を感じない。

その点自動テストは、勝手にやってくれる。ロジックに誤りがないならヒューマンエラーも起きない。
ヒューマンエラーってのは最悪だ。時間を使って、無駄なことをしている。
だから、効率化効率化なんていろんなところで言われているんだ。

自分は自動テストというものに途轍もない「小宇宙(コスモ)」を感じてしまった。

勉強法

書籍
https://leanpub.com/everydayrailsrspec-jp
おすすめ記事
https://qiita.com/jnchito/items/42193d066bd61c740612
実践
http://tech-drill.in/questions/12

最後に

割愛しすぎた部分もあるので、今後はもう少しhogeとか使って、イメージしやすい記事にします!

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

Railsでのhaml実装

書いてあること

1.haml-railsを用いた実装

  • gem導入
  • hamlに変換

2.参考ページ
3.終わりに

1.haml-railsを用いた実装

・gem導入

Gemfile
gem "haml-rails", ">= 1.0", '<= 2.0.1'
# 最下部に記載
ターミナル
$ bundle

導入完了

・hamlに変換

ターミナル
$ rails haml:erb2haml
途中でターミナルに出てきたら
Would you like to delete the original .erb files? (This is not recommended unless you are under version control.) (y/n)
#yを入力してエンター

hamlへ変換完了

2.参考ページ

3.終わりに

hamlはhtmlよりもコードの記述が少し減るので、かきやすいですがAWSにアップロードする際は脆弱性の問題もあるので対策をしないと怖いですね(私まだしてないけど。)
対策についてもまとめたらUPします。
記述等に誤りがございましたらご指摘いただければ幸いです。

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

Ruby on Rails、ハマったところ

Ruby on Railsを使っている中でハマったポイントを適宜追加して行きます。

・gemparanoiaを利用中、application_record.rbacts_as_paranoidと記述してしまい、論理削除を必要としないテーブルで
UnknownAttributeError: unknown attribute 'deleted_at' for Bill.と怒られまくってしまった。

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

Ruby 価値が大きくなる組み合わせ問題 解いてみた

はじめに

毎月先輩から出していただいた課題に取り組んでいます、 mi0です。
11月は 価値が大きくなる組み合わせ問題、いわゆる最適化問題を解きました。
この記事は解く〜レビューをいただくまでの過程を纏めた備忘録です。
こうやったらもっとよくなる、などのご指摘があればコメント頂けると嬉しいです!

過去の記事はこちら!↓

登場人物

    • 社会人2年目PG。3年目が最近の恐怖ワード。最近異様に#selectを乱用している気がして不安になってきた。
  • 出汁巻先輩
    • オムレツちゃんの先輩。大根おろしはほどほどにしてほしいと思っている。コーディングが得意。
  • オムレツちゃん
    • 私の心の中に住んでいる妖精。ケチャップはしっかりかけて欲しいらしい。

出題された問題

価値が大きくなる組み合わせ問題

問題

  • 価格と重量のある、いくつかの品物を重量制限のある入れ物で運ぶ際、価格の合計が大きくなる組み合わせを出力する

メニューの元データ

  • db/data.csvから取得

要求仕様

  • 詰め込む入れ物の重量制限は指定できる
  • 品物は 1 つずつ詰め込むことができる
    • 同じ商品を複数詰め込むことができない
  • 提案する組み合わせ数は指定できる
    • 組み合わせは価格が大きくなる順に並び替える
    • 価格が同じ場合は、重量の重い順に並び替える
    • 組み合わせる品物はidの昇順
  • 引数値の検証は不要
    • 0 <= 入れ物の制限 <= 13
    • 0 <= 提案する組み合わせ数 <= 10
  • 使用するデータの価格と重量の組み合わせはユニーク
    • 例) 価格: 2、重量: 3 の商品は 1 つのみで複数あることは考慮しないものとする
  • 1 つも品物を詰め込まない組み合わせは除外すること
  • 処理が複雑になる場合は、コメントや gem を追加しても良い

メソッドの返す値の形式

  • 例)下記の形式(答えでは無い)

  • Array

    • Hash
    • key: Symbol
    • value:
      • total_price: 合計価格
      • total_weight: 合計重量
      • records: Array
      • Hash: CSV の1レコード(一部型変換)
        • key: Symbol
        • value:
        • id: Integer
        • price: Integer
        • weight: Integer
[
  { records: [{ id: 1, price: 1, weight: 1 },
              { id: 2, price: 2, weight: 2 },
              { id: 3, price: 3, weight: 3 }],
    total_price: 10,
    total_weight: 10 },
  { records: [{ id: 1, price: 1, weight: 1 },
              { id: 2, price: 2, weight: 2 }],
    total_price: 9,
    total_weight: 9 },
  { records: [{ id: 1, price: 1, weight: 1 }],
    total_price: 8,
    total_weight: 8 }
]

※CSVの中身は以下。

data.csv
id,price,weight
1,2,3
2,3,4
3,2,1
4,3,2
5,6,3

フェーズ1 自分で考える

私「CSVかぁ…………(バッチバチのRubyでCSVいじったこと無くないか?って顔)」

オムレツちゃん「大丈夫、それくらいならチョチョイよ!それにしても出汁巻先輩、なかなかの問題を出してきたわね……!」

私「そーなの?何となく出来そうな気がしているんだけども……組み合わせを作っていくんでしょ?その組み合わせを作るのも、パターンだからメソッド使ったらうまいこと出来るんじゃないかなあ(無知)」

オムレツちゃん「(大丈夫かしらこいつ……)」

私「とりあえず全然イメージ湧かないから具体的に組み合わせを作ってみよう!」

私「とりあえず価値が大きいものからぶち込んでいけばその入れ物の中の価値は最大になるんじゃないかな?同じ価値の場合は重量が軽いものを優先的に入れる。と、いい感じに組み合わせが作れるんじゃないでしょうか!」

私「実際のデータを使って纏めてみよう!」

IMG_0165.jpg

IMG_0166.jpg

IMG_0167.jpg

私「ふんふん、具体的な組み合わせがあると考えやすいな〜。これでテストデータも出来たようなものだし、ガシガシ書いて行こう」

私「やることとしてはこんな感じかな〜」

  • ソートされたデータを作る
  • 袋の容量よりも大きいものは除いておく
  • 大きいものから入れられるだけ袋に詰める
  • 詰めたものの中で可能な組み合わせを作る
  • 組み合わせを元に重量と価値を計算する

実際に解いていく

私「よし!かくぞ!」

私「出汁巻先輩から事前にいただいたコードは……と。」

require 'csv'

class Suggest
  def initialize(limit, combinations)
    @limit = limit
    @combinations = combinations
    @data = CSV.read('db/data.csv', headers: true)
  end

  def result
    []
  end
end

私「なるほどなるほど……CSVの取り込みのところだけ少しいじろうかな。ヘッダーの情報とかいらないもんな」

require 'csv'

class Suggest
  def initialize(limit, combinations)
    @limit = limit
    @combinations = combinations
    @data = CSV.read('db/data.csv', headers: true)
  end

  def result
    sorted_data.map { |data| create_suggest(data) }[0..(@combinations - 1)]
  end
end

私「ついでにresultメソッドの理想形も書いておいたぞ!ソートしたデータを使ってなんかメソッド呼んだらいい感じの配列ができて、そこから指定範囲分取り出す!いい感じじゃない!?」

私「もう少し具体的にcreate_suggestを書いておこう………」

require 'csv'

class Suggest
  def initialize(limit, combinations)
    @limit = limit
    @combinations = combinations
    @data = CSV.read('db/data.csv', headers: true)
  end

  def result
    sorted_data.map { |data| create_suggest(data) }[0..(@combinations - 1)]
  end

  private

  def create_suggest
    create_records.map do |result|
      {
        result: result,
        total_price: result.sum { |item| item['price'].to_i }
        total_weight: result.sum { |item| item['weight'].to_i }
      }
  end
end

私「組み合わせを作ってくれるcreate_resultメソッドを呼んだ後に欲しい形にデータを整形してくれるメソッド!うんうん、大まかn

私「よしよし……次はデータをソートしよう!」

require 'csv'

class Suggest
  def initialize(limit, combinations)
    @limit = limit
    @combinations = combinations
    @data = CSV.read('db/data.csv', headers: true)
  end

# 中略

  private

  # 中略

  def sorted_data
    @data.sort_by { |data| data['price'].to_i }.reverse.map do |data|
      { id: data['id'].to_i, price: data['price'].to_i, weight: data['weight'].to_i }
    end
  end
end

私「まずsort_byで価値順に並び替える。sort_byだと価値が低い順に並んじゃうからreverseメソッドで価値が高い順に並び替える。その上でそれぞれの値を文字列から数値に置換しておくよ。以降の処理では値のことを気にしなくてよくなるもんね!」

sort_byのブロック内のdata['price'].to_iを負の数にしてあげるとreverseが不要になることに後日気付きましたが、直感的じゃないような気も……。メソッド呼び出しが少なくなるから-(data['price'].to_i )みたいに書いてあげるのがいいんでしょうか……:thinking:

私「次はそれぞれの組み合わせを作ってくれるメソッドを作ろう!」

require 'csv'

class Suggest
  def initialize(limit, combinations)
    @limit = limit
    @combinations = combinations
    @data = CSV.read('db/data.csv', headers: true)
  end

# 中略

  private

  def sorted_data
    @data.sort_by { |data| data['price'].to_i }.reverse.map do |data|
      { id: data['id'].to_i, price: data['price'].to_i, weight: data['weight'].to_i }
    end
  end

  def create_records
    sorted_data.each_with_object([]) do |record, array|
      next if @limit < record[:weight]
    end
  end
end

私「とりあえずソートしたデータを使って配列を作るからeach_with_objectを使うよ〜!そんでもって袋の重量を超過している品物は最初から省いてしまう処理を書いておく。」

私「次は袋に入るだけ詰め込んでいくよ〜!」

私「サンタクロースみたいで楽しいね!(??????)」

require 'csv'

class Suggest
  def initialize(limit, combinations)
    @limit = limit
    @combinations = combinations
    @data = CSV.read('db/data.csv', headers: true)
  end

# 中略

  private

  def sorted_data
    @data.sort_by { |data| data['price'].to_i }.reverse.map do |data|
      { id: data['id'].to_i, price: data['price'].to_i, weight: data['weight'].to_i }
    end
  end

  def create_records
    sorted_data.each_with_object([]) do |record, array|
      next if @limit < record[:weight]

      array << create_records_pattarn_array
    end
  end

  def create_records_pattarn_array
    sorted_data.each_with_object([]) do |target_item, result|
      result << target_item if result.sum { |r| r[:weight] } + target_item[:weight] <= @limit
    end
  end
end

私「今ある品物の中で一番価値の高い組み合わせを作る。それを最終的な組み合わせを格納するarrayに入れるよ。」

私「で、この最大の組み合わせから作ることのできる全パターンを作ればいいんだけど……。」

私「ど………………………………………………………………。」

私「これ組み合わせの個数バラバラじゃんね……………………?」

私「combination使えないじゃんね……………………?」

オム「気付くのおそ………………」

require 'csv'

class Suggest

  # 中略

  private

  # 中略

  def create_records
    sorted_data.each_with_object([]) do |record, array|
      next if @limit < record[:weight]

      pattarns = create_records_pattarn_array
      array << pattarns
      [pattarns.first].product(pattarns - [pattarns.first]).each { |p| array << p } if pattarns.size > 2
    end
  end

  # 略

end

私「3つ入っている時は、6つの組み合わせが作れるように、3つ以上品物が入っている時はそれぞれの組み合わせを作れるはず。」

私「1回のループでその組み合わせは作りきれないから、ひとまず今できている組み合わせの、一番最初に入っているものベースで作れる組み合わせを全部作る。」

私「create_recordsを全部回し切ったら全パターン作れてるはず!」

オム「それだと被りが出ちゃうんじゃない?」

私「う、うーん……」

私「uniqで……。」

オム「ダメじゃん……………。」

私「これだと処理がめちゃ遅なのは分かるんだけど……今の私ではもう……これくらいしか思い浮かばない……。」

〜〜その後、create_recordsをベースにデータを整え返すような実装に整えた結果が以下〜〜

require 'csv'

class Suggest
  def initialize(limit, combinations)
    @limit = limit
    @combinations = combinations
    @data = CSV.read('db/data.csv', headers: true)
  end

  def result
    create_suggests.sort_by { |suggest| suggest.values_at(:total_price, :total_weight) }.reverse[0..(@combinations - 1)]
  end

  private

  def sorted_data
    @data.sort_by { |data| data['price'].to_i }.reverse.map do |data|
      { id: data['id'].to_i, price: data['price'].to_i, weight: data['weight'].to_i }
    end
  end

  def create_suggests
    records_array = create_records
    return [{ records: [], total_price: 0, total_weight: 0 }] if records_array.empty?

    records_array.map do |records|
      {
        records: records.sort_by { |record| record[:id] },
        total_price: records.sum { |record| record[:price] },
        total_weight: records.sum { |record| record[:weight] }
      }
    end.uniq
  end

  def create_records
    sorted_data.each_with_object([]) do |record, array|
      next if @limit < record[:weight]

      pattarns = create_records_pattarn_array
      array << pattarns
      [pattarns.first].product(pattarns - [pattarns.first]).each { |p| array << p } if pattarns.size > 2
    end
  end

  def create_records_pattarn_array
    sorted_data.each_with_object([]) do |target_item, result|
      result << target_item if result.sum { |r| r[:weight] } + target_item[:weight] <= @limit
    end
  end
end

フェーズ2 レビューをいただく

 
出汁巻「じゃあレビューしていこう」

私「お願いします!」

出汁巻「まず私さんのコードなんだけど」

私「はい」

出汁巻「combinationsに0を渡すと組み合わせが全パターン取得できちゃうんだよね」

私「そんな…………」

出汁巻「これをよく見て」

reverse[0..(@combinations - 1)]

私「…………」

私「アッ!!!!!!!!!」

私「うそ……無理…ダメすぎる……これはダメ……[0..(0-1)][0..-1]で全部じゃん……ちょっとこれは恥ずかしすぎました……[0...@combinations]ですね…………」

出汁巻「そうだね。[0, @combinations]とも書ける。」

私「はい………」

出汁巻「次!#sorted_dataの中でデータをto_iしてたけど、せっかくなら#initializeの中でやった方がより良かったかな。sorted_dataの時点で同じキーの値にto_iを二回当ててたけど、そういうのがなくなる。」

私「あ……確かに。」

出汁巻「それから、以下みたいなコードは早期nextした方が可読性が上がるよ」

[pattarns.first].product(pattarns - [pattarns.first]).each { |p| array << p } if pattarns.size > 2



next if pattarns.size <= 2

[pattarns.first].product(pattarns - [pattarns.first]).each { |p| array << p }

出汁巻「最後になんだけど……私ちゃんの処理、sorted_dataの個数分同じパターンを作ってたよ」

私「えっ」

出汁巻「以下のメソッド見て。」

def create_records
  sorted_data.each_with_object([]) do |record, array|
    next if @limit < record[:weight]

    pattarns = create_records_pattarn_array
    array << pattarns
    [pattarns.first].product(pattarns - [pattarns.first]).each { |p| array << p } if pattarns.size > 2
  end
end

def create_records_pattarn_array
  sorted_data.each_with_object([]) do |target_item, result|
    result << target_item if result.sum { |r| r[:weight] } + target_item[:weight] <= @limit
  end
end

出汁巻「sorted_data起点でやってるから、何回繰り返しても処理が走っちゃって、同じ組み合わせが出来ちゃうよね。これ、本当ならcreate_records_pattarn_arrayrecordを渡すんだったんじゃない?」

私「…………………………………………(死)」

出汁巻「それから、重複する組み合わせが発生してるってことはその分余計なループが回ってるってことだから、その分処理も遅くなるよね」

私「はひ…………。」

出汁巻「この記事とかを参考にしたら、基本の考え方は分かり易かったと思う。私ちゃんは力技でやって一番いいパターンは正解するんだからすげーよ。」

私「あは……(頑張って具体例を出して考えたことが唯一活かされた瞬間)」

※以下、先輩の解答例コード

require 'csv'

#
# 組み合わせを提案するクラス
#
class Suggest
  #
  # 提案クラスインスタンス作成
  #
  # @param [Integer] limit 重量の上限
  # @param [Integer] combinations 組み合わせの要求提案数
  #
  # @return [Object] インスタンス
  #
  def initialize(limit, combinations)
    @limit = limit
    @combinations = combinations
    @data = CSV.read('db/data.csv', headers: true).map do |row|
      {
        id: row['id'].to_i,
        price: row['price'].to_i,
        weight: row['weight'].to_i
      }
    end
  end

  #
  # 結果を取得
  #
  # @return [Array<Hash>] 価値、重量の高い順の組み合わせ
  # @example
  #   Example return output:
  #
  #     [{:records=>
  #        [{:id=>2, :price=>3, :weight=>4},
  #         {:id=>3, :price=>2, :weight=>1},
  #         {:id=>4, :price=>3, :weight=>2},
  #         {:id=>5, :price=>6, :weight=>3}],
  #       :total_price=>14,
  #       :total_weight=>10},
  #      {:records=>
  #        [{:id=>1, :price=>2, :weight=>3},
  #         {:id=>3, :price=>2, :weight=>1},
  #         {:id=>4, :price=>3, :weight=>2},
  #         {:id=>5, :price=>6, :weight=>3}],
  #       :total_price=>13,
  #       :total_weight=>9},
  #      {:records=>
  #        [{:id=>1, :price=>2, :weight=>3},
  #         {:id=>4, :price=>3, :weight=>2},
  #         {:id=>5, :price=>6, :weight=>3}],
  #       :total_price=>11,
  #       :total_weight=>8}]
  #
  def result
    r = calculation.sort do |a, b|
      (b[:total_price] <=> a[:total_price]).nonzero? || b[:total_weight] <=> a[:total_weight]
    end

    r[0, @combinations]
  end

  #
  # 組み合わせの算出
  #
  # @return [Array<Hash>] 価値、重量、データ毎にまとめた組み合わせ
  # @example
  #   Example return output:
  #
  #     [{:total_price=>2,
  #       :total_weight=>1,
  #       :records=>[{:id=>3, :price=>2, :weight=>1}]},
  #      {:total_price=>3,
  #       :total_weight=>2,
  #       :records=>[{:id=>4, :price=>3, :weight=>2}]},
  #      {:total_price=>6,
  #       :total_weight=>3,
  #       :records=>[{:id=>5, :price=>6, :weight=>3}]}]
  #
  def calculation
    aggregate.inject([]) do |array, data|
      next array if data[:records].size.zero?

      array << {
        total_price: data[:value],
        total_weight: data[:records].map { |record| record[:weight] }.sum,
        records: data[:records].sort_by { |record| record[:id] }
      }
    end
  end

  #
  # 価値毎に集計した組み合わせ
  #
  # @return [Array<Hash>] 価値、重量、データ毎にまとめた組み合わせ
  # @example
  #   Example return output:
  #
  #     [{:value=>0, :records=>[]},
  #      {:value=>2, :records=>[{:id=>3, :price=>2, :weight=>1}]},
  #      {:value=>3, :records=>[{:id=>4, :price=>3, :weight=>2}]},
  #      {:value=>6, :records=>[{:id=>5, :price=>6, :weight=>3}]}]
  #
  def aggregate
    array = work_array

    @data.each do |data|
      (0..@limit).to_a.reverse.each do |i|
        next if array[i][:value].nil? || (i + data[:weight]) > @limit

        array[i + data[:weight]] = format_value(array[i], data)
      end
    end

    array
  end

  #
  # 集計時に使用する作業領域
  #
  # @return [Array<Hash>] 初期化された作業領域(重量の上限数 + 1)
  # @example
  #   Example return output:
  #
  #     [{:value=>0, :records=>[]},
  #      {:value=>nil, :records=>[]},
  #      {:value=>nil, :records=>[]}]
  #
  def work_array
    array = Array.new(@limit + 1) { { value: nil, records: [] } }
    array[0][:value] = 0
    array
  end

  #
  # 集計時に
  #
  # @param [Hash] value 加算先のHashオブジェクト
  # @example
  #   Example value output:
  #
  #     {:value=>0, :records=>[]}
  # @param [Hash] data 加算元のHashオブジェクト
  # @example
  #   Example data output:
  #
  #     {:id=>1, :price=>2, :weight=>3}
  #
  # @return [Hash] 初期化された作業領域(重量の上限数 + 1)
  # @example
  #   Example return output:
  #
  #     {:value=>2, :records=>[{:id=>1, :price=>2, :weight=>3}]}
  #
  def format_value(value, data)
    {
      value: value[:value] + data[:price],
      records: value[:records] + [data]
    }
  end
end

最後に

今回は初歩的なミスが目立ったのが良くなかったです。自力でやったが故に、迷走に迷走を重ねてしまったのも良くなかったです。もう少し既に存在する知識を調べて活用しなければ……と痛感しました。最適化問題、難しいですね……。考え方を理解した上で、有事の時に活かせるようにしたいです。

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

[初歩的ミス]データベース作成でつまづいた時の話ー原因から解決まで

個人アプリを作成にあたり、DBを作成していたときにつまづいた話を以下に記載します。
カリキュラムなど指示通りにしかやっていなかったりすると、中々気づけないポイントだったと思います。

何につまづいたのか+試してみたこと。

そもそも何につまづいたのか、以下に記載します。
個人アプリでDBを準備しようといつも通り下記コマンドを実行して、DB作成をしました。

terminal
$ rails db:create

$ rails db:migrate

その後、DBが作成できているか「Sequel Pro」を確認したところ、できていないことが判明。。。
念のため、ターミナルでもmysqlを確認しましたが、見つかりませんでした。

そこで、何かコマンドミスがあったことを疑い、改めてDB作成コマンドを実行しました。

terminal
$ rails db:create
Database 'db/development.sqlite3' already exists
Database 'db/test.sqlite3' already exists

既にDBがあるよ!とのこと。でもやっぱり見つかりません。。。
ここで気づかないのが、初学者な私。まだまだでした。
その後、いろいろ調べた結果、原因が判明します。

原因

まずは結論から、今回の問題の原因は、Database.ymlの記述とSequel proの設定が違うことでした!

以下、細かく記載していきます。
そもそもですが、$rails db:createは[Database.yml]の記述をベースにDBを作成します。
そこで、今まで私が作成していた記述と今回の記述を確認しました。

database.yml
#今までのファイル
default: &default
  adapter: mysql2

#今回のファイル
default: &default
  adapter: sqlite3

上記は一部抜粋ですが、adapterが違ってます!
Sequel proはmysqlのデータを引っ張ってきているので、いくらDBを作成しても出てくるわけないですね。。。
これで原因はわかりました!早速修正していきます。

解決方法

まずはdatabase.ymlを今までのものをベースに書き換えます。

database.yml
default: &default
  adapter: mysql2

まだこのままでは終わりません!
adapterを変更するにはgemも変更する必要があります。

Gemfile
gem 'sqlite3'
 #以下に変更
gem 'mysql2', '>= ver記載

bundle installも忘れずに行いましょう。

terminal
$bundle install

以上で変更は完了です!
改めてDB作成コマンド実行し確認したところ、Sequel proでも確認できました!

やはり自分で作成してみると、新たな気づきがありますね!
引き続き色々試しながらアプリ作成をがんばっていきたいと思います!

以上となります。最後までご覧いただき、ありがとうございました!
今後も学習した事項に関してQiitaに投稿していきますので、よろしくお願いします!
記述に何か誤りなどございましたら、お手数ですが、ご連絡いただけますと幸いです。

参照

[初学者]既存アプリのDBをMySQLに変更する方法
https://qiita.com/shi-ma-da/items/caac6a0b40bbaddd9a6f

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

(雑記)bundle install時のmysqlのエラー対応とか参考にしたコマンドとか

bundle install時のmysqlのエラーに始まり、深い沼にはまりました。というか現在も沼の中にいます。
並行してosアップデートしたせいでなんかえらいことになっています。
いろいろとコマンドいじりすぎて訳がわからなくなったので確認が取れる範囲で覚え書きをば。

書いていて落とし処がわからなくなってきたので、また整理するか、要点だけ改めてまとめるかもしれません。

エラー内容

ターミナル
Gem::Ext::BuildError: ERROR: Failed to build gem native extension.

(中略)

Make sure that `gem install mysql2 -v '0.5.2' --source 'https://rubygems.org/'` succeeds before bundling.

やったこと

まず指摘されたコマンド打ってみた → 解決せず

gem install mysql2 -v '0.5.2' --source 'https://rubygems.org/'

バージョン変更で解決するよ!(バージョンが違うよ!)って記事をいくつかみたので実行(下記のバージョン記述変えたり) → 解決せず

Gemfile
gem 'mysql2', '>= 0.4.4'

このへんで最初のターミナルの中に

ターミナル
You have to install development tools first.

って書いているのに気づいた。なんだ、これで調べれば解決じゃん! → 解決せず。

ちなみに 「 xcode-select --install 」ってコマンドが調べれば出てきます。
これで解決してる人いっぱいいるのに...。
そもそもxcodeって触り始めたくらいの時に入れてるっぽいので入ってるよ!って思いましたが、os更新した後とか、このコマンド入れると解決したりするみたいです。OS更新した時とかに出番ありそうなのでメモ。

 とっても役にたったコマンド

こちらも自分の覚え書きですが。

brew doctor

brew doctorすると、brewの設定とかで不具合がある場合とかに忠告が出るようです。

Warning: You have unlinked kegs in your Cellar.
Leaving kegs unlinked can lead to build-trouble and cause brews that depend on
those kegs to fail to run properly once built. Run `brew link` on these:
  pkg-config
  libtool
  ilmbase
  little-cms2
  libpng
  heroku
  openjpeg
  libde265
  webp
  ruby-build
  yarn
  xz
  openexr
  nodebrew
  heroku-node
  pcre
  jpeg
  telnet

上の場合だと、brew linkしろっていってるのでそのまま実行したら解決しました。(この問題については)

Ken@MacBook-Pro ~ % brew link pkg-config
Linking /usr/local/Cellar/pkg-config/0.29.2... 4 symlinks created
Ken@MacBook-Pro ~ % brew link libtool
Linking /usr/local/Cellar/libtool/2.4.6_1... 20 symlinks created
Ken@MacBook-Pro ~ % brew link ilmbase
Linking /usr/local/Cellar/ilmbase/2.3.0... 18 symlinks created

ひとまず

まだ道半ばですが、ひとまず備忘録として重要そうな部分を書いておきます。
今のところ、mysqlのバージョンの差異によるもの、パスの設定ミス、もしくは少し前に設定をいじってしまったと思われる権限関係のミスと思われます。
(AWSデプロイの際にbundle installができなくなり、その時はrbenvあたりの権限が変わってしまっていたことが原因でした。)
バージョン違いの解決策で解決せず、合間に何をトチ狂ったのかOSアップデートしてしまい他のエラーがわんさか出ているので、ひとまずbrewをきれいにするところからかなぁと。

 参考にさせていただいた先人たち

(他にもいらっしゃいましたが数が多すぎるので特に参照した方々を)
https://qiita.com/tktcorporation/items/0ef8c930fc18ce72c301
https://qiita.com/Yuki-k-lion/items/82a4e0490e9ed38ce545
https://qiita.com/motofumi/items/0f2e7ae1b852f118fe95
https://qiita.com/kota-es/items/98ae6ee84fc59aaae2ea

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

ユーザのプロフィール画像を追加する方法(devise使ってる前提)

ImageMagick(画像変換ツール)の導入

$sudo yum install ImageMagick

mini_magicのインストール

ImageMagicをrailsアプリから使えるようにする。

Gemfile
gem 'mini_magick'
bundle install

deviseのクラス(今回はUser)のモデルの一番最初に以下を追加

User.rb
class User < ApplicationRecord
has_one_attached :image
~
end

viewファイルでform_forでプロフィール画像を追加

<%= f.file_field :image, class: "form-control floating-label", placeholder: "画像" %>

viewファイルで画像の表示

<div class="image" style="background-image: url(<%= rails_blob_path(@user.image) %>) "></div>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【プログラミング歴1年】初心者が全力でアプリを作ってみる。

はじめに

自己紹介

未経験の23歳からIT教育関係に転職し、2019の年末でプログラミング歴が1年になります。
副業でスタートアップの企業にも参加して色々教えてもらってきました。

しかし、仕事が忙しくポートフォリオを作ろうとしても途中で辞めてしまう事が多く・・・

そうだ人生初!ポートフォリオを作成してまとめようという事で1〜10までの足跡を記事にしていきたいと思います。

ベテランエンジニアの方からは見苦しい内容かもしれませんが、アドバイス頂けると幸いです!!!

※半年後の完成を描いています

目次

目次は随時更新します!!

(最終更新日2019/12/11)

アプリケーションについて

名前

未定

目的

新しい物語の伝え方を開拓する。

概要

写真家/イラストレーター小説家のマッチングアプリ

供給できるもの

  • 写真やイラスト付きのイメージ膨らむ小説
  • 写真やイラストと小説の相乗効果を生んだ新しい作品
  • 新感覚の小説の楽しみ方
  • クリエイターの活躍機会を増やす

ペルソナ

写真家/イラストレーター,小説家

  • 写真/イラストが好き
  • 小説が好き
  • 他人に見てもらう機会がない
  • いい作品ができても披露する機会がない
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

(備忘録・思い出)railsでページネーションを手作りする

まず、ページネーションとはこんなやつです↓
pagenation.jpg

ほとんどの人が見たことあると思います。記事系のサイトだと必ずいるやつ。今回はこれを手作りします!

posts_controller.rb
  def index
      @current_page = params[:page].nil? ? 1 : params[:page].to_i
      @max_page = (Post.all.size.to_f / 3).ceil
      @posts = Post.all.order(created_at: :desc).limit(3).offset(3 * (@current_page -1))
      @pagenation = {}

      if @max_page == 0
        @current_page = 1
      elsif @current_page > @max_page && @max_page != 0
        @current_page = @max_page
      end

      @pagenation['<<'] = 1 if @max_page >= 5 && @current_page >= 4

      if @max_page >= 4 && @current_page >= 3
        if @current_page == @max_page
          @pagenation['<'] = @current_page - 3
        else
          @pagenation['<'] = @current_page - 2
        end
      end

      if @max_page >= 0 && @max_page == @current_page
        @pagenation.merge!({@current_page - 2 => @current_page - 2, @current_page - 1 => @current_page - 1, @current_page => @current_page})
      else
        @pagenation.merge!({@current_page - 1 => @current_page - 1, @current_page => @current_page })
      end

      delete_list = [0, -1]
      @pagenation.delete_if do |key, value|
        delete_list.include?(value)
      end

      if @current_page == 1 && @max_page >= 3
        @pagenation.merge!({@current_page + 1 => @current_page + 1, @current_page + 2 => @current_page + 2 })
      elsif (@current_page == 1 && @max_page == 2) || (@current_page != 1 && @max_page != @current_page)
        @pagenation.merge!({@current_page + 1 => @current_page + 1 })
      end

      if @max_page >= 4 && @current_page == 1
        @pagenation['>'] = @current_page + 3
      elsif (@max_page >= 4 && @current_page != 1 && @max_page - @current_page >= 2)
        @pagenation['>'] = @current_page + 2
      end

      @pagenation['>>'] = @max_page if @max_page >= 5 && @max_page - @current_page >= 3

  end

この記事を読んでわかること/感じるかもしれないこと

・kaminariというgemを使わずページネーションをどうやって作るのか理解できる(ほとんどタイトル通り・・・。)
・手作り感が味わえる(きっと楽しいはず!!)

気をつけるポイント

・ところどころ寄り道します。無駄な話はしませんので許してください?‍ 寄り道の際には「寄り道します!」と一声かけます
・文才がないので読みにくいかも
・コードが汚いかもしれません(確実に汚い)
・分量が長すぎるくらい長い

この記事を書くに至った経緯

おそらく皆さんも自分の書いたコードに思い出があると思います。僕の場合は、ページネーションを作成しようとしていたある日、どこの記事を読んでも、「gemを使ってページネーションを実現してみた」といったものが多く、「gem!gem!gem!ってうるさい!えええいgemを使わず手作りしてやる!」とカップラーメンを食べながら心に決めた瞬間でした(「gemを使ってページネーションを実現してみた」系の記事をあげている方すみません。)
そんな思い出があるコードをもう一度見てみると、???となる部分があり、自分の為にも、皆さんのためにも記事を書くことにしました。

どんなページネーションを作成するのか

今回はこんなやつを作成します↓
-3rDG0sY.png

ポイント

①「<<」をクリックすると一番最初の記事、つまり1ページ目に飛ぶ。「>>」をクリックすると一番最後の記事に飛ぶ。
②「<」をクリックすると、表示されていないページ数の1つ前のページに飛ぶ。「>」をクリックすると、表示されていないページ数の1つ後のページに飛ぶ。

例えば、現在のページ数が4ページ目であった場合、

<< < 3 4 5 > >>

「<」をクリックすると、2ページ目に飛ぶということです。(3ページ目ではないのであしからず、、、。)、反対に、「>」をクリックすると6ページ目に飛びます。

③最大のページ数が5ページだった場合、6ページ目は存在しないので、「>>」「>」は共に表示しない。反対に、現在のページ数が1ページ目であった場合は、0ページ目は存在しないので「<<」「<」は表示しない。

④「<<」と「<」のどちらをクリックしても、同じページに飛ぶ場合は、「<<」は表示しない。反対に、「>>」と「>」のどちらをクリックしても、同じページに飛ぶ場合は、「>>」は表示しない。

例えば、現在のページ数が3ページ目であった場合、「<<」「<」は共に1ページ目を表すので

<< < 2 3 4 > >>

こうではなく、

< 2 3 4 > >>

このように表示します。

⑤現在のページ数を表示されているページ数の真ん中に置きます。qiitaのページネーションがわかりやすいはずです。
※注意点として、最大のページ数が6ページだった場合、6ページ目を真ん中には置きません(次ページがないため)。同様に1ページ目も真ん中に置きません(0ページ目は存在しないため)。

文字ばかりで辛い、、、。と思われた方もいると思うので、gifを作成しました。

最大ページ数が3ページの時の挙動
page3.gif
最大ページ数が4ページの時の挙動
page4.gif
最大ページ数が5ページの時の挙動
page5.gif
最大ページ数が6ページの時の挙動
page6.gif

この動きと、5つのポイントと照らし合わせて、全体の理解をしていただけれたらと思います?‍

それでは作成!

ここからがワクワクする部分。まずルーティングを定めます。routes.rbに以下を記述しました。

routes.rb
get 'posts/:page/index' => 'posts#index', as: :posts_page_index

表示をするだけなので、「get」を指定し、postsコントローラーのindexアクションを呼んでいます。また重要な部分は、「posts/:page/index」のところです。この部分の「:page」で現在のページ数を取得します。

例えば、URL上で「posts/1/index」なら1ページ目、「posts/4/index」なら4ページ目といった具合です。

ここまできたら次はpostsコントローラーのindexアクションの記述に移ります。

必要な変数などを定義します。
①現在のページ数を表す「@current_page」

@current_page = params[:page].nil? ? 1 : params[:page].to_i

記述方法に少し、??が浮かぶかもしれませんが、「三項演算子」というのを使っています。(響きがまたカッコいい)
三項演算子は、if文を短く記述できる優れものです。文法はこんな感じです。

条件文 ? trueの場合 : falseの場合

params[:page].nil?が条件文で、trueの場合は@current_pageは、1になり、falseの場合は、posts/:page/indexの「:page」にある値になります。params[:page]でposts/:page/indexの「:page」を取得しているわけです。
params[:page].nil? と条件を置いている理由は、:pageがnilの場合を避ける為です。nilであったらきちんと1ページ目に飛ばしてあげようという算段です。

さらに、重要な点は、paramsで受け取った値は文字列ですので、きちんと数値にしてあげる必要があります。なので、params[:page].to_i とし、数値に直しています。(iはintegerの略)

②最大ページ数を表す「@max_page」

@max_page = (Post.all.size.to_f / 3).ceil

ここでは、最大ページ数を取得します。取得の仕方が面白い!
全体の投稿数が10であるとして、1ページにつき3つ投稿を表示するとしましょう。そうすると、最大ページ数は、4ページになるはずです。3/3/3/1 といった具合ですね。

そのように考えると、10/3 となり、3.33333・・・なので、小数点以下は値を切り上げてあげれば良いはずです。つまり
⑴全体の投稿数を、表示したい値で割る
⑵小数点以下を切り上げる
これらが必要になります。

⑴ですが、Postモデルがあるとしてそこから、Post.all.sizeで全体の投稿数を取得しています。次にto_fをつけて少数の値にしてあげます(fはfloatの略)。なんでこんなことをするかというと、割った際にきちんと小数点以下を出してあげるためです。モデルから全体の数を取り出す時、その値は必ず整数になるはずです。3人のユーザーが一人1記事投稿して、全体の総数が2.8などになるはずはありません。3のはずです。しかしそうなると、割った際に小数点以下は無視されてしまいます。整数型は小数点以下を無視するからです。

そこでto_fをつけると、3は3.0となるんです。?

次は⑵ですが、この部分に当たるのはceilです。ceilは小数点以下を切り上げます。
1.2でも切り上がって2になります。

-----ここで寄り道します!-----
小数点を扱うメソッドとしてceilの他に、round、floorなどが存在します。
ceil → 小数点以下を切り上げ
floor → 小数点以下を切り捨て(1.8は1になります)
round → 小数点以下を四捨五入

今回は、小数点以下は必ず切り上げて欲しいので、ceilを利用します。
-----寄り道終わり-----

よって@max_pageは最大ページ数が入ることになります!

③viewで用いる「@posts」

@posts = Post.all.order(created_at: :desc).limit(3).offset(3 * (@current_page -1))

この部分ですが、viewのところで説明します。

④空ハッシュな「@pagenation」

空ハッシュを用意します。簡単に説明すると、この空ハッシュに、ketとvalueの1セットをたくさん入れていき、あとはviewの方で取り出していきます。

-----ここで寄り道します!-----
ハッシュについて聞きなれない方もいると思うので、説明します。「keyとvalueを1セットにして管理する」ハッシュという仕組みには、言語によって様々な呼び方があります。(あーあのことかと思う人もいるはず。。。)

ruby ・・・ハッシュ
php ・・・連想配列
swift ・・・ディクショナリー
などなど

響きがかっこいいのは、連想配列です。でも僕はディクショナリーの方がしっくりします。というのもkeyとそれに紐づけられたvalueという形はまさに辞書(ディクショナリー)らしいからです。

keyとvalueの関係とは、

fruits = {"apple": "100円", "orange": "80円", "melon": "500"}

このような感じです。"apple"がkeyであり、それに"100円"というvalueが結びついています。配列よりも優れている点は、「順番を気にしなくてもいい」というところです。配列だと先頭が0になるので、順番によって取り出す値が変わってしまいます。しかしハッシュはkeyにvalueが結びついているので、それがどの順番であろうとも、keyを指定すれば取り出すことができます。(今回はview内で@pagenationを先頭から取り出していく為、その利点は失われていますが)

また、rubyには「シンボル」というものが存在します。:pageみたいに先頭に「:」がついたやつです。ここの部分を少しまとめます。シンボルの利点は以下になります。

①表面上は文字列なので、プログラマが理解しやすい
②内部的には、整数で管理されるので、コンピュターは高速に値を比較できる
③同じシンボルは同じオブジェクト(IDが一緒)であるので、メモリの使用効率がいい
④イミュータブルなので、勝手に値を書き換えられない(破壊的なオブジェクトメソッドが効かない)

またシンボルには様々な形があります。

①基本構文

“日本” => “japan”

②keyをシンボル化

:日本 => “japan”

③=>を:で代用する。この時の:はシンボルではない。また、keyの:は省略される

日本 : “japan”

④valueをシンンボル化

日本 : :japan

特にrailsでは、「:key」の形が多く見受けられる。

-----寄り道終わり-----

こうしてコントローラー内で重要な要素、
①現在のページ数を表す「@current_page」
②最大ページ数を表す「@max_page」
④空ハッシュな「@pagenation」
が出揃いました!

なので、コードを上から順番に説明したいと思います。if文が多いので、それを1つのワンブロックと考えて説明します。
※@current_pageも@max_pageも「ページ数」であることを念頭に置いてくださいね?

if @max_page == 0
  @current_page = 1
elsif @current_page > @max_page && @max_page != 0
  @current_page = @max_page
end

まず、@max_pageが0である場合は、@current_pageを1にします。次に、@max_pageが0でなく、かつ@current_pageが@max_pageよりも大きい場合@current_pageは@max_pageの値にします。
例えば、@max_pageが6である時、ユーザーがいたずらでURLを

posts/100/index

とした場合、100ページ目はありません。この時@current_pageは@max_pageを超えてしまうので、@current_page = 6になるようにして調整しています。

 @pagenation['<<'] = 1 if @max_page >= 5 && @current_page >= 4

空ハッシュに値を入れていきます!後ろにif文がついていますが、条件とtrueの場合のすることが逆転してるだけなので、抵抗感はありますが、形自体は理解できるはずです?‍
@pagenation['key'] = value の関係です。<< は1になるので、そこの部分はokなはずです。

次に条件文の@max_page >= 5 && @current_page >= 4 ですが、ここは理解しにくいはずなので図で説明します!(@current_pageは赤色です)
スクリーンショット 2019-12-11 17.49.50.png

これを見ると、@max_page >= 5でかつ@current_page >= 4の時に「<<」が出現することが分かりますね!。なのでこのような条件文にしました。

次に「<」の出現条件です。

if @max_page >= 4 && @current_page >= 3
  if @current_page == @max_page
    @pagenation['<'] = @current_page - 3
  else
    @pagenation['<'] = @current_page - 2
  end
end

ここはif文の中にif文を入れています(if文の入れ子)。また先ほどの図を見れば分かりやすいと思いますが、

「<」が出現する条件として、@max_page >= 4 かつ @current_page >= 3でなければなりません。その場合、@current_page == @max_pageであるならば、「<」は@current_page - 3 を意味します。

それ以外は、@current_page - 2 になります。

if @max_page >= 0 && @max_page == @current_page
   @pagenation.merge!({@current_page - 2 => @current_page - 2, @current_page - 1 => @current_page - 1, @current_page => @current_page})
else
   @pagenation.merge!({@current_page - 1 => @current_page - 1, @current_page => @current_page })
end

さあここで、mergeというメソッドを使っていきます。mergeをすることで、ハッシュ同士を結合することができます。

例えば、

hash1 = {"経済学" => 80, "財政学" => 70, "会計学" => 60}
hash2 = {"経営学" => 75, "会社法" => 65}
hash3 = hash1.merge(hash2)
p hash3
#=> {"経済学"=>80, "財政学"=>70, "会計学"=>60, "経営学"=>75, "会社法"=>65}

このように、元のハッシュのお尻にハッシュが結合されていくわけです。
元のハッシュである@pagenationには、上記の条件を満たした場合、「<<」「<」というkeyがあり、それに結びついたvalueがあるはずです。ここに新たにmergeを使うことで足していきます。

まず条件として、@max_page >= 0 && @max_page == @current_page を満たした場合、

@pagenation.merge!({@current_page - 2 => @current_page - 2, @current_page - 1 => @current_page - 1, @current_page => @current_page})

が行われます。先ほどの図を見てもいいのですが分かりにくいかもしれないので、具体的な例を出しますね!

@max_page = 6 で @current_page = 6の場合、その条件を満たすはずです。その時、ページネーションはこのような形になります。

<< < 4 5 6

この時、4は@current_page - 2であり、5は@current_page - 1、6は@current_pageになります。

よって @pagenation = {'<<' => 1, '<' => 3, 4 => 4, 5 => 5, 6 => 6}
の形になります。

@max_page = 4 で @current_page = 4の場合、<<は出現しませんから、
@pagenation = {'<' => 1, 2 => 2, 3 => 3, 4 => 4}
の形になります。

@max_page >= 0 && @max_page == @current_pageを満たさない場合、例えば
@max_page = 4 で @current_page = 3の場合

< 2 3 4

この時は、

@pagenation.merge!({@current_page - 1 => @current_page - 1, @current_page => @current_page }) 

を実行します。

よって @pagenation = {'<' => 1, 2 => 2, 3 => 3} となります。(4はこの時@pagenationにありません。後ほど付け足す感じです。)

しかしこのようにしていくと問題が発生します。
@max_page >= 0 && @max_page == @current_pageを満たさない場合、@max_page = 4 で @current_page = 1の時は一体どうなるでしょう。

@pagenation.merge!({@current_page - 1 => @current_page - 1, @current_page => @current_page })を実行しますから、

@pagenation = {0 => 0, 1 => 1}

になるはずです。(<< < は、それぞれの条件を満たさないのでありません。)
ですが、ページ数に0ページ目なんてものはありません。

そこで重要になるのが、

delete_list = [0, -1]
@pagenation.delete_if do |key, value|
  delete_list.include?(value)
end

です!目を通しただけで何をしているかわかると思いますが、delete_list = [0, -1] から、@pagenationにその値が、valueにあれば(含めば)、消去しています。

delete_ifについては、この記事が参考になったので、見ていただければ良いと思います。
https://qiita.com/zom/items/4461d786c35ae9eced0b

これで、

@pagenation = {0 => 0, 1 => 1}

↓

@pagenation = {1 => 1} になるのです!

ここまでで前半部分は終了です。どうでしょうか。次は後半部分ですが、少しややこしいかもです!

@current_pageまでを@pagenationに入れてきました。後半部分では、それ以降を@pagenationに入れていきます。

次に、「>」「>>」を除く、@current_page以降、表示される数字の部分です。以下の1ブロックは、@current_page以降、数字を2つつけるべきなのか、1つつけるべきなのか条件づけています。

if @current_page == 1 && @max_page >= 3
   @pagenation.merge!({@current_page + 1 => @current_page + 1, @current_page + 2 => @current_page + 2 })
elsif (@current_page == 1 && @max_page == 2) || (@current_page != 1 && @max_page != @current_page)
   @pagenation.merge!({@current_page + 1 => @current_page + 1 })
end

ここも、if文の入れ子になっていますが、まず最初の条件として

@current_page == 1 であり、かつ @max_page >= 3 の時です。この場合は、@current_page以降、数字を2つつけなけらばなりません。

図を再掲しますが、@current_pageが1であり、@max_pageが3以上の時は、2つ以上数字が並ぶことがわかると思います!
スクリーンショット 2019-12-11 17.49.50.png

この場合、

@pagenation.merge!({@current_page + 1 => @current_page + 1, @current_page + 2 => @current_page + 2 })

を行います。おなじみのmergeですね!先頭に2つつけるので、
@current_pageに+1し、さらに+2したものを結合しています。

さて次に、@current_page以降に数字を1つつける場合はどうでしょう。

(@current_page == 1 && @max_page == 2) || (@current_page != 1 && @max_page != @current_page

①@current_pageが1であり、かつ@max_pageが2である時、
「または」
②@current_pageが1ではなく、かつ@max_pageが@current_pageと値が異なる時です。

②から見ていきましょう。というのも、①は条件が具体的でピンポイントですので、②を押さえてから、①を見た方が、理解しやすいと思うからです。

②の時は、@current_pageと@max_pageが同じであったら、次に表示される数字はありません。ですので@max_page != @current_page をつかって避けないといけません。

しかし、
@current_page != 1 && @max_page != @current_page 時の条件と、初めの
@current_page == 1 && @max_page >= 3

の条件のうち、外れる条件がでてきます。それが

①@current_pageが1であり、かつ@max_pageが2である時です。この時、@current_page以降に表示される数字は、1つなので、きちんと条件づけています。

これらの条件を満たした場合、

@pagenation.merge!({@current_page + 1 => @current_page + 1 })

を行い、@current_pageに+1したものを追加してあげます。

次のブロックで、「>」をつけていきます。

if @max_page >= 4 && @current_page == 1
  @pagenation['>'] = @current_page + 3
elsif (@max_page >= 4 && @current_page != 1 && @max_page - @current_page >= 2)
  @pagenation['>'] = @current_page + 2
end

「>」が現れる時の条件と、それを満たした場合、「>」がどんな値を持つかが重要になります。

まず、

@max_page >= 4 && @current_page == 1 

ですが、

この時に初めて、「>」は出現します。その際当然、「>」は@current_page+3の値を持つことになりますので、

@pagenation['>'] = @current_page + 3

になります。

しかし、それ以外では、「>」は@current_page+2の値を持たせねばなりません。この時の条件が

@max_page >= 4 && @current_page != 1 && @max_page - @current_page >= 2

になります。&&で区切って見ていきますが、まず@max_pageが4以上である必要があります。そして、@current_pageは1であってはなりません。次に、@max_page - @current_page した場合、2以上であれば「>」は@current_page+2の値になります。

そして最後に、「>>」を見ていきます!

@pagenation['>>'] = @max_page if @max_page >= 5 && @max_page - @current_page >= 3

この文法は、「<<」で見た時と同じ形ですので、大丈夫でしょう。それでは、この時、「>>」はどのような条件で出現し、どのような値をもつべきなのでしょうか。

まず、

@max_page >= 5 && @max_page - @current_page >= 3

が、条件文になりますが、@max_pageが5以上である時にしか「>>」は出現しません。また、@max_page - @current_page >= 3でない限り、「>>」は出現してはいけません。というのも

「<<」と「<」のどちらをクリックしても、同じページに飛ぶ場合は、「<<」は表示しない。反対に、「>>」と「>」のどちらをクリックしても、同じページに飛ぶ場合は、「>>」は表示しない。

と決めているからです、、、。こちらは僕の勝手な都合です?‍

その条件を満たした場合、「>>」は最後のページに飛ぶことになりますから、最後のページ = 最大ページ数になり、それはすなわち

@pagenation['>>'] = @max_page

で表すことができます!

これで@pagenationには、その時々の値を持つことができています!

@max_page = 3 で@current_page = 2の時は、

@pagenation = {1 => 1, 2 => 2, 3 => 3} 

@max_page = 6 で@current_page = 3の時は、

@pagenation = {'<' => 1, 2 => 2, 3 => 3, 4 => 4, '>' => 5, '>>' => 6}

といった具合です!

さて次に@pagenationと、先述した@postsを携えて、viewの方で使っていきましょう!!

index.htmk.erb
<div>
<% unless @max_page <= 0 %>
  <ul>
    <% @pagenation.each do |key, value| %>
      <li class="<%= ' active' if value == @current_page %>">
        <%= link_to key, posts_page_index_path(value) %>
      </li>
    <% end %>
  </ul>
<% end %>
</div>

まず、@max_page <= 0 でない時に、ページネーションを表示させてあげています。

unlessはifと反対の意味を持ち、if文が「〜である時」なのに対して、unlessは「〜でない時」を表します。

そして、@pagenationをeach文で回し、それぞれkeyとvalueを取り出していきます!

<li class="<%= ' active' if value == @current_page %>">

ここのclassについてですが、@current_pageであれば、そのページネーションの表示の色を、他のページ数で表示されている色と変えてあげたいはずです。ですので、このように

if value == @current_page とすることで、valueと@current_pageの値が等しければ、activeクラスが付与されて、そこをcssで変えることができるという流れになっています。

また、keyの部分に関しては、aタグをつけたいので、link_toを用いて、そのvalueそれぞれにリンクを貼ります。

posts_page_index_path(value)

となっているのは、ルーティング設定の時に、

routes.rb
get 'posts/:page/index' => 'posts#index', as: :posts_page_index

と、asで名前を変更しているからです。当然、posts_page_indexだけでは、どのページ数か判別できないので、posts_page_index_path(value)と(value)をつけています。(pathもつけることもお忘れなく、、、。)

そして最後に、コントローラー内で登場した、

@posts = Post.all.order(created_at: :desc).limit(3).offset(3 * (@current_page -1))

を説明します。

@max_page = (Post.all.size.to_f / 3).ceil

で、「最大ページ数」を取得することはできました。しかし実際に、「取り出す順番」であったり、「取り出す投稿」は取得していません。

少し疑問に思われる方もいると思うので、踏み込んで説明します。

@max_pageでは、「1ページにつき、最大3投稿表示できるとしたら、最大ページ数はいくつか」を取得したものであり、1ページごとに、どの順番で、どの投稿を表示させるのかは取得できておらず、あくまでその全体の枠組みだけです。ですので、@postsでそれらを取得しています。もちろん、@max_pageと対応させなければ、なりません。もし、全体の投稿数が10で、1ページにつき、投稿を3つまで表示できるとしたら、@max_page = 4になるはずですが、@postsの方で、1ページあたり4つ投稿を取得するとしたら、@max_pageとずれてしまうからです。今回は1ページにつき最大3つ投稿を表示できるようにしていますから、それに合わせます。

@posts = Post.all.order(created_at: :desc).limit(3).offset(3 * (@current_page -1))

まず、Post.allでモデルからデータを引っ張ってきます。その時、

①取り出す順番
②取り出す投稿

を指定しています。①については、order(created_at: :desc) とすることで、作成された日付順に投稿を取得します。日付が新しい投稿を先に取得していくわけです。

そして②です。

limit(3).offset(3 * (@current_page -1))

limitで取り出す投稿の最大数を取得します。当然3つまで投稿を取得して欲しいので、limit(3)です。そして次に@current_pageで取得した、ページ数に合わせて、取得する3つの投稿数を変更させます。それが

offset(3 * (@current_page -1))

にあたる部分です。offsetメソッドは、指定した位置からデータを取得します。例を出すと

12/10
12/9
12/8
12/7
12/4
12/3
11/20
11/18
11/16

というようなデータ(降順で表示)があった場合、limit(3).offset(0)なら、

12/10
12/9
12/8

が取得され、limit(3).offset(3)なら、

12/7
12/4
12/3

が取得されます。なんとなく雰囲気はつかめるはず?‍

なので、@current_pageとoffsetを組み合わせて、@current_pageが1なら(つまり1ページ目)

offset(3 * (@current_page -1))

offset(3 * (1 -1))

offset(3 * 0)

offset(0)

になるといった具合です。

これで、@postsには、その@current_pageに合わせて、投稿を@max_pageに対応させながら取得することに成功しました!

あとは、viewで表示する際に、

index.html.erb
<% @posts.each do |post|%>
  省略
<% end %>

とすればいいでしょう!

終わりに

短く書くつもりが、あれもこれもとなってしまい、非常に読みにくくなってしまったかもしれません。?‍
重複するコードや説明の間違いなどあるかもしれません。ですが、少しでも手作り感を味わっていただければ幸いです。
「あれ、これってどうだったっけ?」というのが、取り除かれてスッキリしました!それでは!

参考

「プロを目指す人のためのRuby入門」伊藤淳一、技術評論社、2019年
配列の複数要素の削除はdelete_ifかreject、-なんかを使おう
Rubyのmergeメソッドでハッシュを結合する方法【初心者向け】
kaminariではないバニラなページネーションをやっていく

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

【Ruby】Discogs::Wrapper でアーティスト情報とかもろもろを取得する

discogsでアーティスト名、ジャンル、リリースされたトラックの情報を取得したい。
discogs APIのwrapperを利用して適宜ロジック等を記載していく。

ドキュメント等

discogsについて
https://ja.wikipedia.org/wiki/Discogs

公式
https://www.discogs.com/ja/

gem
https://github.com/buntine/discogs

アクセストークンの取得

wrapperを使う前にアクセストークンを取得する。

googleアカウントでdiscogsにサインアップ後、User Tokenを取得する。個人的に利用する分には当面はUserTokenで良さそう。

手順

  1. https://www.discogs.com/developers/index.html#page:authentication にアクセス

image.png

  1. Developer Settingsをクリックしてサインアップ

  2. トークンを生成
    image.png

使ってみる

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

Railsチュートリアル 第12章 パスワードの再設定 - 「ログイン画面に、パスワード再設定用のリンクが存在することに対するテスト」に対するテストを実装する

テストコード

変更対象となるテストコードはtest/integration/users_login_test.rbです。

test/integration/users_login_test.rb
  class UsersLoginTest < ActionDispatch::IntegrationTest
    ...略

    test "login with invalid information" do
      get login_path
      assert_template 'sessions/new'
+     assert_select "a[href=?]", new_password_reset_path
      post login_path, params: { session: { email: "", password: ""} }
      assert_template 'sessions/new'
      assert_not flash.empty?
      assert_select 'div.alert-danger'
      get root_path
      assert flash.empty?
    end

    ...略
  end

テストを実行してみる

この時点でテストは成功します。

# rails test
Running via Spring preloader in process 14842
Started with run options --seed 21965

  46/46: [=================================] 100% Time: 00:00:04, Time: 00:00:04

Finished in 4.81443s
46 tests, 198 assertions, 0 failures, 0 errors, 0 skips

このテストは、どうすれば失敗するようになるのか

例えば以下のようにapp/views/sessions/new.html.erbを変更すると…

app/views/sessions/new.html.erb
  <% provide(:title, "Log in") %>
  <h1>Log in</h1>

  <div class="row">
    <div class="col-md-6 col-md-offset-3">
      <%= form_for(:session, url: login_path) do |f| %>

        <%= f.label :email %>
        <%= f.email_field :email, class: 'form-control' %>

        <%= f.label :password %>
-       <%= link_to "(forgot password)", new_password_reset_path %>
+       <%# <%= link_to "(forgot password)", new_password_reset_path %> %>
        <%= f.password_field :password, class: 'form-control' %>

        <%= f.label :remember_me, class: "checkbox inline" do %>
          <%= f.check_box :remember_me %>
          <span>Remember me on this computer</span>
        <% end %>

        <%= f.submit "Log in", class: "btn btn-primary" %>
      <% end %>

      <p>New user? <%= link_to "Sign up now!", signup_path %></p>
    </div>
  </div>

以下のようにテストが失敗します。

# rails test test/integration/users_login_test.rb:12
Running via Spring preloader in process 14868
Started with run options --seed 5918

 FAIL["test_login_with_invalid_information", UsersLoginTest, 2.484278399962932]
 test_login_with_invalid_information#UsersLoginTest (2.48s)
        Expected at least 1 element matching "a[href="/password_resets/new"]", found 0..
        Expected 0 to be >= 1.
        test/integration/users_login_test.rb:12:in `block in <class:UsersLoginTest>'

  4/4: [===================================] 100% Time: 00:00:02, Time: 00:00:02

Finished in 2.48610s
1 tests, 2 assertions, 1 failures, 0 errors, 0 skips

「/password_resets/new へのリンクが1つも描画されていない」という趣旨のメッセージですね。想定したテストが正しく実装されていると言えそうです。

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

Railsチュートリアル 第12章 パスワードの再設定 - 「ログイン画面に、パスワード再設定用のリンクが存在すること」に対するテストを実装する

テストコード

変更対象となるテストコードはtest/integration/users_login_test.rbです。

test/integration/users_login_test.rb
  class UsersLoginTest < ActionDispatch::IntegrationTest
    ...略

    test "login with invalid information" do
      get login_path
      assert_template 'sessions/new'
+     assert_select "a[href=?]", new_password_reset_path
      post login_path, params: { session: { email: "", password: ""} }
      assert_template 'sessions/new'
      assert_not flash.empty?
      assert_select 'div.alert-danger'
      get root_path
      assert flash.empty?
    end

    ...略
  end

テストを実行してみる

この時点でテストは成功します。

# rails test
Running via Spring preloader in process 14842
Started with run options --seed 21965

  46/46: [=================================] 100% Time: 00:00:04, Time: 00:00:04

Finished in 4.81443s
46 tests, 198 assertions, 0 failures, 0 errors, 0 skips

このテストは、どうすれば失敗するようになるのか

例えば以下のようにapp/views/sessions/new.html.erbを変更すると…

app/views/sessions/new.html.erb
  <% provide(:title, "Log in") %>
  <h1>Log in</h1>

  <div class="row">
    <div class="col-md-6 col-md-offset-3">
      <%= form_for(:session, url: login_path) do |f| %>

        <%= f.label :email %>
        <%= f.email_field :email, class: 'form-control' %>

        <%= f.label :password %>
-       <%= link_to "(forgot password)", new_password_reset_path %>
+       <%# <%= link_to "(forgot password)", new_password_reset_path %> %>
        <%= f.password_field :password, class: 'form-control' %>

        <%= f.label :remember_me, class: "checkbox inline" do %>
          <%= f.check_box :remember_me %>
          <span>Remember me on this computer</span>
        <% end %>

        <%= f.submit "Log in", class: "btn btn-primary" %>
      <% end %>

      <p>New user? <%= link_to "Sign up now!", signup_path %></p>
    </div>
  </div>

以下のようにテストが失敗します。

# rails test test/integration/users_login_test.rb:12
Running via Spring preloader in process 14868
Started with run options --seed 5918

 FAIL["test_login_with_invalid_information", UsersLoginTest, 2.484278399962932]
 test_login_with_invalid_information#UsersLoginTest (2.48s)
        Expected at least 1 element matching "a[href="/password_resets/new"]", found 0..
        Expected 0 to be >= 1.
        test/integration/users_login_test.rb:12:in `block in <class:UsersLoginTest>'

  4/4: [===================================] 100% Time: 00:00:02, Time: 00:00:02

Finished in 2.48610s
1 tests, 2 assertions, 1 failures, 0 errors, 0 skips

「/password_resets/new へのリンクが1つも描画されていない」という趣旨のメッセージですね。想定したテストが正しく実装されていると言えそうです。

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

Rails+MySQL+Docker+AWS(EC2, RDS, ALB, Route53, S3)で作成したポートフォリオについて

記事の概要

私が作成したポートフォリオ、「GoodCoffeeByGoodBarista」を解説します。
なぜ作ったか、どう作ったか、今後どうしていくかをまとめました。

実際に作成したサイトやソースコードは下記のリンクからご覧いただけます。
GoodCoffeeByGoodBarista
GitHub

こちらの記事の書き方は下記の記事を参考にさせていただきました。
PHP+MySQLでポートフォリオ作成

なぜ作ったか

私は、愛知県名古屋市にある個人経営のカフェで、2年半ほどバリスタとして勤務しておりました。

ある時、バリスタの大会に出場したのですが、結果は惨敗でした。
バリスタは、生豆の仕入れからロースト、抽出までを全て個人で行い、それをプレゼンするのですが、超小規模店舗で勤務していた私は、生豆の仕入れルートもなければ高価な焙煎機もない、大会で使われる最新のエスプレッソマシンもない、練習に使うミルクも何百本と自腹で用意するしかない環境下で戦うしかありませんでした。
対して、大きな企業が経営するカフェに勤めているバリスタは、会社が持っているルートから最高品質の生豆を仕入れ、会社が用意する材料を使って、最新のエスプレッソマシンで毎日練習ができるのです。

私は、環境の差による大きな挫折を味わいました。

バリスタを辞め、エンジニアになるべく勉強をしていた私は、「エンジニアリングの力で、バリスタ業界を良くできないか」と考えるようになりました。
エンジニアのように気軽に転職をする事ができる文化があれば、スキルを身に付けたいバリスタはより大きな企業に転職できるし、大企業にいてスキルはあるけれどもっと個人の店舗で接客を身に付けたいバリスタの願いも叶えられる。
バリスタが、自分の所属する環境でスキルを身に付けられなかったり、夢を諦めたりしなくて良くなるのではないか。
そんな思いから、バリスタ版Wantedlyのようなサービスを作成し、私のポートフォリオとすることにしました。

スペック

言語

Ruby 2.5.3

フレームワーク

Ruby on Rails 5.2.2

CSSフレームワーク

Bootstrap4

データベース

MySQL 5.7

WEBサーバ

Nginx 1.15.8

開発環境

Docker 19.03.5
docker-compose 1.21.1

バージョン管理

Git 2.24.0

本番環境

AWS (EC2, RDS, ALB, Route53, S3)

主な機能

・バリスタ(カフェ)一覧表示

バリスタの一覧、またはカフェの一覧を表示します。
ログインしていなくてもアクセスできる仕様です。

ezgif.com-video-to-gif.gif

・バリスタ(カフェ)を検索

バリスタの場合、性別で検索できます。

ezgif.com-video-to-gif (1).gif

カフェの場合、店舗名、雇用形態、所在地で検索できます。

ezgif.com-video-to-gif (11).gif

・バリスタ登録

バリスタはトップページから新規作成のリンクを踏むことでユーザー登録ができます。
入力内容は最低限必要な内容のみです。
入力に誤りがあれば、登録はされずエラ〜メッセージが表示されます。

ezgif.com-video-to-gif (12).gif

・オーナー登録

オーナーはナビゲーションにある「採用担当者の方」というところからオーナー用のトップページに移動し、そこから新規作成画面に移動できます。
入力内容は最低限必要な内容のみです。
入力に誤りがあれば、登録はされずエラ〜メッセージが表示されます。

ezgif.com-video-to-gif (13).gif

・バリスタプロフィール編集

登録では基本情報のみの入力なので、プロフィール編集で詳細な情婦を入力していきます。
上記のリンクから自分のプロフィールを確認しながら編集できます。

ezgif.com-video-to-gif (14).gif

・オーナーカフェ情報編集

登録では基本情報のみの入力なので、プロフィール編集で詳細な情婦を入力していきます。
上記のリンクから自分のプロフィールを確認しながら編集できます。

ezgif.com-video-to-gif (15).gif

・ログイン

ログインできます。
バリスタとオーナーではフォームが分けてあります。

ezgif.com-video-to-gif (7).gif

・面談したい(面談に誘いたい)バリスタ(カフェ)に対してメールを送信

ログインしている状態で、気になるバリスタ(カフェ)の詳細ページから面談を申し込む(誘う)内容のメールを送る事ができます。

ezgif.com-video-to-gif (8).gif

スクリーンショット 2019-12-11 16.04.21.png

・退会

プロフィール編集の画面から退会できます。

ezgif.com-video-to-gif (9).gif

開発手順

1.要件定義

今回作成するアプリに必要な機能は、

・バリスタユーザー登録機能
・オーナーユーザー登録機能
・ユーザー一覧表示機能
・ユーザー検索機能
・応募プロフィールや、求人情報作成機能
・面談応募(勧誘)機能

のため、ユーザー情報を保存しておくデータベースが必要であり、尚且つデータベースに保存した情報を動的に表示できるビューが必要です。

また、面談の応募(勧誘)にはメイラー機能を使いたいので、メール用のサーバーも用意する必要があります。

バリスタユーザーとオーナーユーザーで動線を分けたいので、わかりやすい動線づくりを心がけます。

2.環境選定

言語は、自分にとって技術的資産の多いRubyを選択しました。
よってフレームワークもRuby on Railsとしました。

データベースは、もっともメジャーに、広く使用されているMySQLを選択しました。

今回はバックエンド開発がメインだったため、フロントエンド開発の工数を減らす目的でCSSフレームワークを使用しました。
CSSフレームワークにはネット上に公開されている情報のリソースが多いことから、Bootstrapを使用しました。

WEBサーバーには、pumaとの連携が簡単で、かつネット上に情報のリソースが多かったNginxを使用します。

なお、これらの環境はDocker,docker-composeを使用して構築しています。
Dockerを使用したのは、最終的にCircleCIやcapistoranoを用いた自動テスト&ビルド&デプロイを行いたいと考えているためです。

そして、本番環境はAWSのEC2,RDS等を用いて構築します。
これは、個人的にAWSに興味があったため使ってみたかったのと、転職用のポートフォリオとして使用する際、クラウドにAWSを取り入れている企業様が多いと感じたことから、技術アピールができると考えたからです。

3.データベース設計

今回は、2種類のユーザーを作成する必要があるため、ユーザーモデルを一つ作成し、オーナーフラグがtrueかfalseかでユーザーを識別するか、ユーザーモデルを二つ作成してそれぞれで管理するかで悩みましたが、
今回は後者の方法でデータベースを作成することにしました。
理由は、私が参考にしている著書「達人に学ぶDB設計徹底指南書」にて、一つのモデルはなるべくシンプルにし、分けられるところは分けて管理するのが良いとされていたので、その教えを守る形にしました。

4.コーディング

コーディングの際に注意した点は以下の通りです。

・一つの機能を実装する度にRSpecでSystemスペックを記述する
・GitHubFlowを意識した開発(マスターブランチでの作業は基本的にはしない、擬似的にプルリクエストを作成して、マスターブランチにマージする -> リモートリポジトリの変更を、ローカルにpullする)

また、ユーザーをバリスタユーザーとオーナーユーザーに分けて実装していましたが、自身の練習もかねて、バリスタユーザーの登録や認証周りをdeviseで実装し、オーナーユーザーの登録はscaffoldで作成、認証は簡易的なsessionで実装しました。

ただ、ログイン状態のバリスタユーザーをcurrent_userで取得し、ログイン状態のオーナーユーザーをcurrent_ownerで取得するように実装したのですが、これはベストプラクティスではないように感じました。
次回また似たようなアプリケーションを作成する際は、この辺りのDB設計に関してきちんと見直す必要がありそうです。

この段階で、動作確認も兼ねてdevelopment環境でもAWSのS3に画像が保存されるように実装しました。

5.デプロイ

本番環境はAWSで構築しました。
docker-composeでアプリケーション用のコンテナ、Nginxのコンテナ、メイラーのコンテナを用意していたので、それらをまとめてEC2でビルド&実行するようにしました。
データベースはRDSを使用しています。
画像の保存には、S3を使用しており、こちらはproduction環境だけでなく、development環境でもS3に保存するようにしています。
また、今回はEC2は一つしか用意していませんが、後々HTTPS化するのに必要だったので、ALBを配置しました。
独自ドメインは、過去にブログを運営していたときにも利用していて使い慣れたお名前.comから取得し、Route53で設定しました。

AWSを使ったデプロイのために何冊も書籍を読み、様々な記事を読んだので、ネットワーク周りの知識がかなりついたと実感しました。

今後の改善点や追加実装について

アプリケーション自体としては、SNSログインや画像アップロードの際のプレビュー表示などを実装したいと考えております。
加えて、特にフォーム関連のUIを向上させたいと考えています。
具体的には、入力するべき項目をplaceholderでわかりやすくしたりできるかと考えております。

また、要件定義の段階から考えているCircleCIを使用した自動ビルド&テスト、capistoranoを使用した自動デプロイを実装していきたいです。
そのためにも、現在はCircleCIの公式ドキュメントを読みながら準備をしている段階です。

それから、AWSに関しても、画像の配信をCloudFlontで行うことで、より高速化を測ってみたいと思います。

参考文献

Ruby on Rails5.2 速習実践ガイド
プロを目指す人のためのRuby入門
Docker/Kubernetes 実践コンテナ開発入門
Amazon Web Services 基礎からのネットワーク&サーバー構築 改訂版
ゼロからわかる Amazon Web Services超入門 はじめてのクラウド かんたんIT基礎講座
Webを支える技術 ―― HTTP,URI,HTML,そしてREST
リーダブルコード
達人に学ぶDB設計 徹底指南書

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

画像アップロード時に「Missing required arguments: aws_access_key_id, aws_secret_access_key」エラーが発生する

プログラミングスクールのカリキュラムでメルカリのクローンサイトを開発中、商品の画像をデータベースに保存しようとしたところ、タイトルのエラーが発生。

検索しても、該当する記事が見当たらなかったので記事にしました。

エラー画面がこちらです。
スクリーンショット 2019-12-11 16.05.47.png

ローカルで画像を保存しようとしてるだけだし、AWSとか関係ないだろーと思って解決方法を模索していましたが、辿り着いた答えは単純でした。
画像をアップロードするためにCarrierWaveを使用していたのですが、「image_uploader.rb」の記述に問題がありました。

エラーの原因

image_uploader.rb
  # Choose what kind of storage to use for this uploader:
  # storage :file
  storage :fog

storage :fogが適用されているせいでCarrierWaveの保存先がS3になっていました。

解決方法

storage :fogをコメントアウトしてあげて、storage :fileのコメントアウトを解除してあげればエラーが無事解決します。

image_uploader.rb
  # Choose what kind of storage to use for this uploader:
  storage :file
  # storage :fog

補足

ローカルでの作業後、CarrierWaveの保存先をS3に戻す場合は逆の修正を行えば簡単に戻せます。

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

ActiveStorageをcarrierwaveに置換する方法

やりたいこと

  • 既にActiveStorageでのファイルアップロード機構がある
  • ActiveStorageによってアップロードされているファイル群をCarrierwaveに置換したい

知識

  1. ActiveStorageでアップロードされているファイルは、以下でURLを特定することができます。
'https://your_domain.com' + Rails.application.routes.url_helpers.rails_blob_path(task.image, options = {only_path: true})
  1. carrierwaveにはURLからファイルをアップロードできる機能があります。
# 例
Galaxy.create!(name: 'Andromeda', remote_photo_url: 'http://apod.nasa.gov/apod/image/1407/m31_bers_960.jpg', address: 'next to the Milky Way')

実装

上記の2つの知識を組み合わせて、ActiveStorageでアップロード済みの動画をCarrierwaveで置換していきます。

  • task has_one_attached imageとします。
  • carrierwave用としてimage_fileというカラムを追加したとします
09_task.rb
# seedファイルで作成
Task.all.each do |task|
  image_url = 'https://domain.com' + Rails.application.routes.url_helpers.rails_blob_path(task.image, options = {only_path: true})
  task.update!(remote_image_file_url: image_url)

  p '###################################'
  p task.title + 'の画像アップロードが完了しました'
  p '###################################'
end

上記をseed_fuファイルで作成し、Herokuで反映させたい時には以下のコマンドから実行できます。

$ heroku run rails db:see_fu FILTER=09 --app your_app_name
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】Facebookでログインできるようにする

バージョン

Rails 5.1.6
ruby 2.4.0

前提

devise導入済み
herokuにデプロイする

1.Gemfileの編集

gem 'omniauth'
gem 'omniauth-facebook'

を追加して

$ bundle 

2. Facebook Developerの登録

https://developers.facebook.com/
にてアクセス

Screen Shot 0001-12-11 at 15.34.25.png

Screen Shot 0001-12-11 at 15.35.39.png

facebookログインを追加

すると、右側のメニューにfacebookログインという項目が追加される

3.色々な設定

Screen Shot 0001-12-11 at 15.40.06.png

プライバシポリシーのURLを埋める
私の場合、プライバシーポリシのページを作ってなかったので適当に拝借しました

本番環境では有効なOAuthリダイレクトURIの設定が必要

facebookログイン > 設定 > 有効なOauthリダイレクトURLに
...(サイトURL)/users/auth/facebook/callbackを設定

4.Railsでの設定

omniauth用のカラムをuserモデルに追加

$ rails g migration add_columns_to_users provider uid name image
$ rails db:migrate
config/initializers/devise.rb
Devise.setup do |c|
  c.omniauth :facebook, 'App ID', 'App Secret'
end

※ このままgitのリモートリポジトリにあげないこと

userモデルにメソッドを追加

models/user.rb
class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable, :omniauthable # :omniauthable を追加

  def self.form_omniauth(auth)
    where(provider: auth.provider, uid: auth.uid).first_or_create do |u|
      u.provider = auth.provider
      u.uid      = auth.uid
      u.name     = auth.name
      u.email    = auth.info.email
      u.password = Devise.friendly_token[0, 20]
      u.image    = auth.info.image.gsub("picture", "picture?type=large") if u.provider == "facebook"
   end
  end
end

コールバックの設定

config/routes.rb
Rails.application.routes.draw do
  root to: "home#index"
  devise_for :users, controllers: { omniauth_callbacks: 'users/omniauth_callbacks' }
end

コントローラの設定

controllers/omniauth_callbacks_controller.rb
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
  def facebook
    @user = User.from_omniauth(request.env["omniauth.auth"]

    if @user.persisted?
      sign_in_and_redirect @user, event: authentication
      set_flash_message(:notice, :success, kind: "Facebook") if is_navigational_format?
    else
      session["devise.facebook_data"] = request.env["omniauth.auth"].except("extra")
      redirect_to new_user_restration_url
    end
  end

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

facebook-ruby-business-sdkで広告結果データを取得しchartkickでグラフ化

実装したいこと

  • facebookページでかけた広告結果データを取得して画面描画したい
  • 広告結果データ取得にはfacebook-ruby-business-sdkを使う
  • グラフ描画にはchartkickを使用する

トークン取得

developer for facebookにアクセスしてアクセストークンを取得します、下記キャプチャから取得できます

WOW_U_―_マーケティングAPI_-_Facebook_for_Developers.png

画面左側メニューの「設定 > ベーシック」から「app secret」も取得できるので、これも控えておきます。

ここで取得したトークンとシークレットをcredentialsに入れておきます。

Railsアプリから呼び出す

トークンを設定できたら呼び出す準備は完了しているので、必要な記述を追加していきます。

taskテーブル

カラム名
name string
ad_id string

dataテーブル

カラム名
impressions integer
country_name string
task_id bigint

task has_many datas

タスクを登録した時に、広告結果を取得してみます。

app/models/task.rb
require 'facebookbusiness'

class Video < ApplicationRecord
  # facebook marketing API
  FacebookAds.configure do |config|
    config.access_token = Rails.application.credentials.dig(:facebook_ads, :access_token)
    config.app_secret = Rails.application.credentials.dig(:facebook_ads, :app_secret)
  end

  # callback
  after_save :fetch_facebook_insights

  def fetch_facebook_insights
    ad = FacebookAds::Ad.get(ad_id)
    insightss = ad.insights(
      fields: 'impressions',
      breakdowns: 'country',
      date_preset: 'this_quarter'
    )
    insightss.all.each do |insight|
      data.create(
        country_name: insight.country,
        impressions: insight.impressions.to_i
      )
    end
  end
end

地味に気を付けたい点としては、

  • impressionsがStringで返ってくるので型変換が必要
  • countryは AUなどの略称で返ってくるので、UIで日本語表示したい場合は対応が必要

2点目については、facebookの国コード一覧などが役に立つかと思います。

chartkickでグラフ化

READMEに沿って進めれば簡単にグラフ化できます。

https://github.com/ankane/chartkick

show.html.erb
<%= bar_chart @task.datas.group(:country_name).sum(:impressions) %>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ユーザー属性によってdeviseの新規登録フォームを変える方法

実装したいこと

  • ユーザー属性によって異なるフォームパーツを出力する
    • 会社ユーザーであれば、会社名・email・password
    • 通常ユーザーであれば、email・password
  • 認証周りはdeviseで一括管理したいけれど、むやみにcontrollerやviewを増やしたくない

結論: パラメータで条件分岐

deviseのcontrollerは users 下のものを使用する

routes.rb
devise_for :users, controllers: {
  sessions: 'users/sessions',
  registrations: 'users/registrations',
  passwords: 'users/passwords',
  confirmations: 'users/confirmations'
}
app/controller/users/registrations_controller.rb
# GET /resource/sign_up
def new
  super do |resource|
    if params[:as] == 'company'
      build_resource({})
      self.resource.company = Company.new
    end
  end
end
app/views/users/registrations/new.html.erb
<% if params[:as] == 'company' %>
  <h2 class="has-text-weight-bold">会社アカウント作成</h2>
  <%= f.fields_for :company do |company_form| %>
    <div class="field">
      <%= company_form.label :name, class: 'label has-margin-top is-inline-block' %>
      <span class="has-text-danger">*</span>
      <%= company_form.text_field :name, autofocus: true, class: 'input'  %>
    </div>
  <% end %>
<% end %>

こんな感じで書いておけば、パラメータで companyが渡ってきた時だけ、フォームパーツを出力することができます。

app/controller/application_controller.rb
before_action :configure_permitted_parameters, if: :devise_controller?

private
def configure_permitted_parameters
  devise_parameter_sanitizer.permit(:sign_up, keys: [company_attributes: [:name]])
end

サインアップの時にパラメータを送信できるよう許可しておく必要があります。

参考: https://github.com/plataformatec/devise#strong-parameters

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

railsでのデータ取得方法 find,where,group by など

はじめに

railsの基本的なデータ取得方法をまとめます。

allメソッド

全ての要素を取り出すメソッドです。モデルのすべての要素を配列で取得します。

#モデル名.all

#以下が例となります。
User.all

findメソッド

モデルのidをキーとして検索するメソッドです。
idに当たるレコードがない場合はエラー(ActiveRecord::RecordNotFound)を起こします。

#モデル名.find(取得したいレコードのid)

#以下が例となります。
User.find(1) 

find_byメソッド

条件にマッチしたレコードを1件だけ返すメソッドです。
条件に一致するものがない場合はnilを返します。

#モデル名.find_by(検索するテーブルのカラム名: 検索したいデータ)

#以下が例となります。
User.find_by(name: "たかし")

whereメソッド

条件に対しての結果をすべて返すメソッドです。
条件に一致するものがない場合はActiveRecord_Relationクラスを返します。

# モデル.where(検索条件)

#nameがたかしのものを検索
User.where(name: "たかし")

#nameがたかしかつseiがすずきのものを検索
User.find_by("name = ? and sei = ?", "たかし", "すずき")

終わりに

railsで基本的なデータの取得方法について紹介しました。
条件に一致するものがないときはそれぞれ違う結果を返すことは頭に入れておきたいです!

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

RSpecを学ぶ part1

RSpecを使えるように設定

RSpec は Rails ア リケーシ ンにデ ォルトでは含まれていないため、まずインストールする必要
があります。RSpec をインストールするためには Bundler を使います。

group :development, :test do
  gem 'rspec-rails', '~> 3.6.0'
  # Rails で元から追加されている gem は省略
 end

bundle install

次に、テスト用のdbが存在するかどうかを確認する

sqliteの場合、

config/database.yml
test:
  <<: *default
  database: db/test.sqlite3

このように記載されているはず

これで、アプリケーションにspecフォルダを追加して、RSpecの基本的な設定ができるようになりました。
次のコマンドを使って、RSpecをインストールしよう

rails generate rspec:install

このコマンドにより

Running via Spring preloader in process 28211
      create  .rspec
      create  spec
      create  spec/spec_helper.rb
      create  spec/rails_helper.rb

4つのファイルが作成される。

次にRSpecの出力をデフォルトからドキュメント形式にしてみよう
.rspecファイルに、

/sampleapp/.rspec
--require spec_helper
--format documentation

としよう。

次に
rspec binstub を使ってテストスイートの起動時間を速くしよう

次に RSpec テストランナーのために binstub をインストールしましょう。こうしておくとアプリケーションの起動時間を素早くするSpringの恩恵が受けられます。

group:developmentdo
   # 元から書かれている gem は省略 ...
   gem 'spring-commands-rspec'
 end

それから新しい binstub を作成 します。
$ bundle exec spring binstub rspec

こうするとアプリケーションの bin ディレクトリ内に rspec という名前の実行用ファイルが作成される。

そしたら、rspecがちゃんとインストールされているか確認してみよう

$ bin/rspec
このコマンドを実行し、以下のように出力されたらちゃんとインストールできている証拠

ec2-user:~/environment/sample_app (master) $ bin/rspec
Running via Spring preloader in process 23528
No examples found.

Finished in 0.00044 seconds (files took 0.19004 seconds to load)
0 examples, 0 failures

最後に、generateコマンドを使ってアプリにコードを追加する際にRSpec用のファイルも一緒に作ってもらうようにrailsに設定しよう。

ちなみに、RSpecをインストールしたため、generateコマンドを実行してもminitestファイルは作成されなくなった。代わりにスペックファイルが作成されるようになった。

ただ、scaffoldなどをするときに、不必要なスペックファイルまで作成されてしまう可能性があるので、それが作成されないように設定をしておこう。

config/application.rb
require_relative 'boot'

require 'rails/all'

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)

module SampleApp
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 5.1

    config.generators do |g|
      g.test_framework :rspec,
      fixtures: false,
      view_specs: false,
      helper_specs: false,
      routing_specs: false
    end 

    # Settings in config/environments/* take precedence over those specified here.
    # Application configuration should go into files in config/initializers
    # -- all .rb files in that directory are automatically loaded.
    # 認証トークンをremoteフォームに埋め込む
    config.action_view.embed_authenticity_token_in_remote_forms = true
  end
end

このように追加してあげる。

以上で開発環境を作る作業は完了。

Q&A
• test ディレクトリは削除しても良いのですか?

➡️ゼロから新しいア リケーシ ンを作るのであれば イエスです。これまでにア リケーシ ンをある程度作ってきたのであれば、まず rails test コ ンドを実行し、既存のテストがないことを確認してください。既存のテストがあるなら、それらを RSpec に移行させる必要があるかもしれません。

• なぜビューはテストしないのですか?
➡️信頼性の高い ーのテストを作ることは非常に面倒だ からです。さらに ンテナンスしようと思ったらもっと大変になります。ジェ レータを設定する際 に私が述べたように、UI 関連のテストは統合テストに任せようとしています。これは Rails 開発者 の中では標準的な ラクティスです。

第3章モデルスペック

モデルはアプリのコアとなるコード。この部分をしっかりテストできていれば、堅牢な土台を作ることができる。

例えば、モデルスペックは次のような感じになる。

describe User do
# 姓、名、メール、パスワードがあれば有効な状態であること
it "is valid with a first name, last name, email, and password" # 名がなければ無効な状態であること
it "is invalid without a first name"
# 姓がなければ無効な状態であること
it "is invalid without a last name"
# メールアドレスがなければ無効な状態であること
it "is invalid without an email address"
# 重複したメールアドレスなら無効な状態であること
it "is invalid with a duplicate email address"
#  ーザーのフルネームを文字列として返すこと
it "returns a user's full name as a string"
end

また、itの後の""は必ず動詞から始まっていることにも注目しよう。

では、早速ファイルを作っていこう。

rspec:model シェネレータを以下のコマンドで実行してください。

$ bin/rails g rspec:model user

これにより以下のファイルが作成される。

spec/models/user_spec.rb
require 'rails_helper'

RSpec.describe User, type: :model do
  pending "add some examples to (or delete) #{__FILE__}"
end

ここでは、describeメソッドを使って、Userという名前のモデルに関するテストをここに書くよーと宣言してる。

ここで、bin/rspec コマンドでテストの結果を見てみよう。

すると、このような結果が出てきた。

ec2-user:~/environment/sample_app (master) $ bin/rspec 
Running via Spring preloader in process 5058

User
  add some examples to (or delete) /home/ec2-user/environment/sample_app/spec/models/user_spec.rb (PENDING: Not yet implemented)

Pending: (Failures listed here are expected and do not affect your suite's status)

  1) User add some examples to (or delete) /home/ec2-user/environment/sample_app/spec/models/user_spec.rb
     # Not yet implemented
     # ./spec/models/user_spec.rb:4


Finished in 0.00182 seconds (files took 0.34229 seconds to load)
1 example, 0 failures, 1 pending

では、次にdescribeの大枠はそのままに、中身に先ほどの例を書いていこう。

書き終わると以下のようになる

spec/models/user_spec.rb
require 'rails_helper'

RSpec.describe User, type: :model do
  # 姓、名、メール、パスワードがあれば有効な状態であること
  it "is valid with a first name, last name, email, and password"
  # 名がなければ無効な状態であること
  it "is invalid without a first name"
  # 姓がなければ無効な状態であること
  it "is invalid without a last name"
  # メールアドレスがなければ無効な状態であること
  it "is invalid without an email address"
  # 重複したメールアドレスなら無効な状態であること
  it "is invalid with a duplicate email address"
  #  ーザーのフルネームを文字列として返すこと
  it "returns a user's full name as a string"
end

詳細はこの後記載していくが、この状態で bin/rspec を実行して結果を見てみよう。

出力結果は以下のようになる。

ec2-user:~/environment/sample_app (master) $ bin/rspec
Running via Spring preloader in process 5312

User
  is valid with a first name, last name, email, and password (PENDING: Not yet implemented)
  is invalid without a first name (PENDING: Not yet implemented)
  is invalid without a last name (PENDING: Not yet implemented)
  is invalid without an email address (PENDING: Not yet implemented)
  is invalid with a duplicate email address (PENDING: Not yet implemented)
  returns a user's full name as a string (PENDING: Not yet implemented)

Pending: (Failures listed here are expected and do not affect your suite's status)

  1) User is valid with a first name, last name, email, and password
     # Not yet implemented
     # ./spec/models/user_spec.rb:5

  2) User is invalid without a first name
     # Not yet implemented
     # ./spec/models/user_spec.rb:7

  3) User is invalid without a last name
     # Not yet implemented
     # ./spec/models/user_spec.rb:9

  4) User is invalid without an email address
     # Not yet implemented
     # ./spec/models/user_spec.rb:11

  5) User is invalid with a duplicate email address
     # Not yet implemented
     # ./spec/models/user_spec.rb:13

  6) User returns a user's full name as a string
     # Not yet implemented
     # ./spec/models/user_spec.rb:15


Finished in 0.00355 seconds (files took 0.21558 seconds to load)
6 examples, 0 failures, 6 pending

このpendingというのはスペックの中身がないため、保留にしといたよーという意味。

では、実際にテストの中身を実装していこう。

*もしテスト環境 で イグレーシ ンが未実行になっているというエラーが出た場合は、bin/rails db:migrate RAILS_ENV=test というコ ンドを実行してデータ ースの構造を最新にしてください。

RSpecの構文

以前まではshould構文が主流だったが、最近expect構文というのも出てきたらしい。
この2つを比較してみよう。

# 2と1を足すと3になること 
it "adds 2 and 1 to make 3" do
  (2 + 1).should eq 3
end

これに対し、expect構文は

it "adds 2 and 1 to make 3" do
  expect(2 + 1).to eq 3
end 

となる。

eqはマッチャーと呼ばれていて、左辺と右辺を比較するものらしい。

では、実際に1つめの具体的なテストを書いていこう。

spec/models/user_spec.rb
require 'rails_helper'

RSpec.describe User, type: :model do
  # 姓、名、メール、パスワードがあれば有効な状態であること
  it "is valid with a first name, last name, email, and password" do 
    user = User.new(
      name: "hogehoge",
      email: "hogehoge@gmail.com",
      password: "hogehoge")
      expect(user).to be_valid
  end 
  # 名がなければ無効な状態であること
  it "is invalid without a first name"
  # 姓がなければ無効な状態であること
  it "is invalid without a last name"
  # メールアドレスがなければ無効な状態であること
  it "is invalid without an email address"
  # 重複したメールアドレスなら無効な状態であること
  it "is invalid with a duplicate email address"
  #  ーザーのフルネームを文字列として返すこと
  it "returns a user's full name as a string"
end

ここでは、be_validというマッチャーを使って、作られたインスタンスが有効かどうかを判断してくれる。

この場合は有効なので、bin/rspecをすると、以下のように出力される。

ec2-user:~/environment/sample_app (master) $ bin/rspec
Running via Spring preloader in process 6451

User
  is valid with a first name, last name, email, and password
  is invalid without a first name (PENDING: Not yet implemented)
  is invalid without a last name (PENDING: Not yet implemented)
  is invalid without an email address (PENDING: Not yet implemented)
  is invalid with a duplicate email address (PENDING: Not yet implemented)
  returns a user's full name as a string (PENDING: Not yet implemented)

Pending: (Failures listed here are expected and do not affect your suite's status)

  1) User is invalid without a first name
     # Not yet implemented
     # ./spec/models/user_spec.rb:13

  2) User is invalid without a last name
     # Not yet implemented
     # ./spec/models/user_spec.rb:15

  3) User is invalid without an email address
     # Not yet implemented
     # ./spec/models/user_spec.rb:17

  4) User is invalid with a duplicate email address
     # Not yet implemented
     # ./spec/models/user_spec.rb:19

  5) User returns a user's full name as a string
     # Not yet implemented
     # ./spec/models/user_spec.rb:21


Finished in 0.01955 seconds (files took 0.24068 seconds to load)
6 examples, 0 failures, 5 pending

pendingの数が1つ減り、テストが一つ成功したことを意味する。

逆に、メールアドレスを消して、パスワードを6文字以下にしてみると、
以下のように出力された。

Failures:

  1) User is valid with a first name, last name, email, and password
     Failure/Error: expect(user).to be_valid
       expected #<User id: nil, name: "hogehoge", email: "", created_at: nil, updated_at: nil, password_digest: "$2a$04$0MOLfzpAa7BViDYHGAzPo.0zB8zmmXcoGuDA7fFJpHZ...", remember_digest: nil, admin: false, activation_digest: nil, activated: false, activated_at: nil, reset_digest: nil, reset_sent_at: nil> to be valid, but got errors: Email can't be blank, Email is invalid, Password is too short (minimum is 6 characters)
     # ./spec/models/user_spec.rb:10:in `block (2 levels) in <top (required)>'
     # /home/ec2-user/.rvm/gems/ruby-2.6.3/gems/spring-commands-rspec-1.0.4/lib/spring/commands/rspec.rb:18:in `call'
     # -e:1:in `<main>'

Finished in 0.49088 seconds (files took 0.24127 seconds to load)
6 examples, 1 failure, 5 pending

Failed examples:

rspec ./spec/models/user_spec.rb:5 # User is valid with a first name, last name, email, and password

横スクロールしないと見えづらいが、

 but got errors: Email can't be blank, Email is invalid, Password is too short (minimum is 6 characters)

このようにエラーメッセージで何が原因でテストに落ちたか教えてくれる。

バリデーションをテストする

次にバリデーションをテストしていく。

spec/models/user_spec.rb
require 'rails_helper'

RSpec.describe User, type: :model do
  # 姓、名、メール、パスワードがあれば有効な状態であること
  it "is valid with a name, email, and password" do 
    user = User.new(
      name: "hogehoge",
      email: "hogehoge@gmail.com",
      password: "hogehoge")
      expect(user).to be_valid
  end 
  # 名がなければ無効な状態であること
  it "is invalid without a first name" do
    user = User.new(name: nil)
    user.valid?
    expect(user.errors[:name]).to include("can't be blank")
  end 

まず、user.valid? をすることで、falseになる。
それにより、
errors = { name: "~~~"}というエラーメッセージが格納される。

で、includeマッチャにより、can't be blankが含まれているかどうかを確認している。

誤判定ではないか証明しよう

まずは、.to を .to_notに変えて、エクスペクテーションを反転させて見ましょう。

to_notに変えてテストを実行すると、以下のように出力された。

Failures:

  1) User is invalid without a first name
     Failure/Error: expect(user.errors[:name]).to_not include("can't be blank")
       expected ["can't be blank"] not to include "can't be blank"
     # ./spec/models/user_spec.rb:16:in `block (2 levels) in <top (required)>'
     # /home/ec2-user/.rvm/gems/ruby-2.6.3/gems/spring-commands-rspec-1.0.4/lib/spring/commands/rspec.rb:18:in `call'
     # -e:1:in `<main>'

Finished in 0.45446 seconds (files took 0.23381 seconds to load)
5 examples, 1 failure, 3 pending

Failed examples:

rspec ./spec/models/user_spec.rb:13 # User is invalid without a first name

もう一つの方法が、アプリケーション側のコードを変更してテストの実行結果をみる方法がある。

Userモデルのファイルを開いて、nameのバリデーションをコメントアウトしてみよう。

Failures:

  1) User is invalid without a first name
     Failure/Error: expect(user.errors[:name]).to include("can't be blank")
       expected [] to include "can't be blank"
     # ./spec/models/user_spec.rb:16:in `block (2 levels) in <top (required)>'
     # /home/ec2-user/.rvm/gems/ruby-2.6.3/gems/spring-commands-rspec-1.0.4/lib/spring/commands/rspec.rb:18:in `call'
     # -e:1:in `<main>'

Finished in 0.46719 seconds (files took 0.21363 seconds to load)
5 examples, 1 failure, 3 pending

このような出力結果が返ってきた。
注目すべきは、expected [] to include "can't be blank"
である。
to_notの時は、
expected ["can't be blank"] not to include "can't be blank"
と出力された。

つまり、バリデーションを消したので、エラーメッセージが格納されなかったということ。

ちなみに、モデルはTDDで実装するのに適している。
バリデーションの追加は思った以上に忘れやすく、また、テストを書きながらモデルが持つべきバリデーションについて考えれば忘れにくくなる。

emailのユニークバリデーションをテストしてみよう

spec/models/user_spec.rb
it "is invalid with a duplicate email address" do 
    User.create(
      name: "hogehoge",
      email: "hogehoge@gmail.com",
      password: "hogehoge")
    user = User.new(
      name: "foobar",
      email: "hogehoge@gmail.com",
      password: "foobar")
    user.valid?
    expect(user.errors[:email]).to include("has already been taken")
  end 

projectモデルのテストをしてみよう

現段階ではそんなモデルはないが、ある亭で。

プロジェクトは各ユーザーごとにユニークじゃなければいけないというバリデーションをテストしたい。

つまり、各ユーザーは同じ名前のプロジェクト名を作ることができないが、他人と被るぶんには良いという話だ。

このテストは以下のように作成できる。

まずはテストファイルを作成しよう

$ bin/rails g rspec:model project

spec/models/project_spec.rb
#ユーザー単位では重複したプロジェクト名を許可しない
it "does not allow duplicate project names per user" do
  user = User.create(
    name: "hogehoge",
    email: "hogehoge@gmail.com",
    password: "hogehoge")

  user.projects.creates(
    name: "test project")

  new_project = user.projects.build(
    name: "test project")

  new_project.valid?
  expect(new_project.errors[:name]).to include("has already been taken")
end

#2人のユーザーが同じ名前を使うことは許可すること

it "allows two users to share a project name" do
  user = User.create(
    name: "foobar",
    email: "foobar@gmail.com",
    password: "foobar")

  user.projects.create(
    name: "test project")

  other_user = User.create(
    name: "hogehoge",
    email: "hogehoge@gmail.com",
    password: "hogehoge")

  other_project = other_user.projects.build(
    name: "test project")

  expect(other_project).to be_valid
end

こんな感じになる。

では projectモデルには一体どのようなバリデーションがされていたのか?

app/models/project.rb
validates :name, presence: true, uniqueness: { scope: :user_id }

ということである。

また、テストを書いたら、ちゃんと失敗するかどうかも確認しよう。

例えば、to_notにしたり、バリデーションの部分をコメントアウトにしたり、数値しか受け付けないバリデーションなら文字列を渡して見たり、4〜8文字のバリデーションなら、3文字と9文字を渡して見たりということをする。

インスタンスメソッドをテストする

これも例えばの話だが、firstname と lastnameを繋げてフルネームを返してくれる name というインスタンスメソッドがあったとする。

その挙動を確かめるには以下のようなテストを書いていく。

spec/models/user_spec.rb
it "returns a user's full name as a string" do
  user = User.new(
  first_name: "John",
  last_name: "Doe",)

  expect(user.name).to eq "John Doe"
end

という感じになる。

今回はeqというマッチャを使用している。RSpecで等値のエクスペクテーションを書く時は == ではなく、 eqを使う。

クラスメソッドとスコープをテストする

これも例えの話だが、渡された文字列でnoteを検索する機能があったとする。

それをテストするにはどうすれば良いだろうか?

spec/models/note_spec.rb
it "returns notes that match the search term" do
  user = User.create(
    .
    .
    .)

  project = user.projects.create(
    name: "test project",)

  note1 = project.notes.create(
    message: "this is the first note.",
    user: user,)


  note2 = project.notes.create(
    message: "this is second note.",
    user: user,)

  note3 = project.notes.create(
    message: "first, preheat the oven.",
    user: user,)

  expect(Note.search("first")).to include(note1, note3)
  expect(Note.search("first")).to_not include(note2)
end

失敗をテストする

次は先ほどの検索結果のテストに追加していく。
もし、検索したワードにヒットするNoteがなかったらという条件でテストをする

spec/models/note_spec.rb
it "returns notes that match the search term" do
  user = User.create(
    .
    .
    .)

  project = user.projects.create(
    name: "test project",)

  note1 = project.notes.create(
    message: "this is the first note.",
    user: user,)


  note2 = project.notes.create(
    message: "this is second note.",
    user: user,)

  note3 = project.notes.create(
    message: "first, preheat the oven.",
    user: user,)

  expect(Note.search("message")).to be_empty
end

というテストになる。検索した場合だけではなく、結果が返ってこない場合もテストをすると良い。

be_emptyは配列が?空かどうをチェックするマッチャと言える。

describe,context,before,afterを使ってdryなコードを書こう

先ほどのNoteに関するテストは冗長なので、リファクタリングする必要がある。

まず必要なのが、検索機能をテストするdescribeをdescribe Noteブロックの中に作成すること。

これは検索機能にフォーカスするため。
次のような形になる。

spec/models/note_spec.rb
require 'rails_helper'

RSpec.describe Note, type: :model do

  #その他のスペックが並ぶ

  describe "search message for a term" do
    #検索用のexampleが並ぶ
  end 
end 

さらに、contextブロックを2つ加えて、exampleを切り分けよう。
1つ目は一致するデータが見つかるとき、
2つ目は一致するデータが一つも見つからない時

次のようになる。

spec/models/note_spec.rb
require 'rails_helper'

RSpec.describe Note, type: :model do

  #その他のスペックが並ぶ

  describe "search message for a term" do

    context "when a match is found" do
      #一致する場合のexampleが並ぶ。
    end

    context "when no match is found" do
      #一致しない場合のexampleが並ぶ
    end 
  end 
end 

このようにdescribeはクラスや昨日のアウトラインを記述し、
contextでは特定の条件におけるアウトラインを記述するようにするとわかりやすい。

次はbeforeブロックを使ってさらにdryなコードを書いていこう。

beforeブロック内に書かれたコードは、各テストが実行される前に実行される。

ただ、beforeブロックが書かれているdescribeの外側はスコープ適用外なので注意。

次のような形になる。

spec/models/note_spec.rb
require 'rails_helper'

RSpec.describe Note, type: :model do

  before do
    #このファイルの全テストで使用するデータをセットアップ
  end


  #その他のスペックが並ぶ

  describe "search message for a term" do

    before do
      #検索機能の全テストに関連する追加のテストデータをセットアップする
    end

    context "when a match is found" do
      #一致する場合のexampleが並ぶ。
    end

    context "when no match is found" do
      #一致しない場合のexampleが並ぶ
    end 
  end 
end 

afterで後処理をすることも可能だが、RSpecの場合、デフォルトでデータベースの後片付けをやってくれるので、afterを使う場面は少ないらしい。

これを元にNoteクラスのテストを書き直していこう。

spec/models/note_spec.rb
require 'rails_helper'

RSpec.describe Note, type: :model do

  before do
    @user = User.create(
        name: "hogehoge",
        email: "hogehoge@gmail.com",
        password: "hogehoge")

      @project = @user.projects.create(
        name: "test project")
  end


  it "is valid with a user, project, and message" do
    note = Note.new(
      message: "foobar",
      user: @user,
      project: @project,)
    expect(note).to be_valid
  end 

  describe "search message for a term" do

    before do
      @note1 = project.notes.create(
        message: "this is the first note.",
        user: user,)


      @note2 = project.notes.create(
        message: "this is second note.",
        user: user,)

      @note3 = project.notes.create(
        message: "first, preheat the oven.",
        user: user,)
    end

    context "when a match is found" do
      it "returns notes that match the search term" do
        expect(Note.search("first")).to include(@note1, @note3)
    end

    context "when no match is found" do
      it "returns an empty collection" do
        expect(Note.search("message")).to be_empty
      end
    end 
  end 
end 

このような形になる。

三章まとめ

1.期待する結果は能動形で記述すること。

2.起きてほしいことと、起きて欲しくないことをテストすること。

3.describe,context,before,afterを使ってスペックを整理すること。

sample appのuser_specを完成、リファクタリングしてみよう

自分でリファクタリングして見たよ!

spec/models/user_spec.rb
require 'rails_helper'

RSpec.describe User, type: :model do

  before do
    @user = User.create(
      name: "hogehoge",
      email: "hogehoge@gmail.com",
      password: "hogehoge")
  end

  # 姓、名、メール、パスワードがあれば有効な状態であること
  it "is valid with a name, email, and password" do 
    expect(@user).to be_valid
  end 
  # 名がなければ無効な状態であること
  it "is invalid without a first name" do
    @user.name = nil
    @user.valid?
    expect(@user.errors[:name]).to include("can't be blank")
  end 
  # メールアドレスがなければ無効な状態であること
  it "is invalid without an email address" do
    @user.email = nil
    @user.valid?
    expect(@user.errors[:email]).to include("can't be blank")
  end 
  # 重複したメールアドレスなら無効な状態であること
  it "is invalid with a duplicate email address" do 
    other_user = User.new(
      name: "foobar",
      email: "hogehoge@gmail.com",
      password: "foobar")
    other_user.valid?
    expect(other_user.errors[:email]).to include("has already been taken")
  end 

  #パスワードが6文字以下なら無効であること
  it "is invalid password with 5 strings" do 
    invalid_user = User.new(
      name: "foobar",
      email: "foobar@gmail.com",
      password: "fooba")
    invalid_user.valid?
    expect(invalid_user.errors[:password]).to include("is too short (minimum is 6 characters)")
  end 
end

これを実行すると、

ec2-user:~/environment/sample_app (master) $ bin/rspec
Running via Spring preloader in process 6877

User
  is valid with a name, email, and password
  is invalid without a first name
  is invalid without an email address
  is invalid with a duplicate email address
  is invalid password with 5 strings

Finished in 0.48302 seconds (files took 0.24129 seconds to load)
5 examples, 0 failures

このような仕様書が出来上がる!!!!!

注意点としては、itで定義した後に、do を忘れないこと。

お疲れ様でした。

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

[Rails]検索ロジックをmodelで書く(gemなし)

gemなしでの検索機能

controller

sample_controller.rb
def index
  if params[:title]
    @post = Post.title_status(params[:title])
  elsif params[:status]
    @status = params[:status].to_i
    @post = Post.search_status(@status)
  else
    @post = Post.all
  end
end

model

sample.rb
class Post < ActiveRecord::Base
  enum status: %i( 闇属性 光属性 水属性 )

  scope :search_title, -> (title) { where('title LIKE ?', "%#{title}%") if title.present? }
  scope :search_status, -> (status) { where(status: status) if status.present? }
end

view

index.html
  <%= form_with(url:posts_path, method: :get, local: true) do |f| %>
    <%= f.label title %>
    <%= f.text_field :title %>
    <%= f.status %>
    <%= f.select :status, [["闇属性", 0],["光属性", 1],["水属性", 2]], :include_blank => true %>
    <%= f.submit "検索" %>
  <% end %>

おまけ

new.html
  <%= form_with(@post, local: true) do |f| %>
    <%= f.label title %>
    <%= f.text_field :title %>
    <%= f.status %>
    <%= f.select :status, [["闇属性", 0],["光属性", 1],["水属性", 2]], :include_blank => true %>
    <%= f.submit "送信" %>
  <% end %>

テスト

FactoryBot.define do
  factory :post_1 do
    name { '遊戯王カード' }
    status { '闇属性' }
  end

  factory :post_2 do
    name { '遊戯王カード' }
    status { '光属性' }
  end
end
spec/system/post_spec.rb
context '検索機能のテスト' do
  before do
    FactoryBot.create(:post_1)
    FactoryBot.create(:post_2)
  end
  it 'タイトルとステータスで検索' do
    visit posts_path
    fill_in 'title',    with: '遊戯王カード'
    select 'status', from: '闇属性'
    click_button "検索"
    page.html
    expect(page).to have_content '闇属性'
  end
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

VSCodeでRails開発

VSCodeでRails開発

VSCodeで、主にRuby(Railsアプリ)を書く際のストレスを解消するために作った拡張機能を紹介します。

Rails専用

Rails Routes

config/routes.rbの内容に応じて、URLヘルパの入力補完とジャンプが可能になります。
入力補完が効かない箇所でも、railsRoutes.insertコマンドによる入力が可能です。

definition

Rails Partial

パーシャルファイルの入力補完、ジャンプ、作成が可能になります。

Definition

Quick Open Rails

Railsアプリ内の各種ファイルを種類別に簡単に開けるようになります。
似たような拡張機能はいくつかあったのですが、微妙に欲しいものと違ったので作りました。
デフォルト設定の状態で以下の特徴があります。(変更可能)

  • appディレクトリ以下は自動的にサブディレクトリ名でカテゴライズされる
  • マイグレーションファイルは新しい順に表示

open

Rails DB Schema

db/schema.rbの内容に応じて補完やジャンプが可能になります。
補完が効かない箇所でもrailsDbSchema.insertコマンドによる入力が可能です。

Completion

Rails I18n

入力補完やマウスホバーでの翻訳表示が可能となります。
類似パッケージとの違いとしては以下になります。

  • 補完対象のメソッドを設定可能
  • localesファイルへのジャンプは未サポート
  • 対象ファイルに応じたprefixが補完候補に優先表示される
    • 例: app/views/home/show.html.hamlの場合はviews.home.show

translate

Haml Lint

VSCodeからhaml-lintを利用できます。
一部quick-fixにも対応しています。

Rails Extension Pack

自作以外のパッケージも含めたRails向け拡張パックです。

Rails以外でも使える拡張

Autocomplete Symbols

ワークスペース内のシンボルによる入力補完が可能となります。
デフォルトでは4文字以上入力した場合にその他補完候補が存在しない場合のみ表示されるようになっていますが、任意のキーで呼び出すことも可能です。
何となくしか名前を覚えていないメソッドを呼びたい場合に重宝します。
シンボルの解析についてはruby拡張のrubyLocate設定を有効にしています。(vendor/bundle以下にGemをインストールしておけばGemのメソッドも補完できます)

demo

Autocomplete Words

開いている他のタブからも単語を入力補完の候補として表示します。
Atomではデフォルトで同様の動きなのですが、VSCodeはアクティブなファイルからしかキーワード補完による候補が表示されない点が困りました。
類似パッケージはあったのですが、動作が安定していなかったので自分が必要としている最低限の機能で作ってみました。

demo

Snippets by pattern

ファイル名のパターン別にスニペットを管理できます。
また既存のコードを簡単にスニペット化するためのコマンドも付いています。

alt

詳細: VSCodeのスニペット機能をより便利にする方法 - Qiita

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

Railsチュートリアル 第12章 パスワードの再設定 - 前提

何をしていくか

「パスワードを忘れた」というユーザーのために、「パスワードの再設定」という操作ができるようにします。

「パスワードの再設定」とは

パスワードをダイジェストとしてRDBに保存している場合、ハッシュ関数の性質上、パスワードそのものを通知することはできません。そのため、「再設定」という操作が必要になります。大まかな手順は以下です。

  1. パスワードを再設定するユーザーは、新たなパスワードをフォームに入力し、アプリケーションに送信する
  2. アプリケーションは、1.で設定されたパスワードを元に新たにハッシュ値を生成する
  3. 2.で生成したハッシュ値を、当該ユーザーの新たなパスワードダイジェストとしてRDBに保存する

ユーザーの真正性を確認する方法

「第11章 アカウントの有効化」と同様の手順となります。

  1. ユーザーは、パスワード再設定用リンクを受け取るためのメールアドレスをアプリケーションに送信する
  2. アプリケーションは、パスワード再設定用のトークンとダイジェストの組を生成する
  3. アプリケーションは、2.で生成されたパスワード再設定用ダイジェストをRDBに保存する
  4. アプリケーションは、2.で生成されたパスワード再設定用トークンを含むURLを生成する
  5. アプリケーションは、4.で生成されたURLを含むメールを作成し、1.で指定されたメールアドレスに送信する
  6. ユーザーは、5.で送信されたメールに記載されたURLをクリックする
  7. アプリケーションは、6.でクリックされたURL(トークン)と2.でRDBに保存されたダイジェストを比較し、ユーザーの真正性を確認する
  8. アプリケーションは、ユーザーにパスワード再設定用フォームを返す

「第11章 アカウントの有効化」との類似点と相違点

類似点

今回実装する「パスワードの再設定」と、第11章で実装した「ユーザーの有効化」には、以下のような類似点があります。

  • トークンとダイジェストの組を生成する
  • トークンを含むURLを生成する
  • ユーザーの登録メールアドレスにメールを送信する
  • トークンを含むURLへのGETリクエストをトリガーとして認証を行う

相違点

一方で、以下のような相違点もあります。

  • パスワードの再設定を実装する際には、既存のビューの変更を伴う
  • ビューを2つ新たに実装する必要がある
    • パスワード再設定用メールを受け取るメールアドレスの入力フォーム
    • 再設定するパスワードそのものの入力フォーム

モックアップ

上述「相違点」でビュー3つが登場しました。Railsチュートリアル本文においては、当該ビューのレイアウトについて、以下のモックアップが提示されています。

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

まるで魔法だな(パーシャルをrenderし繰り返し処理をする)

魔法かな??

これはユーザーの一覧を取得したい!!(単一のユーザーを表す部分をパーシャルとして分けたい)って時に知って驚いたこと

とりあえずeachを使ってみる

一覧表示ということでeachかな?ってことでこう書いた

<% @users.each do |user| %>
  <%= render user %>
<% end %>

実はこれ良くなくてパーシャルをrenderする際はeachを使うべきではないみたい
@usersという変数がコントローラーにあることが前提になってしまうので、再利用性が低くなってしまい、せっかくパーシャルに分けたメリットが薄くなってしまうため。

collectionを使ってみる

<%= render partial: 'users/user', collection: @users %>

eachがダメなのはわかったのでcollectionで書いてみた これはキタかな?

しかし残念☆    良いんだけど冗長!!!
どうすればいいんだ、、、

たどり着いた良いコード

  <%= render @users %>

えっこれだけで一覧表示できんの!?
最初はすごく驚いた。まるで魔法かなんかかと
実際これでrender先のパーシャル、_user.html.erb(単一のユーザーを表す部分)を持ってきてユーザーの一覧を表示することができた

これを機に。。。

・ 繰り返し処理の時はeach確定!みたいな思考にならなくなった
・ 可読性の高いコードを意識するようになった

最後に

これから一覧表示の課題に取り組むRUNTEQ生にネタバレを防ぐ形でユーザー一覧にしました
アドベントカレンダー素敵すぎてエモいです

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

Rails6のwebアプリをherokuでデプロイしてdb:migrateしたときのエラーを解決した話

目的

  • コマンドheroku run rake db:migrateを実行したときに出たエラーの解決したときの話をまとめる

エラー概要

  • herokuにpush後にコマンドheroku run rake db:migrateを実行した際に下記のエラーが出た。

    $ heroku run rake db:migrate
    Running rake db:migrate on ⬢ study-record... up, run.3764 (Free)
    rake aborted!
    Mysql2::Error::ConnectionError: Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2)
    /app/vendor/bundle/ruby/2.5.0/gems/mysql2-0.5.2/lib/mysql2/client.rb:90:in `connect'
    /app/vendor/bundle/ruby/2.5.0/gems/mysql2-0.5.2/lib/mysql2/client.rb:90:in `initialize'
    /app/vendor/bundle/ruby/2.5.0/gems/activerecord-6.0.0/lib/active_record/connection_adapters/mysql2_adapter.rb:24:in `new'
    /app/vendor/bundle/ruby/2.5.0/gems/activerecord-6.0.0/lib/active_record/connection_adapters/mysql2_adapter.rb:24:in `mysql2_connection'
    /app/vendor/bundle/ruby/2.5.0/gems/activerecord-6.0.0/lib/active_record/connection_adapters/abstract/connection_pool.rb:879:in `new_connection'
    /app/vendor/bundle/ruby/2.5.0/gems/activerecord-6.0.0/lib/active_record/connection_adapters/abstract/connection_pool.rb:923:in `checkout_new_connection'
    /app/vendor/bundle/ruby/2.5.0/gems/activerecord-6.0.0/lib/active_record/connection_adapters/abstract/connection_pool.rb:902:in `try_to_checkout_new_connection'
    /app/vendor/bundle/ruby/2.5.0/gems/activerecord-6.0.0/lib/active_record/connection_adapters/abstract/connection_pool.rb:863:in `acquire_connection'
    /app/vendor/bundle/ruby/2.5.0/gems/activerecord-6.0.0/lib/active_record/connection_adapters/abstract/connection_pool.rb:587:in `checkout'
    /app/vendor/bundle/ruby/2.5.0/gems/activerecord-6.0.0/lib/active_record/connection_adapters/abstract/connection_pool.rb:431:in `connection'
    /app/vendor/bundle/ruby/2.5.0/gems/activerecord-6.0.0/lib/active_record/connection_adapters/abstract/connection_pool.rb:1111:in `retrieve_connection'
    /app/vendor/bundle/ruby/2.5.0/gems/activerecord-6.0.0/lib/active_record/connection_handling.rb:231:in `retrieve_connection'
    /app/vendor/bundle/ruby/2.5.0/gems/activerecord-6.0.0/lib/active_record/connection_handling.rb:199:in `connection'
    /app/vendor/bundle/ruby/2.5.0/gems/activerecord-6.0.0/lib/active_record/tasks/database_tasks.rb:238:in `migrate'
    /app/vendor/bundle/ruby/2.5.0/gems/activerecord-6.0.0/lib/active_record/railties/databases.rake:85:in `block (3 levels) in <top (required)>'
    /app/vendor/bundle/ruby/2.5.0/gems/activerecord-6.0.0/lib/active_record/railties/databases.rake:83:in `each'
    /app/vendor/bundle/ruby/2.5.0/gems/activerecord-6.0.0/lib/active_record/railties/databases.rake:83:in `block (2 levels) in <top (required)>'
    /app/vendor/bundle/ruby/2.5.0/gems/rake-13.0.0/exe/rake:27:in `<top (required)>'
    /app/vendor/bundle/ruby/2.5.0/gems/bundler-2.0.2/lib/bundler/cli/exec.rb:74:in `load'
    /app/vendor/bundle/ruby/2.5.0/gems/bundler-2.0.2/lib/bundler/cli/exec.rb:74:in `kernel_load'
    /app/vendor/bundle/ruby/2.5.0/gems/bundler-2.0.2/lib/bundler/cli/exec.rb:28:in `run'
    /app/vendor/bundle/ruby/2.5.0/gems/bundler-2.0.2/lib/bundler/cli/exec.rb:28:in `run'
    /app/vendor/bundle/ruby/2.5.0/gems/bundler-2.0.2/lib/bundler/cli/exec.rb:28:in `run'
    /app/vendor/bundle/ruby/2.5.0/gems/bundler-2.0.2/lib/bundler/cli.rb:465:in `exec'
    /app/vendor/bundle/ruby/2.5.0/gems/bundler-2.0.2/lib/bundler/vendor/thor/lib/thor/command.rb:27:in `run'
    /app/vendor/bundle/ruby/2.5.0/gems/bundler-2.0.2/lib/bundler/vendor/thor/lib/thor/invocation.rb:126:in `invoke_command'
    /app/vendor/bundle/ruby/2.5.0/gems/bundler-2.0.2/lib/bundler/vendor/thor/lib/thor.rb:387:in `dispatch'
    /app/vendor/bundle/ruby/2.5.0/gems/bundler-2.0.2/lib/bundler/cli.rb:27:in `dispatch'
    /app/vendor/bundle/ruby/2.5.0/gems/bundler-2.0.2/lib/bundler/vendor/thor/lib/thor/base.rb:466:in `start'
    /app/vendor/bundle/ruby/2.5.0/gems/bundler-2.0.2/lib/bundler/cli.rb:18:in `start'
    /app/vendor/bundle/ruby/2.5.0/gems/bundler-2.0.2/exe/bundle:30:in `block in <top (required)>'
    /app/vendor/bundle/ruby/2.5.0/gems/bundler-2.0.2/lib/bundler/friendly_errors.rb:124:in `with_friendly_errors'
    /app/vendor/bundle/ruby/2.5.0/gems/bundler-2.0.2/exe/bundle:22:in `<top (required)>'
    /app/bin/bundle:104:in `load'
    /app/bin/bundle:104:in `<main>'
    Tasks: TOP => db:migrate
    (See full trace by running task with --trace)
    

エラーの予想

  • pushまでは問題なかったため、直前に実施したDB系の設定がおかしいのでは??と予想した。
  • ソケット系のエラーが出ているので、その辺の情報も怪しいと思った。

調査

  1. 直前に設定していたDB系の設定を確認

    • 下記コマンドを実行して設定を確認した。

      $ heroku config
      
- 上記コマンドの出力↓

    ```
    === study-record Config Vars
    CLEARDB_DATABASE_URL:     mysql://b60b5336b9085d:754f140c@us-cdbr-iron-east-05.cleardb.net/heroku_f69d4fc63e3b43f?reconnect=true
    DB_HOSTNAME:              us-cdbr-iron-east-05.cleardb.net
    DB_NAME:                  heroku_f69d4fc63e3b43f
    DB_PASSWORD:              754f140c
    DB_PORT:                  3306
    DB_USERNAME:              b60b5336b9085d
    LANG:                     en_US.UTF-8
    RACK_ENV:                 production
    RAILS_ENV:                production
    RAILS_LOG_TO_STDOUT:      enabled
    RAILS_SERVE_STATIC_FILES: enabled
    SECRET_KEY_BASE:          0d58d7c950379d4bfe741b3ae465b46aaa159ae398c2aaaea5010ae2d817b308a90a7d4702e6281ad609c94de1acf03cb50c1e39d21d9e66af636d812f2e823f
    ```

おかしいところ発見

  • 設定のCLEARDB_DATABASE_URLのURLがmysqlから始まっていることに気が付いた。
  • 自分が使用しているのmysql2なのにmysqlで良いのかと違和感があった。
  • 下記コマンドを実行してmysql2に修正した。

    $ heroku config:set DATABASE_URL='mysql2://b60b5336b9085d:754f140c@us-cdbr-iron-east-05.cleardb.net/heroku_f69d4fc63e3b43f?reconnect=true'
    
  • 再度下記コマンドを実行してheroku側でdb:migrateを行なったところ正常に実行できた。

    $ heroku run rake db:migrate
        Running rake db:migrate on ⬢ study-record... up, run.5104 (Free)
    D, [2019-12-10T15:16:34.677916 #4] DEBUG -- :    (2.4ms)  SET NAMES utf8mb4,  @@SESSION.sql_mode = CONCAT(CONCAT(@@sql_mode, ',STRICT_ALL_TABLES'), ',NO_AUTO_VALUE_ON_ZERO'),  @@SESSION.sql_auto_is_null = 0, @@SESSION.wait_timeout = 2147483
    D, [2019-12-10T15:16:34.715120 #4] DEBUG -- :    (2.2ms)  SELECT @@innodb_file_per_table = 1 AND @@innodb_file_format = 'Barracuda'
    D, [2019-12-10T15:16:34.732371 #4] DEBUG -- :    (16.9ms)  CREATE TABLE `schema_migrations` (`version` varchar(255) NOT NULL PRIMARY KEY) ROW_FORMAT=DYNAMIC
    D, [2019-12-10T15:16:34.752768 #4] DEBUG -- :    (13.9ms)  CREATE TABLE `ar_internal_metadata` (`key` varchar(255) NOT NULL PRIMARY KEY, `value` varchar(255), `created_at` datetime(6) NOT NULL, `updated_at` datetime(6) NOT NULL) ROW_FORMAT=DYNAMIC
    D, [2019-12-10T15:16:34.757508 #4] DEBUG -- :    (2.4ms)  SELECT GET_LOCK('3434563884671206245', 0)
    D, [2019-12-10T15:16:34.774649 #4] DEBUG -- :    (3.1ms)  SELECT `schema_migrations`.`version` FROM `schema_migrations` ORDER BY `schema_migrations`.`version` ASC
    I, [2019-12-10T15:16:34.775957 #4]  INFO -- : Migrating to CreateUsers (20191106122609)
    == 20191106122609 CreateUsers: migrating ======================================
    -- create_table(:users)
    D, [2019-12-10T15:16:34.795305 #4] DEBUG -- :    (15.9ms)  CREATE TABLE `users` (`id` bigint NOT NULL AUTO_INCREMENT PRIMARY KEY, `name` varchar(255), `email` varchar(255), `created_at` datetime(6) NOT NULL, `updated_at` datetime(6) NOT NULL) ROW_FORMAT=DYNAMIC
       -> 0.0168s
    == 20191106122609 CreateUsers: migrated (0.0169s) =============================
    
    D, [2019-12-10T15:16:34.811775 #4] DEBUG -- :    (4.0ms)  BEGIN
    D, [2019-12-10T15:16:34.814482 #4] DEBUG -- :   primary::SchemaMigration Create (2.6ms)  INSERT INTO `schema_migrations` (`version`) VALUES ('20191106122609')
    D, [2019-12-10T15:16:34.816842 #4] DEBUG -- :    (2.2ms)  COMMIT
    I, [2019-12-10T15:16:34.816964 #4]  INFO -- : Migrating to CreatePosts (20191110100157)
    == 20191110100157 CreatePosts: migrating ======================================
    -- create_table(:posts)
    D, [2019-12-10T15:16:34.832775 #4] DEBUG -- :    (15.0ms)  CREATE TABLE `posts` (`id` bigint NOT NULL AUTO_INCREMENT PRIMARY KEY, `content` text, `created_at` datetime(6) NOT NULL, `updated_at` datetime(6) NOT NULL) ROW_FORMAT=DYNAMIC
       -> 0.0154s
    == 20191110100157 CreatePosts: migrated (0.0155s) =============================
    
    D, [2019-12-10T15:16:34.835642 #4] DEBUG -- :    (2.3ms)  BEGIN
    D, [2019-12-10T15:16:34.838096 #4] DEBUG -- :   primary::SchemaMigration Create (2.3ms)  INSERT INTO `schema_migrations` (`version`) VALUES ('20191110100157')
    D, [2019-12-10T15:16:34.840385 #4] DEBUG -- :    (2.1ms)  COMMIT
    I, [2019-12-10T15:16:34.840476 #4]  INFO -- : Migrating to AddStudyTimeHashTagToPosts (20191112145912)
    == 20191112145912 AddStudyTimeHashTagToPosts: migrating =======================
    -- add_column(:posts, :study_time, "decimal")
    D, [2019-12-10T15:16:34.861032 #4] DEBUG -- :    (19.9ms)  ALTER TABLE `posts` ADD `study_time` decimal
       -> 0.0202s
    -- add_column(:posts, :hash_tag, "text")
    D, [2019-12-10T15:16:34.877828 #4] DEBUG -- :    (16.4ms)  ALTER TABLE `posts` ADD `hash_tag` text
       -> 0.0168s
    == 20191112145912 AddStudyTimeHashTagToPosts: migrated (0.0371s) ==============
    
    D, [2019-12-10T15:16:34.880640 #4] DEBUG -- :    (2.2ms)  BEGIN
    D, [2019-12-10T15:16:34.882939 #4] DEBUG -- :   primary::SchemaMigration Create (2.1ms)  INSERT INTO `schema_migrations` (`version`) VALUES ('20191112145912')
    D, [2019-12-10T15:16:34.885466 #4] DEBUG -- :    (2.3ms)  COMMIT
    D, [2019-12-10T15:16:34.898276 #4] DEBUG -- :   ActiveRecord::InternalMetadata Load (2.7ms)  SELECT `ar_internal_metadata`.* FROM `ar_internal_metadata` WHERE `ar_internal_metadata`.`key` = 'environment' LIMIT 1
    D, [2019-12-10T15:16:34.909203 #4] DEBUG -- :    (2.3ms)  BEGIN
    D, [2019-12-10T15:16:34.911642 #4] DEBUG -- :   ActiveRecord::InternalMetadata Create (2.3ms)  INSERT INTO `ar_internal_metadata` (`key`, `value`, `created_at`, `updated_at`) VALUES ('environment', 'production', '2019-12-10 15:16:34.906194', '2019-12-10 15:16:34.906194')
    D, [2019-12-10T15:16:34.914188 #4] DEBUG -- :    (2.3ms)  COMMIT
    D, [2019-12-10T15:16:34.916800 #4] DEBUG -- :    (2.4ms)  SELECT RELEASE_LOCK('3434563884671206245')
    

自分用メモ

  • db:migrate系のエラーはだいたい設定のミスなのでよく見直すこと。

付録

  • 誤っていたBDの設定ファイルと正常なDBの設定ファイルを下記に記載する。
  • 誤っていたDBの設定ファイル↓

    CLEARDB_DATABASE_URL:     mysql://b60b5336b9085d:754f140c@us-cdbr-iron-east-05.cleardb.net/heroku_f69d4fc63e3b43f?reconnect=true
    DB_HOSTNAME:              us-cdbr-iron-east-05.cleardb.net
    DB_NAME:                  heroku_f69d4fc63e3b43f
    DB_PASSWORD:              754f140c
    DB_PORT:                  3306
    DB_USERNAME:              b60b5336b9085d
    LANG:                     en_US.UTF-8
    RACK_ENV:                 production
    RAILS_ENV:                production
    RAILS_LOG_TO_STDOUT:      enabled
    RAILS_SERVE_STATIC_FILES: enabled
    SECRET_KEY_BASE:          0d58d7c950379d4bfe741b3ae465b46aaa159ae398c2aaaea5010ae2d817b308a90a7d4702e6281ad609c94de1acf03cb50c1e39d21d9e66af636d812f2e823f
    
  • 正常なDBの設定ファイル↓

    CLEARDB_DATABASE_URL:     mysql2://b60b5336b9085d:754f140c@us-cdbr-iron-east-05.cleardb.net/heroku_f69d4fc63e3b43f?reconnect=true
    DB_HOSTNAME:              us-cdbr-iron-east-05.cleardb.net
    DB_NAME:                  heroku_f69d4fc63e3b43f
    DB_PASSWORD:              754f140c
    DB_PORT:                  3306
    DB_USERNAME:              b60b5336b9085d
    LANG:                     en_US.UTF-8
    RACK_ENV:                 production
    RAILS_ENV:                production
    RAILS_LOG_TO_STDOUT:      enabled
    RAILS_SERVE_STATIC_FILES: enabled
    SECRET_KEY_BASE:          0d58d7c950379d4bfe741b3ae465b46aaa159ae398c2aaaea5010ae2d817b308a90a7d4702e6281ad609c94de1acf03cb50c1e39d21d9e66af636d812f2e823f
    
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む