InfoQに、Weeという継続ベースのWebアプリケーションフレームワークが紹介されていたので、試してみました。
gemでインストール後、インストールディレクトリ下のexamples/demo.rbを実行すると、自動的にWEBrickが起動します。
Counterというカウントの増減ができるページが表示され、++で1増加、--で1減算となるのですが、途中で戻るボタンを押下し、その後++/--を押下しても戻った地点から計算されます。
同じことをHTTP Sessionを使ってやると、戻るボタンを押下しても最後にsubmitした値からの加減値となり、挙動が異なります。数値をhiddenに埋め込むとできるので、この例だとメリットを感じづらいところはありますが。
Weeのアプリケーションではaction実行時にbacktrackというメソッドが呼ばれ、そこでステートを保存することができます。
1 2 3 4 |
def backtrack(state) super state.add_ivar(self, :@count, @count) end |
それがURLのpage-idと関連付けられることで、戻るボタン押下時にステートが戻るようになっているとのこと。
The solution to this problem is to take snapshots of the components state after an action is performed and restoring the state before peforming actions. Each action generates a new state, which is indicated by a so-called page-id within the URL.
次にexamples/continuation.rbを動かしてみました。こちらは、callccメソッドにMessageBoxコンポーネントを引数で渡して呼ぶと、htmlが表示されます。表示されたページにはOK/Cancelボタンがあり、どちらかを押下するかによって次の処理が決まる、というものです。
clickメソッドが1回呼び出されると、その中のcallccメソッドの呼び出し回数分(条件によって2~3回)ページが表示されている、ということになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
$LOAD_PATH.unshift "../lib" require 'rubygems' require 'wee' require 'demo/messagebox' class MainPage < Wee::Component def initialize super add_decoration(Wee::PageDecoration.new("Test")) end def click if callcc Wee::MessageBox.new('Really quit?') callcc Wee::MessageBox.new('You clicked YES') else callcc Wee::MessageBox.new('You clicked Cancel') callcc Wee::MessageBox.new('super') end end def render(r) r.anchor.callback_method(:click).with('show') end end Wee.runcc(MainPage) |
あまり継続の良さを理解できていませが、複数のリクエストに跨ってステートフルなコンポーネントを簡単に扱うことができるのがWeeの特徴の1つなのだと思います。
上記ソースのcallccはWeeのメソッドで、Kernel.callccとは異なりますが、その実装は以下のようにKernel.callccを呼び出していました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
# # Similar to method #call, but using continuations. # def callcc(component) delegate = Wee::Delegate.new(component) answer = Wee::AnswerDecoration.new add_decoration(delegate) component.add_decoration(answer) answ = Kernel.callcc {|cc| answer.answer_callback = cc session.send_response(nil) } remove_decoration(delegate) component.remove_decoration(answer) return *answ.args end |
jGrowlは、Mac OS XのGrowlをモチーフとしたメッセージを表示してくれるjQueryのプラグインです。 jQuery自体のバージョンは、1.3以上が推奨されてます。
今回はrailsのflash内のメッセージをjGrowlで表示させてみます。
1. ファイルを配置します。
1 2 3 4 5 6 |
<script type="text/javascript"> $.jGrowl.defaults.position = 'center'; <% if flash[:notice] %> $.jGrowl('<%= flash[:notice] %>', { header: 'notification', theme: 'iphone', life: 1000 }); <% end %> </script> |
‘life’で表示期間(ミリ秒)を指定します。ユーザが閉じるまで表示させたままにすることも可能です。
メッセージボードの画像とテーマ(iphone)は、jGrowlに付属しているサンプルをそのままjgrowl_custom.cssとして切り出して使っています。また、メッセージボードの表示位置について、デフォルトだとあまり選択肢がない(top-left, top-right, bottom-left, bottom-right, centerの計5種類)ので、同ファイル内で’center’の設定を上書き、表示位置を調整しました。1 2 3 4 5 |
body > div.jGrowl.center {
top: 250px;
left: 250px;
width: 0%;
} |
あとはflash[:notice]にメッセージを入れると、以下のように表示されます。

「日付コントロールを変える」の最後で、プラグインの読み込みをconfig/environment.rbに記述しました。
require 'yads' |
その後調べてみたら、vendor/plugin/(プラグイン名)/init.rbで上記コードを記述すれば良いことが分かりました。各プラグインのinit.rbはrailsの初期化プロセスから自動的に呼び出される為、script/plugin install ~ でインストールすれば、フレームワークのメソッド(前回の場合はActionView::Helper::DateHelper)を再定義することができます。
でもinit.rbが自動的に呼び出されるのであれば、最初からinit.rbでメソッドの再定義を行えば良いはず。
ということで、vendor/plugin/yads/lib/yads.rbの中身をvendor/plugin/yads/init.rbに移動して実行したところ、メソッドが再定義されておらず、元々のドロップダウンリストを使った日付コントロールが表示されていました。コードは以下のとおりです。
[vendor/plugins/yads/init.rb]1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
# Include hook code here module ActionView module Helpers module DateHelper def date_select(object_name, method, options = {}, html_options = { }) options[:size] ||= 12 html = InstanceTag.new(object_name, method, self, options.delete(:object)).to_input_field_tag("text", options) js = <<EOS <script> $('##{object_name}_#{method}').datepicker({ dateFormat: 'yy-mm-dd'}); </script> EOS html.concat(js) end end end end |
init.rbの中でrequireして再定義するのと、init.rbの中で直接再定義することの違いが分からず、webで調べてみたところ、以下のようなページがありました。
Platte daddy: Rails plugins: keep init.rb thinRails plugins' initializer script, init.rb, is currently invoked via eval, not require—so it inherits whatever module-space Rails calls it from. If you reopen any classes in init.rb itself (like the will_paginate guys quite reasonably attempted to define Hash#slice), your changes will be made—but to the wrong module. So, to avoid strange gotchas, consider init.rb just a generic hook point to kick things off, and always require in any code that's to do actual work at plugin load time.
「If you reopen any classes in init.rb itself, your changes will be made—but to the wrong module.」と記述されているので、init.rbで再定義してもダメなのはどうも正しいようですが、その理由がわからない…。そもそもプラグインの初期化時にはフレームワークのメソッドが定義されていないのか、と考えましたが、init.rbでrequireされるファイル内で再定義してもタイミングは同じはずだし…。
requireに何か特殊な仕掛け(たとえば遅延評価とか)があるのかと思い、Rubyのrequireメソッドを再定義しているActiveSupport::Dependenciesのログを出力させてみたりしたものの、プラグインの初期化時にはログが出力されていないことから、あまり関係あるようにも思えない、という結論に至り。
プラグインのinit.rbはどのように呼び出されているか調べてみると、以下のようになっていました。
[rails-2.3.2/lib/rails/plugin.rb]1 2 3 4 5 6 7 8 9 10 |
def evaluate_init_rb(initializer) if has_init_file? silence_warnings do # Allow plugins to reference the current configuration object config = initializer.configuration eval(IO.read(init_path), binding, init_path) end end end |
evalでinit.rbを評価しているので、普通に再定義できそう…と暫く気がつかなかったのですが、evalで評価ということは呼び出し側の名前空間(今回の場合はRails::Plugin)を引き継いでしまうので、それを考慮に入れる必要がありました。つまり、init.rbで以下のように記述すると、
1 2 3 |
module ActionView module Helpers module DateHelper |
1 2 3 |
module ::ActionView module Helpers module DateHelper |
結論としては、プラグインのinit.rbでメソッドの再定義はできるものの、lib下のファイル内で再定義してinit.rbでrequireすべき。
前回、「scaffoldした際に、引数で指定される各フィールドの型によって自動的に決まるwebのコントロールを、自分の都合の良いように変えてみたい」と思い、scaffoldのコードを見てみました。
その後よく考えてみると、field_typeによってコントロールを表示するメソッドがあるのだから、それ自体を上書きすればよいことに気がつきました。これだとscaffoldするかどうかは関係なく、既存のプロジェクトにも使えます。
ということで、今回は日付コントロールをカスタマイズしてみます。
scaffoldの引数に「date」というsql_typeを指定すると、new/editでは以下のようなコントロールが表示されます。
これを、jQuery UI Datepickerに変えてみます。
scaffoldによって生成されたnew.html.rbで、このコントロールを表示するためのコードは以下になります。1 2 3 4 |
<p> <%= f.label :atdate %><br /> <%= f.date_select :atdate %> </p> |
「f.date_select」を上書きすれば良いことがわかります。上書きする手段はいくつかあると思いますが、今回はそれ用のプラグインを作成しました。(今回はYADS[=Yet Another DateSelect]という名前のプラグインにしています)
vendor/plugins/yads/lib/yads.rb1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
module ActionView module Helpers module DateHelper def date_select(object_name, method, options = {}, html_options = { }) options[:size] ||= 12 html = InstanceTag.new(object_name, method, self, options.delete(:object)).to_input_field_tag("text", options) js = <<EOS <script> $('##{object_name}_#{method}').datepicker({dateFormat: 'yy-mm-dd'}); </script> EOS html.concat(js) end end end end |
1 2 3 |
<%= stylesheet_link_tag 'scaffold', 'jquery/ui-lightness/jquery-ui-1.7.2.custom.css' %> <script type="text/javascript" src="/javascripts/jquery/jquery-1.3.2.min.js"></script> <script type="text/javascript" src="/javascripts/jquery/jquery-ui-1.7.2.custom.min.js"></script> |
require 'yads' |

日付の入力がドロップダウンリストからテキストボックスへと変わり、テキストボックスをクリックするとdatepickerが表示されます。
config/environment.rbでrequireしているのがイマイチなので、時間があれば自動的にロードさせられないか調べてみます。
scaffoldした際に、引数で指定される各フィールドの型によって自動的に決まるwebのコントロールを、自分の都合の良いように変えてみたいと思います。その為に、scaffoldに関連するコードを読んでみます。
前回railsからコピーしてきたtemplateから、view_edit.html.erbを見てみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<h1>編集 <%= singular_name %></h1> <%% form_for(@<%= singular_name %>) do |f| %> <%%= f.error_messages %> <% for attribute in attributes -%> <p> <%%= f.label :<%= attribute.name %> %><br /> <%%= f.<%= attribute.field_type %> :<%= attribute.name %> %> </p> <% end -%> <p> <%%= f.submit 'Update' %> </p> <%% end %> <%%= link_to 'Show', @<%= singular_name %> %> | <%%= link_to 'Back', <%= plural_name %>_path %> |
attribute.field_typeによって、コントロールが決まる仕組みになっています。field_typeは以下のようになっていました。
[rails-2.3.2/lib/rails_generator/generated_attribute.rb]1 2 3 4 5 6 7 8 9 10 11 12 |
def field_type @field_type ||= case type when :integer, :float, :decimal then :text_field when :datetime, :timestamp, :time then :datetime_select when :date then :date_select when :string then :text_field when :text then :text_area when :boolean then :check_box else :text_field end end |
変数typeは、script/generate scaffoldの引数に指定する「名前:sql_type」のsql_typeになります。
ちなみに、同ファイルにはdefaultというメソッドが含まれており、以下のようになっています。1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
def default @default ||= case type when :integer then 1 when :float then 1.5 when :decimal then "9.99" when :datetime, :timestamp, :time then Time.now.to_s(:db) when :date then Date.today.to_s(:db) when :string then "MyString" when :text then "MyText" when :boolean then false else "" end end |
先ほどのfield_typeメソッドの定義から、scaffoldの引数にoneday:dateと指定すると、「f.date_select :oneday」というコードが生成されます。
このfという変数は何かとform_forやその先のfields_forを辿っていくと、デフォルトではActionView::Base.default_form_builder(=FormBuilder)のインスタンスになります。
[actionpack/lib/action_view/helpers/form_helper.rb]1 2 3 4 5 |
def fields_for(record_or_name_or_array, *args, &block) ... builder = options[:builder] || ActionView::Base.default_form_builder yield builder.new(object_name, object, self, options, block) end |
上記コードより、指定されたsql_typeによって任意のwebのコントロールを配置するには、カスタムFormBuilderを作ってform_forのoptionsに:builderを明示するか、FormBuilderの各メソッドを上書くことにより実現できそうです。
昨日の『ソフトウエア開発プロフェッショナル』を読んだ後、エンジニアリングを軽視してはいけないと思いました。本書中にあった簡単に石を運ぶような仕組みというのは、事前準備の賜物でしょう。ということで、railsのscaffoldをカスタマイズしてみることに。 ActiveScaffoldみたいなものを自前で用意できたら便利かと思い、ちょっとやってみました。バージョンはRails 2.3.2です。まずはviewのテンプレートのカスタマイズから。
my_scaffoldを実行します。
これで編集したテンレプートが適用されていることを確認できます。
これまで作ってきたものを、Ruby1.8.7+Rails2.1.1の環境からRuby1.9.1+Rails2.3.2に移行してみたいと思います。
1. rake rails:update:configsで失敗
1 2 3 4 5 6 |
masayuki@ubuntu-vm:~/work/rails/shrimp$ rake rails:update:configs (in /home/masayuki/work/rails/shrimp) rake aborted! undefined method `>=' for nil:NilClass /home/masayuki/work/rails/shrimp/Rakefile:4:in `require' (See full trace by running task with --trace) |
2. gettextで失敗
1 2 3 4 5 6 7 |
masayuki@ubuntu-vm:~/work/rails/shrimp$ rake rails:update:configs (in /home/masayuki/work/rails/shrimp) rake aborted! /usr/local/ruby-1.9.1-p129/lib/ruby/gems/1.9.1/gems/gettext-1.93.0/lib/gettext/iconv.rb:102: invalid multibyte char… /usr/local/ruby-1.9.1-p129/lib/ruby/gems/1.9.1/gems/gettext-1.93.0/lib/gettext/iconv.rb:102: invalid multibyte char… /usr/local/ruby-1.9.1-p129/lib/ruby/gems/1.9.1/gems/gettext-1.93.0/lib/gettext/iconv.rb:102: syntax error, … puts Iconv.iconv("EUC-JP", "UTF-8", "ほげ").join |
3. gettext/rails→gettext_rails
gettext_rails provides the localization for Ruby on Rails-2.3 or later using Ruby-GetText-Package. |
4. config.cache_template_extensions
undefined method `cache_template_extensions=' for ActionView::Base:Class |
5. app/controllers/application.rb→app/controllers/application_controller.rb
Rails2.3から、application.rbというファイル名が変更になっている為。リリースノートを読むと、rake rails:updateとすれば良かったらしい…。
6. jrailsの更新
jrailsはRuby1.9対応にする必要があります。
7. incompatible character encodings: ASCII-8BIT and UTF-8これまでずっとcoLinuxで開発をしてきました。が、ゲストOSとして使用してきたUbuntu7系のサポートが終わり、また容量もかなりギリギリになってきたので、この機会にUbuntu8に乗り換えようとcoLinuxのサイトに行ってみたところ、ダウンロードページが表示できませんでした・・・。
ということで、これを機にVMwareに乗り換えようと思い、VMware PlayerとUbuntu 8.04 LTSをダウンロード。
NATで外部に接続できるようにする為、ホストOSのネットワーク接続(「ローカルエリア接続)のプロパティ→「共有」タブにて、「VMnet8」(「ローカルエリア接続4」でした…)を選択し、「ネットワークのほかのユーザに、このコンピュータのインターネット接続をとおして接続を許可する」にチェック。この際、VMnet8のIP4のアドレスを書き換えてしまい、ゲストOS側から外に出ることができず大分はまりました。
VMware Playerがインストールされているディレクトリに「vmnetcfg.exe」というファイルがあり、これを実行するとNATサービスの設定の一部がわかるのですが、このサービスのアドレスがVMnet8の書き換える前のアドレスになっていた為、NATサービスに接続できなかったようです。
結局ホストOSとVMnet8のネットワークアダプタのアドレスを元に戻し、ホストOSから外部に接続できることを確認することができました。
herokuというrailsアプリケーションのホスティングサービスを使ってみました。以前はブラウザでrailsアプリが開発できる、ということでしたが、今はheroku gardenという名前になっています。ブラウザを使った開発も面白そうですが、まずはheroku.comを使ってみます。 heroku.comを使用する場合は、自分の環境でrailsアプリを作成したものをgit pushすると、herokuにdeployされます。料金はストレージと通信料、そして各種オプションで決まるようです。ストレージ5Mで通信がほとんど無ければfreeなので、その範囲内で試してみます。
まずはherokuをインストール。$ gem install heroku |
$ heroku keys:add |
1 2 3 |
$ rails foobar $ cd foobar $ git init && git add . && git commit -m "first commit" |
$ heroku create |
1 2 3 |
$ heroku destroy hollow-ice-37 $ heroku create foobar(実際はアプリの名前) $ git push heroku master |
Missing the Rails 2.1.1 gem. Please `gem install -v=2.1.1 rails`, update your RAILS_GEM_VERSION setting in config/environment.rb for the Rails version you do have installed, or comment out RAILS_GEM_VERSION to use the latest version installed. |
1 2 3 |
$ script/generate scaffold task name:string start:datetime end:datetime owner:integer $ rake db:migrate $ script/server |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
$ heroku logs ==> log/production.log <== # Logfile created on Thu Jun 18 11:14:08 -0700 2009 Processing TasksController#index (for 59.156.119.10 at 2009-06-18 11:14:17) [GET] ActiveRecord::StatementInvalid (PGError: ERROR: relation "tasks" does not exist : SELECT * FROM "tasks" ): app/controllers/tasks_controller.rb:5:in `index' /home/heroku_rack/lib/static_assets.rb:9:in `call' /home/heroku_rack/lib/last_access.rb:15:in `call' thin (1.0.1) lib/thin/connection.rb:80:in `pre_process' thin (1.0.1) lib/thin/connection.rb:78:in `catch' thin (1.0.1) lib/thin/connection.rb:78:in `pre_process' thin (1.0.1) lib/thin/connection.rb:57:in `process' thin (1.0.1) lib/thin/connection.rb:42:in `receive_data' eventmachine (0.12.6) lib/eventmachine.rb:240:in `run_machine' eventmachine (0.12.6) lib/eventmachine.rb:240:in `run' thin (1.0.1) lib/thin/backends/base.rb:57:in `start' thin (1.0.1) lib/thin/server.rb:150:in `start' thin (1.0.1) lib/thin/controllers/controller.rb:80:in `start' thin (1.0.1) lib/thin/runner.rb:173:in `send' thin (1.0.1) lib/thin/runner.rb:173:in `run_command' thin (1.0.1) lib/thin/runner.rb:139:in `run!' thin (1.0.1) bin/thin:6 /usr/local/bin/thin:19:in `load' /usr/local/bin/thin:19 |
$ heroku rake db:migrate |
roleを取り入れたいと思い、プラグインを探してみたところ、rolerequirementというプラグインが見つかったので、早速試してみました。
script/plugin install git://github.com/timcharper/role_requirement.git |
インストール後、roleを表すRoleクラスを作成します。すでにrestful_authenticationを使用していた為、userというモデルができており、下記を実行後に「rake db:migrate」を行うことにより、roles、roles_usersという2つのテーブルが生成されます。
script/generate roles Role User |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
mysql> desc roles; +-------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +-------+--------------+------+-----+---------+----------------+ | id | int(11) | NO | PRI | NULL | auto_increment | | name | varchar(255) | YES | | NULL | | +-------+--------------+------+-----+---------+----------------+ 2 rows in set (0.00 sec) mysql> desc roles_users; +---------+---------+------+-----+---------+-------+ | Field | Type | Null | Key | Default | Extra | +---------+---------+------+-----+---------+-------+ | role_id | int(11) | YES | MUL | NULL | | | user_id | int(11) | YES | MUL | NULL | | +---------+---------+------+-----+---------+-------+ 2 rows in set (0.00 sec) |
あとはroleをuserに割り当てます。今回はroles、role_usersに対してSQLで直接レコードをinsertしました。 次に、各コントローラに対して必要なroleを設定していきます。
1 2 3 4 |
class RolesController < ApplicationController layout "master" before_filter :login_required require_role "admin" |
さくらで動かしていたアプリケーションを移行しているのですが、capifyされたrailsのアプリケーションについては、capistoranoで移行してみました。
まずは、$ apt-get install subversion |
$ cap deploy:cold |
1 2 3 |
.. ** [wrap-trap.net :: out] svn: Can't make directory '/opt/shrimp/releases/20090604151717': Permission denied .. |
1 2 3 4 5 6 7 8 9 10 |
$ cap deploy:setup * executing `deploy:setup' * executing "sudo -p 'sudo password: ' mkdir -p /opt/shrimp /opt/shrimp/releases /opt/shrimp/shared /opt/shrimp/shared/system /opt/shrimp/shared/log /opt/shrimp/shared/pids && sudo -p 'sudo password: ' chmod g+w /opt/shrimp /opt/shrimp/releases /opt/shrimp/shared /opt/shrimp/shared/system /opt/shrimp/ shared/log /opt/shrimp/shared/pids" servers: ["wrap-trap.net"] [wrap-trap.net] executing command ** [out :: wrap-trap.net] command finished |
1 2 3 4 5 6 7 |
set :runner, 'masayuki' set :group, 'users' set :use_sudo, true ... after 'deploy:setup' do try_sudo "chown -Rf #{runner}:#{group} #{deploy_to}" end |
http://lee.hambley.name/capistrano-2.5.0/rdoc/
ちなみに、deploy先の環境に合わせて、after_symlinkでファイルを上書いています。これが普通のやり方なのかわかりませんが。1 2 3 4 5 6 7 8 |
task :after_symlink do %w{database.yml envrinment.rb}.each do |f| run "cp -f #{shared_path}/files/#{f} #{current_path}/config/#{f}" end %w{production.rb}.each do |f| run "cp -f #{shared_path}/files/#{f} #{current_path}/config/environments/#{f}" end end |