Railsで綺麗なURLにしたいと思うと一つのControllerに機能が集中して困ることがあります。

/comments
/posts/1/comments
/users/1/comments
# config/routes.rb:
Foo::Application.routes.draw do
  resources :comments
  resources :posts do
    resources :comments
  end
  resources :users do
    resources :comments
  end
end

例えばこんな風にしたい時。

# app/controllers/comments_controller.rb:
class CommentsController < ApplicationController
  def index
    @comments =
      if params[:post_id]
        Post.find(params[:post_id]).comments
      elsif params[:user_id]
        User.find(params[:user_id]).comments
      else
        Comment.all
      end
  end
end

こんな風に書く?えーキモーイ。そもそもそれぞれの場合でviewが全然違うんですけどーみたいな場合。

そんなんねぇ俺の糞みたいな悩みはねぇStack Overflowさんに聞けば一発なんですよ。

Rails Namespace vs. Nested Resource - Stack Overflow

controllerのnamespaceでスッキリ書けるみたいです。

/comments
/posts/1/comments
/users/1/comments
# config/routes.rb:
Foo::Application.routes.draw do
  resources :comments
  resources :posts do
    resources :comments, controller: 'posts/comments'
  end
  resources :users do
    resources :comments, controller: 'users/comments'
  end
end
# app/controllers/comments_controller.rb:
class CommentsController < ApplicationController
  def index
    @comments = Comment.all
  end
end

# app/controllers/posts/comments_controller.rb:
class Posts::CommentsController < ApplicationController
  def index
    @comments = Post.find(params[:post_id]).comments
  end
end

# app/controllers/users/comments_controller.rb:
class Users::CommentsController < ApplicationController
  def index
    @comments = User.find(params[:user_id]).comments
  end
end
$ rake routes
(snip)
comments GET    /comments(.:format)                       comments#inde
post_comments GET    /posts/:post_id/comments(.:format)     posts/comments#index
user_comments GET    /users/:user_id/comments(.:format)     users/comments#index
(snip)

おおお、これはスッキリ!

Stack Overflow脳の恐怖。

Railsの設定ファイルを環境毎に書けるプラグインをいつも見失う。なんて名前だったっけ?

rails configとか検索し辛いワードなのでここに記す。(via @fakestarbabyさん)

railsjedi/rails_config - GitHub

SinatraやPadrinoに対応してるのにrails_configとは是如何に。

production環境ではexception_notificationでエラーをメールしてる人も多い思います。(AirBreakとかも)

怖話でもそうやってエラーメールが一通も来ない状態にしたいんですが、例えば下記のような”バグじゃないエラー”は皆様どうやって対応してるんでしょう?

A ActionView::MissingTemplate occurred in home#index:

 Missing template home/index, application/index with {:handlers=>[:erb, :builder, :haml], :formats=>["*/*;q=0.01"], :locale=>[:ja, :ja]}. Searched

formatsに*/*;q=0.01なんて文字列を送ってくるのは単なるスパム。使ってるformatだけに限定するように何か書く?うーん・・・。

怖話をruby1.8.7 & rails3.0.8からruby1.9.2 & rails3.1.1の最新状態に更新しました。引っかかったところメモ。

yamlのエラー

$ rake db:migrate
rake aborted!
couldn't parse YAML at line 267 column 42

rake db:migrateがエラー。

require 'yaml' YAML::ENGINE.yamler= 'syck'

config/boot.rbの2行目に下記を追加。

require 'yaml'
YAML::ENGINE.yamler = 'syck'

日本語

db/seeds.rbで日本語を使ってたので先頭に下記magick commentを追加。

# coding: utf-8

何でもかんでもmagick commentを入れるのは良くないので必要なところにだけ。rubyのm17nは素晴らしいと思います。

Array#choice

ruby1.9.2ではArray#choiceが無くなってる。Array#sampleを使えばいい。

Fixtures

FixturesがActiveRecord::Fixturesになりました。

db/seeds.rbでfixtureを読み込む場合(test用のとは別に用意してる場合とか)

require 'active_record/fixtures'
Fixtures.create_fixtures("#{Rails.root}/db/seed", 'stories')

こんな感じだったんですが、

require 'active_record/fixtures'
ActiveRecord::Fixtures.create_fixtures("#{Rails.root}/db/seed", 'stories')

こうなりました。

Asset Pipeline

developmentで余裕こいてるとproduction世界でひっそり幕を閉じる。(via ブロントさん)

Asset Pipelineを大きく下記の4つに分けて考えるとわかりやすいです。

  • preprocessing(coffee -> jsなど)
  • concatenation(複数のファイルを一つにする)
  • compression(圧縮)
  • fingerprinting(ファイル名にハッシュ値を付ける)

対象は下記3つ

  • javascript
  • stylesheet
  • image

productionではrake assets:precompileで上記4つを対象3つに施してpublic/assetsフォルダーに入れ、nginx等のWebサーバーに配信させてrailsは関与しません。アプリ起動前からpublic/assets以下に必要な全てのファイルが揃って準備完了している必要があります。

precompileされるにはファイルがsprocketsの記法= require tree .等やconfig.assets.precompile += %w()のリストに入ってる必要があります。ここから漏れているファイルはpublic/assetsに無いことになるので表示されません。

jsの中でひっそり使われてる画像や特定のページでだけ読み込んでるcss、そういえば使ってたjqueryプラグインなど、とても見落としやすいので気を付けなければなりません。

それを乗り越えればnginxでcache期限無限のフィーバー状態です。でも既存アプリのバージョンアップなどでは上記見落としを無くすのはとても難しいと思います。もし見逃しそうならば・・・

config.assets.compileをtrueにするんだな。
おまえにもかぞくがいるだろう…

怖話でコメント欄がスパムで埋め尽くされて困ったので日本語が入ってないコメントは弾くようにしました。

「日本語が入ってない場合弾く」というのはMovableTypeの時からあるやり方ですが、イマイチ綺麗に実装できる感がしなくて控えてました。しかしmojiという素晴らしいgemを使ったら簡単に出来ました。

class Comment < ActiveRecord::Base
  validates_format_of :body,
    :with => Moji.regexp(Moji::ZEN_ALNUM | Moji::ZEN_KANA | Moji::ZEN_KANJI),
    :message => I18n.t('errors.messages.not_a_zenkaku')
end

mojiは色々な文字種の正規表現を持っていて手軽に組み合わせたりできます。最初はMoji::ZENで全角文字があればOKにしました。しかし実データ(スパム)にはギリシャ文字やキリル文字が存在していたのでそれも除外するようにしました。

$ rails c -E production
>> Comment.all.each {|c| c.destroy unless c.valid? }

これで今までの全てのスパムが消滅。今のところ新しいスパムも防げてるようでスッキリ。

カウンターキャッシュのリセット

以前、update_countersメソッドを使ってカウンターキャッシュを設定したつもりになっていました。しかし、update_countersメソッドは与えた数だけカウンターが増えるという動作だったようです。代わりにreset_countersメソッドを使えばcomments.countの数にresetしてくれるようです。

Story.all.each do |story|
  Story.reset_counters(story.id, :comments)
end

Facebookページ・アプリでハマるところシリーズ。

RailsでFacebookアプリ作ります。FacebookアプリはIFRAME内のページをPOSTメソッドで呼び出します。エントリー一覧(/entries)のようなものをFacebookアプリのトップページにしようとしたらアレ?となります。(POSTしたらcreate呼ばれるから)

オーマイゴッド!じゃあ/entries/newをPOSTメソッドも受け入れるようにしてこれをトップにしよう。しかしFacebookアプリは/で終わるURLしか指定できません。仕方ないので/entries/new/を受け入れるようにしよう・・・。

しかしログインしたはずが情報が引き継がれません。おかしいな。

WARNING: Can't verify CSRF token authenticity

ログを見るとCSRF tokenのWARNING。考えてみりゃ当然ですが、FacebookからPOSTで呼ばれる時にはCSRF tokenなんぞ付いてないのでWARNINGが出ます。そして危ないのでsessionは一旦破棄されるわけです。自動ログアウト。

paperclipを使ってて「縦長の画像をアップしたら横長になる」という問題。

これはpaperclipが悪いんじゃなくて、元のファイルもMacのPreviewで見ると縦長なんだけど、ブラウザで見ると横長。MacのPreview他、ExifのOrientationタグに対応したソフトで見ると縦長に見える。

Webアプリ的にはちょっと困る。「縦長の画像をアップしたら横長になった!」と言われても見てるツールの違いで最初から横長なんだから。

imagemagickのconvertにはExifのOrientationタグの内容に合わせて画像データを回転させてくれる超便利な-outo-orientオプションがあるのでそれを使えばいい。

paperclipはrmagickを使わず、convertコマンドを呼び出すだけで、好きなオプションが渡せるというイカシタ作りになっているのでこんな感じでOK。

class User < ActiveRecord::Base
  has_attached_file :picture, convert_options: {all: '-auto-orient'}
end

allは複数のstyleが合った場合に全部にこのオプションを付けるという指定。originalも残るから安心。

# config/initializers/omniauth.rb
Rails.application.config.middleware.use OmniAuth::Builder do
  if Rails.env.production?     
    provider :twitter, 'xxxxxxxxxxx', 'xxxxxxxxxxxxxxxxxxxx'
  else
    provider :twitter, 'ooooooooooo', 'oooooooooooooooooooo'
  end
end

今まで通りだと動きません。test/test_helper.rbに下記を追加。

require 'shoulda/rails'

今βのshoulda 3.xなら大丈夫なのでもうすぐこれも必要無くなるみたいです。

rails3.1でMySQLからやってくる文字列がASCII-8BITになっているのでto_jsonすると壊れる(to_jsonがencodingを見て処理するので)。sqlite3では起こらない。

環境はSnow Leopard、ruby1.9.2-p290、homebrewで入れたmysql 5.1.54。

% rails new foo
% cd foo
% vi Gemfile
(...)
gem 'mysql'
(...)
% bundle
% vi config/database.yml
(...)
development:
  adapter: mysql
  encoding: utf8
  database: foo_development
  pool: 5
  username: root
  password: 
  host: localhost
  socket: /tmp/mysql.sock
(...)
% rails g model post title:string
% rake db:create
% rake db:migrate
% vi db/seeds.rb
Post.create!(title: 'うんk')
% rake db:seed
% rails c
ruby-1.9.2-p290 :001 > puts Post.first.title
うんk
 => nil 
ruby-1.9.2-p290 :002 > Post.first.title.encoding
 => #<encoding:ascii-8bit>
ruby-1.9.2-p290 :003 > puts Post.first.title.to_json
"\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd\ufffd"
 => nil

CentOS 5.6でも同じ。

解決:

mysql gemはruby1.9.1からのencodingに対応してない。だからmysql2を使えば解決でした。

% vi Gemfile
(...)
  gem 'mysql2'
(...)
% vi config/database.yml
(...)
  adapter: mysql2
(...)
% rails c
ruby-1.9.2-p290 :001 > Post.first.title.encoding
 => #<Encoding:UTF-8>

adapterにmysql2と書けるというところが盲点でした・・・。