[rails] Mysql で Incorrect datetime value が発生

Rails 2.3.4 と Mysql 5.0 (Windows) の環境で、Unit Test を実行すると下記のエラーが発生。

>rake test:units
  1) Error:
test_should_authenticate_user(UserTest):
ActiveRecord::StatementInvalid: Mysql::Error: Incorrect datetime value: '2009-10-16 07:56:13 UTC' for column 'remember_token_expires_at' at row 1: INSERT INTO `users` (`salt`, `updated_at`, `crypted_password`, `remember_token_expires_at`, `id`, `remember_token`, `login`, `created_at`, `email`) VALUES ('356a192b7913b04c54574d18c28d46e6395428ab', '2009-10-15 07:56:13', '39e3509c8a1dff4e5b35850d970992321c4f1358', '2009-10-16 07:56:13 UTC', 1, '77de68daecd823babbb58edb1c8e14d7106e83bb', 'quentin', '2009-10-10 07:56:12', 'quentin@example.com')

Mysql の sql-mode を空白に設定することで解決しました。

my.ini
# Set the SQL mode to strict
#sql-mode="STRICT_TRANS_TABLES,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION"
sql-mode=''

参考:

Now, open the my.ini file located at c:program files/MySQL/MySQL
Server X.x/my.ini.
Add a # at the begining of the line: sql-mode="STRICT_TRANS_TABLES,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION"
Type sql_mode='' on the next line and save the file.
restart mysql

fixed it

[Rails] Re: restful_authentication rspec failures "Mysql::Error: Incorre

ラベル: ,

[Rails][Plugins] multimodel-forms プラグインがすごい便利

has_many な関連を、ひとつのフォームで登録したいことはよくありますが、そんなときにおすすめなのが、 multimodel-forms プラグインです。



たとえば、ひとつの記事に複数のコメントの場合は、以下のようになります。



まずは、ベースプロジェクトを作成です。



rails sample
cd sample
ruby script/generate scaffold article title:string body:text
ruby script/generate model comment body:text article_id:integer
rake db:migrate


# ---------- app/models/article.rb ----------
class Article < ActiveRecord::Base
has_many :comments
end


# ---------- app/models/comment.rb ----------
class Comment < ActiveRecord::Base
belongs_to :article
end


multimodel-forms プラグインをインストール




ruby script/plugin install git://github.com/sudothinker/multimodel-forms.git


model を修正


# ---------- app/models/article.rb ----------
class Article < ActiveRecord::Base
has_many_with_attributes :comments
end


view を修正


# ---------- app/views/layouts/articles.html.erb ----------
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="content-type" content="text/html;charset=UTF-8" />
<title>Articles: <%= controller.action_name %></title>
<%= stylesheet_link_tag 'scaffold' %>
<%= javascript_include_tag :defaults %>
</head>
<body>

<p style="color: green"><%= flash[:notice] %></p>

<%= yield %>

</body>
</html>


# ---------- app/views/articles/show.html.erb ----------
<p>
<b>Title:</b>
<%=h @article.title %>
</p>

<p>
<b>Body:</b>
<%= simple_format(h(@article.body)) %>
</p>

<h2>Comments</h2>
<ol>
<% @article.comments.each do |c| %>
<li><%= simple_format(h(c.body)) %> <small>(commented <%= time_ago_in_words(c.created_at) %> ago)</small></li>
<% end %>
</ol>

<%= link_to 'Edit', edit_article_path(@article) %> |
<%= link_to 'Back', articles_path %>


# ---------- app/views/articles/new.html.erb ----------
<h1>New article</h1>

<% form_for(@article) do |f| %>
<%= f.error_messages %>

<%= render :partial => 'form', :locals => { :f => f } %>
<p>
<%= f.submit "Create" %>
</p>
<% end %>

<%= link_to 'Back', articles_path %>


# ---------- app/views/articles/edit.html.erb ----------
<h1>Editing article</h1>

<% form_for(@article) do |f| %>
<%= f.error_messages %>

<%= render :partial => 'form', :locals => { :f => f } %>
<p>
<%= f.submit "Update" %>
</p>
<% end %>

<%= link_to 'Show', @article %> |
<%= link_to 'Back', articles_path %>


_form.html.erb を追加


# ---------- app/views/articles/_form.html.erb ----------
<p>
<%= f.label :title %><br />
<%= f.text_field :title %>
</p>
<p>
<%= f.label :body %><br />
<%= f.text_area :body, :rows => 7 %>
</p>

<h3>Comments</h3>
<div id="comments">
<%= render :partial => 'comments/comment', :collection => @article.comments %>
</div>

<%= add_link "Add Comment", :comment %>


comments/_comment.html.erb を追加


# ---------- app/views/comments/_comment.html.erb ----------
<p class="comment">
<% fields_for_associated :article, comment do |comment_form| %>
<%= comment_form.text_area :body, :rows => 2, :index => nil %>
<%= delete_link_for(comment, "Delete", comment_form) %>
<% end %>
</p>


さっそく動作確認です。




非常に便利な、 multimodel-forms プラグインですが、 acts_as_list をサポートしているので、並び替えも簡単にできるようになります。

ラベル:

Windows で git



Rails の Plugin をインストールしようとしたら、.git な GitHub(ぎっとはぶ)のもので、インストールできませんでした。


GitHub とは

git のホスティングサービス。Rails で作成されており、使いやすいインターフェイスが特徴。


Rails や RSpec 等、また http://gems.github.com/ の Rubygems のレポジトリソース等、Ruby 関係のライブラリのコードを中心とした様々なオープンソースの開発の場所ともなっている。無料で 100M 使えるアカウントを作ることができ、git レポジトリの作成、公開が可能。もちろん Web 上から変更履歴等も参照可能である。


またワンクリックで github で公開されている OSS のコードを fork して開発することが可能で、自分の変更を加えたレポジトリを pull してほしい、等の要望もワンクリックで可能である。


参考:githubとは - はてなキーワード



というわけで、Rails 界では今後 GitHub に移行していくようなので、後ればせながら、git をインストールしました。


git は、 MOONGIFT: » WindowsでGitをはじめるなら「msysGit」:オープンソースを毎日紹介 を参考に、 msysGit をインストール。


これで、 GitHub にあるプラグインもインストールできるようになります。


ruby script/plugin install git://github.com/rubypond/semantic_form_builder.git

ラベル:

Rails で、アプリケーションから ER図 を生成できる RailRoad を試してみる



Rails には、アプリケーションのモデル・コントローラの内容や関係が記述されたクラス図を、リバースエンジニアリングして生成してくれる RailRoad という便利なツールがあります。


設計は軽くすませて、すぐにプログラミングしていくことが多い Rails アプリケーションですが、全体像を把握したい場合や、他の人に見せたい場合などは、こういうツールがあると便利ですね。


というわけで、実際に使ってみました。


インストール


Graphviz をまずはインストール


railroad をインストール


gem install railroad

Rake タスクとして実行できるようにする


lib/task/diagrams.rake
namespace :doc do
namespace :diagram do
desc "Generate Model diagrams."
task :models do
# SVG
sh "railroad -i -l -a -m -M | dot -Tsvg | sed 's/font-size:14.00/font-size:11.00/g' > doc/models.svg"
# PNG
sh "railroad -i -l -a -m -M | dot -Tpng > doc/models.png"
end

desc "Generate Controller diagrams."
task :controllers do
# SVG
sh "railroad -i -l -C | neato -Tsvg | sed 's/font-size:14.00/font-size:11.00/g' > doc/controllers.svg"
# PNG
sh "railroad -i -l -C | neato -Tpng > doc/controllers.png"
end
end
desc "Generate Model and Controller diagrams."
task :diagrams => %w(diagram:models diagram:controllers)
end

クラス図の生成


rake doc:diagrams

すばらしいですね。

ラベル:

Rails で migration 時に、Column Comment を設定する

Railsで、マイグレーション作成時に、カラムにコメントを設定し、それをデータベースに設定する ColumnComments という便利なプラグインがあります。

というか、標準では設定できないんですね・・・。


インストールは次の通りです。



  1. こちらより、ZIPファイルをダウンロード

  2. 解凍して column_comments ディレクトリを vendor/plugins へコピー


使い方は、マイグレーション時に次のように記述します。


Example migration:

def self.up
create_table "users" do |t|
t.column "first_name", :string, :comment => "The member's given name."
end

column_comment "tags", "id", "The unique ID of any tag in the system."
end

そしてさらに、テーブルの情報を model と fixture にコメントとして書き込んでくれる annotate_models プラグインもバンドルされています。が、 annotate_models.rb が古いので、こちらは本家のものと置き換えて、コメントを表示するように修正します。


ついでに、 annotate_modelsにindexの情報を付加する - Hello, world! - s21g を参考に、インデックス情報もつけちゃいます。


vendor/plugins/column_comments/lib/annotate_models.rb - self.get_schema_info
  def self.get_schema_info(klass, header)
info = "# #{header}\n#\n"
info << "# Table name: #{klass.table_name}\n#\n"

# index info by http://blog.s21g.com/articles/318
indices = {}
klass.connection.indexes(klass.table_name).each do |index|
index.columns.each do |column_name|
indices[column_name] ||= []
indices[column_name] << "#{index.name}"
indices[column_name].last << "(unique)" if index.unique
end
end

max_size = klass.column_names.collect{|name| name.size}.max + 1
klass.columns.each do |col|
attrs = []
attrs << "default(#{quote(col.default)})" if col.default
attrs << "not null" unless col.null
attrs << "primary key" if col.name == klass.primary_key
if index = indices[col.name]
attrs << index.join(' ')
end

col_type = col.type.to_s
if col_type == "decimal"
col_type << "(#{col.precision}, #{col.scale})"
else
col_type << "(#{col.limit})" if col.limit
end
info << sprintf("# %-#{max_size}.#{max_size}s:%-15.15s %s", col.name, col_type, attrs.join(", ")).rstrip
info << "\n"
# column comment
unless col.comment.blank?
info << "# #{col.comment}"
info << "\n"
end
end

info << "#\n\n"
end

使い方は、次の Rake タスクを実行します。


rake annotate_models

これでもう、テーブルのカラム名を調べるためにデータベースを見る必要はなくなりそうです。

ラベル:

Rails で Database から Fixture を抽出したい

Rails で、データベースから yaml(やむる) 形式のフィクスチャを抽出するには、ar_fixtures が有名です(手元の Ruby on Rails 逆引きクイックリファレンス Rails 2.0対応 でも紹介されています)。


ですが、実際使ってみると、日本語(UTF8)がうまく表示されなかったり、項目の並びが適当だったりと、あまりよろしくありませんでした。


そこで、データベースからテストフィクスチャを抽出する(to_yaml 不使用) - Rails で行こう! - Ruby on Rails を学ぶ で紹介されている Rake タスクを使うとばっちりうまくいきます。



ar_fixtures は内部的に to_yaml というメソッドを使っていて、これが UTF-8 の文字列をうまく扱えない。そこで、



日本語をto_yamlするとエンコードされてしまう問題を安直な方法で解決する


to_yamlでUTF-8な日本語がbinaryになってしまう問題を回避するRailsプラグイン


みたいな hack が必要になってくる。


そんなわけで、Chad Flower「Rails レシピ」のレシピ41「生データからのテストフィクスチャの抽出」(p155) のソースコードをベースに次のような Rake タスクを作ってみた。to_yaml は使ってないので、UTF-8 文字列にまつわる頭痛とも無縁だ。興味があれば、使ってみてほしい。



参考:データベースからテストフィクスチャを抽出する(to_yaml 不使用) - Rails で行こう! - Ruby on Rails を学ぶ


ラベル:

Aptana RadRails で test_helper.rb の LoadError が発生する



Edge Rails (2.1.0) と、Aptana RadRails (1.0.3.200807071913NGT)の環境下で、ツールバー上のテストボタンでユニット・テストの実行をすると、以下のエラーが発生します。


c:/ruby/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:27:in `gem_original_require': no such file to load -- test_helper (LoadError)

コマンドでのテストは問題ないので、「rake test:units」とすればいいのですが、次のようにすれば解決できます。



There is an open issue on the Aptana issue tracker, but it doesn’t seem to be resolved yet.  Some people have suggested prefixing your requires with the test directory when requiring the test_helper file at the start of your test files -but I don’t think that’s a good solution, especially if you’re working with multiple developers.  Frustrated by not getting my daily green bar fix, I found this work-around:


  1. From the Eclipse Preferences option, choose Ruby | Installed Interpreters

  2. Select your interpreter (I use the Standard VM default interpreter named usr) and choose ‘Edit’. 

  3. Add -Itest to the Default VM Arguments option.  Don’t forget the leading dash!


  4. Click ‘OK’. 


参考:Chris Cruft » Blog Archive » Aptana RadRails and the test_helper.rb LoadError


ラベル:

Rails で、一つのフォームで複数のモデルを扱う

先日のRails講習で、いいことを学んだので忘れないうちにメモです・・。


Rails では、基本、一つの Form に一つの Model なのですが、 fields_for というヘルパーを使用することで複数のモデルを扱えます。


一対一のモデルを一度に更新する場合などに使えそうです。




  <% form_for @person, :url => { :action => "update" } do |person_form| %>
First name: <%= person_form.text_field :first_name %>
Last name : <%= person_form.text_field :last_name %>

<% fields_for @person.permission do |permission_fields| %>

Admin? : <%= permission_fields.check_box :admin %>
<% end %>
<% end %>

参考:Rails Framework Documentation


ラベル:

Rails 2.1.0 をインストールしてみる

久しぶりに、Railsの環境を更新しようと下記のコマンドこ実行すると、エラーがでました。
やはり、すんなりといきませんねぇ~。


> gem update --system
> gem update rails
Updating installed gems
Updating rails
ERROR: While executing gem ... (Gem::InstallError)
invalid gem format for c:/ruby/lib/ruby/gems/1.8/cache/activesupport-2.1.0.gem

いろいろ調べてみると、activesupportを手動でインストールすることで解決できるようです。



http://rubyforge.org/projects/activesupport/


Download gem to a local file, then


sudo gem install --local activesupport-2.1.0.gem


All fixed.



参考:Riding Rails: Rails 2.1: Time zones, dirty, caching, gem dependencies, caching, etc


ラベル:

ホスト名とOpenID

Rails で OpenID を試してみようと思って思わぬところで躓いたのでメモです。


ホスト名に使用できる文字は、英数字文字(a-zA-Z0-9)および、ハイフン(-)となっています。


ホスト名について

ホスト名(hostname)は,[RFC1034]の3.及び[RFC1123]の2.1 で示される形式をとる。すなわち,"."によって分離されたドメインラベルの列であって,各々のドメインラベルは,英数字文字(alphanum)で開始及び終了し,"-"文字を含んでもよい。完全限定ドメイン名の最も右にあるドメインラベルは,数字で始まってはならない。そのために,IPv4アドレスとは構文的に区別されるドメイン名になる。


Uniform Resource Identifiers (URI): Generic Syntax: Main(日本語)



テストのために自分のOpenIDを取得したのですが、これがまた、上で述べたルールに反したものを取得してしまいました・・。


取得したIDは、「ishikawa_rs.openid.ne.jp」なのですが、アンダーバーが入っています><


このIDをOpenIDのプラグインであるopen_id_authenticationに通すと、「ishikawa_rs.openid.ne.jp is not an OpenID URL」と言われてしまいます。


エラーが発生するところをirbで再現してみると下記のようになります。


irb(main):001:0> require 'uri'
=> true
irb(main):002:0> url = "ishikawa_rs.openid.ne.jp"
=> "ishikawa_rs.openid.ne.jp"
irb(main):003:0> uri = URI.parse(url.to_s.strip)
=> #
irb(main):004:0> uri = URI.parse("http://#{uri}") unless uri.scheme
URI::InvalidURIError: the scheme http does not accept registry part: ishikawa_rs
.openid.ne.jp (or bad hostname?)
from c:/ruby/lib/ruby/1.8/uri/generic.rb:195:in `initialize'
from c:/ruby/lib/ruby/1.8/uri/http.rb:78:in `initialize'
from c:/ruby/lib/ruby/1.8/uri/common.rb:488:in `new'
from c:/ruby/lib/ruby/1.8/uri/common.rb:488:in `parse'
from (irb):4
irb(main):005:0>

bad hostnameです。


そもそも、なぜこのIDになったかというと、「ishikawa.rs」にしようとしたところ、「ドット(.)はだめです。アンダーバー(_)は使えるよ。」と言われたからでした・・。



OpenIDを取得する際、ユーザIDがホスト名になる場合はご注意ください。


※そして、困ったことにopenid.ne.jpではアカウントの削除ができないようです。

ラベル: ,

Rails と AIR で、付箋紙アプリ

Stickynotes

Rails の最新は 2.0.2 ですし、AIR は正式版の 1.0 が公開されました。

情報はある程度追ってはいるものの、やはり実際に試してみないとなかなか身につきません。


というわけで、以前から気になっていた、[Think IT] 第1回:付箋紙アプリケーションを作ろう!を参考に、Ruby on RailsとAIRによるデスクトップ付箋紙アプリケーションを作ってみました。


Adobe AIR のインストールはこちらから。


サンプル付箋紙アプリをお試しいただく場合は、こちらから。

ちなみに、このアプリはユーザ管理はしておりません。ので、大変ソーシャルな付箋アプリです(汗


Download Stickynotes AIR


基本的な動作は、[Think IT] 第1回:付箋紙アプリケーションを作ろう!と、Ruby on RailsとAdobe AIRでデスクトップアプリを作る - Pokeal.COMをベースに、なんとなくタスクトレイアイコンも使ってみました。ついでに、常に前面表示も可能です。


Railsに関しては、実質、次の2行のみです。これでRestfulなバックエンドアプリのできあがりです・・。


 $ ruby script/generate scaffold sticky body:text x:float y:float width:integer height:integer
$ rake db:migrate

なお、Railsアプリは、先日紹介した、Herokuで構築しています。


AIRでは、透過するTextFieldを作るのにちょっと苦労しました。
ポイントは、blendModeをLAYERにしたSpriteでした。


dynamic textfield alpha problem - kirupaForum

I am not sure if this is quite what you are looking for but here is a solution I found for adjusting alpha on a TextField object.


// create an empty sprite
var rect:Sprite = new Sprite();
rect.blendMode = BlendMode.LAYER;

addChild(rect);

var txtField:TextField = new TextField();
txtField.txtColor = 0x000000;
txtField.alpha = .5;
txtField.appendText("SOME TEXT");

rect.addChild(txtField);

the key is to set the blendMode property on the sprite to LAYER.



以下、参考までにソースコードです。


Menu.mxml


<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" width="300" height="200" creationComplete="onCreationComplete();" closing="closing(event);">
<mx:Script>
<![CDATA[
import mx.core.BitmapAsset;

[Embed(source="icons/broken-16x16.png")]
private var icon16:Class;

private var stickies:Array = [];

private function create():void {
setStatus(">> Create new sticky");

var sticky:Sticky = new Sticky();

sticky.setAlwaysInFront(isAlwaysInFront.selected);
sticky.save();
sticky.show();
stickies.push(sticky);

}

private function onCreationComplete():void {
setStatus(">> Initializing...");

// is supports system tray icon
if (NativeApplication.supportsSystemTrayIcon) {

var images:Array = [];
images.push((new icon16() as BitmapAsset).bitmapData);
nativeApplication.icon.bitmaps = images;

var systemTrayIcon:SystemTrayIcon = (nativeApplication.icon as SystemTrayIcon);

systemTrayIcon.tooltip = "Stickynotes";

var nativeMenu:NativeMenu = new NativeMenu();
var menuItemNew:NativeMenuItem = new NativeMenuItem("New Stickynote");
menuItemNew.addEventListener(Event.SELECT, function(e:Event):void {
create();
});
var menuItemReload:NativeMenuItem = new NativeMenuItem("Reload Stickynotes");
menuItemReload.addEventListener(Event.SELECT, function(e:Event):void {
load();
});
var menuItemRestore:NativeMenuItem = new NativeMenuItem("Restore window");
menuItemRestore.addEventListener(Event.SELECT, function(e:Event):void {
restore();
});
var menuItemExit:NativeMenuItem = new NativeMenuItem("Exit");
menuItemExit.addEventListener(Event.SELECT, function(e:Event):void {
exit();
});
nativeMenu.addItem(menuItemNew);
nativeMenu.addItem(menuItemReload);
nativeMenu.addItem(menuItemRestore);
nativeMenu.addItem(menuItemExit);
systemTrayIcon.menu = nativeMenu;

}

load();
}

private function load():void {
setStatus(">> Loading from http://stickynotes.heroku.com ...");

closing(null);

var request:URLRequest = new URLRequest("http://stickynotes.heroku.com/stickies.xml");
request.method = 'GET';
var loader:URLLoader = new URLLoader();
loader.addEventListener(Event.COMPLETE, function(e:Event):void {
messages.text += e.target.data + "\n";

var xml:XML = new XML(e.target.data);
for each (var element:Object in xml.sticky) {
var sticky:Sticky = new Sticky();
sticky.id = element.id;
sticky.editor.text = element.body;
sticky.window.x = element.x * Capabilities.screenResolutionX;
sticky.window.y = element.y * Capabilities.screenResolutionY;
sticky.window.width = element.width;
sticky.window.height = element.height;
sticky.setAlwaysInFront(isAlwaysInFront.selected);
sticky.updateStatus();
sticky.show();

stickies.push(sticky);
}

clearStatus();
});
loader.load(request);
}

private function setStatus(s:String):void {
this.status = s;
}
private function clearStatus():void {
this.status = "";
}

private function closing(e:Event):void {
for each (var sticky:Sticky in stickies) {
if (sticky) {
sticky.window.close();
}
}
stickies = [];
}

private function saveAll():void {
setStatus(">> Save stickies");

for each(var sticky:Sticky in stickies) {
sticky.save();
}
}

private function onChangeHandle():void {
for each (var sticky:Sticky in stickies) {
sticky.setAlwaysInFront(isAlwaysInFront.selected);
}
}

]]>
</mx:Script>
<mx:Button left="10" top="10" label="New" id="new_btn" click="create();" />
<mx:Button left="60" top="10" label="Reload" id="load_btn" click="load();" />
<mx:Button right="10" top="10" label="Save" id="save_btn" click="saveAll();" />
<mx:CheckBox left="10" top="40" label="always in front" id="isAlwaysInFront" change="onChangeHandle();" />
<mx:TextArea right="10" top="70" left="10" bottom="10" id="messages" />
</mx:WindowedApplication>


Sticky.as


package {
import flash.text.*;
import flash.display.*;
import flash.events.*;
import flash.system.*;
import flash.net.*;
import mx.controls.*;

public class Sticky {
public var window:NativeWindow; // sticky window
public var editor:TextField; // sticky edit area
public var id:Number; // primary key
private var button:SimpleButton = new SimpleButton(); // cloase button
private var resizeHandle:SimpleButton = new SimpleButton(); // resize handle
private var x:Number; // sticky x
private var y:Number; // sticky y
private var height:Number; // sticky height
private var width:Number; // sticky width
private var body:String; // sticky body
private var sprite:Sprite = new Sprite();
private static var RESIZE_HANDLE_SIZE:int = 20;

/* create sticky window */
public function Sticky():void {
var initOptions:NativeWindowInitOptions = new NativeWindowInitOptions();
initOptions.systemChrome = NativeWindowSystemChrome.NONE;
initOptions.transparent = true;
initOptions.type = NativeWindowType.LIGHTWEIGHT;

window = new NativeWindow(initOptions);
window.alwaysInFront = false;
window.stage.align = StageAlign.TOP_LEFT;
window.stage.scaleMode = StageScaleMode.NO_SCALE;

// layer for transparent editor
sprite.blendMode = BlendMode.LAYER;
window.stage.addChild(sprite);

// for edit area
editor = new TextField();
editor.x = editor.y = 0;
editor.selectable = true;
editor.border = false;
editor.type = TextFieldType.INPUT;
editor.multiline = true;
editor.background = true;
editor.wordWrap = true;
editor.backgroundColor = 0xE6E082;
editor.alpha = 1;
sprite.addChild(editor);

// window position of center
window.x = 0.5 * Capabilities.screenResolutionX - 300 * 0.5
window.y = 0.5 * Capabilities.screenResolutionY - 100 * 0.5

// size for window and edit area
window.width = editor.width = 300;
window.height = editor.height = 100;

// resize for window and edit area
window.stage.addEventListener(Event.RESIZE, function(e:Event):void {
resized();
});

// move and resize
window.stage.addEventListener(MouseEvent.MOUSE_DOWN, function(e:MouseEvent):void {
var x:Number = e.stageX;
var y:Number = e.stageY;
if (y > window.height - RESIZE_HANDLE_SIZE && x > window.width - RESIZE_HANDLE_SIZE) {
//window.startResize(NativeWindowResize.BOTTOM_RIGHT);
} else {
window.startMove();
}
});

window.stage.addEventListener(MouseEvent.MOUSE_UP, function(e:MouseEvent):void {
if (isChanged()) {
save();
}
});

editor.addEventListener(FocusEvent.FOCUS_OUT, function(e:FocusEvent):void {
if (isChanged()) {
save();
}
});

} // end of function Sticky

// show window
public function show():void {
// create close button
button.x = window.width - 15;
button.y = 5;
button.upState = createBox(0xE6E082, 10, 0.3);
button.overState = createBox(0xE6E082, 10, 1);
button.downState = createBox(0xCCCCCC, 10, 1);
button.hitTestState = button.upState;
button.addEventListener(MouseEvent.CLICK, function(e:MouseEvent):void {
var request:URLRequest = new URLRequest("http://stickynotes.heroku.com/stickies/" + id + ".xml");
request.method = 'DELETE';
var loader:URLLoader = new URLLoader();
loader.load(request);
window.close();
});
window.stage.addChild(button);

// create resize handle
resizeHandle.x = window.width - 15;
resizeHandle.y = window.height - 15;
resizeHandle.upState = createResizeHandle(0xE6E082, 10, 0.3);
resizeHandle.overState = createResizeHandle(0xE6E082, 10, 1);
resizeHandle.downState = createResizeHandle(0xCCCCCC, 10, 1);
resizeHandle.hitTestState = button.upState;
resizeHandle.addEventListener(MouseEvent.MOUSE_DOWN, function(e:MouseEvent):void {
window.startResize(NativeWindowResize.BOTTOM_RIGHT);
});
window.stage.addChild(resizeHandle);

window.visible = true;
}

// call after window resized
public function resized():void {
editor.width = window.width;
editor.height = window.height;
button.x = window.width - 15;
button.y = 5;
resizeHandle.x = window.width - 15;
resizeHandle.y = window.height - 15;
}

// save sticky
public function save():void {
// create URL of sticky
var request:URLRequest
if (!id) {
request = new URLRequest("http://stickynotes.heroku.com/stickies.xml");
request.method = 'POST';
} else {
request = new URLRequest("http://stickynotes.heroku.com/stickies/" + id + ".xml");
request.method = 'PUT';
}

// create paramater for edit
var variables:URLVariables = new URLVariables();
variables['sticky[x]'] = window.x / Capabilities.screenResolutionX;
variables['sticky[y]'] = window.y / Capabilities.screenResolutionY;
variables['sticky[width]'] = window.width;
variables['sticky[height]'] = window.height;
variables['sticky[body]'] = editor.text;
request.data = variables;

// send
var loader:URLLoader = new URLLoader();
loader.addEventListener(Event.COMPLETE, function(e:Event):void {
var xml:XML = new XML(e.target.data);
if (!id) {
id = xml.id;
}
updateStatus();
});
loader.load(request);
}

public function setAlwaysInFront(value:Boolean):void {
window.alwaysInFront = value;
if (value) {
editor.alpha = 0.85;
} else {
editor.alpha = 1;
}
}

public function updateStatus():void {
// sticky status
x = window.x;
y = window.y;
width = window.width;
height = window.height;
body = editor.text;
}

/* private methods ***********/

// close button
private function createBox(color:uint, radius:Number, vis:Number):Shape {
var xShape:Shape = new Shape();
xShape.graphics.lineStyle(1, 0x000000);
xShape.graphics.beginFill(color);
xShape.graphics.drawRect(0, 0, radius, radius);
xShape.graphics.moveTo(0, radius);
xShape.graphics.lineTo(radius, 0);
xShape.graphics.moveTo(radius, radius);
xShape.graphics.lineTo(0, 0);
xShape.graphics.endFill();
xShape.alpha = vis;
return xShape;
}

private function createResizeHandle(color:uint, radius:Number, vis:Number):Shape {
var xShape:Shape = new Shape();
xShape.graphics.lineStyle(1, 0x000000);
xShape.graphics.beginFill(color);
xShape.graphics.moveTo(0, radius);
xShape.graphics.lineTo(radius, 0);
xShape.graphics.lineTo(radius, radius);
xShape.graphics.lineTo(0, radius);
xShape.graphics.endFill();
xShape.alpha = vis;
return xShape;
}

// is paramater changed
private function isChanged():Boolean {
return (x != window.x || y != window.y ||
width != window.width || height != window.height ||
body != editor.text);
}

} // end of class Sticky

} // end of package

ラベル: ,

ブラウザだけでRuby on Rails - Heroku -


以前、TechCrunchの記事Herokuを知って、すぐにベータテスタとして登録したのですが、先日アカウント登録のメールが届いたのでさっそく使ってみました。


Herokuとは、ブラウザの中だけで、Railsのアプリケーションを開発できるサービスです。また、作成したアプリは、Amazon EC2上で簡単にホストすることもできるようです。


気になるRailsのバージョンは「2.0.2」になるようです。また、データベースはPostgresなようです。





ほんとうに簡単に開発からデプロイまでできるので、ちょっとしたアプリを作るにはもってこいですね。
それに、後に公開されるプレミアム版では、サブドメインではなく、独自ドメインも使用可能になるようですので、要注目です。

ラベル:

Rails API を読む


Railsに限らず、Documentは非常に重要で、とても参考になるものです。

いつもはRails Framework Documentを参考にするのですが、他に以下のようなサイトもあるようです。知らなかった・・・w



Ruby on Rails Manual
過去のバージョンのDocumentも揃っています。



Rails API with the AJAX flavor
AjaxでSuggestしてくれるので便利!



gotAPI/HTML - Instant search in HTML and other developer documentation
AJAX and Frameworks(Prototype.js等)や、HTML/CSS/Javascript等、Rails以外のも多数載ってます。こちらもAjaxでSuggest!これはいい!

ラベル:

Edge Railsとruby-Gettext


先日、新しいRailsプロジェクトを作る機会がありまして、せっかくなのでRESTfulなRailsでいこうと思い、Edge Rails使うことを決意。



ですが、さっそく躓いたので、以下問題と解決です。ご参考までに。



アプリを起動するとエラーが発生し、development.logに以下のエラーが。

DISPATCHER FAILSAFE RESPONSE (has cgi) Wed Jun 20 21:18:28 +0900 2007
Status: 500 Internal Server Error
You have a nil object when you didn't expect it!
You might have expected an instance of ActiveRecord::Base.
The error occurred while evaluating nil.[]
C:/ruby/lib/ruby/1.8/cgi.rb:1165:in `[]'
C:/ruby/lib/ruby/gems/1.8/gems/gettext-1.9.0-mswin32/lib/gettext/locale_cgi.rb:26:in `system'
C:/ruby/lib/ruby/gems/1.8/gems/gettext-1.9.0-mswin32/lib/gettext/locale.rb:88:in `system'
C:/ruby/lib/ruby/gems/1.8/gems/gettext-1.9.0-mswin32/lib/gettext/locale.rb:96:in `default'
C:/ruby/lib/ruby/gems/1.8/gems/gettext-1.9.0-mswin32/lib/gettext/rails.rb:276:in `render_file'
:
:



[gettext-u-en] Problem with gettext 1.9.0 and RESTful rails appを参考に、問題解決(ここまでくるのに、数日の間Google先生に質問攻めでした・・・)

--- /opt/local/lib/ruby/gems/1.8/gems/gettext-1.9.0/lib/gettext/locale_cgi.rb 2007-05-28 16:11:27.000000000 +0300
+++ Desktop/locale_cgi.rb 2007-05-28 16:11:49.000000000 +0300
@@ -23,7 +23,7 @@ module Locale
def system
return @@default_locale unless @@cgi
cgi_ = cgi
- if ret = cgi_["lang"] and ret.size > 0
+ if not cgi_.params.empty? and ret = cgi_["lang"] and ret.size > 0
elsif ret = cgi_.cookies["lang"][0]
elsif lang = cgi_.accept_language and lang.size > 0
num = lang.index(/;|,/)



上記問題が発生したのは、

svn co http://dev.rubyonrails.org/svn/rails/trunk rails

でとってきたEdge Railsでした。

今しがた、再度Edge Railsを取り直したら上記問題は発生せずでした・・・

rake rails:freeze:edge TAG=rel_1-2-3



なんだったんだろw

ラベル:

logに残されるパスワードをフィルタリングする



Railsアプリを作成していて、動作の確認等で非常に重要なログファイルですが、このログには様々な情報が残されます。

例えばユーザ登録画面等で送信したパラメータもログに残されます。

ユーザ登録の際は、パスワードを入力したりしますので、パスワードがそのまま平文で残されるのはちょいとまずいものです。


Processing UserController#signup (for 192.168.0.25 at 2007-03-24 10:41:08) [POST]
Session ID: 42d8820cd16b8a672b86f737d8d6b4e8
Parameters: {"user"=>{"password_confirmation"=>"testpassword", "lastname"=>"Tarou", "firstname"=>"Test", "login"=>"tester", "password"=>"testpassword", "email"=>"test@test.com"}, "commit"=>"Signup", "action"=>"signup", "controller"=>"admin/user"}


そこで、以下のようにするとログに残されるパスワードをフィルタリングすることができます。

filter_parameter_logging(*filter_words) {|key, value| ...}


class ApplicationController < ActionController::Base
filter_parameter_logging "password"
end



すばらしい!


Processing UserController#signup (for 192.168.0.25 at 2007-03-24 10:43:50) [POST]
Session ID: 42d8820cd16b8a672b86f737d8d6b4e8
Parameters: {"user"=>{"password_confirmation"=>"[FILTERED]", "firstname"=>"Test", "lastname"=>"Tarou", "password"=>"[FILTERED]", "login"=>"tester2", "email"=>"test2@test.com"}, "commit"=>"Signup", "action"=>"signup", "controller"=>"admin/user"}

Filtering Sensitive Logs


ラベル:

J-PHONE/3.0で、postがうまくいかない



携帯向けアプリを構築していて、特定の端末(おそらく)の場合だけ、post送信時、sessionが維持されない現象を確認。

post送信されるパラメータで、「"_session_id"=>"901cbb6b5a6d515a99a06373d20ba2f4?page=1"」というように、セッションIDのなかになぜか他のパラメータが混入する。

これでは当然、「そんなセッションありません」、ということになってsessionが維持できなくなります。



で、あれこれ試行錯誤した結果、どうやら、formのアクションで、自動でURLパラメータとしてsession_idを付与しているところが問題のよう。

formのアクションを直打ちして解決しました。



ちなみに自動でsession_idを付与してくれるのは、ActiveHeartのTransSidのお陰です。



参考:

RubyOnRails を使ってみる 【第 5 回】 ActiveHeart


ラベル:

file_column and capistrano

Railsで画像ファイルをアップロードする場合、file_columnというプラグインがとても使えます。

このpluginは、画像のアップロード、保存、サムネイル作成といった面倒な処理を劇的に簡略化してくれます。


データベースにファイル名を保存するカラムを追加し、

add_column :entry, :image, :string


モデルで、file_column pluginを指定します。

class Entry < ActiveRecord::Base
file_column :image
end



この場合の保存先は、

public/[model_name]/[attribute_name]/[id]/[file_name].jpg

になります。


サムネイルを作成する場合は、次のようにします。

class Entry < ActiveRecord::Base
file_column :image,
:magick => {
:versions => {
:thumb => "50x50",
:midle => "100x100",
:large => "800x600"
}
}
end



この場合はそれぞれ、

public/[model_name]/[attribute_name]/[id]/[file_name].jpg

public/[model_name]/[attribute_name]/[id]/thumb/[file_name].jpg

public/[model_name]/[attribute_name]/[id]/midle/[file_name].jpg

public/[model_name]/[attribute_name]/[id]/large/[file_name].jpg

に保存されます。


ファイルをアップロードするには以下のhelperを使用します。

<%= file_column_field "entry", "image" %>


アップロードした画像を表示するには、以下のhelperを使用します。

<%= url_for_file_column "entry", "image" %>



サムネイルを表示するには、

<%= url_for_file_column "entry", "image", "thumb" %>




上記のように簡単に、非常に簡単に画像処理を扱えるようになるわけですが、capistranoでデプロイしている場合、画像の保存先がデフォルトのままだと、デプロイするたびに画像ファイルをコピーしなければなりません。

これではあんまりですが、ちゃんと回避策がありました。

file_columnのオプションに、root_pathというのがありまして、デフォルトの保存先を変更できます。

以下のように、capistranoが自動でリンクしてくれるsystemディレクトリに画像を保存するように指定すれば、いちいちコピーしなくてもよさそうです。

class Entry < ActiveRecord::Base
file_column :image,
:magick => {
:versions => {
:thumb => "50x50",
:midle => "100x100",
:large => "800x600"
}
},
:web_root => "system/files/",
:root_path => File.join(RAILS_ROOT, "public", "system", "files")
end




参考:

HowToUseFileColumn

ラベル:

増加するログファイルへの対処


Railsでは、ログファイルに絶えず情報が追加されていき、ものすごい勢いで肥大化していくわけですが、でかいログファイルは様々な面でよくありません。

ですので、定期的にログファイルのローテーションをするわけですが、主にlogrotateを使用する方法と、Loggerを使用する方法があるようです。

ただし、FastCGIをマルチで使用している場合は、各FastCGIプロセスにてLoggerインスタンスが存在し、同一ログをローテートしてしまうため、Loggerの使用は控えたほうが良いようです。

というわけで、logrotateを使用します。

/path/to/your/app/log/*.log {
daily
missingok
rotate 14
compress
delaycompress
notifempty
copytruncate
create 0666 daemon daemon
}

参考:

DeploymentTips in Ruby on Rails

ラベル:

ActiveRecodStoreな場合のセッションの掃除


Railsでは、セッションが自動生成されるのですが、自動削除はしてくれません。

ですので、自分で掃除をしなければいけません。


session_cleaner.rb

class SessionCleaner
def self.remove_stale_sessions
CGI::Session::ActiveRecordStore::Session.destroy_all(['updated_on < ?', 1.days.ago])
end
end

上記クラスをlibあたりに保存し、cronで毎日自動実行させます。

0 0 * * * ruby /full/path/to/script/runner -e production "SessionCleaner.remove_stale_sessions"


参考:

Removing Stale Rails Sessions


ラベル:

jpmobile使用時のfunctionalテスト



[03/18追記]



下記は何れも既に対応されております。ご苦労様です。

詳しくはdara日記 [jpmobile]をご覧下さい。




dara日記 - jpmobile - A Rails plugin for Japanese mobile-phones



jpmobileというプラグインを使用すると、携帯向けのサイト構築が非常に楽になります。

jpmobileを使用すると、以下のことができるようになります。


  • 携帯電話の判別

  • 端末位置情報の取得

  • 端末製造番号、契約者番号等の取得

  • IPアドレスの検証(キャリアが公開しているIPアドレス帯域からのアクセスか判定)




ものすごく便利なプラグインなのですが、functionalテスト時にはまりました。

NoMethodError: undefined method `mobile?' for #

mobile?そんなメソッドありません。とのことです。

で、悩んだ挙句導き出した答えは、
ActionController::TestRequest.class_eval { include Jpmobile::CgiRequestExpansion}

を、テストクラスにて記述します。

きっと、テスト用のリクエストにjpmobileの機能をincludeするってことだと思います(w)。



それと、ソフトバンク携帯からの実機確認時に、なぜか携帯端末と判定されないという問題が発生。

こちらは、vendor/plugins/jpmobile/lib/jpmobile/cgi_request_expansion.rb のSoftbank端末判定部分を

when /^Softbank/

から、
when /^SoftBank/

に修正しました。




ラベル:

DateHelperでid属性を使いたい



RailsにはDateHelperという便利なものがありまして、これを利用すると簡単に日付や時刻のセレクトボックスを作成できます。

ですが、なぜかidや、class等の属性が無視されてしまいます。(Rails 1.1.6にて。最新のものはどうなの?)

javascriptで操作したいときなど、idがないと困ります。

そんなときは、下記のようにするとよいです。(bad hack!)


<%= select_day(Date.today, :prefix => 'search[date][day]" id="day_field', :discard_type => true) %>


参考:

Rails Forum / Ruby on Rails Help and Discussion Forum / How to give an ID to an input when using select helpers?


ラベル: