マイグレーション

http://railsdoc.com/references/rake%20db:migrate

実行

$ bundle exec rake db:migrate
バージョン指定
$ bundle exec rake db:migrate VERSION=201010190000
環境指定
$ bundle exec rake db:migrate RAILS_ENV=test

現在のバージョン確認

$ bundle exec rake db:version

前回のマイグレーションを取り消す

$ bundle exec rake db:rollback

CarrierWaveのファイル名変換(original_filename)の挙動

ファイルアップロード機構にCarrierWaveを利用しています。
アップロードしたファイル名は、

self.file_column.file.original_filename

のように取得出来ますが、デフォルトでは日本語を利用出来ません。
ファイル名に日本語を利用する場合、
./config/initializers/carrierwave.rb

CarrierWave::SanitizedFile.sanitize_regexp = /[^[:word:]\.\-\+]/

のように設定します。
ここまではググればすぐ出てくる情報なのですが、
「Simple (1).csv」というファイル名が、「Simple__1_.csv」に変換されることが分かりました。

CarrierWaveのコードを見てみると、先の設定は、
./vendor/bundles/ruby/2.1.0/gems/carrierwave-0.10.0/lib/carrierwave/sanitized_file.rb

    def sanitize(name)
      name = name.gsub("\\", "/") # work-around for IE
      name = File.basename(name)
      name = name.gsub(sanitize_regexp,"_")
      name = "_#{name}" if name =~ /\A\.+\z/
      name = "unnamed" if name.size == 0
      return name.mb_chars.to_s
    end

上記の、

name = name.gsub(sanitize_regexp,"_")

で変換されます。
指定した正規表現を「_」に変換するわけです。
[:word:]はPOSIX文字クラスで「単語構成文字」を表現する正規表現になりますので、ここを調整すると修正出来そうです。

http://docs.ruby-lang.org/ja/1.9.3/doc/spec=2fregexp.html
調べたところ、

[:print:] 表示可能な文字(空白を含む)

という文字クラスがあり、これを使えばファイル名に制限を無くすことが出来るはず。

./config/initializers/carrierwave.rb

CarrierWave::SanitizedFile.sanitize_regexp = /[^[:print:]]/

としたところ予想通り変換を回避する事が出来たのですが、念のため色々な記号をファイル名に使い試したところ、「+」が「半角スペース」に変換されてしまう事が分かりました。

どうもコードの意図通りに動いていないようです。

pry(main)> "+".gsub("[^[:print:]]","_")
=> "+"

やはり変換されない。

./vendor/bundles/ruby/2.1.0/gems/carrierwave-0.10.0/lib/carrierwave/sanitized_file.rb

    def sanitize(name)
      name = name.gsub("\\", "/") # work-around for IE
      name = File.basename(name)
      name = name.gsub(sanitize_regexp,"_")
      name = "_#{name}" if name =~ /\A\.+\z/
      name = "unnamed" if name.size == 0
      return name.mb_chars.to_s
    end

に渡ってくるパラメータ(name)を見てみたところ、すでに「+」が半角スペースになっていました。
これはコアの動きっぽいと感じつつ、小一時間デバックをしてみたところ原因が分かりました。

./vendor/bundles/ruby/2.1.0/gems/rack-1.5.2/lib/rack/multipart/parser.rb

      def get_filename(head)
        filename = nil
        if head =~ RFC2183
          filename = Hash[head.scan(DISPPARM)]['filename']
          filename = $1 if filename and filename =~ /^"(.*)"$/
        elsif head =~ BROKEN_QUOTED
          filename = $1
        elsif head =~ BROKEN_UNQUOTED
          filename = $1
        end
        if filename && filename.scan(/%.?.?/).all? { |s| s =~ /%[0-9a-fA-F]{2}/ }
          filename = Utils.unescape(filename)
        end
        if filename && filename !~ /\\[^\\"]/
          filename = filename.gsub(/\\(.)/, '\1')
        end
        filename
      end

上記の、

filename = Utils.unescape(filename)

が犯人でした。
このメソッドの実態は、
./vendor/bundles/ruby/2.1.0/gems/rack-1.5.2/lib/rack/utils.rb

    if defined?(::Encoding)
      def unescape(s, encoding = Encoding::UTF_8)
        URI.decode_www_form_component(s, encoding)
      end
    else
      def unescape(s, encoding = nil)
        URI.decode_www_form_component(s, encoding)
      end
    end
    module_function :unescape

で、この中の、

URI.decode_www_form_component

が変換処理を行っていました。
http://docs.ruby-lang.org/ja/2.0.0/method/URI/s/decode_www_form_component.html
ドキュメントを見ると、「"+" という文字は空白文字にデコードします」とあります。

すっきりしました。

ファイル名に"+"が使えないのはRails(Rack)の仕様という事でよさそうです。

GithubのイベントをChatworkに通知する

ChabotというChatworkが提供しているアプリを使います。

http://c-note.chatwork.com/post/69274738468/chabot
上記公式情報に説明がなかった点、修正が必要だった点をご紹介します。

  • Apacheリバースプロキシの設定
  • 不具合修正
  • Githubの設定

Node環境整備

まずNode.jsの環境を作ります。

$ curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.29.0/install.sh | bash
$ nvm install v5.1
$ npm install express (なくてもよいかな)
$ nvm alias default v5.1.1
パスを通す
$ vi .bash_profile
if [[ -s ~/.nvm/nvm.sh ]];
 then source ~/.nvm/nvm.sh
fi

chabotインストール

$ npm install chabot -g

アプリ作成

$ cd ./path/to/app
$ chabot create chabot -d ./

以上でアプリが作成され、Github用のbot設定がデフォルトで用意されます。

不具合の修正

bots/github.js
Github用のプログラムが作成されますが、このままではエラーが出て動きませんでした。

module.exports = function (chabot) {
    // WebHook で受けたデータをセット
    var payload = JSON.parse(chabot.data.payload);
    // ChatWork API の endpoint をセット
    var endpoint = '/rooms/' + chabot.roomid + '/messages';
    // templats/ 内のメッセージテンプレートを読み込む
    var template = chabot.readTemplate('github.ejs');
    // WebHook で受けたデータでメッセージテンプレートを描画
    var message_body = chabot.render(template, payload);

    // ChatWork API でメッセージ送信
    chabot.client
        .post(endpoint, {
            body: message_body
        })
        .done(function (res) {
            chabot.log('done');
        })
        .fail(function (err) {
            chabot.error(err);
        });
};

どうも、chabot.roomidに値がセットされないようです。

module.exports = function (chabot) {

    chabot.roomid = 123456789;

    // WebHook で受けたデータをセット
    var payload = JSON.parse(chabot.data.payload);
    // ChatWork API の endpoint をセット
    var endpoint = '/rooms/' + chabot.roomid + '/messages';
    // templats/ 内のメッセージテンプレートを読み込む
    var template = chabot.readTemplate('github.ejs');
    // WebHook で受けたデータでメッセージテンプレートを描画
    var message_body = chabot.render(template, payload);

    // ChatWork API でメッセージ送信
    chabot.client
        .post(endpoint, {
            body: message_body
        })
        .done(function (res) {
            chabot.log('done');
        })
        .fail(function (err) {
            chabot.error(err);
        });
};

のようにroomidを書いてあげる事で解決しました。

Githubの設定

リポジトリのメニュー、

Settings > Webhooks & services > Add webhook

からアプリURLを登録しますが、Content typeを「application/json」にするとパースエラーが発生しました。
「application/x-www-form-urlencoded」とすると正常に動くのですが、NodeアプリなのでJsonで動いて欲しいところです。。

アプリの初期設定では、Pushイベントのみの対応となっています。
templates/github.ejs

IssueとPull Requestへのコメントも通知したかったので、追記したテンプレートが、
https://github.com/t-shida/chabot-github-template
になります。

Githubの設定としては、

Which events would you like to trigger this webhook? > Let me select individual events. 
  • Push
  • Issues
  • Issue comment
  • Pull request review comment

にチェックを入れます。

Apacheの設定

アプリをポート8080で起動する想定で、バーチャルホストを以下のように設定

<VirtualHost *:80>
  ServerName chabot.domain.jp
  ProxyPass / http://localhost:8080/
  ProxyPassReverse / http://localhost:8080/
  ProxyPreserveHost On
</VirtualHost>

ProxyPreserveHostがポイントです。これがないと動きません。

foreverインストール

$ cd ./path/to/app
$ node app.js

で起動出来ますが、実際の運用ではデーモンとして動かしたいですので、foreverを利用します。

$ npm install forever -g
$ forever start app.js

とします。


あまりメンテされていない印象ですが、以上の内容で正常に動作しており、メールでの確認が億劫なメンバーには好評です。

AASMからRadioボタンを描画する

モデルのAASMはこう。

  aasm column: "approving_state" do
    state :applied
    state :tested
    state :rejected
    state :approved, initial: true

    event :apply, after: :send_apply do
      transitions from: [:tested, :rejected, :approved], to: :applied
    end

    event :test, after: :send_test do
      transitions from: :applied, to: :tested
    end

    event :reject do
      transitions from: [:applied, :tested], to: :rejected
    end

    event :approve, after: :send_approve do
      transitions from: :tested, to: :approved
    end
  end

これをViewにて表示

        <% 
          v = []
          Company.aasm.states.each do |s| 
            next if s.state_machine.config.column.to_s != 'approving_state'
            state = s.name.to_s
            label = t("activerecord.attributes.company.approving_states.#{state}")
            event = ''
            s.state_machine.events.each_key do |k|
              if s.state_machine.events[k].transitions[0].to.to_s == state
                event = k.to_s
                break
              end 
            end
            disabled = true
            disabled = false if @company.send("may_#{event}?") || @company.approving_state == state
            disabled = true if state == 'applied' && @company.approving_state != 'applied'
            checked = @company.approving_state == state
            v.push({:state => state, :label => label, :event => event, :disabled => disabled, :checked => checked})
          end 
        %>
        <% v.each do |r| %>
          <div class="radio-inline">
            <%= f.radio_button :approving, r[:state], {:disabled => r[:disabled], :checked => r[:checked]} %><%= f.label "approving_#{r[:state]}".to_sym, r[:label] %>
          </div>
        <% end %>

却下されたけどね。

コンソール

$ rails console -e development
$ rails console -e development --sandbox
$ Rails.env

コンソールリロード

pry(main)> reload!

routes確認

pry(main)> show-routes

スキーマ確認

pry(main)> show-models

SQLを非表示

pry(main)> ActiveRecord::Base.logger = nil

個別のパスを確認

[1] pry(main)> app.api_hoge_path
=> "/api/hoge"
[2] pry(main)> app.api_hoge_url
=> "http://www.example.com/api/hoge"

メール送信

[16] pry(main)> ActionMailer::Base.mail(to: "mail@gmail.com", from: "mail@gmail.com", subject: "test", body: "hoge").deliver

DB接続

rails db
rails db -e production

SQLダンプ出力

$ bundle exec rake db:structure:dump
$ bundle exec rake db:structure:dump RAILS_ENV=production

文字コードをUTF8に揃える

show variables like "chara%";

                                                                                                                • +
Variable_name Value
                                                                                                                • +
character_set_client utf8
character_set_connection utf8
character_set_database latin1
character_set_filesystem binary
character_set_results utf8
character_set_server utf8
character_set_system utf8
character_sets_dir /usr/share/mysql/charsets/
                                                                                                                • +

character_set_client : クライアント側で発行したsql文はこの文字コードになる
character_set_connection : クライアントから受け取った文字をこの文字コードへ変換する
character_set_database : 現在参照しているDBの文字コード
character_set_results : クライアントへ送信する検索結果はこの文字コードになる
character_set_server : DB作成時のデフォルトの文字コード
character_set_system : システムの使用する文字セットで常にutf8が使用されている


/etc/my.cnf

[mysqld]
character-set-server=utf8 #mysqldセクションの末尾に追加
[client]
default-character-set=utf8 #clientセクションを追加

/etc/init.d/mysqld restart