OSXのユーザ辞書の作り方

投稿者 nanki 2007-11-26 04:20:00 GMT

Leopardからは、小学館の辞書データとWikipediaが追加されて、とっても便利なDictionary.app.

たぶん、キーボード使用を前提とした小気味よいインターフェースがいいんだと思う。 慣れてしまうと、もう生のWikipediaを使う気はなかなか起きない。

こうなると、なんでもかんでも、同じインターフェースで済ませたくなるもので、そんな時に、こんな検索しにくいページを見たら、「Dictionary.appで検索できて然るべき」と思ってしまうのが自然な流れ。

というわけで、ライフゲームの辞書、Life LexiconをDictionary.app用に変換します。

まずは、テンプレートをコピーしてくる。 試しにmakeすると、あっさり辞書が作られる。 make installすれば、~/Library/Dictionaries 以下にコピーされ、Dictionary.appから検索できる状態になる。(Dictionary.appの環境設定で有効にしないとだめかも)

$ cp -r /Developer/Examples/Dictionary Development Kit/project_templates dict
$ cd dict
$ make
"""/Developer/Extras/Dictionary Development Kit"/bin"/build_dict.sh"  "My Dictionary" MyDictionary.xml MyDictionary.css MyInfo.plist
- Building My Dictionary.dictionary.
- Cleaning objects directory.
- Preparing dictionary template.
- Preprocessing dictionary sources.
- Extracting index data.
- Preparing dictionary bundle.
- Adding body data.
- Preparing index data.
- Building key_text index.
- Building reference index.
- Fixing dictionary property.
- Copying CSS.
- Copying other resources.
- Finished building ./objects/My Dictionary.dictionary.
echo "Done."
Done.

重要なのは、MyDictionary.xmlファイル。 辞書の元となるデータをパースして、XMLファイルを作ってやる。

<?xml version="1.0" encoding="UTF-8"?>                                                                               
<d:dictionary xmlns="http://www.w3.org/1999/xhtml" xmlns:d="http://www.apple.com/DTDs/DictionaryService-1.0.rng">    
  <d:entry id="blinker puffer" d:title="blinker puffer">                                                             
    <d:index d:value="blinker puffer" d:title="blinker puffer" />                                                    
    <d:index d:value="puffer" d:title="blinker puffer" />                                                            
    <h1><span class="headword">blinker puffer</span></h1>                                                            
    <span class="meaning">Any <a href="x-dictionary:r:puffer">puffer</a> whose output is <a href="x-dictionary:r:blinker">blinker</a>s.  However, the term is particularly used for p8 c/2 puffers.  ....
    </span>
  </d:entry>
  <!-- 以下d:entry の山 -->
</d:dictionary>

意味はほとんど見た目通り。
それぞれの項目は、インデックスと内容のXHTMLで構成される。 他の単語を参照する、href=”x-dictionary:r:ID”とかが特徴的か。 上の例では「blinker puffer」という単語に対して、pufferでも検索に引っかかって欲しいので、indexに加えてある。

パースしてXMLを吐くコードは、汎用性がないのでここには載せないが、今回は以下のようなLifeを表す図を、rcairoでpng画像に変換もしている。

OO..
O..O
..OO

画像などのリソースは、dict/OtherResources 以下の適当な場所に置いておき、普通のHTMLのように、<img>タグを使って埋め込む。

<p><img src="Images/blinker%20puffer_0.png"/></p>

これで、

$ make
$ make install

すれば、ユーザ辞書の完成。

続き

さて、これだけで終わりではもったいないので、先日作ったQuartz Compositionを組み込めないか、頑張ってみる。

HTMLにQuartz Compositionを組み込むのは簡単で、EMBEDタグを使って、

<embed id="composition" type="application/x-quartzcomposer" src="LifeLexicon.qtz" width="300px" height="300px" />

とする。

これで、普通のムービーならスタートするのだけど、ライフゲームなので第0世代の画像を渡してやらないといけない。

Quartz ComposerのWebKitプラグインにはJavaScript APIが用意されていて、publishしたパラメータを設定できるようになっている。 が、文字列しか設定できないようだ。画像はどうやって渡すのだろう。

調べてみると、Compositionの中で、Image Downloaderというパッチを使うらしい。 Image Downloaderは指定されたURLの画像をImageとして出力するパッチ。 なるほど。 ImageDownloaderの入力を、imageLocationという名前でPublishして・・・

var composition = document.getElementById("composition");
composition.setInputValue("imageLocation", "fullpath.png");

こんな感じになるのかな。 しかし、フルパス指定じゃないといけないのが面倒だなぁ・・・

先生、こちらにできあがったものが用意してあります。

結局、location.href と、EMBEDに勝手に付け加えたimage属性から、フルパスを取得する仕組みに。 文中のSpaceはCompositionというか、Universeというか。

//LifeLexicon.js
(function () {
  var waitForSpaces = function (spaces) {
    if (spaces.length == 0) return;

    if (spaces[0].loaded && spaces[0].loaded()) {
      var space = spaces.shift();
      var base = location.href.substr(0, location.href.lastIndexOf("/"))
      space.setInputValue("imageLocation", base + space.getAttribute('image'));
    } 
      
    with ({callee: arguments.callee}) {
      setTimeout(function () {callee(spaces)}, 100);
    }
  };

  var arrayFromNodeList = function (nodelist) {
    var result = [];
    for (var i = 0; i < nodelist.length; i++) {
      result.push(nodelist.item(i));
    }
    return result;
  };

  waitForSpaces(arrayFromNodeList(document.getElementsByTagName('EMBED'))); 
})();

なぜか、<script src="LifeLexicon.js">だと読み込まれないので、

<script>
  var tag = document.createElement("SCRIPT");
  tag.src = "./LifeLexicon.js";
  document.getElementsByTagName("HEAD")[0].appendChild(tag);
</script>

というコードも、<d:entry>内に埋め込むように。

これで「生きた」LifeLexiconが完成。

LifeLexicon

CREDITをどこかに入れないと配布できないらしいので、欲しい人には直接送ります。 いないだろうけど。

誰か、Ruby Reference Manualをこれで。

参考:

Railsアプリケーションの国際化 - Globalize

投稿者 nanki 2007-08-26 10:15:00 GMT

Globalizeを使ってみる。

セットアップ

適当なRailsアプリを作って、Globalizeをインストール。

$ script/plugin install http://svn.globalize-rails.org/svn/globalize/trunk
....
$ mv vendor/plugins/trunk vendor/plugins/globalize
$ rake globalize:setup

CSV::Cellがinspectされたようなエラーが出るので、vendor/plugins/globalize/tasks/data.rake を修正する。

--- ./vendor/plugins/globalize/tasks/data.rake.orig     0000-00-00 00:00:00.000000000 +0000
+++ ./vendor/plugins/globalize/tasks/data.rake  0000-00-00 00:00:00.000000000 +0000
@@ -17,3 +17,3 @@

-    columns = reader.shift.map { |column_name| cnx.quote_column_name(column_name) }
+    columns = reader.shift.map { |column_name| cnx.quote_column_name(column_name.data) }
     column_clause = columns.join(', ')
@@ -24,3 +24,3 @@
       raise "No header defined"     unless column_clause
-      values_clause = row.map { |v| cnx.quote(v).gsub('\\n', "\n").gsub('\\r', "\r") }.join(', ')
+      values_clause = row.map { |v| cnx.quote(v.data).gsub('\\n', "\n").gsub('\\r', "\r") }.join(', ')
       sql = "INSERT INTO #{table_name} (#{column_clause}) VALUES (#{values_clause})"

さらに、vendor/plugins/globalize/data/language_data.csv の空行がエラーを起こしているので修正。

$ rake globalize:setup
...

成功。

View翻訳

base_languageを英語に。

# config/environment.rb
Globalize::Locale.set_base_language("en-US")
$ script/console
>> "March [month]".t
=> "March"
>> Globalize::Locale.set("ja-JP")
=> #<Globalize::Locale...
>> "March [month]".t
=> "3月"
>> Time.now.loc "%A"
=> "日曜日"

ふむふむ。この辺は、gettextの方が使いやすそうかなぁ。

Model翻訳

適当なモデルを作成

$ script/generate model Product
...
# db/migrate/001_create_products.rb
  def self.up
    create_table :products do |t|
      t.column :name, :string
      t.column :manufacturer, :string
    end
  end
$ rake db:migrate
$ script/console
>> p = Product.create :name => "Pucchin Pudding", :manufacturer => "Glico"
=> #<Product:...
>> p.name
=> "Pucchin Pudding"
>> p.manufacturer
=> "Glico"

# jaで保存。
>> Globalize::Locale.set("ja-JP")
>> p.name = "プッチンプリン"
>> p.manufacturer = "グリコ"
>> p.save
=> true

# jaで読む。
>> p.reload
>> p.name
=> "プッチンプリン"
>> p.manufacturer
=> "グリコ"

# enで読む。
>> Globalize::Locale.set("en-US")
>> p.reload
>> p.name
=> "Pucchin Pudding"
>> p.manufacturer
=> "Glico"

ちゃんと動いてる。

この時、productsテーブルにはbase_languageで格納され、それとは別にglobalize_translationsテーブルに、翻訳情報を保持している。

sqlite> SELECT * FROM globalize_translations WHERE id > 7088;
id          type              tr_key      table_name  item_id     facet       built_in    language_id  pluralization_index  text                   namespace
----------  ----------------  ----------  ----------  ----------  ----------  ----------  -----------  -------------------  ---------------------  ----------
7089        ModelTranslation              products    18          name        t           2723                              プッチンプリン
7090        ModelTranslation              products    18          manufactur  t           2723                              グリコ

Object#_が定義されていたので、gettextとケンカするかと思ったけど、一緒に動いた。 偶然かもしれないけど。

screenshot

そんなこんなで、↑こんなアプリケーションができてしまう。
はたして、使う機会は訪れるのか?

追記:

  • eager loadingを行う時は、:include_translatedを使う。
  • が、対応が中途半端で、user.product_nameとアクセスしないとだめっぽい。
class User
  belongs_to :product
end

user = User.find(:first, :include_translated => :product)

user.product.name #=> NG
user.product_name #=> OK
参考:

Infinity

投稿者 nanki 2007-08-01 23:05:00 GMT

よいこは真似してはいけない、変数名。

$ irb -Ku

∞ = 1.0 / 0
=> Infinity

(-∞..4).include? 5
=> false

(4..∞).include? 5
=> true

これが普通に動くRubyに感謝。0で割るとInfinityはかなり気持ち悪いけど。

長い名前にすると、..の左側にいるときと右側にいるときのバランスが悪い。
でも、∞は、見た目短いけど、タイプしづらい。ooかな?

Infinity = 1.0 / 0

-Infinity..4.000000
 4.000000..Infinity

これか!

Inf = 1.0 / 0

-Inf..4.0
 4.0..Inf

あるいは、これくらいで手を打つか。


OSC2007Kansai

投稿者 nanki 2007-07-22 07:30:00 GMT

に行ってきました。

今回は、Railsハンズオンセミナーの講師をやったけど、一日目は、プロジェクタへの接続を確認してなくて、みなさまにご迷惑をおかけしました。ごめんなさい。

ちゃんと設定したのに、二日目もやっぱりつながらなくて、okkezさんに迷惑かけました。ごめんなさい。

二日目の懇親会で見た楽器、ウダーがスゴかった。
販売してたら危うく購入するところだったのに。残念。
今後に期待しています。

あと、お子様連れのお子様たちがとってもかわいかったよぅ。子供ほしー。

帰りは、京都駅から四条まで、数人で散歩。
翻訳の仕事をしている外国の方(といってももうほとんど日本の方)も一緒で、東本願寺に掲げてあった看板のコピーをだしに、面白い話が色々聞けた。

OSCの内容の話が無い!

参考:


:conditions の遅延評価

投稿者 nanki 2007-07-05 15:38:00 GMT

泥臭い&あんまり使わないので忘れがちだが、Associationを定義する時に、シングルクォートを使うと、参照時のインスタンスで評価し直してくれる。

  class Road
    belongs_to :node, :foreign_key => [:meshcode, :nodeno], :conditions => 'nodes.linkno = #{linkno}'
  end

  Road.find(:first).node # => "SELECT * FROM nodes WHERE nodes.linkno = 1"
  class Road
    belongs_to :node, :foreign_key => [:meshcode, :nodeno], :conditions => 'nodes.linkno = #{table_name}.linkno'
  end

  Road.find(:first, :include => :node) # => "SELECT * FROM roads LEFT OUTER JOIN nodes ON ... WHERE nodes.linkno = roads.linkno"

ここでtable_nameが(やってみたら)使えるのは、

# lib/active_record/associations.rb
  class JoinBase
    attr_reader :active_record, :table_joins
    delegate    :table_name, :column_names, :primary_key, :reflections, :sanitize_sql, :to => :active_record

このdelegateのおかげか?

次のように書けば、どちらでもいける。

  belongs_to :node, :foreign_key => [:meshcode, :nodeno], :conditions => 'nodes.linkno = #{linkno rescue "#{table_name}.linkno"}'

いいのか、こんなんで。


Rails開発用便利スクリプト

投稿者 nanki 2007-06-18 17:01:00 GMT
#!/usr/bin/env ruby

EDITOR = ENV['EDITOR'] || 'vim'

files = `find . -type f ! -path './tmp/*' ! -path './log/*' ! -path '*/.svn/*' #{ARGV.map{|v| "#{/^-/ === v ? '!' : ''} -path '*#{v.gsub(/^-/, '')}*' "}.join}`
system("#{EDITOR} #{files.split.join(' ')}")
puts files

というスクリプトをvifという名前でコマンドにしておいて、$ vif .jsでJavaScriptファイルを全て開いたり、$ vif controller -testとかで、テストを除くコントローラ関係のファイルをすべて開いたり。

EclipseのGoToFileみたい。


古代遺跡発掘

投稿者 nanki 2007-05-17 04:17:00 GMT

ずいぶんと昔に書いたコードを、最新のRailsに移植してみようか、と思って見てみると、

# in config/environment.rb

require_gem 'rails', '= 0.12.1'

さらに古い地層には、CGIKitで書かれた先Rails時代のコードが・・・。

3 日坊主日記 Rails 0.12.1 -> 0.13.0

完全な後方互換性? んなわけないって! ;)

はい、先生。


RubyInlineがすごい

投稿者 nanki 2007-03-12 14:14:00 GMT

Rubyコード中にCのコードを埋め込めるRubyInlineを使って、 ボトルネックとなっているメソッドを置き換える。

# rubyinline.rb
def benchmark
  s = "a" * 10000

  test = Test.new
  t = Time.now
  1000.times{test.string_xor(s, s)}
  Time.now - t
end
  
class Test
  def string_xor(str1, str2)
    result = str1.clone
    str1.length.times do |i|
      result[i] ^= str2[i]
    end
    result
  end
end
  
b1 = benchmark
  
begin
  require 'inline'
  class Test
    inline do |builder|
      builder.c <<-EOF
        VALUE
        string_xor(VALUE str1, VALUE str2)
        {
          VALUE result = rb_str_new(RSTRING(str1)->ptr, RSTRING(str1)->len);
          int i;
          for (i = 0; i < RSTRING(str1)->len; i++) {
            RSTRING(result)->ptr[i] ^= RSTRING(str2)->ptr[i];
          }
          return result;
        }
      EOF
    end
  end
rescue LoadError
end
  
b2 = benchmark
  
p b1/b2

長さ10000の文字列同士のxorを1000回取る、というプログラムでテスト。

$ ruby -rubygems rubyinline.rb
192.710154532523

192倍!

2007/4/6 追記:

そういえば、GCを切るのを忘れていたと思って、GC無しで実行したら、300倍になった。

参考:

RubyInline


RubyのGeometry系ライブラリ - パース対決

投稿者 nanki 2007-03-06 18:29:00 GMT

必要だったのでベンチマーク対決してみた。 対象は、GEOS, GeoRuby, おまけで自分で実装したWKBパーサ。

GEOSは、JavaのJTSをC++に移植したもので、RubyのバインディングがSWIGで用意されている。

GeoRubyは、PureRuby実装。最近は、地図業界で使われることの多いShapeファイルや、xBaseファイルを扱うクラスも追加されたようだ。

最後のは、僕が初めてRubyで書いたまじめなコード。 大量のデータを処理する目的があったので、エラー処理もすっ飛ばして、可能な限り速く動くようにしてある。 中身はほとんどunpack<<[]

こんなコードで約二万件のMultiLineStringをパース。

# require やらARのモデル定義は省略
def benchmark
  roads = Road.find(:all).map{|road| road.the_geom[4..-1]}
  t = Time.now
  roads.each do |road|
    yield road
  end
  p Time.now - t
end
  
benchmark do |road|
  Geos::geom_from_wkb(road)
end
  
benchmark do |road|
  GeoRuby::SimpleFeatures::Geometry.from_ewkb(road)
end
  
benchmark do |road|
  Geometry::WKBParser.parse(road)
end

結果は、

0.375728  #Geos
5.319374  #GeoRuby
1.353812  #Geometry

やはりC++コードは速い。SWIGのオーバーヘッドはどれくらいなんだろう。

Rubyのコードもそこそこ。頑張れば数倍程度。思ったより遅くない。

さらに、YARVでもやってみた。 ActiveRecord は動かなかったので(大幅に仕様の変ったというsend でこけた時点であきらめた)Marshal.dumpしたデータを利用。 再コンパイルが面倒だったのと、実質のコードはほとんどRuby側じゃないのでGEOSは除外。

3.836262  #GeoRuby
0.699637  #Geometry

YARV速っ。

参考:

発展課題の解答を用意してみました。
一解答例にすぎませんが、参考にしてください。

急いで作ったので間違ってたらツッコんでください。

:per_page に指定した数字に合わせて、前のン件, 次のン件としてみよう。

ActionController::Pagination::Paginator#items_per_pageを使って、
$ vi app/views/bbs/list.rhtml

<%= link_to "前の#{@comment_pages.items_per_page}", { :page => @comment_pages.current.previous }
            if @comment_pages.current.previous %>
<%= link_to "次の#{@comment_pages.items_per_page}", { :page => @comment_pages.current.next }
            if @comment_pages.current.next %>

リファレンスマニュアルでは、Attributesのところにあって、なんの説明もないので、見つけにくいかもしれません。

http://localhost:3000/bbs/edit/1 と言う風に直接アクセスすると、誰の発言でも自由に編集できてしまう。コントローラから使ってないメソッドを削除して、編集ができないようにしよう。

app/controllers/bbs_controller.rb中の、edit,show,updateメソッドを削除し、verify の行にある、:updateも消す。
app/views/bbs/edit.rhtmlapp/views/bbs/show.rhtml二つのファイルも消します。

投稿フォームを /list/bbs の一番上に移動して、直接投稿できるようにしてみよう。

app/views/bbs/list.rhtmlの上の方に、

<div>
  <%= start_form_tag :action => 'create' %>
    <%= render :partial => 'form' %>
    <%= submit_tag "投稿" %>
  <%= end_form_tag %>
</div>

を追加。

# app/controllers/bbs_controller.rb
   def list
     @comment_pages, @comments = paginate :comments, :per_page => 10, :order => 'created_at desc'
     @comment ||= Comment.new # ||= は、create から呼ばれた時に@commentを上書きしないため。
   end
  
   def create
     @comment = Comment.new(params[:comment])
     if @comment.save
       flash[:notice] = 'Comment was successfully created.'
       redirect_to :action => 'list'
     else
       list # この行追加
       render :action => 'list' # エラーがあった場合の戻り先を変更
     end
   end

   # new アクションは削除

app/views/bbs/new.rhtmlも消してしまいましょう。

Commentにvalidatesを追加して、必須項目を決めよう。

title, body, name を必須項目に、email, homapage は空じゃなかったら、正規表現で判定をしています。 email の方の正規表現は、 validates_format_of のサンプルコードから、homepageの方は先頭がhttp://またはhttps://かをチェックしています。 凝りたい人はどうぞ。

# app/models/comment.rb
class Comment < ActiveRecord::Base
  validates_presence_of [:title, :body, :name]
  validates_format_of :email, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :if => proc{|comment| !comment.email.blank?}
  validates_format_of :homepage, :with => %r|^https?://|, :if => proc{|comment| !comment.homepage.blank?}
end

また、次のようなテストコードを書いて、

$ rake test:units

を実行することで、正しそうに動いているかチェックできます。

テストコード↓を書いてから、実装↑を書くと、TDD.

# test/unit/comment_test.rb
require File.dirname(__FILE__) + '/../test_helper'
  
class CommentTest < Test::Unit::TestCase
  def setup
    @comment = Comment.new(:title => 'hi!', :body => 'hello!', :name => 'bob', :email => "", :homepage => "")
  end
  
  def test_validation
    assert_valid @comment
  end
  
  def test_presence_on_title
    @comment.title = nil
    @comment.save
    assert_not_nil @comment.errors.on(:title)
  end
  
  def test_presence_on_body
    @comment.body = nil
    @comment.save
    assert_not_nil @comment.errors.on(:body)
  end
  
  def test_presence_on_name
    @comment.name = nil
    @comment.save
    assert_not_nil @comment.errors.on(:name)
  end
  
  def test_email
    @comment.email = 'bob@rails6.com'
    @comment.save
    assert_valid @comment
  
    @comment.email = 'bob.rails6.com'
    @comment.save
    assert_not_nil @comment.errors.on(:email)
  end
  
  def test_homepage
    @comment.homepage = 'https://bob.rails6.com'
    @comment.save
    assert_valid @comment
  
    @comment.homepage = 'http://bob.rails6.com'
    @comment.save
    assert_valid @comment
  
    @comment.homepage = 'ftp://bob.rails6.com'
    @comment.save
    assert_not_nil @comment.errors.on(:homepage)
  end
end

削除キーを入力しないと削除できないようにしてみよう。

まず、migration でdelete_key というカラムを追加します。

$ script/generate migration AddDeleteKey
      exists  db/migrate
      create  db/migrate/004_add_delete_key.rb
# vi db/migrate/004_add_delete_key.rb
class AddDeleteKey < ActiveRecord::Migration
  def self.up
    add_column :comments, :delete_key, :string
  end
  
  def self.down
    remove_column :comments, :delete_key
  end
end

$ rake db:migrate

次に、投稿フォームに、削除キー入力フィールドを作り、

<!-- app/views/bbs/_form.rhtml -->
<p><label for="comment_delete_key">削除キー</label><br/>
<%= text_field 'comment', 'delete_key'  %></p>

削除ボタンのとなりにテキストフィールドを追加。

<!-- app/views/bbs/list.rhtml -->
     <td>投稿日時:<%= comment.created_at.strftime("%Y/%m/%d %H:%M:%S") %></td>
     <td>
       <%= start_form_tag :action => 'destroy', :id => comment %>
       <%= text_field_tag 'delete_key' %>
       <%= submit_tag "削除" %>
       <%= end_form_tag %>
     </td>

その値を受けて、保存された削除キーと、入力された削除キーが一致しないとdestroyしないように、コントローラを書き換える。

# app/controllers/bbs_controller.rb
   def destroy
     c = Comment.find(params[:id])
     c.destroy if !c.delete_key.blank? && c.delete_key == params[:delete_key]
     redirect_to :action => 'list'
   end

今回のテストコードは、コントローラのテストなので、以下の通り。

まず、テストデータを二つ用意。 片方は、削除キー’bob123’、片方は削除キーなし(削除できない)

# test/fixtures/comments.rb
bob:
  id: 1
  title: hi
  body: hello
  name: bob
  delete_key: bob123
undeletable:
  id: 2
  title: hi
  body: hello
  name: bob
  delete_key: ''
# test/functional/bbs_controller_test.rb
require File.dirname(__FILE__) + '/../test_helper'
require 'bbs_controller'
  
# Re-raise errors caught by the controller.
class BbsController; def rescue_action(e) raise e end; end
  
class BbsControllerTest < Test::Unit::TestCase
  # テスト用データである、fixture をロード。(上で書いたやつ)
  fixtures :comments
  
  # test_* の前に実行される。
  def setup
    @controller = BbsController.new
    @request    = ActionController::TestRequest.new
    @response   = ActionController::TestResponse.new
  end
  
  # 削除キーを指定しないと削除されない
  def test_destroy_without_delete_key
    id = comments(:bob).id
    assert_not_nil Comment.find(id)
    post :destroy, :id => id
    assert_not_nil Comment.find(id)
  end
  
  # 削除キーを指定すると削除される
  def test_destroy_with_delete_key
    id = comments(:bob).id
    assert_not_nil Comment.find(id)
    post :destroy, :id => id, :delete_key => comments(:bob).delete_key
    assert_raise(ActiveRecord::RecordNotFound) {
      Comment.find(id)
    }
  end
  
  # 削除キーが空のレコードは、空の削除キーで削除できない
  def test_destroy_record_having_empty_delete_key
    id = comments(:undeletable).id
    assert_not_nil Comment.find(id)
    post :destroy, :id => id, :delete_key => comments(:undeletable).delete_key
    assert_not_nil Comment.find(id)
  end
end