railsでboolean値のvalidationをする方法

ちょっと罠にかかりました。

validates :publish, presence: true

いつも通りpresence: trueを書いていました。

こうすると、errorになります。

なぜ、そんなことが起こるかです。

rails/presence.rb at d57356bd5ad0d64ed3fb530d722f32107ea60cdf · rails/rails · GitHub

  def validates_presence_of(*attr_names)
    validates_with PresenceValidator, _merge_attributes(attr_names)
  end

https://github.com/rails/rails/blob/47eadb68bfcae1641b019e07e051aa39420685fb/activemodel/test/models/person_with_validator.rb#L6

class PresenceValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    record.errors[attribute] << "Local validator#{options[:custom]}" if value.blank?
  end
end

value.blank?になっていますね。

[1] pry(main)> false.blank?
=> true
[2] pry(main)> true.blank?
=> false

この結果、falseの場合に反応しています。

なので、別のvalidationを使います。

validates :publish, inclusion: { in: [true, false] }

これでtrue or falseが存在する場合に反応するようになります。

意外なところで存在を知った。

SQLでクロス集計をしてみる

SQLって表も簡単に作成できるんだなって最近感動しております。

今回やろうとしていることは、クロス集計です。

2000 10 10 8 9
2001 9 10 8 9
2002 10 10 8 10

年毎の作品を集計したいとかありそうじゃないですか。

ということで、やってみました。

テーブルの構成

mysql> show columns from works;
+------------------------+--------------+------+-----+---------+----------------+
| Field                  | Type         | Null | Key | Default | Extra          |
+------------------------+--------------+------+-----+---------+----------------+
| id                     | bigint(20)   | NO   | PRI | NULL    | auto_increment |
| title                  | varchar(255) | NO   |     | NULL    |                |
| season_year            | int(11)      | NO   |     | NULL    |                |
| season                 | int(11)      | NO   |     | NULL    |                |
| created_at             | datetime     | NO   |     | NULL    |                |
| updated_at             | datetime     | NO   |     | NULL    |                |
+------------------------+--------------+------+-----+---------+----------------+

season_yaerに2017

seasonに「0 = 春」「1 = 夏」「2 = 秋」「3 = 冬」

こういうデータが入っています。

SQL

春夏秋冬をどうするかということになります。

ここでcase文を使用します。

select season_year as 年,
      count(case when season = 0 then 1 else null end) as 春,
      count(case when season = 1 then 1 else null end) as 夏,
      count(case when season = 2 then 1 else null end) as 秋,
      count(case when season = 3 then 1 else null end) as 冬
  from works
 group by season_year;
      count(case when season = 0 then 1 else null end) as 春,←ここが大事。

case文であればcountして、なければcountしないnullを入れます。

+------+-----+-----+-----+-----+
| 年   | 春  | 夏  | 秋  | 冬  |
+------+-----+-----+-----+-----+
| 2000 |  17 |  15 |  12 |  16 |
| 2001 |  10 |   8 |  10 |  10 |
| 2002 |  12 |  16 |  10 |  11 |
| 2003 |  15 |  18 |  10 |  16 |
| 2004 |  21 |  15 |  15 |  14 |
| 2005 |  12 |  20 |  17 |  11 |
| 2006 |  19 |  18 |  14 |  15 |
| 2007 |  15 |  18 |  19 |  13 |
| 2008 |  16 |  18 |  27 |  20 |
| 2009 |  28 |  23 |  26 |  26 |
+------+-----+-----+-----+-----+

結果はこんな感じになります。

ここからさらに発展させます。

全体・前期・後期で知りたいんだってパターンですね。

select season_year as 年,
      count(case when season = 0 then 1 else null end) as 春,
      count(case when season = 1 then 1 else null end) as 夏,
      count(case when season = 2 then 1 else null end) as 秋,
      count(case when season = 3 then 1 else null end) as 冬,
      count(season) as 全体,
      count(case when season in(0, 1) then 1 else null end) as 前期,
      count(case when season in(2, 3) then 1 else null end) as 後期
  from works
 group by season_year;

inで条件分岐してあげます。

+------+-----+-----+-----+-----+--------+--------+--------+
| 年   | 春  | 夏  | 秋  | 冬  | 全体   | 前期   | 後期   |
+------+-----+-----+-----+-----+--------+--------+--------+
| 2000 |  17 |  15 |  12 |  16 |     60 |     32 |     28 |
| 2001 |  10 |   8 |  10 |  10 |     38 |     18 |     20 |
| 2002 |  12 |  16 |  10 |  11 |     49 |     28 |     21 |
| 2003 |  15 |  18 |  10 |  16 |     59 |     33 |     26 |
| 2004 |  21 |  15 |  15 |  14 |     65 |     36 |     29 |
| 2005 |  12 |  20 |  17 |  11 |     60 |     32 |     28 |
| 2006 |  19 |  18 |  14 |  15 |     66 |     37 |     29 |
| 2007 |  15 |  18 |  19 |  13 |     65 |     33 |     32 |
| 2008 |  16 |  18 |  27 |  20 |     81 |     34 |     47 |
| 2009 |  28 |  23 |  26 |  26 |    103 |     51 |     52 |
+------+-----+-----+-----+-----+--------+--------+--------+

以上です。

case文がこんな使い方あるとはなーって感じです。

Deviseでユーザー登録後のリダイレクト先を変更する

Deviseのカスタマイズに少し苦労しました。

大体標準通りに使えば問題ないのかもしれませんが、少し加工しようとするとちょっと調べないといけないですね。

今回行うことは、Deviseでユーザー登録後に、Thanksページに飛ばすことです。

リダイレクト自体は割と簡単なのですが、コントローラーにアクションを追加するのに手間取りましたorz

コントローラーをカスタマイズする

今回のDeviseはユーザーモデルをベースとして考えています。

まずはカスタマイズするために、下記のコマンドを打ち込みます。

rails generate devise:controllers Users

これでテンプレートが作成されるかと思います。

変更するコントローラーはclass Users::RegistrationsController < Devise::RegistrationsControllerになります。

thanksアクションを入れます。

class Users::RegistrationsController < Devise::RegistrationsController
  # before_action :configure_sign_up_params, only: [:create]
  # before_action :configure_account_update_params, only: [:update]

  # GET /resource/sign_up
  # def new
  #   super
  # end

  # POST /resource
  def create
    super
  end

  # GET /resource/edit
  # def edit
  #   super
  # end

  # PUT /resource
  # def update
  #   super
  # end

  # DELETE /resource
  # def destroy
  #   super
  # end

  # GET /resource/cancel
  # Forces the session data which is usually expired after sign
  # in to be expired now. This is useful if the user wants to
  # cancel oauth signing in/up in the middle of the process,
  # removing all OAuth session data.
  # def cancel
  #   super
  # end

  def thanks
  end

  # protected

  # If you have extra params to permit, append them to the sanitizer.
  # def configure_sign_up_params
  #   devise_parameter_sanitizer.permit(:sign_up, keys: [:attribute])
  # end

  # If you have extra params to permit, append them to the sanitizer.
  # def configure_account_update_params
  #   devise_parameter_sanitizer.permit(:account_update, keys: [:attribute])
  # end

  # The path used after sign up.
  # def after_sign_up_path_for(resource)
  #   super(resource)
  # end

  # The path used after sign up for inactive accounts.
  # def after_inactive_sign_up_path_for(resource)
  #   super(resource)
  # end
end

routesを設定する

ここが少し戸惑いました。

  devise_for :users, controllers: {
    registrations: 'users/registrations'
  }

上記を追加して、registrationsの動作をusers/registartionsに設定します。

次に、アクションをthanksページのroutesを追加します。

  devise_scope :user do
    get 'users/thanks' => 'users/registrations#thanks'
  end

devise_scopeで設定しないと、request.env[“devise.mapping”]でroutesの設定がされません。

https://github.com/plataformatec/devise/blob/88724e10adaf9ffd1d8dbfbaadda2b9d40de756a/lib/devise/rails/routes.rb#L361

    def devise_scope(scope)
      constraint = lambda do |request|
        request.env["devise.mapping"] = Devise.mappings[scope]
        true
      end

      constraints(constraint) do
        yield
      end
    end

こんな常識知らないよって感じですが、設定しましょう。

ユーザー登録後のリダイレクト先を変更する

  def after_inactive_sign_up_path_for(resource)
    users_thanks_path
  end

users/registarationコメントアウトされていると思います。

ここに追加すれば終わりです。

まとめ

ドキュメントとソースコードを見よう。

find_by_カラム名のメソッドが動的に生成されていたという事実

例えば、Fooモデルにtitleというカラムがあるとする。

Foo.find_by(titile: "foo")

Foo.find_by_title("foo")

下でも検索できる。

いつも上ばっかり使っていたので、下のパターンがあるとは知らなかった。

補足

rubocop的には上の方がいい。

Class: RuboCop::Cop::Rails::DynamicFindBy — Documentation for rubocop (0.49.1)

rails-syle-guide的に非推奨なので。

ttyとptsについて

dokcer runのオプションで意味不明だったので、調べていました。

docker run -it ←こいつ

  % docker run --help
  -i, --interactive                    Keep STDIN open even if not attached
  -t, --tty                            Allocate a pseudo-TTY

ここでTTYが出てきました。

TTYとは

ttyとは、標準入出力となっている端末デバイス(制御端末、controlling terminal)の名前を表示するUnix系のコマンドである。 元来ttyとはteletypewriter(テレタイプライター)のことを指す。

tty - Wikipedia

ターミナルで打つとこうなります。

  % tty
/dev/ttys004

実際にターミナルで打っている端末の情報ですね。

よくある遊びです。

ここでターミナルを別のウィンドウで立ち上げます。

  % tty  ←ターミナル1
/dev/ttys004

  % tty  ←ターミナル2
/dev/ttys006

ターミナル1のウィンドウから下記コマンドを実行する

% echo "Hello tty" > /dev/ttys006

  % Hello tty←ターミナル2

これを実験するとどういうものかわかりやすいと思います。

pts

ptsはsshでログインした時の端末の情報です。

tty
/dev/pts/0

ということは、誰かがsshで同時接続していた場合に、相手をびっくりさせることができますね。

【参考】

d.hatena.ne.jp

ローカル環境のURLをlocalhost以外にする方法

localhostってださいなって思う時があるじゃないですか?

そんな時に名前をhostを変える方法です。

/etc/hostsがあると思います。

この設定ファイルに付け足します。

127.0.0.1 foo.com

これでrails sするとhttp:foo.com:3000でアクセスできます。

:3000がいるのはポートの関係ですね。

こうすると、「こいつわかってるやつじゃね?」って感じられそう。

以上です。

CSVでファイルを保存するなら、jsonで保存する方が楽ということに気づいた

CSVでhashの値を保存する場合分解しないといけないじゃないですか?

めんどくさいですよね。

例えばこういうデータです。

class FooApi
  HEADER = %w(foo bar baz)

  def export
    CSV.open('foo.csv', 'w:utf-8', headers: HEADERS, write_headers: true, force_quotes: true) do |csv|
      results.each do |result|
        line = [
          result[:foo],
          result[:bar],
          result[:baz]
        ]
        csv.puts line
      end
    end
  end

  private

  def results
    [ {
        foo: 1,
        bar: 2,
        baz: [1, 2]
      },
      {
        foo: 3,
        bar: 4,
        baz: [3, 4]
      },
    ]
  end
end

これよりももっとカラムが増えることなんて多いですよ。

HEADERとか考えるのめんどくさいなって思いました。

そこでjsonで書き出せば何も考えなくていいことに気づきました。

  def export
    FIle.open('foo.json', 'w') { |f| f.puts results }
  end

これで全てが終わります。

  def import
    @x = open('foo.json') {|f| JSON.load(f)}
  end

JSONの場合、keyがStringになって嫌だ!ってことがあると思います。

そんな時のためのwith_indifferent_accessです。

ActiveSupport頼みになりますが、これでアクセスできるようになります。

  @x.map!(&:with_indiffrent)access)

もちろん、クライアントによってはjsonって何?ってお客さんも多いでしょう。

ただ、自分の作業やjsonの意味がわかる人なら、ガンガン使うべきだなって思いました。

以上です。

正規表現で〜から〜までのパターン

こういうやつです。

宮藤官九郎(第1話、第2話、最終話)で名前と後ろを分けたい場合。

(?<name>.[^((]*)(?<supplement>.*)?

否定を間に挟んで、それ以外までにする。

これがミソ。

このパターン多いと思うし、覚えておこ。

正規表現で名前付きキャプチャが便利だった

これがあれば何にマッチさせているかわかるじゃん!って感動しました。

正規表現でキャプチャするときに$1で変数格納されますが、この場合何にマッチさせているのかがわかりづらいときがあります。 何より量が増えてくるとしんどいです。

使い方

(?<name>正規表現を書いてください)

サンプルです。

text =  ["塚本連平(MMJ)", "小松隆志(MMJ)", "六車俊治(テレビ朝日)"]
text.split(/\s/).map do |director|
  m = director.match(/(?<name>.*)(?<organization>.*)/)
  {
    name: m[:name],
    organization: m[:organization]
  }
end

=> [{:name=>"塚本連平", :organization=>"(MMJ)"}, {:name=>"小松隆志", :organization=>"(MMJ)"}, {:name=>"六車俊治", :organization=>"(テレビ朝日)"}]

何をやっているのかがわかりやすいです。

$1 よりも圧倒的にやりやすい。

今後とも使っていきます。

rubyでeach_with_indexでindexの値を0以外から始める方法

rubyでeach x indexをしたいパターンが多いです。

そのために、each_with_indexというメソッドがあります。

each_with_index

こちらは基本的に初期値が0からになります。

array = %w(foo bar baz)
array.each_with_index do |x, index|
  puts "#{index}番目#{x}"
end

=>
0番目foo
1番目bar
2番目baz

1から始めたいという場合は、下記のようにすることで回避できます。

array = %w(foo bar baz)
array.each_with_index do |x, index|
  puts "#{index + 1}番目#{x}"
end

=>
1番目foo
2番目bar
3番目baz

ただ、いちいち+ nを書かないといけないので、めんどくさいです。

each.with_index

そこで、each.with_indexを使用します。

こちらは()を入れることで開始位置を指定できます。

array = %w(foo bar baz)
array.each.with_index(1) do |x, index|
  puts "#{index}番目#{x}"
end
=>
1番目foo
2番目bar
3番目baz

まとめ

indexを0から始める場合は、each_with_index 0以外から始める場合は、each.with_index(n)をするのがいいのかなと個人的には思いました。