HamlでTextarea(Rails)

Rails + Haml で textarea を出力しようとすると、textarea 内に表示しようとしたテキストまでインデントされてしまう。

find_and_preserve というヘルパーメソッドを使えば、中身のインデントは保持されるようだ。

= find_and_preserve(f.text_area)

Simple Form を使ってる場合も、同様にいける。

= find_and_preserve(f.input :comment, as: :text)

参考URL: How do I stop Haml from indenting the contents of my pre and textarea tags?

HamlでTextarea(Rails)

ActiveRecord::Store 便利

 ActiveRecord で、条件によってカラムが変わったりするのを serialize した列にテキトウにぶっ込んでおくというありがちな実装の時に、実カラムのように透過的に扱えて validation とかも簡単にかけられる ActiveRecord::Store ちゃん便利。

 例えば、SNSのアカウントを sns_accounts 列に Serialize して入れる場合(普通入れないが…)

ActiveRecord::Store を使わない時

class User
  serialize :sns_accounts, Hash
end

とかして、以下のような感じにします。

user = User.new
user.sns_accounts = {twitter:'ecpplus', github:'ecpplus'}

ActiveRecord::Store を使った時

class User
  store :sns_accounts, accessors: [:twitter, :github]
end

としておけば、下記のような感じに出来ます。

user = User.new
user.twitter = 'ecpplus'
user.github = 'ecpplus'

validation したい時も特に何も考えず、通常の列と同じようにすればOKです。

class User
  store :sns_accounts, accessors: [:twitter, :github]
  validates :twitter, presence:true
  validates :github, presence:true
end

Controller から使う時は、Strong Parameters で permit してあげるのを忘れないであげてね!

params.require(:user).permit(:twitter, :github)
ActiveRecord::Store 便利

unit test の fixtures で関連がないテーブルの primary key が取りたい

Rails の unit test を書いていて、関連がないテーブルの primary key が知りたい状況があった。

関連があるテーブルだと、

class User < ActiveRecord::Base
has_many :posts
end
class Post < ActiveRecord::Base
belongs_to :user
end

みたいなときに

test/fixtures/users.yml

chihaya:
name: 千早

test/fixtures/posts.yml

first:
user: chihaya
title: くっ…!
body: 先日フェスがあったのですが…

みたいな感じにすると関連が作れる。

今回は、単純なログみたいなもので、色んなテーブルの id を、target_id みたいなところにしまって、クラス名と合わせてユニークになるように管理してた。数が多かったのと関連付けて取る必要が無かったので、関連を作ってなかったんだけれども、fixture を書く時に、外部の id の取得方法に迷った。

つまり、先ほどの例で、has_many, belongs_to が無かった時にどうするかということです。
users.yml に、 id:1 とか書くというのも1つの方法です。

id を指定せずに rake fixtures:load ってすると、id が結構大きな数字になりますが、あれはラベルの文字列によって計算された値みたいですね。

ActiveRecord::Fixtures.identify(label) というメソッドで計算されていて、プラットフォーム依存で、同じ文字列を与えれば同じ値が返ってくる仕組みになっているようです。

そうすると、先ほどの例だと

test/fixtures/posts.yml

first:
user_id: <%= ActiveRecord::Fixtures.identify(:chihaya) %>
title: くっ…!
body: 先日フェスがあったのですが…

とすると、常に千早の id が取れるようになります。ActiveRecord::Fixtures.identify(:chihaya) の値は、見て分かるようにテーブルに依存することはなく、別テーブルで chihaya というラベルを使うと、同じ id になります。ActiveRecord::Fixtures が見つからないときは、 require ‘active_record/fixtures’ とします。

なんだろうなーと思ってた巨大な整数の id の計算方法がわかってすっきりしました。

unit test の fixtures で関連がないテーブルの primary key が取りたい

Nginx の X-Accel-Redirect を Rails で使う

 Nginx で認証つきで大容量ファイルをダウンロードさせたい時は、X-Accel-Redirect を使います。Rails のファイル操作を介さず、Nginxで直接クライアントへデータを送れます。Lighttpd とか Apache でいうところの、X-sendfile と同じような感じでしょうか。
Lighttpd の場合はこちら -> Rails&Lighttpdで大容量ファイルをダウンロード【変なヤバいもんログ内】

 ヘッダに、X-Accel-Redirect でパスを指定して、そのパスを Nginx 側で受けるだけですinternal って付けるのがポイントです。そうすると、このように内部からしか参照できなくなります。外からそのパスを叩いても見れないということになります。

 Rails2以前の場合は自分でヘッダに値を設定する必要があり、Rails3以降は設定を変更するだけでいけるようです。

# Rails2以前
def download
  # 認証処理などを挟むことができます。  
  path = "/bigfiles/long_movie.mp4"
  response.headers['X-Accel-Redirect'] = path
  response.headers['Content-Type'] = "application/force-download" 
  response.headers['Content-Disposition'] = "attachment; filename=\"#{File.basename(path)}\"" 
  response.headers['Content-length'] = File.size(RAILS_ROOT + path)
  render :nothing => true
end
 
# Rails3
def download
  # 認証処理などを挟むことができます。  
  path = "/bigfiles/long_movie.mp4"
  send_file path, 
             :type => "application/force-download",
             :disposition => "attachment; filename=\"#{File.basename(path)}\""
end
 
# config/environments/production.rb
# 下記行をコメントアウト (Apache, lighttpd の場合はこの行を使う)
# config.action_dispatch.x_sendfile_header = "X-Sendfile"
# 下記の行を有効にする (nginxの場合はこの行を使う)
config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect'
location / {
    client_max_body_size    200m;
    proxy_read_timeout 60;
    proxy_connect_timeout 60;
    proxy_pass http://railsapp;
}

location /bigfiles {
    # -> /path/to/railsroot/bigfiles
    root /path/to/railsroot/;
    internal;
}

gist に上げてあります。https://gist.github.com/708959

補足

 逆に、ちょっとしたアプリを動かすときに、いちいち nginx の設定とかせずに Unicorn とかから直接ファイルを送りたい場合もあります。Rails3になってから、config/environments/productioin.rb がデフォルトのままだと send_file すると 0bytes になってしまいます。

 下記の2行をコメントアウトしておくと、Unicorn なりで直接ファイル送信ができるようになります。

# config.action_dispatch.x_sendfile_header = "X-Sendfile"
# config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect'

 ついでに、静的ファイルも Unicorn なりで表示したいような手抜き構成のときは、下記の設定もお忘れなく。

config.serve_static_assets = false
Nginx の X-Accel-Redirect を Rails で使う

accepts_nested_attributes_for のメモ

 忘れやすいので、 accepts_nested_attributes_for の使い方についてメモ。

 Company に複数の Employees がいるとき。

class Company
has_many :employees
accepts_nested_attributes_for :employees
end
 
class Employee
  belongs_to :company
end

View で、下記のように fields_for を使う。Company作成時に、1人の Employee を同時に作る時の例

< %= form_for @company do |f| %>
  name < %= f.text_field :name %><!-- Company の name -->
  < %= f.fields_for :employees do |employee_form| %>
    employees name %lt;%= employee_form.text_field :name %><!-- Employee の name -->
  < % end  %>
  < %= f.submit "Create" %>
< % end %>

コントローラでは、特に何もしなくていいです。

def create
  @company = Company.create!(params[:company])
  redirect_to @company
end
accepts_nested_attributes_for のメモ

Rails の Dynamic Scopes での delete_all は外部キーを NULL にするだけ

Shop has_many Items という状況のとき、とある shop が持っている items を、下記のように取ることが出来ます。それに対して、条件を設定することも出来ます。

>> @shop.items 
=> [Item id:21 shop_id: 1, Item id: 22, shop_id: 1, ....]
>> @shop.item.find_all_by_category("CPU") 
=> [Item id:23 shop_id: 1, Item id: 26, shop_id: 1, ....]

そこで、同様にとある Shop の Items を全部 delete_all にしようとしたのですが、DELETE されるのではなく、外部キーにNULLがセットされるという挙動になっています。

>> @shop.items.delete_all

とすると

  Item UPDATE (3.0ms)   UPDATE `items` SET shop_id = NULL WHERE (shop_id = 1 AND id IN (22,23,24,25,26))

のようになります。

Rails の API を見ると

# File vendor/rails/activerecord/lib/active_record/base.rb, line 897
897:       def delete_all(conditions = nil)
898:         sql = "DELETE FROM #{quoted_table_name} "
899:         add_conditions!(sql, conditions, scope(:find))
900:         connection.delete(sql, "#{name} Delete all")
901:       end

みたいになっているのですが、Dynamic Scopes はまた別のところで定義されているっぽいです。

>> @shop.items.destroy_all

としてみると、下記のように DELETE になりますが、SQL が発行されまくってるのでだめぽいです。そもそも destroy は、destroy をトリガーにしている各種処理をしてから DELETE が行われるので、そもそも delete とは意味合いが違うというので仕方ないです。

Item Destroy (3.0ms)   DELETE FROM `items` WHERE WHERE id = 21
Item Destroy (3.0ms)   DELETE FROM `items` WHERE WHERE id = 22
Item Destroy (3.0ms)   DELETE FROM `items` WHERE WHERE id = 23

ふつうに Item.delete_all を呼ぶかなぁ

Rails の Dynamic Scopes での delete_all は外部キーを NULL にするだけ