2010/10/07

nested_attributes で error_messages_for のエラーメッセージがうまくローカライズされない。

Just trying to enable correct i18n for error_messages for using nested_attributes. Used this patch but moved to an initializer instead.

Rails 3.0.0.beta4 をいじっているときのこと。。。

nested_attributes を使って親モデルと子モデルを同時に編集する際、error_messages_for を使ってバリデーションのエラーメッセージを吐き出してるんですが、なぜか子モデルのエラーだけがローカライズされない。
例:
class User < ActiveRecord::Base
  has_many :posts
  accepts_nested_attributes_for :posts

  validates_presence_of :name
end

class User < ActiveRecord::Base
  belongs_to :user

  validates_presence_of :title

end
で、
Posts title は必須です
名前は必須です
みたいに出力されてしまう。
運よくAnton Bangratzさんがパッチを書いてくれていた。Anton さんありがとうー!!
でも、パッチするのは嫌だったので、config/initializer/errors_i18n.rb と適当なファイルを作って、下記のように。。。
ActiveModel::Errors.module_eval do

  def full_messages
    full_messages = []

    each do |attribute, messages|
      messages = Array.wrap(messages)
      next if messages.empty?

      if attribute == :base
        messages.each {|m| full_messages << m }
      else
        # attr_name = attribute.to_s.gsub('.', '_').humanize
        attr_name = convert_name(attribute.to_s) 
        attr_name ||= attribute.to_s.gsub('.', '_').humanize        
        attr_name = @base.class.human_attribute_name(attribute, :default => attr_name)
        options = { :default => "%{attribute} %{message}", :attribute => attr_name }

        messages.each do |m|
          full_messages << I18n.t(:"errors.format", options.merge(:message => m))
        end
      end
    end

    full_messages
  end

  def convert_name(name)
    default = nil
    if name =~ /(.+)\.(.+)/
      base_name = $1
      attribute = $2
      base = ActiveSupport::Dependencies.constantize(base_name.camelize)
      if (base.respond_to? :model_name)
        default = "#{base.model_name.human} #{base.human_attribute_name(attribute.to_sym, :default => attribute.humanize)}"
      end
    end
    default 
  end  
  
end
因みに、ActiveModel::Errors::ClassMethods.module_eval do では Name Error になった。なぜだ?

2010/08/07

simple_captcha + Windows で no encode delegate for this image format error

Rails 3 用の simple_captcha plugin にアップグレードしたら動かなくなりました。 ログを見ると
StandardError (Error while running convert: convert: no encode delegate for this image format `'C:/Users/Me/AppData/Local/Temp/simple_captcha,4020,0.jpg'' @constitute.c/WriteImage/1114.
Paperclip を Windows で動かそうとしたときのエラーと同じように、シングルクオートが問題なのか?
実際、問題の convert コマンドの ' を " に変えてマニュアル実行したら動きました。

ならば。。。
vendor/plugins/simple_captcha/lib/simple_captcha/image.rb (line 72)
# params << "label:#{text} '#{File.expand_path(dst.path)}'"
params << "label:#{text} \"#{File.expand_path(dst.path)}\""
vendor/plugins/simple_captcha/lib/simple_captcha/image.rb (line 64)
# params << "-gravity 'Center'"
params << "-gravity \"Center\""
で解決

2010/08/05

Paperclip を Rails 3 + Windows で動かすニャンコ

一日ハマッタので備忘録


今まで動いていたアプリを 2.3.8 から Rails 3.0.0beta4 にアップグレードしようとしたときのこと。
参考までに、今回の Gemfile
gem 'rmagick', :require => 'RMagick' 
gem "aws-s3", ">= 0.6.2",:require => "aws/s3"
gem "paperclip", ">= 2.3.3", :git => "git://github.com/thoughtbot/paperclip.git"

ところが、Paperclip が動かない。。。。 Image C:/Users/Me/AppData/Local/Temp/stream,4508,0.jpg is not recognized by the 'identify' command. だそうな。 identify (ImageMagick) が見つからないらしいので、チェックする。
$ which identify
/c/Program Files/ImageMagick-6.5.6-Q8/identify
こちらに対処法があったので、その通りに config/development.rb に追加
Paperclip.options[:command_path] = "/c/Program Files/ImageMagick-6.5.6-Q8/"
ところが undefined method `exitstatus' for nil:NilClass と。。。orz
/c/Program Files/ImageMagick-6.5.6-Q8/identify -format %wx%h "C:/Users/Me/AppData/Local/Temp/stream,5880,0.jpg[0]"
をアクセスしようとして、エラっている模様。じゃあ、コマンドプロムプトでマニュアル実行してみる。
> "/c/Program Files/ImageMagick-6.5.6-Q8/identify" -format %wx%h "C:/Users/Me/AppData/Local/Temp/stream,5880,0.jpg[0]"
> 指定されたパスが見つかりません。
なるほど。でも windows 風だとOK。
>"c:\Program Files\ImageMagick-6.5.6-Q8\identify" -format %wx%h "C:/Users/Me/Ap
pData/Local/Temp/stream,5880,0.jpg"
> 500x499 
なので、config/development.rb を次のように編集
Paperclip.options[:command_path] = "c:\\Program Files\\ImageMagick-6.5.6-Q8\\"
なおった!!ばんざ~い!

あと、念のためにこちらのスレッドにある変更も・・・
bundler の gem ディレクトリの各ファイルを編集(僕の場合は C:\Users\Me\.bundle\ruby\1.8\bundler\gems\paperclip-XXX-master\lib\paperclip)
因みに、インストールに失敗した別のバージョンの Paperclip gem が残っていてエラっていたこともあるので、綺麗にするように。 command_line.rb
    def shell_quote(string)
      return "" if string.nil? or string.blank?
      # string.split("'").map{|m| "'#{m}'" }.join("\\'")
      string.split("'").map{|m| "\"#{m}\"" }.join("\\'") 
    end
thumbnail.rb
    def transformation_command
      scale, crop = @current_geometry.transformation_to(@target_geometry, crop?)
      trans = []
     # trans << "-resize" << "'#{scale}'" unless scale.nil? || scale.empty?
     # trans << "-crop" << "'#{crop}'" << "+repage" if crop
       trans << "-resize" << "\"#{scale}\"" unless scale.nil? || scale.empty?
       trans << "-crop" << "\"#{crop}\"" << "+repage" if crop
      trans
    end

2010/9/2 Hosoku


It turns out adding this would be enough. Create a file initializers/paperclip.rb with the following contents.

if RUBY_PLATFORM == 'i386-mingw32'
  module Paperclip
    def self.quote_command_options(*options)
      options.map do |option|
        option.split("\"").map{|m| "\"#{m}\"" }.join("\\\"")
      end
    end
  end
end

2010/08/02

Setting http expires in .htaccess

YSlow gave me an "E" on one of my websites so I started tweaking things a little. One of the things that made a big difference was setting http expiration headers.

In my .htaccess I added...
ExpiresActive On
ExpiresByType text/javascript A604800
ExpiresByType text/css A604800
ExpiresByType image/gif A2592000
ExpiresByType image/jpeg A2592000
ExpiresByType image/png A2592000
ExpiresActive On
ExpiresDefault "access plus 30 days"

2010/07/29

Find out when Daylight Savings Time (DST) starts and ends using Ruby (on Rails)

Recently encountered a requirement to find out whether a particular date falls within daylight savings time. After banging my head on the wall for a few hours trying to figure it out, I found that the ruby TimeWithZone object makes this quite simple.
@mytime = Time.zone.now.in_time_zone("London")
# Creates a TimeWithZone instance (instead of just a Time instance)

@mytime.period.local_start = 2010-03-28T02:00:00+00:00 Local Standard Time 
# This is when daylight savings time starts

@mytime.period.local_end = 2010-10-31T02:00:00+00:00 Local DST 
# This is when dst ends

---------------------------------- 
@sometime = Time.parse('2010-3-27 01:00:00').in_time_zone("London") 
# 2010-03-26 16:00:00 +0000 
@sometime.period.std_offset 
# 0 
@mytime.period.local_after_start?(@sometime) 
# false 
@mytime.period.local_before_end?(@sometime) 
# true 
---------------------------------- 
@sometime = Time.parse('2010-3-29 05:00:00').in_time_zone("London") 
# 2010-03-28 21:00:00 +0100 
@sometime.period.std_offset 
# 3600 << Note that std_offset now holds the offset time for daylight savings in seconds
@mytime.period.local_after_start?(@sometime) 
# true 
@mytime.period.local_before_end?(@sometime) 
# true 
---------------------------------- 
@sometime = Time.parse('2010-12-29 05:00:00').in_time_zone("London") 
# 2010-12-28 20:00:00 +0000 
@sometime.period.std_offset 
# 0 
@mytime.period.local_after_start?(@sometime) 
# true 
@mytime.period.local_before_end?(@sometime) 
# false 

2010/07/10

Sometimes I feel like killing Aptana...

I'm probably doing something stupid, but always seem to lose my project files in Aptana. "You attempted to go into a closed project" it tells me. Well, Aptana, if you show me the folder list I can probably open the project so I can go into it. But no, you have to make this such a challenge every time. The solution for me, it turns out, is to
  1. Go to my Ruby Explorer
  2. Right click on the empty window
  3. Select "open project"
  4. Open whatever project Aptana is insisting I want to get into.
  5. Go back to the Project Tree
  6. Keep clicking the "Go Up" icon to reveal the entire workspace
Another 20 minutes spent tearing my hair out...

2010/07/01

collection_select の値を rjs に渡す

備忘録
<%= f.collection_select(:something , Company.find(:all), :id, :name, 
  {},
  {:onchange => remote_function(:url => {:action => "change_vendor"}, :with => "'id='+this.value")}
 )%> 

:with で含めた値がパラメータとして渡されます。

2010/05/19

ネコがはまる: 530 5.7.0 Must issue a STARTTLS command first

Rails Actionmailer を使って gmail からメール送信してみる。
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
  :enable_starttls_auto => true,
  :address        => "smtp.gmail.com",
  :port           => 587,
  :domain         => "admin@xxxx.com",
  :authentication => :plain,
  :user_name      => "admin@xxxx.com",
  :password       => "xxxxxx" 
}
530 5.7.0 Must issue a STARTTLS command first と怒られる。 config/initializers/smtp_tls.rb とか、適当なファイルを作って、下記を入れる。
require "openssl"
require "net/smtp"
 
Net::SMTP.class_eval do
  private
  def do_start(helodomain, user, secret, authtype)
    raise IOError, 'SMTP session already started' if @started
    check_auth_args user, secret, authtype if user or secret
 
    sock = timeout(@open_timeout) { TCPSocket.open(@address, @port) }
    @socket = Net::InternetMessageIO.new(sock)
    @socket.read_timeout = 60 #@read_timeout
    @socket.debug_output = STDERR #@debug_output
 
    check_response(critical { recv_response() })
    do_helo(helodomain)
 
    raise 'openssl library not installed' unless defined?(OpenSSL)
    starttls
    ssl = OpenSSL::SSL::SSLSocket.new(sock)
    ssl.sync_close = true
    ssl.connect
    @socket = Net::InternetMessageIO.new(ssl)
    @socket.read_timeout = 60 #@read_timeout
    @socket.debug_output = STDERR #@debug_output
    do_helo(helodomain)
 
    authenticate user, secret, authtype if user
    @started = true
  ensure
    unless @started
      # authentication failed, cancel connection.
      @socket.close if not @started and @socket and not @socket.closed?
      @socket = nil
    end
  end
 
  def do_helo(helodomain)
    begin
      if @esmtp
        ehlo helodomain
      else
        helo helodomain
      end
    rescue Net::ProtocolError
      if @esmtp
        @esmtp = false
        @error_occured = false
        retry
      end
      raise
    end
  end
 
  def starttls
    getok('STARTTLS')
  end
 
  def quit
    begin
      getok('QUIT')
    rescue EOFError
    end
  end
end
これでOK

2010/05/08

ネコがチャレンジする Git

git 移行備忘録。

Gitのインストール

http://git-scm.comから

Git Bash でSSHキーを作成

次に gitbash (ウィンドウズコマンドラインではなく)で、ユーザー設定。
git config --global user.name "Matatabi"
git config --global user.email tech@xxxx.com
cd ~/.ssh
.ssh が無いと言われたので、
$ ssh-keygen -t rsa -C "tech@xxxx.com"
Generating public/private rsa key pair.
Enter file in which to save the key (/c/Users/Me/.ssh/id_rsa):
Created directory '/c/Users/Me/.ssh'.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /c/Users/Me/.ssh/id_rsa.
Your public key has been saved in /c/Users/Me/.ssh/id_rsa.pub.
The key fingerprint is:
cf:67:80:46:66:bf:cd:49:fb:99:ec:63:47:0b:e2:f1 tech@jzool.com
id_rsa.pub の公開鍵を github に登録。とりあえず、github.com にssh できるか試してみると。。。
ssh git@github.com
> Permission denied (publickey)
といわれる。Github のヘルプに、rsa でダメなら dsa を試してくれと書いてあったので、さっそく dsa で再チャレンジ。
ssh-keygen -t dsa
この公開鍵も github に登録。
$ ssh git@github.com
ERROR: Hi matatabi! You've successfully authenticated, but GitHub does not provide shell access 
Connection to github.com closed.
再度 ssh してみると、Error と出るが、認証は出来ているのでこれでOK!
(結局github にssh するのに Putty を使うことになったので 最終的には putty keygen でRSAキーペアを作りました)

プロジェクトを git にチェックイン

$ cd /d/Webapps/myapp
$ git init
> Initialized empty Git repository in D:/Webapps/myapp/.git/
これで、.git というリポジトリファイルがプロジェクトルートに作成される。
.git/config を見てみる
[core]
 repositoryformatversion = 0
 filemode = false
 bare = false
 logallrefupdates = true
 symlinks = false
 ignorecase = true
これに autocrlf = false を一行追加しておく。そうしないと、add したときに, Warning: LF will be replaced by CRLF in FILENAME のワーニングを連発されてしまう。やられた。。。 次に、git で管理しないファイルを ignore するための設定。(gitbash から、vi が使えるのは便利)
$ vi .gitignore
myapp/.gitignore
log/*.log
tmp/**/*
doc/api
doc/app
public/assets/*
public/cache/*
.tmp*
.sandbox*
ところが、ここで git status を打つと、下記のように log や tmp フォルダが完全に管理から外されているのが分かります。 フォルダすら出来ないのはまずいので、管理したいディレクトリの中に .gitignore ファイルを入れていく。苦肉の策。
touch log/.gitignore
touch tmp/.gitignore
touch doc/.gitignore
touch public/cache/.gitignore
touch public/assets/.gitignore
因みに、Rails3 だとこの辺を全部自動生成してくれるのはとってもたすかる。
次に、プロジェクトファイルをとりあえず git に追加
git add .
git commit
github を remote リポジトリに追加
git remote add origin git@github.com:myaccount/myproject.git
github に push
$ git push origin master
Counting objects: 3, done.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 4.56 KiB, done.
Total 3 (delta 0), reused 0 (delta 0)
To git@github.com:jzool/jzool.com.git
 * [new branch]      master -> master

2010/05/07

ternary operator を使いこなす

コードを見直してると、こんなのがよくあります。
if self.something
  something += x
else
  something = 0
end
これ、ternary operator 使ったら綺麗なのになー、といつも思うので備忘録
self.something ? something += x : something = 0
# condition ? do_true : do_false
View でも便利
%lt;%= session[:user_id] ? @user.name : "<a href='...' >ログインしてください</a>" %>
ちなみに、日本語では三項演算子というらしい。知らなかった。。。

2010/05/05

Rails ブラウザからタイムゾーンをゲット

javascript でブラウザからタイムゾーンをゲットする方法。 適当に .js ファイルを作り、ブラウザのオフセット時間をクッキーに入れる。例えば、set_tzoffset.js
document.cookie = 'tzoffset='+ (new Date()).getTimezoneOffset();
全てのページで読み込みたく無い場合は、おなじみ content_for でヘッダーに js ファイルを追加
<% content_for :head do %>
<%= javascript_include_tag 'set_tzoffset' %>
<% end %>
コントローラーで呼び出し
if cookies[:tzoffset]
  gmt_offset = ActiveSupport::TimeZone[-cookies[:tzoffset].to_i/60]
  # オフセットは「差」が「分」で取得されるため、マイナス掛けて60で割る。
end
問題は、同じオフセットで複数のタイムゾーンがあるため、時間はあっていても、必ずしも正しいロケーションのものが取得できないこと。例えば、同じ gmt + 9 のオフセットでは、Tokyo, Osaka 等がある。うーん、こればかりは。。。。

2010/04/19

fieldWithErrors div でレイアウトが崩れてしまう件

フォームで、activerecord error があると、対象の入力フィールドのレイアウトが崩れてしまう件。 問題は、エラーのあるフィールドが fieldWithErrors という div でくくられてしまうこと。 initializer を作るか、environment.rb に次を入れると、div が span に変わる
config/initializers/errors.rb
ActionView::Base.field_error_proc = Proc.new { |html_tag, instance| "<span class=\"fieldWithErrors\">#{html_tag}</span>" }
ついでに errors.css とか編集するといいかも
.fieldWithErrors {
  padding: 2px;
  background-color: red;
}
↓
.fieldWithErrors input, .fieldWithErrors select, .fieldWithErrors textarea {
  border: 2px solid red;
}

2010/04/15

各namespace でapplication_controller みたいな仕組みを作る

スカイプ英会話のカフェトークでは生徒、講師、管理者でそれぞれネームスペースをもってるんですが、各ネームスペースで application_controller のように、一つの親コントローラに共通メソッドをまとめたいと思いました。 まず、application_controller.rb に全てで使う共通メソッドなどを入れる。
class ApplicationController < ActionController::Base
  include SslRequirement              
  before_filter :set_user_language    
  before_filter :set_timezone

  def set_timezone
    Time.zone = session[:tz] if session[:tz]
  end
   
  private
  def set_user_language
    I18n.locale = params[:locale] || 'ja'
  end
   
end
次に、例えば管理者 admin ネームスペースで使うルートコントローラを作る (admin_root_controller.rb)
class AdminRootController < ApplicationController
  ssl_required :all
  before_filter :authenticate_admin
  before_filter :admin_login_required
  
  def admin_login_required
    if session[:admin_id]
      return true 
    else
      flash[:warning]= t('common.please_login')
      unless request.request_uri == "/admin/login/logout"
        session[:return_to_path] = request.request_uri
      end
      redirect_to :controller => "admin/login", :action => "index"
      return false
    end
  end

  def authenticate
    authenticate_or_request_with_http_basic do |username, password|
      username == "xxxxx" && password == "xxxxxxxxxxxx"
    end
  end

  protected
  def ssl_required?
    Rails.env.production? #本番環境以外はSSL しない
  end

end
最後に普通のコントローラー
class Admin::LoginController < AdminRootController
  layout 'admin'
  skip_before_filter :admin_login_required

  def logout
    
  end

  def login

  end

end
こんな感じ・・・

2010/04/14

Filter chain halted as [:ensure_proper_protocol] でハマル

開発機でうまくいっていたページが本番機では Page Not Found エラーになってしまう。
ログを見てみると、なにやら Redirect しようとしている模様。
Redirected to http://cafetalk.com/en/user/reqs/confirm_req
Filter chain halted as [:ensure_proper_protocol] rendered_or_redirected.
なるほど、SslRequirement Pluginに、SSLが必要なページを渡してなかったのが問題でした。
ssl_requrement :index, :mycustom_action
404メッセージに惑わされて、ハマってしまいました orz.

2010/04/10

検索フォームにコダワル

いろんなところで使いそうなので、検索フォームの作り方をまとめてみた。(突っ込みどころは沢山あると思います。)

まず view
<% form_tag request.path, :method => 'get' do %>
<table>
 <tr>
  <td><%= t('common.created_at') %></td>
  <td><%= calendar_date_select_tag(:created_from, params[:created_from], :time => false)%> - <%= calendar_date_select_tag(:created_to, params[:created_to], :time => false)%></td>
 </tr>
 <tr>
  <td><%= t('common.keyword') %></td>
  <td><%= text_field_tag :keyword, params[:keyword], :size => 30 %> ID <%= text_field_tag :id, params[:id], :size => 10 %></td>
 </tr>
 <tr>
  <td>sort_by</td>
  <td><%= select_tag(:sort_by, options_for_select([['created_at', 'created_at'], ['updated_at', 'updated_at'], ['login_at', 'login_at'], ['ID', 'id'] ], 'created_at' ) ) %> <%=select_tag(:sort_dir, options_for_select([[t('common.asc'), 'ASC'], [t('common.desc'), 'DESC']], 'DESC'))%> 
  </td>
 </tr>
 <tr>
  <td></td>
  <td><%= submit_tag t('common.search_with_conditions') %> <input type="reset" value="Reset!"></td>
 </tr>
</table>
<%end %> 
リセットボタンは便利。また、select_tag にオプションを渡す場合は options_for_select を使用。
<%= select_tag(:sort_by, options_for_select([['created_at', 'created_at'], ['updated_at', 'updated_at'], ['login_at', 'login_at'], ['ID', 'id'] ], 'created_at' ) ) %>
selected = 'created_at' を指定している。

controller
    @users = User.full_search(params[:keyword], params[:page], :id => params[:id],
      :created_from => params[:created_from], :created_to => params[:created_to],
      :sort_by => params[:sort_by], :sort_dir => params[:sort_dir]
      )
model
メソッドには自由にパラメータを渡せるように options={} を使う
  def self.x_search(search, page, options = {})
    sql = "((display_name like '%#{search}%') OR (profile like '%#{search}%') OR (first_name like '%#{search}%') OR (last_name like '%#{search}%') OR (email like '%#{search}%') OR (mobile like '%#{search}%'))"

    if options[:id] && options[:id] != ""
      sql << " AND id = '#{options[:id]}'" 
    end

    if options[:created_from] && options[:created_from] != ""
      from = Time.parse(options[:created_from]).to_s(:db)
      # If timezone conversion required use Time.zone.parse(options[:created_to]).utc.to_s(:db)
      sql += " AND created_at >= '#{from}'"
    end
  
    if options[:created_to] && options[:created_to] != ""
      to = Time.parse(options[:created_to]).to_s(:db) 
      sql += " AND created_at <= '#{to}'"
    end
  

    if options[:sort_by] && options[:sort_by] != "" && options[:sort_dir] && options[:sort_dir] != ""
      order = options[:sort_by] + " " + options[:sort_dir]
    else
      order = 'created_at DESC'
    end    

    paginate(:per_page => 30, :page => page, :conditions => sql, :order => order)
  end

2010/04/06

Service Temporarily Unavailable でハマル

サイトをアクセスしようとすると、

Service Temporarily Unavailable

The server is temporarily unable to service your request due to maintenance downtime or capacity problems. Please try again later.

と、503 エラー

/etc/httpd/logs/ で最新のエラーログを見ると
(111)Connection refused: proxy: HTTP: attempt to connect to 127.0.0.1:3000 (127.0.0.1) failed
ためしに、マニュアルで mongrel をリスタートしてみる
> mongrel_rails cluster::restart --clean
!!! Configuration file does not exist. Run mongrel_rails cluster::configure.
cluster::restart reported an error. Use mongrel_rails cluster::restart -h to get help.
Rails アプリケーションフォルダに入って、mongrel cluster configure してみる。
mongrel_rails cluster::configure -e production -p 3000 -N 2
mongrel_rails cluster::restart --clean
すると、スタートしたみたい。
** Daemonized, any open files are closed.  Look at tmp/pids/mongrel.3000.pid and log/mongrel.3000.log for info.
** Starting Mongrel listening at 0.0.0.0:3000
** Starting Rails with production environment...
** Rails loaded.
** Loading any Rails specific GemPlugins
** Signals ready.  TERM => stop.  USR2 => restart.  INT => stop (no restart).
** Rails signals registered.  HUP => reload (without restart).  It might not work well.
** Mongrel 1.1.5 available at 0.0.0.0:3000
** Writing PID file to tmp/pids/mongrel.3000.pid

proxy: BALANCER: (balancer://mongrel_cluster). All workers are in error state
> ps aux

 
15:23   0:07 /usr/bin/ruby /usr/bin/mongrel_rails start -d -e production -p 3000 -P tmp/pids/mongrel.3000.pid -l log/mongrel.300
0.log
15:23   0:07 /usr/bin/ruby /usr/bin/mongrel_rails start -d -e production -p 3001 -P tmp/pids/mongrel.3001.pid -l log/mongrel.300
1.log

動いている。。。

2010/04/01

accepts_nested_attributes_for : 親モデルのフォームで子モデルも編集する方法

Service モデルが ServiceFile モデルと association で繋がっている場合。。。 ※ Railscasts 参照

models/service.rb

has_many :service_files, :dependent => :destroy
accepts_nested_attributes_for :service_files, 
    :reject_if => lambda { |a| a[:uploaded_data].blank? }, 
    :allow_destroy => true

models/service_files.rb

belongs_to :service

/service/new.html.erb & /service/edit.html.erb

<% form_for([:pro, @service], :url => {:action => :create},:html => {:multipart => true})  do |f| %>
  <!-- 子モデルにアップロードデータがあるので、multipart を指定 -->
  <% f.fields_for :service_files do |builder| %>
    <%= render "service_file_fields", :f => builder %>
  <% end %>
  <p><%= link_to_add_fields "Add", f, :service_files %></p>
<% end %>

_service_file_fields.html.erb

<div class="fields">
 <%= f.file_field :uploaded_data %>
 <%= link_to_remove_fields "<img src='/images/icons/cross.png'/>", f %>
</div>
※ application.js, application_helper.rb のメソッドは Railscasts 参照

はまったこと

undefined method `reflect_on_association' for NilClass:Class
・・・が
問題は<%= link_to_add_fields t('common.add_obj', :obj => t('file.singular')), f, :service_files %> に正しいクラスを渡してなかった。

<% form_for(@service, :url => {:action => :create},:html => {:multipart => true}) do |f| %>

を下に変えたら問題なし。(service は pro のネームスペース下)

< <% form_for([:pro, @service], :url => {:action => :create},:html => {:multipart => true}) do |f| %>

2010/03/18

Reading in the product language table

While loading the list of products on Jzool.com I naturally wanted to load the associated translation which is stored in a different table.

The best way to do this to use eager loading which is a cool thingie you do when you want to read associated tables in one go.

So, in the Product model I do this:

 has_many :product_languages, :dependent => :destroy
 has_one :t, :class_name => 'ProductLanguage',
   :conditions => ['locale = ?', I18n.locale],
   :select => 'id, product_id, name, short'

The "t" stands for "translated". Just wanted to keep it short. I could very well do has_many :ts by the way.

Now, in my paginated search method I use the include.

 def self.search(search, page)
   paginate :include => :t, :per_page => 20, :page => page,
            :conditions => ['name like ?', "%#{search}%"], :order => 'created_at desc'
 end

And then I can just call the translated name by doing:

<%= @product.t.name %>
However, this actually ends up generating two sql statements. Here's an example using two models - Category and CategoryLanguage where the latter holds the localized name of the category.
def localized_children
Category.find :all, :conditions => ['categories.parent_id = ? and categories.display = ?', self.id, true],
   :order => 'categories.sort_id asc',
   :select => 'categories.id, categories.name, categories.parent_id', :include => :t
end
This results in the following two sql statements
[4;35;1mCategory Load (1.0ms) [0m   
[0mSELECT categories.id, categories.name, categories.parent_id FROM `categories` WHERE (categories.parent_id = 307 and categories.display = 1) ORDER BY categories.sort_id asc [0m
[4;36;1mCategoryLanguage Load (1.0ms) [0m    [0;1mSELECT id, category_id, name FROM `category_languages` WHERE (`category_languages`.category_id IN (308,313,480,310,309,311,504,312) AND (locale = 'en'))
So it seems like the best way to go is simple sql.
def localized_children
   Category.find_by_sql(["
     SELECT DISTINCT c.id, c.parent_id, cl.name
     FROM categories AS c JOIN category_languages AS cl ON c.id = cl.category_id
     WHERE c.display = ? AND c.parent_id = ? AND cl.locale = ?
     ORDER BY c.sort_id ASC",
     true, self.id, I18n.locale])
end
Which results in one clean sql
SELECT DISTINCT c.id, c.parent_id, cl.name
FROM categories AS c JOIN category_languages AS cl ON c.id = cl.category_id
WHERE c.display = 1 AND c.parent_id = 307 AND cl.locale = 'en'
ORDER BY c.sort_id ASC
But in the view Use <%= @product.name %> instead of <%= @product.t.name %>

2010/03/10

modx eform の内容を DB に入れる方法(ID 番号採番も)

またまた rails とは無関係ですが、modx の eform を使って、データベースに投稿内容を保存する方法をトライしてみました。一番やりたかったことは、テーブルのレコードIDを取得して、メールに表示させること。

一般的に使われそうな「お問合せフォーム」を例に、簡単なサンプルを作成してみました。

まず、MySQL からテーブルを作成

CREATE TABLE `modx_inquiries` (
  `id` int(10) unsigned NOT NULL auto_increment,
  `name` varchar(255) NOT NULL,
  `subject` varchar(255) NOT NULL,
  `email` varchar(100) NOT NULL,
  `message` text NOT NULL,
  `created_at` int(20),
  PRIMARY KEY  (`id`)
);

複数の異なるフォームがある場合、それぞれテーブルを追加する必要があります。逆に、一つのmodx インスタンスで、いくらでも応用できるということ。
フォームのチャンクを作成 db_inquiries

<p class="error">[+validationmessage+]</p>
<form method="post" action="[~[*id*]~]" id="EmailForm" name="EmailForm" >
 <fieldset>
  <input name="formid" type="hidden" value="ContactForm" />
  <table>
  <tr>
  <th width="30%">お名前<em>*</em></th>
  <td><input name="name" id="cfName" class="text short" type="text" eform="お名前::1:" /></td>
  </tr>
  <tr>
  <th>メールアドレス<em>*</em></th>
  <td><input name="email" id="cfEmail" class="text long" type="text" eform="メールアドレス:email:1" /></td>
  </tr>
  <tr>
  <th>表題<em>*</em></th>
  <td>
   <select name="subject" id="cfRegarding" eform="Form Subject::1">
    <option value="一般のお問合せ">一般のお問合せ</option>
    <option value="その他">その他</option>
   </select>
  </td>
  </tr>
  <tr>
  <th>内容<em>*</em></th>
  <td><textarea name="message" id="cfMessage" rows="4" cols="20" eform="内容:textarea:1"></textarea></td>
  </tr>
  <tr>
  <th></th>
  <td><em>*</em>は必須項目です。</td>
  </tr>
  <tr>
  <th></th>
  <td><input type="submit" name="contact" id="cfContact" class="button" value="メッセージを送る" /></td>
                </tr>
                <tr>
                <td colspan="2">ご登録される個人情報はお問い合わせへのご返答及びそれに付随する内容に関してのみ、利用させていただきます。詳細は<a href="[~133~]">プライバシーポリシー</a>をご覧ください。</td>
                </tr>
                </table>
 </fieldset>
</form>
snippet dbInquiries の作成 (function 名は何でもOK)
<?php
function dbInquiries(&$fields)
 {
  global $modx;
  // Init our array
  $dbTable = array();
  $dbTable['name'] = $modx->db->escape($fields['name']);
  $dbTable['subject'] = $modx->db->escape($fields['subject']);
  $dbTable['email'] = $modx->db->escape($fields['email']);
  $dbTable['message'] = $modx->db->escape($fields['message']);
  $dbTable['created_at'] = time();

         // Run the db insert query
  $dbQuery = $modx->db->insert($dbTable, 'modx_inquiries' );
                $fields['record_id'] = $dbQuery; // so that eform can access this field
                $fields['subject'] = $dbTable['subject']." (".$fields['record_id'].")";
  return true;
 }
?>

$fields['record_id'] = $dbQuery; は、eform が新しく生成されたレコードの ID を取得を可能にするための追加。
$fields['subject'] = $dbTable['subject']." (".$fields['record_id'].")"; はメールの表題に subject とレコード ID を追加する。

問い合わせフォームでの eform コール

[!dbInquiries!]
[!eForm? &subject=`上書きされます` &to=`[(emailsender)]` &formid=`ContactForm` &eFormOnBeforeMailSent=`dbInquiries` &tpl=`db_inquiries` &thankyou=`3` &report=`db_inquiries_report` &automessage=`db_inquiries_thanks` !]
こちらのスレッドのまとめ

2010/03/01

Rails でSSLを使用する

英語のチュートリアルですが、かなりグッド。
ウィンドウズのローカルマシーンで SslRequirement プラグインをインストール
ruby script/plugin install ssl_requirement

application_controller.rb
include SslRequirement

# Below to disable ssl unless system is production
# 下は、本番環境のみ ssl を使う場合。
protected
def ssl_required? 
  ((self.class.read_inheritable_attribute(:ssl_required_actions) || []).include?(action_name.to_sym)) && RAILS_ENV == 'production' 
end  
コントローラーで、ssl を使うアクションを指定
ssl_required  :index, :show, :edit

正しく指定していないページにSSLでアクセスしようとすると次のようなエラーが。。。
Redirected to http://cafetalk.com/en/user/reqs/confirm_req
Filter chain halted as [:ensure_proper_protocol] rendered_or_redirected.
こちらにベターなバージョンあり。 /2010/04/namespace-applicationcontroller.html
上の例だと、application_controller で ssl_required :all ができない

2010/02/25

rjs でflash を書き換える

flash[:notice] = t('request.confirm_success')
page.replace_html :notice, flash[:notice]
flash.discard

2010/02/18

calendar_date_select plugin doesn't accept minute intervals

calendar_date_select で、分のインターバルを00、30等に設定していても、最初に設定されるデフォルト分数が現在時刻の分の値をとってきてしまう問題の解消法
if (isNaN(this.date)) this.date = new Date();
を、以下に変更
if (isNaN(this.date)) {
   this.date = new Date();
   this.date.setMinutes(Math.ceil(this.date.getMinutes() / this.options.get("minute_interval")) * this.options.get("minute_interval"));
}

2010/02/17

Tips on views

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
  <head>
    <title><%= h(yield(:title) || "Untitled") %></title>
    <%= stylesheet_link_tag "/jqtouch/jqtouch.min.css", "/jqtouch/themes/apple/theme.min.css" %>
    <%= javascript_include_tag "/jqtouch/jquery.1.3.2.min.js", "/jqtouch/jqtouch.min.js", "mobile" %>
    <%= yield(:head) %>
  </head>
  <body>
    <div class="current">
      <%- if show_title? -%>
      <div class="toolbar">
        <%= link_to "Back", nil, :class => "back" unless current_page? root_path %>
        <h1><%=h yield(:title) %></h1>
        <%= link_to "Full Site", root_url(:mobile => 0), :class => "button", :rel => "external" %>
        <%= yield(:toolbar) %>
      </div>
      <%- end -%>
      
      <% unless flash.empty? %>
        <div class="info">
        <%- flash.each do |name, msg| -%>
          <%= content_tag :div, msg, :id => "flash_#{name}" %>
        <%- end -%>
        </div>
      <% end %>
    
      <%= yield %>
    </div>
  </body>
</html>
<% content_for :head do %>
  <%= stylesheet_link_tag 'projects' %>
<% end %>
http://railscasts.com/episodes/199-mobile-devices

2010/02/16

言語指定で翻訳する方法


I18n.t('sometext', :locale='ja') 

javascript で戻るボタン

<input type=button value="Back" onClick="history.go(-1)">

2010/02/09

localized_country_select で国名選択

ruby script/plugin install git://github.com/karmi/localized_country_select.git
rake import:country_select 'ja'
<%= localized_country_select(:user, :country, [], :include_blank => 'Please choose...') %>
<%= I18n.t @user.country, :scope => 'countries' %>

2010/02/04

has_many association でソート順を指定

モデルのなかで
has_many :children, :order => "sort ASC"
その他、ソートに便利な・・・
for child in @parent.children.reverse
# ...
end

2010/02/03

Site5 で Google Apps を使えるように設定する。

備忘録 入力行が足りないが、一度設定すると出てくる。

2010/02/01

activerecord エラーを追加する方法

  def before_update
    if !check_something?
      self.errors.add(:name, "There's an error with the name")
      self.errors.add_to_base("There's an error with the name")
      return false
    end    
    return true
  end

2010/01/28

Linux コマンド備忘録

検索

find . -exec grep "searchthisstring" '{}' \; -print

# ファイルへ出力
find . -exec grep "searchthisstring" '{}' \; -print > filename.txt 
# 1日以内に変更されたファイルの検索
find . -mtime -2 -exec
# 1日以内に変更されたファイルを検索してディレクトリに移動
find . -mtime -2 -exec mv "{}" ./../P_delete/ \;

その他ヘルプ





2010/01/27

include と include で select を使う方法

Invoice(請求書) というテーブルに customer (顧客)が紐づいている場合、たいてい請求書と一緒に顧客名も表示する必要があります。
<% for invoice in @invoices %>
  <%= invoice.price %> <%= invoice.customer.name %>
<% end %>
のように。でも、この場合、invoice の数 + 1 のsql 文が発生するので無駄が多い。
そんなときは、関連テーブルも include するべし。
@invoices = Invoice.find(:all, :conditions => {..}, :include => :customer)
さらに、include したテーブルの一部しか使わない場合、:select も使いたいが、なぜか標準では、:inlcude したテーブルに :select が使えない。
@invoices = Invoice.find(:all, :conditions => {..}, :include => :customer, :select => 'customers.name, customer.id')
なんてできるといいのに。。。

これを可能にする pluging が Snow Giraffe の Eager loading select plugin. Rails 2.3 まで対応。

2010/01/16

セッションの有効期限を設定

config/initializers/session_store.rb
ActionController::Base.session_options[:expire_after] = 2.hours
こんな感じ

Ruby でファイルの存在をチェック

ローカルにあるファイル
if File.exist?(path)
...
end

リモートのファイル
require 'open-uri' # environments.rb に入れちゃう
begin
  if open(URI.parse(path))
    # ファイルが存在したら実行
  end
rescue => deadmeat
  if deadmeat.message =~/somestring like 403/ 
  # (Errno::ENOENT, OpenURI::HTTPError, etc...)などの exception を表示しない
  # ファイルが存在しなかったらやること
  end
end

文字列操作

http://api.rubyonrails.org/classes/ActiveSupport/Multibyte/Chars.html#M000961

2010/01/15

Rails datetime 設定

Unix timestamp を rails datetime に変更

@somedate = Time.at(1260519945).utc

DB風に表示

@date.to_s(:db)
# 2009-12-11 08:25:45 


strftime 備忘録

example: object.strftime("%Y/%m/%d %H:%M")  
%a - The abbreviated weekday name (``Sun'')
%A - The full weekday name (``Sunday'')
%b - The abbreviated month name (``Jan'')
%B - The full month name (``January'')
%c - The preferred local date and time representation
%d - Day of the month (01..31)
%H - Hour of the day, 24-hour clock (00..23)
%I - Hour of the day, 12-hour clock (01..12)
%j - Day of the year (001..366)
%m - Month of the year (01..12)
%M - Minute of the hour (00..59)
%p - Meridian indicator (``AM'' or ``PM'')
%S - Second of the minute (00..60)
%U - Week number of the current year,
starting with the first Sunday as the first
day of the first week (00..53)
%W - Week number of the current year,
starting with the first Monday as the first
day of the first week (00..53)
%w - Day of the week (Sunday is 0, 0..6)
%x - Preferred representation for the date alone, no time
%X - Preferred representation for the time alone, no date
%y - Year without a century (00..99)
%Y - Year with century
%Z - Time zone name 
%% - Literal ``%'' character 

2010/01/11

一つのフォームに複数の submit ボタンを入れる

たまに一つのフォームに複数の submit ボタンをつけて、どれをクリックするかでロジックを分けたい場合がある。 application.js
function submitWithValueAndConfirm(formid, commitValue, text)
{
 if (window.confirm(text)) {
     var objForm = document.getElementById(formid);
  objForm.commit_value.value = commitValue;
  objForm.submit();
 } else {
  return false;
 }
}


view
<% form_for :req, @req, :url => {:action => :update}, :html => {:method => :put, :id => "action"} do |f| %>
    <input type="hidden" name="commit_value" />
    <%= f.submit "CONFIRM", :onClick => ("return submitWithValueAndConfirm('action','confirm', 'Will do. Okay?');") %>
<%= f.submit "KILL", :onClick => ("return submitWithValueAndConfirm('action','kill', 'Will delete. Okay?');") %>
    <%= f.submit I18n.t('btn_something'), :onClick => ("return submitWithValueAndConfirm('action','confirm', '" + I18n.t('something') + "');") %>
<% end %>
上で return confirm はポップアップメッセージ用。:onclick を使用すると、:confirm が使えないため。

controller
case params[:commit_value]
  when "confirm"
    ...
  when "kill"
    ...
  when "something"
    ...
end

因みに、リンクでフォームをサブミットする方法

<% form_for ([:user, @user], :url => {:action => :create}, :html => {:name => :login}) do |f| %>
   ...
   <a href='#' onclick="document.login.submit();" >Login</a>

<% end %>

Activerecord エラーメッセージの国際化

Activerecord で表示されるエラーメッセージを翻訳したいこともある。 そんな場合、デフォルトでは activerecord.errors のスコープを使う
ja:
  activerecord:
    models:
      memo: メモ
    errors:
      models:
        memo:
          attributes:
            content:
             blank: 空っぽさ  

2010/01/09

Ajax を使ってコメントを挿入

Ajax をつかって、記事(article)にコメント(comment)を挿入するほうほう。よく使うわりにはいつもはまってしまうので備忘録。(カフェトークでは、レッスンにメモを追加するところにつかってます)

article.rb
class Article < ActiveRecord::Base
  belongs_to :user
  has_many :comments
  after_update :save_comments
  
  def new_comment_attributes=(comment_attributes)
    comment_attributes.each do |attributes|
      comments.build(attributes) if attributes[:content].length > 0
    end
  end
  
  # ここでは使ってない
  def existing_comment_attributes=(comment_attributes)
    comments.reject(&:new_record?).each do |comment|
      attributes = comment_attributes[comment.id.to_s]
      if attributes
        comment.attributes = attributes
      else
        comments.delete(comment)
      end
    end
  end
  
  def save_comments
    comments.each do |comment|
      comment.save(false)
    end
  end  
  
  # 特定のユーザのみに表示するためのメソッド
  def user_comments
    self.comments.find(:all, :conditions => {:display_user => true})
  end
end

comment.rb
class Comment < ActiveRecord::Base
  belongs_to :article 
end
views/articles/show.html.erb
<div id="comments">
 <% for comment in @article.pro_comments %>
  <%= render :partial => 'comment_item', :locals => {:comment_item => comment} %>
 <% end %>
</div>
<%= link_to_function "コメント追加" do |page|
      page.insert_html :bottom, :add_comment, :partial => 'comment' , :object => comment.new
<% end%>
<div id="add_comment"></div>

views/pro/articles/_comment_item.html.erb
<div class="comment" id="comment_<%= comment_item.id %>">
 <%= comment_item.content%>
</div>

views/articles/_comment.html.erb
<div class="add_comment" >
<% @comment = Comment.new %>
<% form_remote_for ([:comment, @article]), 
  :url => add_comment_article_path(@article), :html => { :method => 'put' } do |f| %>
   <%= f.text_area :content %>
  <%= f.submit "追加"  %>
<% end %>
</div>

add_comment.rjs
if params[:comment]
  @comment= comment.new(params[:comment])
  if @comment.save
    comment_item_id = "comment_" + @comment.id.to_s
    page.insert_html :bottom, "comments", :partial => "comment_item", :object => @comment
    page[comment_item_id].visual_effect :highlight, :duration => 0.5, :startcolor => "#66CCFF"
    page.replace_html "add_comment", ""
  else
    @comment.errors.each{|attr,msg| page.alert("#{attr} - #{msg}")}
  end      
end

routes.rb
map.resources :articles, :member => {:add_comment => :put}

RJS 参考色々

最近 rjs を使うことが多くなってきたので、備忘録

特定のアイテムの下にインサートする場合

page.insert_html :bottom, "item_" + params[:id], :partial => "item", :object => item
この場合、partial と object名は一致している必要がある。

ハイライト

page[:item].visual_effect :highlight, :duration => 0.5, :startcolor => "#66CCFF"
特定のアイテムをハイライトしたい場合(例 div id="item_{アイテムid}")
item_id = "item_" + item.id.to_s
page[item_id].visual_effect :highlight, :duration => 0.5

その他色々

page.alert("hello")

# 表示トグル
page['mytest'].toggle

page.alert(params.inspect)

target_field = "prefix_" + params[:target]
page[target_field].value = temp

page.visual_effect :toggle_appear, params[:somename], :duration => 0.5
page[:item_form].remove
page.redirect_to root_path
page.replace_html 'someid', :partial => 'somepartial', :object => @someobject
page.insert_html :bottom, 'someid', :partial => 'somepartial', :locals => {:this => this, :that => that}
page << "new Ajax.Request('"+ url_for(:action => :some_ajax_action, :params =>{:myparam => myparam} )+"')"
page.visual_effect :shake 'score'
page.visual_effect :fade  'dodo'
page.delay(1.5) do 
  page.visual_effect :fade 'sunset'
end

2010/01/07

Rails Timezone の設定色々

困ったときのRails timezone api

Timezone 基本設定

config/environments/production.rb 等、各環境後とに・・・
config.time_zone = "Tokyo"
ApplicationController でのタイムゾーン設定
class ApplicationController < ActionController::Base
  before_filter :set_timezone
  def set_timezone
    Time.zone = session[:tz] if session[:tz]
  end
end
そして、ログインに使うコントローラーで session[:tz] を設定
class User::LoginController < UserRootController
  def index
    if request.post?
      if session[:user_id] = user.authenticate(params[:user][:email], params[:user][:password])
        user = User.find(session[:user_id])
        session[:tz] = user.time_zone if user.time_zone
        flash[:notice] = t('logged_in')
        if session[:return_to_path]
          redirect_to session[:return_to_path]
        else
          redirect_to user_root_path
        end
      else
        flash[:error] = t('login.failed')
        session[:user_id] = nil
      end
    end
  end
end

その他色々便利なタイムゾーン系メソッド

特定のタイムゾーンで表示する
sometime.in_time_zone("Hawaii")
# @user.time_zone にユーザーのタイムゾーンが入っている場合
sometime.in_time_zone(@user.time_zone)


オフセット、またはタイムゾーン文字列からタイムゾーンを返す
※特定のタイムゾーンがRails に存在するかどうかチェックするのに便利。Cafetalkでは、旧システムからデータを移行する際に使用しました。
ActiveSupport::TimeZone['Tokyo']
> (GMT+09:00) Tokyo
ActiveSupport::TimeZone[8]
ActiveSupport::TimeZone[+8]
> (GMT+09:00) Osaka


タイムゾーンの名前だけを表示
ActiveSupport::TimeZone[+8].name
> Osaka
オフセットを表示
Time.zone.utc_offset/3600
> -5 など。。。
フォームでタイムゾーンのプルダウンリストを表示
<%= f.time_zone_select( :time_zone, nil, :include_blank => true) %>
http://mad.ly/2008/04/09/rails-21-time-zone-support-an-overview/
http://marklunds.com/articles/one/402
http://api.rubyonrails.org/classes/ActiveSupport/TimeZone.html#M001513

2010/01/06

Cron で実行する

プログラムを自動的に実行したいとき。例えば、cafetalk.com では、レッスンの24時間前にリマインダーメールを送ってます。
Linux 系のサーバーで cron を使う場合。(下の例は5分毎に、Mymodelのmymethodを実行)
*/5 * * * * /usr/bin/ruby /var/www/apps/{project}/script/runner 'Mymodel.mymethod' -e production

因みに、Windows で動かす場合。
ruby script/runner 'Mymodel.mymethod' -e development

2010/01/04

Amazon S3 を使ったバックアップなんてどーですか

バックアップはかなり苦労が多い。今まで僕がやってたのは定期的にシェルスクリプトを書いて、別のサーバーに転送する方法。 でも、S3 を使えばらくかなー。 ↓こちらに方法があるので、いずれチャレンジして報告します。 http://paulstamatiou.com/how-to-bulletproof-server-backups-with-amazon-s3

Paperclip と Amazon S3 でファイルを保存する

オンラインショップのサーバー要領がちょっとキツクなったので、画像データを Amazon S3 を使って保存してみょーと思い立つ。
ざっくり説明しますと、Amazon S3が外部ストーレージサービス。データをやり取りした分だけ課金されます。1GBで15円ほどなので、安くは無いけど、いろんなメリットを考えると高くもない。とりあえずテストしてみることに決定~。

まず、Amazon S3 に登録

S3 のデータを見るには『S3Fox』が便利

Firefox のプラグイン『S3Fox』を使うと、FTPクライアント風にAmazon S3の中がみれるよ。

Paperclip 設定の変更

以前、Paperclip の使い方についてメモりましたが、そこからちょいと手を加えます。 environment.rb に aws gem を追加
config.gem "aws-s3", :lib => "aws/s3"
(require する際に require = "aws/s3" とする場合は上のように、 :lib => "aws/s3" を加えて記述) この後、rake gems:install を実行するのを忘れずに。 config/s3.yml を作成
access_key_id: ABCD...
secret_access_key: xYz...
キーとシークレットはここから。 モデルの変更
 #通常バージョン
 has_attached_file :image,
   :styles => {:s => "150x150>", :m =>"320x320>", :l => "500x500>"},
   :url  => "/assets/products/:id/p_:style.:basename.:extension",
   :path => ":rails_root/public/assets/products/:id/p_:style.:basename.:extension"

 #↓S3 バージョン
 has_attached_file :image,
   :styles => {:s => "150x150>", :m =>"320x320>", :l => "500x500>"},
   :storage => :s3,
   :s3_credentials => "#{RAILS_ROOT}/config/s3.yml",
   :path => "products/:id/:style.:extension",
   :url  => ":s3_domain_url",
   :bucket => S3_BUCKET #S3_BUCKET は production/environment.rb development/environment.rb で定義

:s3_domain_url だと http://mybucket.s3.amazonaws.com/products/16152/l.jpg のようになる。
因みに、上で指定した bucket をあらかじめ作っておく必要があるのですー↓ この場合、S3Fox で作成。 これで準備はOK。さっそくアップロードしてみると。。。 入ってる!感激~

Installing plugins from GitHub

ruby script/plugin install git://github.com/thoughtbot/paperclip.git
Somehow I couldn't get this to work with active_scaffold (which I don't use now, anyway). It tells me
Plugin not found: ["git://github.com/activescaffold/active_scaffold.git"]
I have gitbash installed on my Windows so I was finally able to do this.
cd vendor/plugins
git clone git://github.com/activescaffold/active_scaffold.git -o rails-2.3

Paperclip で画像アップロード

ファイルや画像を簡単にアップロードできるプラグイン Paperclip の紹介。
http://github.com/thoughtbot/paperclip/tree/master

最初からテーブルに含める場合

      # migration ファイルに以下を追加
      t.string    :image_file_name, :image_content_type
      t.integer   :image_file_size
      t.datetime  :image_updated_at
      t.string    :image_remote_url
最後の image_remote_url は基本的には必要ないが、URLから画像アップロードしたいので、今回は追加しておく。

既存のテーブルに追加する場合

class AddImageColumnsToProducts < ActiveRecord::Migration
    def self.up
      add_column :products, :image_file_name,    :string
      add_column :products, :image_content_type, :string
      add_column :products, :image_file_size,    :integer
      add_column :products, :image_updated_at,   :datetime
    end

    def self.down
      remove_column :products, :image_file_name
      remove_column :products, :image_content_type
      remove_column :products, :image_file_size
      remove_column :products, :image_updated_at
    end
end

モデルの設定

  has_attached_file :image, 
    :styles => {:s => "150x150>", :m =>"320x320>", :l => "500x500>"},
    :url  => "/assets/products/:id/p_:style.:basename.:extension",
    :path => ":rails_root/public/assets/products/:id/p_:style.:basename.:extension"  
URLからのアップロードを可能にする場合は次も追加

  # ====================================
  # Paper Clip upload from url
  # ====================================
  attr_accessor :image_url    
  before_validation :download_remote_image, :if => :image_url_provided?
  validates_presence_of :image_remote_url, :if => :image_url_provided?, :message => 'is invalid or inaccessible'

  private
 
  def image_url_provided?
    !self.image_url.blank?
  end
 
  def download_remote_image
    self.image = do_download_remote_image
    self.image_remote_url = image_url
  end
 
  def do_download_remote_image
    io = open(URI.parse(image_url))
    def io.original_filename; base_uri.path.split('/').last; end
    io.original_filename.blank? ? nil : io
    rescue # catch url errors with validations instead of exceptions (Errno::ENOENT, OpenURI::HTTPError, etc...)
  end
フォーム
<% form_for ([:admin, @page]), :html => {:multipart => true } do |f| %>
  <%= f.file_field(:image) %>
  ...or provide a URL: <%= f.text_field :image_url %>
<% end %>
表示
<%= image_tag @foobar.image.url(:m) if @foobar.image? %>

Rails 2.2.2 > 2.3.5 uninitialized constant ApplicationController でハマリマス

2.2.2 から 2.3.5 にアップグレードしたら、下記のエラー発生。
uninitialized constant ApplicationController
どうやら、2.3 から、application コントローラーのファイル名が application_controller.rb になったらしいです。ファイル名を変更したらOKになりました。

Rails gem 2.2.2 > 2.3.5 アップグレード時にハマリマス

Rails を 2.3.5にフリーズした後、さっそくエラー
`report_activate_error': RubyGem version error: rack(1.0.0 not ~> 1.0.1) Gem::LoadError)
なるほど、rack が必要そうなので、gem をインストール
gem install rack
Successfully installed rack-1.1.0
1 gem installed
これでも動かない。そりゃ、そうでですね。よく見たら必要なバージョンは 1.0.1。気を取り直して、バージョン指定で。
gem install rack --version 1.0.1 --no-rdoc --no-ri
Successfully installed rack-1.0.1
1 gem installed
これでOK。

特定のバージョンで rails の gem をフリーズする

特定のバージョンでフリーズしたいこともあるなり。
rake VERSION=2.3.5 rails:freeze:gems
config/environments.rb の編集も忘れずに
RAILS_GEM_VERSION = '2.3.5' unless defined? RAILS_GEM_VERSION

Rails 言語をパスで管理する方法

http://waraukigoya.com/ja/mypage のように、URLパスの一部を使って言語管理する方法があります。 パッチがあったので適用。
diff --git a/actionpack/lib/action_controller/routing/route_set.rb b/actionpack/lib/action_controller/routing/route_set.rb
index ff44849..f3a4f09 100644
--- a/actionpack/lib/action_controller/routing/route_set.rb
+++ b/actionpack/lib/action_controller/routing/route_set.rb
@@ -170,18 +170,24 @@ module ActionController
               def #{selector}(*args)
                 #{generate_optimisation_block(route, kind)}
 
+                locale = #{route.segment_keys.include?(:locale) ? '{ :locale => I18n.locale || I18n.default_locale }' : '{}'}
+
                 opts = if args.empty? || Hash === args.first
                   args.first || {}
                 else
                   options = args.extract_options!
-                  args = args.zip(#{route.segment_keys.inspect}).inject({}) do |h, (v, k)|
+
+                  segments = #{route.segment_keys.inspect}
+                  #{'segments.delete(:locale) if segments.size > args.size' if route.segment_keys.include?(:locale)}
+
+                  args = args.zip(segments).inject({}) do |h, (v, k)|
                     h[k] = v
                     h
                   end
                   options.merge(args)
                 end
 
-                url_for(#{hash_access_method}(opts))
+                url_for(#{hash_access_method}(locale.merge(opts)))
               end
               protected :#{selector}
             end_eval
次に routes.rb ファイルを設定する必要あり。
  map.connect ':controller/:action/:id'
  map.connect ':controller/:action/:id.:format'
  
  # ====================================================
  # Put all non locale controlled routes above this line
  # 通常ルートの下に、path_prefix で管理したルートを設定
  # ====================================================
  
  map.resources :mypages, :path_prefix => "/:locale"
  map.root :controller => "mypages", :action => "index", :locale => "ja"
  
  map.connect '/:locale/:controller/:action/:id'
  map.connect '/:locale/:controller/:action/:id.:format'
これをやっておけば、リンクヘルパーを使った際、自動的に現在の locale が引き継がれるので便利。
例えば、http://mysite.com/ja/home

の中で、<%= link_to "記事", mypages_path %> と書くと、次のようになる

<a href="http://mysite.com/ja/mypages">記事
もちろん、アプリケーションコントローラーで言語の設定は必要。

Making a link to change locale


application_helper.rb
  def change_locale(locale)
    link = request.parameters
    link[:locale] = locale
    link
  end

Blogger でソースコードを綺麗に embed する方法

開発系の備忘録をブログにアップしようと思ったけど、ブロガーだとソースコードが綺麗に表示されない。なので、google のライブラリ Prettify を使ってみた。
def generate_request_status_select(f)
return f.select( :request_status, [
[ _("Waiting for Quote"), 1],
[ _("Waiting for Customer Decision"), 2],
[ _("Incomplete Order"), 3]
])
end
参考: PMJ Engineering's article on using prettify to paste pretty code onto Blogger. こちらも。