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` !]
こちらのスレッドのまとめ