木曜日, 8月 09, 2007

[Ruby][Rails]InputDrawとrailsで作る手書き掲示板

InputDrawとrailsを使って手書き掲示板を作成してみました。InputDrawとは手書き可能なフォームを簡単に作成できるライブラリです。下記のような手書きキャンバスを作成できます。



scaffoldで雛型作成


まずはscaffoldで雛型を作成します。

rails inputdraw
cd inputdraw
./script/generate scaffold_resource post title:string nickname:string comment:text image:string svg:text created_at:datetime updated_at:datetime


データベース準備


inputdraw_developmentというデータベースを作成しておき、マイグレーションを実行します。

rake db:migrate


file_columnプラグインのインストール


画像アップロード処理にはfile_columnプラグインを利用します。これを使うとアップロード処理と、レコードとファイルパスの関連付けを自動でおこなってくれます

./script/plugin install http://opensvn.csie.org/rails_file_column/plugins/file_column/trunk


InpudDrawのインストール


サイトからファイルをダウンロードし、public/javascriptsディレトリに配置します。
|-- javascripts
| |-- inputdraw
| | |-- inputdraw.free-non-commercial.1.2.swf
| | |-- inputdraw.js
| | `-- swfobject.js


ImageSizeのインストール


キャンバスのサイズは画像サイズから求めることにしました。
file_columではRMagickを使って画像を簡単に加工できるようになっていますが、今回はサイズが欲しいだけだったのでImageSizeを利用しました。
gem install imagesize


テンプレートの編集


app/view/layout/posts.rhtml

InputDrawに必要なファイルを読み込みます
<%= javascript_include_tag 'inputdraw/swfobject' %>
<%= javascript_include_tag 'inputdraw/inputdraw' %>


app/view/layout/new.rhtml
Imageをfile_fieldに変更し、フォームタイプをmultipartにします。
svgとcreated_at,updated_atはとりあえず削除します。
<% form_for(:post, :url => posts_path, :html => { :multipart => true }) do |f| %>

<b>Image</b><br />
<%= f.file_field :image %>


app/view/posts/index.rhtml
イメージの部分をタグに置き換えます。svgは表示しないので削除します。
 <td><%= image_tag url_for_file_column(post, "image"), :height => 150 %></td>


app/view/posts/show.rhtml
svgの部分をInputDrawのキャンバスにする要素で置き換えます。表示用に使用したいのでここではsrc要素を指定しています。
imageタグの部分は削除します。
<p>
<b>Svg:</b>
<div id="place"></div>

</p>

<!-- 末尾に追加 -->
<script type="text/javascript">
//<[![CDATA
new InputDraw("/javascripts/inputdraw/inputdraw.free-non-commercial.1.2.swf", "place",

{src_svg:"<%=h @post.svg %>",
width:"<%= @post.image_width %>",
height:"<%= @post.image_height %>",

animation:60,
background_image: '<%= url_for_file_column(@post, "image") %>'});
//]]>
</script>


app/view/posts/edit.rhml
svgのテキストエリアは非表示にします。変わりにキャンバス用の要素とInputDrawのコードを追加します。編集用に使う場合はid要素でsvgデータを保持している入力コントロールのIDを指定します。

<div id="place"></div>

<div style="visibility:hidden; display:none">

<%= f.text_area :svg %>
</div>

<!-- 末尾に追加 -->
<script type="text/javascript">

//<[![CDATA
new InputDraw("/javascripts/inputdraw/inputdraw.free-non-commercial.1.2.swf", "place",
{id:"post_svg",

width:"<%= @post.image_width %>",
height:"<%= @post.image_height %>",
animation:60,

background_image: '<%= url_for_file_column(@post, "image") %>'});
//]]>
</script>


モデルの編集


app/models/post.rb
file_columnと、ImageSizeのコードを追加します。

require 'rubygems'
require 'image_size'

class Post < ActiveRecord::Base

file_column :image

def image_width
image_file ? image_file.get_width : 0
end

def image_height
image_file ? image_file.get_height : 0
end

def image_file
if (image && !@image_file )
open(image ,"rb") do |img|
@image_file = ImageSize.new(img)
end

end
@image_file
end
end



以上で完成です。あとはサーバを起動してhttp://localhost:3000/postsにアクセスするだけです。

参考書籍



Railsを学ぶにはやはりRails開発者であるDavid Heinemeier Hanssonの著書がオススメです。前半はショッピングカートを題材にした実践的なWebアプリケーションのチュートリアル、後半はActiveRecordなど各コンポーネントの詳細説明とRailsの基本から詳細までが網羅された内容となっています。また、ところどころにある"David曰く"の囲みが面白く、例えば「テーブル名はなぜ複数形にするべきなのか?」といった内容に
テーブル名を複数形にしておくことで、Select a Product from products.(商品テーブルから商品を一つ選択してください)という文章が違和感なく理解できるようになります。
と回答されていたりと、Railsがなぜそういった設計にいたったのかという経緯が解説されていたりします。



参考


月曜日, 6月 25, 2007

[JavaScript][Ext]Ext.jsのフォームコンポーネントと入力チェック

既存のhtmlを元にJavaScriptでリッチコントロール機能の追加や入力チェックを行う方法を紹介します。Ext1.1 beta1を元にしています。

基本構文

// フォームオブジェクト生成
var comp = new Ext.form.TextField();
// HTMLElementへフォームオブジェクトを適用
comp.applyTo('elem-id');


入力エラー時のツールチップを有効にするには以下のコードを入れておく必要があります。
Ext.QuickTips.init();


DateField
まずは何かと使い道の多そうなDateField。年/月/日形式にするためにはformatを指定する必要があります。
var date = new Ext.form.DateField({
allowBlank:false,
format: 'Y/m/d'
});
date.applyTo('birth-input');

これだけで通常のテキストフィールドにカレンダー入力コントロールと日付チェックを追加できます。


入力エラー時はツールチップでエラーメッセージが表示されます。デフォルトでは英語ですが、日本語リソースを読み込むとメッセージも日本語になります。
<script src="ext-1.1-beta1/source/locale/ext-lang-ja.js" type="text/javascript" charset="utf-8"></script>



Validation
通常のテキストフィールド。フォーマットチェックを行いたい場合はvtypeでvalidation typeを指定します。例えばurlなら以下のような感じ。
var url = new Ext.form.TextField({
allowBlank: false,
vtype: 'url' // SEE ALSO Ext.form.VTypes
});
url.applyTo('url-input');


Custom Validation
続いてemail。vtype: 'email'もできますがDoCoMo式のアットマークの前にピリオドがあるアドレスは通らないのでカスタムバリデーションでやってみます。validationに値一つを受け取るfunctionオブジェクトを設定します。チェックOKの場合はtrueを、エラーの場合はエラー文字列を返すという仕様です。
var mail = new Ext.form.TextField({
allowBlank: false,
// Custom validation
validator: function(fvalue) {
// Ext.form.VTypesより。@の前の.も許可
var email = /^([\w]+)(.[.\w]+)*@([\w-]+\.){1,5}([A-Za-z]){2,4}$/;
if (email.test(fvalue)) {
return true;
}
return String.format('{0}はメールアドレスとして正しくありません。', fvalue)
+ Ext.form.VTypes.emailText;
}
});
mail.applyTo('mail-input');


Number Field
数値のみ許可する入力フィールド。アルファベットが入力できなくなります(日本語は残念ながらOK)
var income = new Ext.form.NumberField({
allowBlank: false
});
income.applyTo('income-input');


Combo Box
select要素からExt.form.Comboboxオブジェクトにするのは少し違う手順。applyToするのではなくtransformで元になるselect要素を指定します。
var combo = new Ext.form.ComboBox({
typeAhead: true,
triggerAction: 'all',
width:135,
forceSelection:true,
transform: 'blood-select'
});


Textarea
テキストエリアも同様です。growを設定しておくと入力量に応じてエリアが自動的に拡張されます。
var carrier = new Ext.form.TextArea({
grow: true, // 入力量に応じてテキストエリアが拡張される
growMax: 200
});
carrier.applyTo('carrier-area')


Html Editor
Ext1.1からWYSIWYGなhtmlエディターコンポーネントが追加になっています。これも使い方は同様。
var promo = new Ext.form.HtmlEditor({
width: 600,
height: 300
});
promo.applyTo('promotion-area');


JavaScriptでフォームをデザインするサンプルは以下にあります。個人的にはデザインとロジックの分離ができる後からコントロールを追加するパターンの方が好みです。(サンプルでform.htmlが有りますがまだ作りかけなようです)

参考


火曜日, 6月 12, 2007

[JavaScript][Ext]ソート可能なテーブルを実装する

絶対お勧め!JavaScriptでテーブルソート「Table Sorter」


普通のテーブルタグに対して、ヘッダ部分はtheadタグとthタグで作るだけでいい。データ部分は全体をtbodyタグで囲んで、tdタグで記述するだけだ。


Extを使っても同様の方法でソート可能なテーブルを実装できます。
以下のサンプルからExt.grid.TableGridクラスをもらってきて使うだけです。

From Markup Grid Example

Script
var grid = new Ext.grid.TableGrid('the-table');
grid.render();


ロード時にページ内のテーブル全てに適用したければ下記のような感じでOKです。

Ext.onReady(function() {
Ext.select('table').each(function (e) {
var grid = new Ext.grid.TableGrid(e);
grid.render();
} )
});


このGridはかなり高機能で、列クリックによるソートだけではなく、ドラッグによる列の入れ替えや、
ポップアップメニューでの表示列の切り替え、列の固定なども行えます。
インクルードファイルの数やリソースの数など設置は若干大変ですが、それさえ乗り越えればかなり手軽でよいと思います。



参考


火曜日, 5月 22, 2007

[JavaScript][Ext]Ext.jsの使い方メモ(Core機能)

Ext.jsの使い方メモです。Core機能をまとめてみようと思います。

最初に必要なライブラリを読み込みます。(ext-1.0に展開されたソースがある前提)

<script src="ext-1.0/adapter/yui/yui-utilities.js" type="text/javascript" charset="utf-8"></script>
<script src="ext-1.0/adapter/yui/ext-yui-adapter.js" type="text/javascript" charset="utf-8"></script>
<script src="ext-1.0/ext-all-debug.js" type="text/javascript" charset="utf-8"></script>


スタイルシートの読み込みます。
自分で全部定義するのは大変なので付属のものを使います。
<link rel="stylesheet" href="ext-1.0/resources/css/ext-all.css" type="text/css" media="screen" charset="utf-8" />


テーマを変更したい場合は上に加えてythema-xxx.cssを読み込むだけです。
<link rel="stylesheet" href="ext-1.0/resources/css/ythema-aero.css" type="text/css" media="screen" charset="utf-8" />


基本はExt.Elementオブジェクトです。
var elem = Ext.get('my-div');

でExt.Elementオブジェクトが返ってきます。これはHTMLElementオブジェクトをラップしたオブジェクトで、各種プロパティの設定/取得や、アニメーション、AJAX機能が利用できます。

表示非表示の切り替えは、show(), hide()メソッドでそれぞれ行います。
elem.show();
elem.hide();


デフォルトではvisibilityプロパティを変更して表示/非表示を制御します。
要素ブロックごと消したい場合はenableDisplayMode()としてからそれぞれのメソッドを呼び出すとdisplayプロパティで表示/非表示を切り替えてくれます。
elem.enableDisplayMode();
elem.hide();


イベントハンドラの設定

イベントハンドラの設定はExt.Element.onメソッドで行います。これはxt.Element.addListnerのaliasになっています。
elem.on('click', function(event, target) {
// 処理
});

ハンドラの第一引数はExt.EventObject、第2引数はイベントソースのHTMLElementオブジェクトとなります。

アニメーションエフェクト
エフェクト系はExt.Fxで定義されています。
Ext.FxのメソッドはExt.Elementのprototypeにコピー(Ext.apply)されているので、Ext.Elementオブジェクト経由なら特に意識することなく利用できます。

elem.highlight();
elem.fadeOut();
elem.fadeIn();
elem.switchOff();
elem.switchOn();
// etc...


またFx系のメソッドはElementが返ってくるので、でつないでアニメーションチェーンにすることができます。
elem.highlight().fadeOut().fadeIn().switchOff().switchOn();


セレクター
CSSセレクターで要素を選択するにはExt.selectメソッドを使用します。
var elems = Ext.select('.myclass');

戻り値はExt.CompositeElementオブジェクトです。こちらは名前のとおりCompositeパターンに
なっているので、Ext.Elementのメソッドを呼び出すと透過的に全てのオブジェクトに対して
メソッドが呼び出されます。
// myclassクラスが設定されている全ての要素に対してclickイベントハンドラを設定
elems.on('click', function() { ... } );


要素を一つずつ処理する場合はeachを使います。途中でbreakしたい場合はfalseを返します。
continueは単にreturnです。
elems.each(function (elem) {
if ( condition ) {
// ... なんらかの処理
return false; // breakする
} else {
// ... なんらかの処理
return; // continueする
};
// ...
});


XPathセレクター

XPathで要素を選択するにはExt.queryメソッドを使います。こちらはDomQuery.selectのaliasです。
こちらの戻り値はExt.CompositeElementではなくHTMLElementのArrayオブジェクトとなります。
おそらくExt.queryの実装であるDomQueryクラスがExtに依存しないようにするためだと思われます。

Ext.Elementオブジェクトに対してselect, queryを行うと、その要素の子要素以下に限定して
要素を選択します。
elem.select('.myclass') // same as '#' + elem.dom.id + ' .myclass';
elem.query('.myclass') // same as '#' + elem.dom.id + ' .myclass';

ただselectの方はグループ化に対応していない(1.0.1a)らしく最初のセレクターにしかidを追加して
くれないようです。(queryの方は上手くいく)

Mindomoマインドマップにしてみました。

次はAJAX周りで遊んで見る予定。

参考


木曜日, 4月 19, 2007

[JavaScript]Ext.js 1.0が正式リリース

リッチなJavaScriptライブラリのExt.jsが正式リリースされたようです。

サンプルのようにかなりカッコイイコンポーネントが用意されています。ドキュメントも充実していて結構使いやすい印象です。

最近話題のApolloでも、サンプルアプリケーションで使われていたりします。
Fresh Reader

また超リッチJavaScriptコンポーネント集「Ext」がprototype.jsに対応にもあるようにCoreとなるライブラリを切り替えられるというのもユニークな点かと思います。

さらに全てをダウンロードしなくても、必要な部分だけをbuildしてダウンロードさせてくれるサービスも公開されています。
コアライブラリを選択し必要なコンポーネントを選択すると、その分だけのコードをまとめてダウンロードできます。ただしcssや画像などは別途ダウンロードしておく必要があるようです。(zip中のresourcesを使う)

UIコンポーネント以外でもDomQueryも便利そうです。XPath、CSSセレクターで、CSS3レベルのセレクターをほぼサポートしていて、しかもかなり高速とのこと。CSSセレクターの実装はブラウザによって結構差がありますが、これを使えばそういった心配もいらないので助かりそうです。

月曜日, 1月 29, 2007

[Testing][Perl]Test::WWW::SeleniumでPerlからSelenium RCを操作する

Selenium IDEを使うとhtmlの各種プログラミング言語用のテストコードも出力してくれます。
Perlの場合はTest::WWW::Seleniumモジュールを利用します。

前記事「Selenium RCとSelenium IDEでWEBアプリのUIテストを簡単自動化」の操作で出力されるPerlコードは以下のようになります。

Selenium IDEによって出力されるコード

use strict;
use warnings;
use Time::HiRes qw(sleep);
use Test::WWW::Selenium;
use Test::More "no_plan";
use Test::Exception;

my $sel = Test::WWW::Selenium->new( host => "localhost",
                                    port => 4444,
                                    browser => "*firefox",
                                    browser_url => "http://localhost:4444" );

$sel->open_ok("/");
$sel->type_ok("q", "Perl");
$sel->click_ok("btnG");
$sel->wait_for_page_to_load_ok("30000");
$sel->is_text_present_ok("Perl の検索結果");


スクリプトはutf8で保存します。browser_urlがlocalhostになってしまうようなのでwww.google.co.jpに変更します。

スクリプト実行前にSelenium RCを起動しておきます。
java -jar selenium-server.jar


続いてテストスクリプトを実行します。
ok 1 - open, /
ok 2 - type, q, Perl
ok 3 - click, btnG
ok 4 - wait_for_page_to_load, 30000
not ok 5 - is_text_present, Perl の検索結果
# Failed test 'is_text_present, Perl の検索結果'
# at test_utf8.t line 19.
1..5
# Looks like you failed 1 test of 5.

最後のテストで失敗してしまいました。verboseモードを使って実行手順をトレースしてみまると以下のようなコードを送信しているようでした。
---> Requesting http://localhost:4444/selenium-server/driver/?cmd=isTextPresent&1=Perl%20%C3%A3%C2%81%C2%AE%C3%A6%C2%A4%C2%9C%C3%A7%C2%B4%C2%A2%C3%A7%C2%B5%C2%90%C3%A6%C2%9E%C2%9C&sessionId=1170055300866
Got result: OK,false


日本語の部分がおかしいようなのでコードを追ってみると以下のようにutf8フラグ付を前提としているようです。
URI::Escape::uri_escape_utf8(shift @args);

テストスクリプトにuse utf8をつけて再度実行してみます。
ok 1 - open, /
ok 2 - type, q, Perl
ok 3 - click, btnG
ok 4 - wait_for_page_to_load, 30000
ok 5 - is_text_present, Perl の検索結果
1..5

上手く実行できました。

このようにプログラムからテストを実行すると、テストに必要なデータの準備から後始末までを自動化できます。例えばユーザーの新規登録フローをテストするといったケースでは登録しようとするユーザーのデータがないことが前提になりますので、まずテストユーザのデータを削除してからブラウザを起動して登録フローのテストを実行することで繰り返しテストすることが可能です。DBがトランザクションをサポートしているものであれば最後にrollbackしてしまえば不要なデータも残らず快適です。

このあたりのノウハウは前項でも紹介したWEB+DB Pressで詳しく紹介されています。Perlは無いですが。。。


最終的なコード
use strict;
use warnings;
use Time::HiRes qw(sleep);
use Test::WWW::Selenium;
use Test::More "no_plan";
use Test::Exception;
use utf8;

my $sel = Test::WWW::Selenium->new( host => "localhost",
                                    port => 4444,
                                    browser => "*firefox",
                                    browser_url => "http://www.google.co.jp",
                                     );

$sel->open_ok("/");
$sel->type_ok("q", "Perl");
$sel->click_ok("btnG");
$sel->wait_for_page_to_load_ok("30000");

$sel->is_text_present_ok("Perl の検索結果");


木曜日, 1月 18, 2007

[Perl]Benchmarkモジュールの小ネタ

PPerlのベンチを取りたくて、timeコマンドとかでも充分差は出るのですが、繰り返し実行される際の差を取りたかったのでBenchmarkモジュールでやろうとしたら少しおかしな結果になりました。

ちなみにPPerlとはスクリプトをコンパイルしてデーモンとして常駐させることで、起動時のオーバーヘッドをなくして高速化するというモジュールです。これもPerl Hacksに載ってます。

ベンチ対象(hello.pl)

use strict;
use Template;

my $tmpl = <<__TMPL__;
str = [% str %]
__TMPL__

my $tt = Template->new();
$tt->process(\$tmpl, { str => 'hello' });

print "hello\n"だけでやるとプロセス間通信のオーバーヘッドの方が大きいのかかえって遅くなったので、適当に大きめなモジュールを使えということでTemplateを使用。

で、ベンチのプログラムですが、以下のようにSYNOPSISどおりに実行すると
#!/usr/local/bin/perl
use strict;
use Benchmark qw(timethese);

timethese(100, {
    pperl => sub {
        my $ret = `/usr/bin/pperl -w --no-cleanup hello.pl`;
        die "error" unless ($ret =~ m|hello|);
    },
    perl => sub {
        my $ret = `/usr/bin/perl -w hello.pl`;
        die "error" unless ($ret =~ m|hello|);
    },
});


こんな感じになります。
Benchmark: timing 100 iterations of perl, pperl...
    perl: 42 wallclock secs ( 0.01 usr 0.03 sys + 38.64 cusr 2.56 csys = 41.24 CPU) @ 2500.00/s (n=100)
    pperl: 6 wallclock secs ( 0.01 usr 0.03 sys + 0.34 cusr 0.38 csys = 0.76 CPU) @ 2500.00/s (n=100)


wallclockとかはちゃんと出ているんですがRateがおかしい。これでも結果はわかるといえばわかるのですが気にいらないのでいろいろ調べていると、第3引数にstyleを指定することができるというのを見つけました。これを使うと親プロセスだけの時間とか子プロセスだけの時間とか計ってくれるようです。今回は子プロセスの実行時間が欲しいので'nop'としました。

スタイルの指定
#!/usr/local/bin/perl
use strict;
use Benchmark qw(timethese :hireswallclock);

timethese(100, {
    pperl => sub {
        my $ret = `/usr/bin/pperl -w --no-cleanup hello.pl`;
        die "error" unless ($ret =~ m|hello|);
    },
    perl => sub {
        my $ret = `/usr/bin/perl -w hello.pl`;
        die "error" unless ($ret =~ m|hello|);
    },
}, 'nop');


実行結果
Benchmark: timing 100 iterations of perl, pperl...
    perl: 47.9732 wallclock secs (44.66 cusr + 3.14 csys = 47.80 CPU) @ 2.09/s (n=100)
    pperl: 7.10064 wallclock secs ( 0.35 cusr + 0.47 csys = 0.82 CPU) @ 121.95/s (n=100)

うまく出ました。それにしてもallのとき(デフォルト)は親子の合計で/sも出してくれればいいのにと思うのは私だけでしょうか?

ちなみにこれを調べている過程で':hireswallclock'を見つけました。これをuseのときに指定しておけばwallclockもTime::HiResで出してくれます。ちょっと便利。