2014年4月11日金曜日

SBI証券から無料でリアルタイム株価を取得するライブラリ

HFT(もどき)が個人でもできないかと方法を探していたのですが、個人にストリーミングの株価データを配信してくれるところが見つからず。
しかたがないので、SBI証券のリアルタイム更新を利用して、ほぼリアルタイム(3秒ディレイくらい)で株価を取得できるライブラリを作りました。
SBI証券のアカウントは必須です。

まずはインストール

gem install Sbirsp
使い方のサンプル
require "sbirsp"

Sbirsp.configure do |config|
  config.username = "user_id"
  config.password = "password"
end

@client = Sbirsp::Client.new
@client.code = 9984
@client.show_stock_price

loop do
  if @old_price != @client.price
    puts "#{@client.price}"
    @old_price = @client.price
  end
end

複数クライアントを同じプロセス内で起動した時の動作が怪しいので、なんとか調整したい。

 githubにソースコードをアップしています。
  https://github.com/face-do/sbirsp

2013年3月15日金曜日

rubyからr言語を使って、キャンペーン前後で売上に変化があったか判断する

同一期間ではないので、対応のない場合のt検定を行います。
rubyからR言語を使うには、rsrubyというライブラリを使う。

まずはRのインストール
$ brew install R

で、rのpathを設定します。
$ export R_HOME=/Library/Frameworks/R.framework/Resources

次にrsrubyのインストール
$ gem install rsruby -- --with-R-include=/Library/Frameworks/R.framework/Headers --with-R-lib=/Library/Frameworks/R.framework/Libraries

で、準備は完了。

今回はこんな感じの想定です。
・送料無料キャンペーン(今まで送料無料ではなかった部分の売上が増える=平均単価が減少する仮説)
・送料無料キャンペーンをやっていなかった、去年の売上と今年の売上を比較
・月初から今日までの期間を考える


以下がコード

まずはヒストリカルグラフで確認
require 'rsruby'

r = RSRuby::instance
x = Order.where("created_at between ? and ?" , Time.now.beginning_of_month, Time.now).map(&:price).(&:to_f)
y = Order.where("created_at between ? and ?" , Time.now.beginning_of_month - 1.years, Time.now - 1.years).map(&:price).(&:to_f)

r.eval_R(<<-RCOMMAND)

hist(#{y.join(",")}, breaks = 5 ,col = "#0000ff40", border = "#0000ff", freq = TRUE)
hist(#{x.join(",")}, breaks = 5 ,col = "#ff00ff40", border = "#ff00ff", freq = TRUE, add = TRUE)

RCOMMAND
一応F検定
r.var_test(x, y)

t検定
r.t_test(x, y, altenative="two.sided")

するとこんな感じの結果がでます。

 {"statistic"=>{"t"=>1.8121550934930575}, "parameter"=>{"df"=>4.0}, "p.value"=>0.1441855869309796, "conf.int"=>[-14560.490812856453, 69286.49081285646], "estimate"=>{"mean of x"=>32613.0, "mean of y"=>5250.0}, "null.value"=>{"difference in means"=>0.0}, "alternative"=>"two.sided", "method"=>"Welch Two Sample t-test", "data.name"=>"c(63210L, 14385L, 74970L, 5250L, 5250L) and c(5250L, 5250L)"}

2013年3月4日月曜日

ruby2.0 on Rails4.0でABテストができる短縮URLサービスを作った

先日ruby2.0が公開され、Rails4.0がついにbetaとしてgem化されていたので、早速使ってみた。

作ったサービスは、こちら http://url-s.herokuapp.com/

ソースはgithubにあげてあります。

これは2種類以上のURLを登録できる短縮URLサービスです。
外部サービスを利用していてABテストができないときに、2種類のランディングページを用意しておいて、短縮URLでランダムに飛んでもらいテストするという用途を想定しています。

以下ruby2.0とRails4.0を使ってみた感想。

  • 起動早い。2.0のおかげだと思うけど、前の環境には戻りたくない
  • マジックコメントいちいち書かなくていいの最高!
  • strong paramerterでちょいはまり。特にネストしたモデルのを扱うなら、attributesの方のidを許可しないと、updateした時に、新規で作成されてしまうので注意。
  • find_byメソッドのほうが直感的でよい。というかなんでそうじゃなかったの?

結論。 新規でアプリ作る場合、特に躊躇する理由もないので、この組み合わせで積極的に使っていきたい。

2013年1月19日土曜日

railsでorder時に特定IDのデータだけ、前に持ってくる方法

例えば、IDが1〜10のデータがある時に、IDの降順にしようとした時には
Model.order("id desc").all
などとするが、時々この中の一部(例えばID:3)は先頭に、それ以外はIDの降順にしたいときなどがある。
そんなときは、
Model.order("'id' = CASE WHEN id = 3 THEN 0 ELSE 'id' END").order("id desc").all
などとすると、できる。

2013年1月10日木曜日

vanityとchankoを使ってRailsで簡単安全にABテストをする

vanityはRailsのABテスト用ライブラリ。導入が一番簡単っぽい。
 元々はテキストとか画像をのABテストを行うためのもののようだが、 chankoという限定公開用のライブラリを使って、機能単位でもABテストができるようにしてみる。

まずはGemfileに
gem 'chanko', :git => 'git://github.com/cookpad/chanko.git'
gem "vanity"
を記述して、
# bundle install

それぞれに必要な初期設定をする。
# rails generate chanko:install
# rails generate vanity
# rake db:migrate

config/vanity.ymlを作成
development:
  adapter: active_record
  active_record_adapter: mysql2
  host: localhost
  database: DBNAME
  user: USERNAME
  password: PASSWORD

development.rbに以下を記述
 Vanity.playground.collecting = true

vanityのDashboard用コントローラを作成
class VanityController < ApplicationController
  include Vanity::Rails::Dashboard
end 

ルーティングを設定
match '/vanity(/:action(/:id(.:format)))', :controller=>:vanity

測定用のユーザアイデンティティを設定。
class ApplicationController < ActionController::Base
  use_vanity :current_user
end

Railsのルートディレクトリ下にテストと測定用のファイルを作成
#mkdir -p experiments/metrics

postした回数測定用のファイルを作成
experiments/metrics/post.rb
metric "Post" do
  description "Postした回数"
end

コントローラの好きな場所に、 track! :post を入力すれば、その場所がよばれた回数を測定できるようになります。
class PostController < ApplicationController
  def create
     track! :post
       # ...投稿する処理
     end
  end
end

テスト用ファイルを作成
experiments/post_labels.rb
ab_test "Post labels" do
  description "テスト"
  alternatives true, false
  metrics :post
end

次にchanko用設定
# rails generate chanko sample
module Sample
  include Chanko::Unit

  active_if do |context, options|
    ab_test(:price_options) 
  end
  scope(:controller) do
    function(:controller_show) do
      # controller code here
    end
  end
  scope(:view) do
    function(:view_show) do
      render :partial => "/show"
    end
  end
end
で、好きな場所にinvoke(:sample, :controller_show) をおいたり、
invoke(:sample, :show) を置けばOK。
chankoの詳しい使い方はこちらを参考に。

どちらが優位かなんてのは、Vanityが表示してくるので、 評価しやすいはず。

production環境でうまく動かなかったので、
そのときはredisによる接続をためしてみるといいかも。
production:
  adapter: redis
  host: localhost



 参考:
http://eccyan.hatenablog.com/entry/2011/12/08/223603
https://github.com/assaf/vanity
http://webandy.com/articles/a-b-testing-with-vanity

2012年12月5日水曜日

rubymotionとrailsの連携を試行錯誤した話

この記事はRubyMotion Advent Calendar 2012の5日目記事です。

rubymotionメリットのひとつに、サーバサイド(Rails)とクライアントサイドが同じ言語でかけることがあると思いますが、
思いの外Railsとどう連携していくかの情報が少ないような気がします。
なので他の人の参考になればと、個人的に試行錯誤したことを残しておこうと思います。

ちなみに、one minutesというアプリを、rubymotionで作りましたので、よければダウンロードしてください。

試行錯誤1、babble-wrapで都度処理を書く

先述のone minutesでは、表示させるニュース情報をサーバからjsonで取得するだけの単純作業なので、以下の様な感じで実装しました。

BW::HTTP.get(SERVER_URL) do |response|
  if response.ok?
    @data = BW::JSON.parse(response.body.to_str)
  else
    App.alert(response.error_message)
  end
end

実装はかなり楽でしたが、複数情報を取得したい先がある場合や、postを行うときには、かなり面倒なことになってしまいました。
それが↓です。

試行錯誤2、babble-wrapで都度処理を書く(その2)

ソーシャルアプリを作ろうとした際の実装です。
アプリ概要は、facebookみたいなものなので、記事の投稿、投稿に対するコメント、投稿に対するいいね、それぞれを表示させるタイムラインの実装が必要でした。
それが下記コード群です。

#記事読み込み
BubbleWrap::HTTP.get("#{SERVER_URL}/entries.json?page=#{@page}") do |response|
  if response.ok?
    json = BubbleWrap::JSON.parse(response.body.to_str)
    unless json.count == 0
      @table_dates << json
      self.tableView.reloadData
      @page += 1
      @readMoreButton.enabled = true
      @readMoreButton.hidden = false if @page
    else
      @page = nil
      @readMoreButton.hidden = true
    end
  else
    App.alert(response.error_message)
   end
end
#記事投稿
  BubbleWrap::HTTP.post("#{SERVER_URL}/entries.json", {payload: data})
#コメント
 BubbleWrap::HTTP.post("#{SERVER_URL}/comments.json", {payload: data}) do |response|
    if response.ok?
      App.alert("コメントしました")
    elsif response.status_code.to_s =~ /40\d/
     App.alert("comment failed")
    else
     App.alert(response.error_message)
   end
 end
#いいね
BubbleWrap::HTTP.post("#{SERVER_URL}/likes.json", {payload: data}) do |response|
   if response.ok?
      App.alert("likeしました")
    else
     App.alert(response.error_message)
   end
 end

企画自体が没になったため、コレ以上コードは書いていないのですが、
重複が多く、メンテナンス性も非常に悪いものができてしまいました。

で、このあとECアプリを作ることになりました。
これまでの経験を踏まえ、もう少しRailsライクに実装したいと感じるように。

具体的には、
1.new、find、save、allなどで情報が取得や保存ができるように
2.バリデーションエラーは、その内容までも分かるように
3.ネットワークエラーなどがでたら、バリデーションエラーなどとは別に判別できるように

という理想を掲げ、作ってみた現実(ライブラリ)はこちら。
https://github.com/face-do/motion-rails-model

初めて作ったライブラリでもあり、非常にできはよくないのですが、それでも一応の理想は実現できました。
(といっても、babble-wrapのさらにwrapperライブラリだったりしますが。。。)

使い方としては、最初にアクセスしたいURLやbasic認証の情報を入力


    RM::Model.set_url("#{URL}/api/")
    RM::Model.set_username("username")
    RM::Model.set_password("password")

その後モデル用のクラスを作り、ライブラリを継承。

class Orders < RM::Model
  attr_accessor :name
 
  def attributes_update(json)
    @name = json['name']
    super
  end
end

これで、

order = Orders.new
order.name = "hogehoge"
order.save do |x|
  if x.ok?
    App.alert("保存しました。")
  end
end

とかすると、
railsに対して
/api/orders.json?name=hogehoge
のpostメソッドを実行するし、

Orders.find(1) do |x|
  if x.ok?
    @order = x.body
  end
end

で、
/api/orders/1.json
のgetをしたりします。

またgetしてきた、Objectに対してsaveをすると、
/api/orders/1.json
に対してputをするようになってます。

これを作った後、メタプログラミングrubyを読みまして、
もう少し抽象化ができそうな気がしているので、機会を見て修正していく予定です。

2012年10月29日月曜日

今ウェブサイトを作るなら必須のアイコン画像サイズ一覧


漏れがないようにまとめてみました。

サイト全体
favicon
16×16

facebook(OGP)対策
200x200

ウェブクリップアイコン(ios、android)対策
114×114(iPhone、iPod touchのRetina)
57×57(iPhone、iPod touchの非Retina)
144×144(iPadのRetina)
72×72(iPadの非Retina)

一括作成サービスができてくれるといいなあ。

2012年9月29日土曜日

rubymotion用ライブラリの作り方

ライブラリのコードは予め書いておき、またrubygemsに公開するなら、
そのアカウントを取得しておく。

適当なフォルダで
# bundle gem motion-hogehoge
で、gemの雛形を作成

.gemspecに必要な項目を入力。
依存ライブラリがある場合は、ここに
gem.add_dependency "bubble-wrap", "~>1.1.4"
などと追記しておく。

次に、lib/motion-hogehoge.rbというファイルができているので、ここを修正。
rubymotionではrequireが対応していないので、rubymotion用に書き直す必要がある。
以下がサンプル

unless defined?(Motion::Project::Config)
  raise "This file must be required within a RubyMotion project Rakefile."
end

Motion::Project::App.setup do |app|
  Dir.glob(File.join(File.dirname(__FILE__), 'motion-hogehoge/*rb')).each do |file|
    app.files.unshift(file)
  end
end

ライブラリのコードは、lib/motion-hogehoge/以下に入れておく。

rubygemsに登録する場合は、テストが通ってる必要がある。
とりあえず通すだけなら、以下でOK。

app/app_delegate.rbに
class AppDelegate
  def application(application, didFinishLaunchingWithOptions:launchOptions)
    @window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds)
    @window.rootViewController = UIViewController.alloc.init
    @window.makeKeyAndVisible
    true
  end
end
spec/main_spec.rbに
describe "Application 'modeltest'" do
  before do
    @app = UIApplication.sharedApplication
  end

  it "has one window" do
    @app.windows.size.should == 1
  end
end

で、
#bundle install
#rake spec
でテストが通ることを確認する。

問題がなければ、
gem build motion-hogehoge.gemspec
で、gemファイルを作成し、

gem push motion-hogehoge-0.0.1.gem
で、rubygemsにアップする。

アップするときに取得したアカウント情報が聞かれるので入力すればOK。

2012年9月28日金曜日

Railsとjsonでやり取りするmotion-rails-modelというrubymotion用ライブラリを作ってみた

Railsと連携する際、babble-wrapを使っていたのですが、
取得先が複数になったときに面倒だったのでラッパーライブラリを作ってみました。
Active Record風のメソッドで、Rest風にアクセスをします。

github

使い方
rails側はindex,show,create,update,destoryをjsonで返信するようにします。 アプリ側では適当にmodelクラスを作って、motion-rails-modelを継承し、
attr_accessorとattributes_updateに項目を追加します。
class Entries < RM::Model
  attr_accessor :title, :description
  
  def attributes_update(json)
    @title          = json['title']
    @description    = json['description']
    super
  end
end
利用するときは、まずurlを設定します。
class AppDelegate
  def application(application, didFinishLaunchingWithOptions:launchOptions)
    RM::Model.set_url("http://localhost:3000/")

    @window = UIWindow.alloc.initWithFrame(UIScreen.mainScreen.bounds)
    @window.rootViewController = RootViewController.alloc.init
    @window.makeKeyAndVisible
    true
  end
end
あとは、
Entries.all do |x|
  if x
    p x
  else
    p "error"
  end
end
で、
http://localhost:3000/entriesにアクセスし
Entries.find(1) do |x|
  if x
    p x
  else
    p "error"
  end
end
で、
http://localhost:3000/entries/1
にアクセス。
@entry = Entries.new
@entry.title = "title"
@entry.description = "description"
@entry.save do |x|
  if x
    p x
  else
    p "error"
  end
end
で、
http://localhost:3000/entries
にpost。
@entry.title = "title2"
@entry.save do |x|
  if x
    p x
  else
    p "error"
  end
end
で、http://localhost:3000/entries/1
にput。
@entry.destory do |x|
  if x
    p x
  else
    p "error"
  end
end
で、http://localhost:3000/entries/1
にdeleteでアクセスするようになってます。

TODOとして
attributes_updateを設定しなくてもいいようにする。 エラー処理をもう少ししやすく。

2012年9月26日水曜日

現在のフォルダにあるファイルをリネームする


files = Dir::entries(Dir::pwd)

files.each do |f|
File.rename(f, f.sub(/aaa-/, '')) if f =~ /aaa-/
end

csvを読み込んで、別のcsvの項目のうち2つ一致した項目を表示する

# -*- coding: utf-8 -*-
require 'csv'

a_csvs=[]
b_csvs=[]
succsess = []
failed = []
a_csvs_tmp = CSV.open('a.csv', 'r')
b_csvs_tmp = CSV.open('b.csv', 'r')


a_csvs_tmp.each{|x| a_csvs << x }
b_csvs_tmp.each{|x| b_csvs << x }

a_csvs.each do |x|
a = b_csvs.select{|b| x[0] == b[0] and x[1] == b[1] }
unless a.empty?
  succsess << a
else
  failed << x
end
end

puts "見つかったモノ"
p succsess.flatten
puts "失敗したモノ"
p failed.flatten

2012年7月24日火曜日

iosとrailsでinstagramのクローンサービスを作る

iosとrailsでinstagramのクローンサービスを作る

railsと連携したネイティブアプリを作りたかったので、
instagramのクローンサービスを作ってみました。

完成度はかなり低く、多分全体実装の5%くらいのですが、
会員登録(ログイン)、登録者に紐付いたタイムラインの表示、写真(加工)、ユーザ検索、フォロー、アンフォロー
あたりまではできるようになっています。

ソースはいつものようにgithubに。
https://github.com/face-do/clonestagram

server側のコードと、client側のコードがセットではいってます。

実際に試す場合には、
画像のアップロードには、carrierwaveを使ってS3にあげているので、そのトークンを変更し、
またios側で通信先のURLをすべて変更してください。

会員登録(ログイン)の処理はserver側でdeviseを使っていて、
ios側がusernameとpasswordをjsonで送信すると、その結果をjsonで返してくれるので、
それをうけて適当に処理するようにしてます。

TODOとしては、
フィルタがしょぼいので調整する。
各種バグの修正。
ユーザ個別画面の修正。

参考:
http://wp.serpere.info/archives/2110
http://d.hatena.ne.jp/tomute/20091121/1258884514
http://d.hatena.ne.jp/sparkgene/20120422/1335075063
http://oneworld-inc.jp/blog/?p=148

iosのライブラリ
https://github.com/glassonion1/R9HTTPRequest
https://github.com/ldandersen/scifihifi-iphone/tree/master/security
http://stig.github.com/json-framework/

2012年3月23日金曜日

自分用のdropbox automatorが作れる!dropbox+herokuでtwitterに写真を自動投稿する方法

dropboxにファイルを上げると、自動的にfacebookやflickrに投稿するようにできるdropbox automatorというサービスがあります。
このサービスはtwitterへのphoto投稿が対応していないので、dropbox apiとherokuを使って、できるようにしてみたいと思います。
今回のファイルはgithubにあげておきました。

とりあえずgemをインストール
# vim Gemfile

下記を記述。

source "http://rubygems.org"
gem "clockwork"
gem 'dropbox-sdk'
gem 'twitter'


# bundle install

次にdropbox apiを使えるようにこちらから登録してください。
ログイン後、アプリ登録画面で「create an App」をクリック
適当にアプリ名と説明を入れて、「App folder」の方をチェックして、登録。

アプリの詳細画面でApp keyとApp secretを確認してください。
apiを使うためには、コレ以外にrequest_tokenとaccess_tokenが必要なのですが、
そのためにはブラウザでフォルダへの許可を取る必要があります。

なので、まずはその取得用のスクリプトを作成。


# -*- coding: utf-8 -*-
require 'dropbox_sdk'

APP_KEY = 'INSERT-APP-KEY-HERE'
APP_SECRET = 'INSERT-APP-SECRET-HERE'
ACCESS_TYPE = :app_folder
session = DropboxSession.new(APP_KEY, APP_SECRET)

request_token = session.get_request_token

authorize_url = session.get_authorize_url
puts "AUTHORIZING", authorize_url
gets

access_token = session.get_access_token

p "request_token:", request_token
p "access_token:", access_token



上のスクリプトを一端起動してください。
途中urlが表示されると思いますので、それをブラウザに入力。
認証完了後、ターミナルに戻り、エンターを押してください。

request_tokenとaccess_tokenのkeyとsecretがそれぞれ表示されると思いますので、メモしておきます。

次に実際に動かすスクリプトの作成。

herokuのcedar stackでは、webの代わりにスクリプトを動かしっぱなしにできるので、それを利用します。
具体的には、clockworkというライブラリを使い、一定ごとにdropboxのapiを叩き、新規ファイルが登録されていれば、それを投稿という流れ。

設定用のファイルを作成。
# vim Procfile

cron: bundle exec clockwork clock.rb


コードは下記の通り。

# -*- coding: utf-8 -*-
require 'clockwork'
require 'twitter'
require 'dropbox_sdk'
include Clockwork
@time = Time.now

Twitter.configure do |config|
config.consumer_key = 'XXXXXXXXXXXXXXXX'
config.consumer_secret = 'XXXXXXXXXXXXXXXX'
config.oauth_token = 'XXXXXXXXXXXXXXXX'
config.oauth_token_secret = 'XXXXXXXXXXXXXXXX'
end

APP_KEY = 'INSERT-APP-KEY-HERE'
APP_SECRET = 'INSERT-APP-SECRET-HERE'
ACCESS_TYPE = :app_folder
session = DropboxSession.new(APP_KEY, APP_SECRET)
session.set_request_token('REQUEST_TOKEN_KEY', 'REQUEST_TOKEN_SECRET')
session.set_access_token('ACCESS_TOKEN_KEY', 'ACCESS_TOKEN_SECRET')
client = DropboxClient.new(session, ACCESS_TYPE)

handler do |job|
filedata = nil
file_metadata = client.metadata('/')
filedata = file_metadata["contents"].map { |x| x if Time.parse( x["modified"]) > @time }
filedata.each do |f|
unless f == nil
file = client.get_file(f["path"])
filename = Time.now.to_i.to_s
File.open("/tmp/" + filename, "w") {|f| f.write file}
tmp = File.open("/tmp/" + filename, "rb")
p tmp
Twitter.update_with_media(@time.strftime("%F %H:%M"), tmp)
end
end
@time = Time.now
end

every(1.minutes, 'check')


apiのキーはそれぞれ先程メモったものなどを入力してください。

あとはherokuに上げるだけ。
# git init
# git add .
# git commit -m 'first commit'

# heroku create --stack cedar
# git push heroku master
# heroku scale cron=1

ちゃんと動いているかどうか確認します。
# heroku ps
起動しているプロセスがcron.1だけのはず。

# heroku logs --tail
Triggering checkみたいなログがでているはずです。

あとは、dropboxに先程登録したアプリ名のファイルができているので、画像をコピーしてみます。(なぜかgitがうまく投稿できませんでした。。。)
twitterに自動投稿されていれば成功です。

ファイル名や種類、アップするフォルダで処理を変えるようにすれば、自分なりの「dropbox automator」がつくれます。

2012年3月19日月曜日

iphoneで撮った写真をherokuにアップロードする(iosアプリ)

herokuで無料のimage uploaderを作るの続き。

mongolabを使って、写真のアップローダを作れるようにしましたが、せっかくなのでiphoneから写真を直接あげられるようにしておきます。
写真をアップロードはhttpを使ってサーバにpostします。

postするサンプルを作っている人がいたので、これをベースにして使うことに。
https://github.com/tochi/HTTPFileUploadSample

HTTPFileUploadSampleViewController.hを以下のように修正

@interface HTTPFileUploadSampleViewController : UIViewController
{
IBOutlet UITextField *codeTextField;
IBOutlet UIImageView *_imageView;
}

- (IBAction)postButtonClicked:(id)sender;
- (IBAction)showCameraSheet:(id)sender;
@property (retain, nonatomic) IBOutlet UITextField *codeTextField;


HTTPFileUploadSampleViewController.mを以下のように修正
httpFileUpload postWithUriは自分のherokuのURLに修正してください。


#import "HTTPFileUploadSampleViewController.h"

@implementation HTTPFileUploadSampleViewController
@synthesize codeTextField = _codeTextField;

- (void)dealloc
{
[_imageView release];
[_codeTextField release];
[super dealloc];
}

- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
}

#pragma mark - View lifecycle
- (void)viewDidLoad
{
[super viewDidLoad];
_codeTextField.returnKeyType = UIReturnKeyDone;
_codeTextField.delegate = self;
}

- (BOOL)textFieldShouldReturn:(UITextField *)textField {
[_codeTextField resignFirstResponder];
return YES;
}

- (void)viewDidUnload
{
[_imageView release];
_imageView = nil;
[codeTextField release];
codeTextField = nil;
[self setCodeTextField:nil];
[super viewDidUnload];
}

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
return (interfaceOrientation == UIInterfaceOrientationPortrait);
}

- (IBAction)postButtonClicked:(id)sender
{
// Get image data.
//UIImage *image1 = [UIImage imageNamed:@"Icon.png"];

// File upload.
HTTPFileUpload *httpFileUpload = [[HTTPFileUpload alloc] init];
httpFileUpload.delegate = self;
[httpFileUpload setPostString:self.codeTextField.text withPostName:@"name"];
[httpFileUpload setPostImage:_imageView.image withPostName:@"photo" fileName:@"Icon.png"];
[httpFileUpload postWithUri:@"http://XXXX.herokuapp.com/users/photo.json"];
[httpFileUpload release], httpFileUpload = nil;
}

- (IBAction)showCameraSheet:(id)sender {
// アクションシートを作る
UIActionSheet* sheet;
sheet = [[UIActionSheet alloc]
initWithTitle:@"Select Soruce Type"
delegate:self
cancelButtonTitle:@"Cancel"
destructiveButtonTitle:nil
otherButtonTitles:@"Photo Library", @"Camera", @"Saved Photos", nil];
[sheet autorelease];

// アクションシートを表示する
[sheet showInView:self.view];
}

- (void)actionSheet:(UIActionSheet*)actionSheet
clickedButtonAtIndex:(NSInteger)buttonIndex
{
// ボタンインデックスをチェックする
if (buttonIndex >= 3) {
return;
}

// ソースタイプを決定する
UIImagePickerControllerSourceType sourceType = 0;
switch (buttonIndex) {
case 0: {
sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
break;
}
case 1: {
sourceType = UIImagePickerControllerSourceTypeCamera;
break;
}
case 2: {
sourceType = UIImagePickerControllerSourceTypeSavedPhotosAlbum;
break;
}
}

// 使用可能かどうかチェックする
if (![UIImagePickerController isSourceTypeAvailable:sourceType]) {
return;
}

// イメージピッカーを作る
UIImagePickerController* imagePicker;
imagePicker = [[UIImagePickerController alloc] init];
[imagePicker autorelease];
imagePicker.sourceType = sourceType;
imagePicker.allowsImageEditing = YES;
imagePicker.delegate = self;

// イメージピッカーを表示する
[self presentModalViewController:imagePicker animated:YES];
}

- (void)imagePickerController:(UIImagePickerController*)picker
didFinishPickingImage:(UIImage*)image
editingInfo:(NSDictionary*)editingInfo
{
// イメージピッカーを隠す
[self dismissModalViewControllerAnimated:YES];
// オリジナル画像を取得する
UIImage* originalImage;
originalImage = [editingInfo objectForKey:UIImagePickerControllerOriginalImage];

// グラフィックスコンテキストを作る
CGSize size = { 300, 400 };
UIGraphicsBeginImageContext(size);

// 画像を縮小して描画する
CGRect rect;
rect.origin = CGPointZero;
rect.size = size;
[originalImage drawInRect:rect];

// 描画した画像を取得する
UIImage* shrinkedImage;
shrinkedImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

// 画像を表示する
_imageView.image = shrinkedImage;
}

- (void)imagePickerControllerDidCancel:(UIImagePickerController*)picker
{
// イメージピッカーを隠す
[self dismissModalViewControllerAnimated:YES];
}

- (void)httpFileUpload:(NSURLConnection *)connection
didFailWithError:(NSError *)error
{
NSLog(@"%@", error);
}

- (void)httpFileUploadDidFinishLoading:(NSURLConnection *)connection
result:(NSString *)result
{
NSLog(@"%@", result);
UIAlertView *alert = [[UIAlertView alloc]
initWithTitle:@""
message:@"投稿完了しました。"
delegate:nil
cancelButtonTitle:@"OK"
otherButtonTitles:nil, nil];
[alert show];
[alert release];
}
@end


次にここを参考にxibを作成。アクションシートを追加してください。(コードは上のものにすでに入ってます。)
http://news.mynavi.jp/column/iphone/001/index.html
同じような感じで、textfieldを追加してください。(codeTextField)

次にrails側。
こちらをベースに修正します。
https://github.com/face-do/heroku-image-uploader
UsersController.rbを以下の用に修正。

def photo
@user = User.new(:name => params[:name], :photo => params[:photo] )

respond_to do |format|
if @user.save
format.html { redirect_to @user, notice: 'User was successfully created.' }
format.json { render json: @user, status: :created, location: @user }
else
format.html { render action: "new" }
format.json { render json: @user.errors, status: :unprocessable_entity }
end
end
end

config/routes.rbに以下を追記
post "users/photo" => 'users#photo'

app/controllers/application_controller.rbを以下のように修正。

protect_from_forgery :except => :photo


で、herokuにあげて、iosアプリをiphoneに転送すれば、できます。
一応今回のファイルをgithubにあげておきました。
rails アプリのほう
iphoneクライアントの方

2012年3月18日日曜日

herokuで無料のimage uploaderを作る

herokuで無料のimage uploaderを作る

herokuは非常に便利ですが、read onlyなのでアップローダーを作ったりできません。
もしやろうとするとS3を使った方法が一般的のようですが、若干利用料金がかかってしまいます。
なので無料で作れる方法を考えてみました。

herokuでは画像を直接アップすることはできませんが、DBに直接保存することができます。
しかしherokuのデフォルトのものは、5MBしかありません。
そこでmongolabという、mongodbを240MBまで無料提供してくれるサービスを利用します。
なおmongolabはherokuにadd-onとして提供されているため、セットアップは簡単です。

githubにファイルをあげておいたので、参考にしてください。
https://github.com/face-do/heroku-image-uploader

まずはrailsプロジェクトを作成します。
ただしmongodbを使うため、active recordを切るようにしておきます。

# rails new uploader -O

次に、gemfileに以下を追記


gem 'mongoid', '~>2.1'
gem 'bson_ext', '~>1.3'
gem 'carrierwave'
gem 'carrierwave-mongoid', :require => 'carrierwave/mongoid'


# bundle install

で、mongodbに接続するための設定のひな形を作る。
# rails generate mongoid:config

config/mongoid.ymlのproductionを以下のように修正

production:
uri: <%= ENV['MONGOLAB_URI'] %>



carrierwaveの設定をする
#vim config/initializers/carrierwave.rb

以下を記述

CarrierWave.configure do |config|
config.storage = :grid_fs
config.grid_fs_connection = Mongoid.database
config.grid_fs_access_url = "/images"
end


アップローダのひな形を作る
# rails g scaffold page title:string
# rails g uploader photo

app/models/user.rbは以下のように


class User
include Mongoid::Document
field :title, :type => String
mount_uploader :photo, PhotoUploader
end


app/uploaders/photo_uploader.rbは以下のように修正


# encoding: utf-8
class PhotoUploader < CarrierWave::Uploader::Base
include CarrierWave::RMagick
storage :grid_fs
def store_dir
"uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
end
version :thumb do
process :resize_to_limit => [200, 200]
end
end


app/views/users/_form.html.erbの
の上に以下を追記



<%= f.label :photo %>

<%= image_tag( @user.photo_url ) if @user.photo? %>
<%= f.file_field :photo %>
<%= f.hidden_field :photo_cache %>



このままだと画像が表示されないので、画像表示用のメソッドを作る。
まずはapp/controllers/users_controller.rbに以下を追記


require 'mongo'
class UsersController < ApplicationController
def serve
gridfs_path = env["PATH_INFO"].gsub("/images/", "")
begin
gridfs_file = Mongo::GridFileSystem.new(Mongoid.database).open(gridfs_path, 'r')
self.response_body = gridfs_file.read
self.content_type = gridfs_file.content_type
rescue
self.status = :file_not_found
self.content_type = 'text/plain'
self.response_body = ''
end
end


config/routes.rbに以下を追記
match "/images/uploads/*path" => "users#serve"

これでアプリ側の設定は終了。git でcommitしておきます。
# git add .
# git commit -m 'first commit'

次にheroku側の設定をします。
#heroku create --stack cedar
#heroku addons:add mongolab:starter

あとはherokuにdeployするだけ
#git push heroku master

2012年2月11日土曜日

facebookアプリでsessionが維持できない

facebookでiframeを使ったアプリをつくろうとした時に、session内に情報を入れられない時がある。


protect_from_forgery :except => :index


そのときは、sessionを使いたいコントローラーだけ、上記のように除外する。

2011年12月28日水曜日

nginxとunicorn上でrails3.1アプリを動かす。ついでにcapistranoを使ってデプロイ

かなりハマったので、メモ。

まずアプリ側。
Gemfileに以下を追加。

group :deployment do
gem 'capistrano'
gem 'capistrano_colors'
end

gem 'therubyracer'
gem 'unicorn'

で、bundle install。

次にcapistrano設定ファイルを作成
# capify .

#config/deploy.rb

# capistranoの出力がカラーになる
require 'capistrano_colors'

# cap deploy時に自動で bundle install が実行される
require "bundler/capistrano"

#rvm setting
set :rvm_type, :user
$:.unshift(File.expand_path('./lib', ENV['rvm_path']))
require "rvm/capistrano"
set :rvm_ruby_string, '1.9.2@rails3.1' #ここにgemset名を入力

set :user, "サーバーのユーザー名"
set :port, 22 #サーバーのポート番号
set :use_sudo, false #sudoをするかどうか。
ssh_options[:forward_agent] = true

#repository setting
set :application, "sample" #アプリケーション名
set :scm, :git #gitを使う
set :repository, "ssh://user@example.com:22/home/user/git/sample.git"
set :deploy_to, "/home/user/sample/"
default_environment["LD_LIBRARY_PATH"] = "$LD_LIBRARY_PATH:/usr/local/lib"



# Or: `accurev`, `bzr`, `cvs`, `darcs`, `git`, `mercurial`, `perforce`, `subversion` or `none`

role :web, "example.com" # Your HTTP server, Apache/etc
role :app, "example.com" # This may be the same as your `Web` server
role :db, "example.com", :primary => true # This is where Rails migrations will run

#sqlite3を使う場合、dbをshareフォルダに入れる。
task :db_setup, :roles => [:db] do
run "mkdir -p -m 775 #{shared_path}/db"
end

namespace :deploy do
task :start, :roles => :app do
run "cd #{current_path}; bundle exec unicorn_rails -c config/unicorn.rb -E production -D"
end
task :restart, :roles => :app do
if File.exist? "/tmp/unicorn.pid"
run "kill -s USR2 `cat /tmp/unicorn.pid`"
end
end
task :stop, :roles => :app do
run "kill -s QUIT `cat /tmp/unicorn.pid`"
end
end

namespace :assets do
task :precompile, :roles => :web do
run "cd #{current_path} && RAILS_ENV=production bundle exec rake assets:precompile"
end
task :cleanup, :roles => :web do
run "cd #{current_path} && RAILS_ENV=production bundle exec rake assets:clean"
end
end
after :deploy, "assets:precompile" #デプロイ後にassets compileをするように。
set :normalize_asset_timestamps, false #rails3.1対策


次にunicornの設定
#config/unicorn.rb

application = 'sample'

# ワーカーの数
worker_processes 2

# ソケット
listen "/tmp/unicorn.sock"
pid "/tmp/unicorn.pid"

# ログ
if ENV['RAILS_ENV'] == 'production'
shared_path = "/home/user/#{application}/shared"
stderr_path = "#{shared_path}/log/unicorn.stderr.log"
stdout_path = "#{shared_path}/log/unicorn.stdout.log"
end

# ダウンタイムなくす
preload_app true

before_fork do |server, worker|
if defined?(ActiveRecord::Base)
ActiveRecord::Base.connection.disconnect!
end
old_pid = "/tmp/unicorn.pid.oldbin"
if File.exists?(old_pid) && server.pid != old_pid
begin
Process.kill("QUIT", File.read(old_pid).to_i)
rescue Errno::ENOENT, Errno::ESRCH
end
end
end

after_fork do |server, worker|
if defined?(ActiveRecord::Base)
ActiveRecord::Base.establish_connection
end
end


environmentsのproduction内で
config.serve_static_assets = true
にする。
※こうしないとcssがロードされない。

あとdatabase.ymlのproductionを
database: ../../shared/db/production.sqlite3
にする。



次はサーバ側。
nginxをyumでインストールするために、repoに追加。
# sudo vim /etc/yum.repo.d/nginx.repo


[nginx]
name=nginx repo
baseurl=http://nginx.org/packages/rhel/$releasever/$basearch/
gpgcheck=0
enabled=1


で、インストール。
#sudo yum install nginx

nginxの設定ファイルはこんな感じ。
#sudo vim /etc/nginx/conf.d/sample.conf


upstream unicorn {
server unix:/tmp/unicorn.sock;
}

server {
listen 80;
server_name example.com;

root /home/user/sample/current/public;
error_log /home/user/sample/current/log/error.log;

location / {
if (-f $request_filename) { break; }
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_pass http://unicorn;
}

}


あとは、capistranoでデプロイして、nginxを起動するだけ。必要があればchkconfigで自動起動設定をする。
capistranoでデプロイするときは、
#cap deploy:setup
#cap db_setup
#cap deploy:cold
の順で。


参考:
http://d.hatena.ne.jp/ntaku/20111112/1321093327
http://aerial.st/archive/2011/06/16/nginx-unicorn-rails-on-mac/

2011年12月8日木曜日

jQuery UIを使って、オートコンプリート機能を実装してみる@rails3.1

facebookなどの検索BOXで途中まで入力すれば、結果の一部が出てくるあれです。

rails3.1からjQueryがデフォルトになったのですが、jQuery UIはまだ有効になっていないので有効にする。

application.jsの//= require jqueryの下あたりに以下を追記
//= require jquery-ui

で、ビューに検索BOXをJSで作る。

<input type="text" id="textbox1"></input>
<script type="text/javascript" charset="utf-8">
$(function(){
$("#textbox1").autocomplete({
source : "/auto_complete"
});
})
</script>



sourceの部分をルーティングで設定する。
get 'auto_complete' => 'api#auto_complete'

コントローラーはこんな感じで


def auto_complete
if request.xhr?
data = Array.new
data_items = Data.where('name like ?', "%#{params[:term]}%")
data_items.each do |f|
data << f.name
end
return render data
end
end



検索BOXで入力したキーワードは都度、sourceで設定したアドレスに対して、params[:term]で送られます。
で、結果をjsonで返せば動作するのですが、そのまま返すと不必要なデータもそのまま送ってしまうので、必要なカラムのデータだけ送るよう、配列を作りなおしてます。
もう少しうまいやり方もありそうですが。。。
あと、request.xhr?と指定すると、ajax以外からのアクセスを弾いてくれるようです。

ちなみにCSSを使う場合には、assetsのcssフォルダ下にjquery-ui-1.8.16.custom.cssをおいて、jsを呼び出すのと同じように
*= require_jquery-ui-1.8.16.custom
をかけばOK
画像は、assets/image/jquery-uiの下にimageフォルダごとコピーすればOK。

参考:http://d.hatena.ne.jp/naoty_k/20110925/1316969446
http://blog.livedoor.jp/satoyansoft/archives/65458957.html

Railsでapiっぽいのを作って、iOSアプリと連携してみる

iOS(iphone)アプリで位置情報を取得、それをrailsアプリに送信してDBに登録するようにしてみます。
なおiOSについてはまだまだ勉強不足のため、「まるごと学ぶiPhoneアプリ制作教室」内に記載してあったコードを参考にしています。
またrailsアプリ内には位置情報の取得まで作っておきますが、iOS部分では省きます。

まずはiOSアプリの方。
ViewController.m

#import "ViewController.h"
#import "Location.h"
#import "JSON.h"

@implementation ViewController
@synthesize codeTextField;

- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Release any cached data, images, etc that aren't in use.
}

#pragma mark - View lifecycle

- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
}

- (void)viewDidUnload
{
[self setCodeTextField:nil];
[super viewDidUnload];
// Release any retained subviews of the main view.
// e.g. self.myOutlet = nil;
}

- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
}

- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
}

- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
}

- (void)viewDidDisappear:(BOOL)animated
{
[super viewDidDisappear:animated];
}

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
// Return YES for supported orientations
return (interfaceOrientation != UIInterfaceOrientationPortraitUpsideDown);
}

- (NSString *)getCurrentDate {
NSDateFormatter *dateFormatter = [[[NSDateFormatter alloc] init] autorelease];
NSString *dateFormat = @"yyyy/MM/dd-mm:ss:SSS";
[dateFormatter setTimeZone:[NSTimeZone timeZoneWithAbbreviation:@"JST"]];
[dateFormatter setDateFormat:dateFormat];
NSString *date = [dateFormatter stringFromDate:[NSDate date]];
return date;
}

- (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation {


Location *myLocation = [[[Location alloc] init] autorelease];
myLocation.latitude = [NSString stringWithFormat:@"%f", newLocation.coordinate.latitude];
myLocation.longitude = [NSString stringWithFormat:@"%f", newLocation.coordinate.longitude];
myLocation.time = [self getCurrentDate];
myLocation.identificationCode = [codeTextField text];

NSDictionary *locationDictionary = [NSDictionary dictionaryWithObjectsAndKeys:
myLocation.latitude, @"latitude",
myLocation.longitude, @"longitude",
myLocation.time, @"time",
myLocation.identificationCode, @"identificationCode",
nil];
NSString* jsonString = [locationDictionary JSONRepresentation];
NSLog(@"JSON: %@", jsonString);

NSURL *serviceURL = [NSURL URLWithString:@"http://0.0.0.0:3000/location.json"];
NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:serviceURL];
[req setHTTPMethod:@"POST"];
[req addValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
[req setHTTPBody:[jsonString dataUsingEncoding:NSUTF8StringEncoding]];

NSURLResponse *resp= nil;
NSError *error= nil;
NSData *result = [NSURLConnection sendSynchronousRequest:req returningResponse:&resp error:&error];

if (error) {
NSLog(@"error!");
} else {
NSLog(@"Result:%@", result);
}

}
- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error{
}
- (IBAction)logStartButton:(id)sender {
if (locationManager == nil) {
locationManager = [[CLLocationManager alloc] init];
}
locationManager.delegate = self;
[locationManager startUpdatingLocation];
}
- (void)dealloc {
[codeTextField release];
[super dealloc];
}
@end


ViewController.h

#import <UIKit/UIKit.h>
#import "CoreLocation/CoreLocation.h"

@interface ViewController : UIViewController <CLLocationManagerDelegate> {
@private
UITextField *codeTextField;
UIButton *logStartButton;
CLLocationManager *locationManager;
}
@property (retain, nonatomic) IBOutlet UITextField *codeTextField;
- (IBAction)logStartButton:(id)sender;

@end


Location.h

#import <Foundation/Foundation.h>

@interface Location : NSObject {
NSString *latitude;//緯度
NSString *longitude;//経度
NSString *time;//時間
NSString *identificationCode;//自分を特定するためのIDコード
}
@property (nonatomic, assign) NSString *latitude;
@property (nonatomic, assign) NSString *longitude;
@property (nonatomic, assign) NSString *time;
@property (nonatomic, assign) NSString *identificationCode;

@end


#import "Location.h"

@implementation Location
@synthesize latitude;
@synthesize longitude;
@synthesize time;
@synthesize identificationCode;

@end



Location.m

#import "Location.h"

@implementation Location
@synthesize latitude;
@synthesize longitude;
@synthesize time;
@synthesize identificationCode;

@end

CoreLocationライブラリは別途入れてください。
またここから「JSON v2.3.2 (iOS)」をダウンロードして、その中からClassesフォルダを同じプロジェクトファイル内にコピーしておいてください。
なおserviceURL = [NSURL URLWithString:@"http://0.0.0.0:3000/location.json"]のドメイン部分は自分なりに。
xibも適当にボタンとテキストフィールドを。名称は、それぞれ「logStartButton」「codeTextField」で。
わからないときは、「まるごと学ぶiPhoneアプリ制作教室」を参考にしてください。
サンプルコードがここにあったりします。

次にrailsの方。
ApiController

class ApiController < ApplicationController

def post
location = Location.new(params[:api])
respond_to do |format|
if location.save
format.json { head :ok }
else
format.json { render json: location.errors, status: :unprocessable_entity }
end
end
end

def get
@location = Location.where(:identificationCode => params[:identificationCode]).limit(5)

respond_to do |format|
format.json { render json: @location }
end
end

end

ルーティングとして以下を追加。

post 'location(.:format)' => 'api#post'
get 'location(.:format)' => 'api#get'

データベースに以下のカラムを作る
「latitude」
「longitude」
「time」
「identificationCode」
で、マイグレして起動して、iPhoneアプリを起動し、ボタンを押せば、1秒ごとにrailsのDBに位置情報とID、時間が登録されていくはずです。
ポイントは、jsonデータをpostメソッドで送信するとそれぞれのparams[:項目名]で取得できること。
それができれば簡単ではないかと。

今回簡易的にするため外のpostメソッドをそのまま受け入れましたが、セキュリティ的には問題あるので、実用にはもうちょい工夫が必要そうです。

2011年10月18日火曜日

プログラム歴9ヶ月でも1日でサイトが作れる!ハッカソンに参加する3つの理由

今年の1月後半からプログラミングを始め、はや9ヶ月。
時間をかければ簡単なWEBサービスを作れるところまできましたが、ハッカソンに参加したらなんと1日で(簡単なものとはいえ)WEBサービスを作ることができました。
作ったのはreblogramというサービスで、簡単にいうとinstagram上の写真をワンクリックでtumblrにreblogできるサイトです。
instagramのPCビューアも兼ねているので、PCでinstagramのTLを眺めつつ、気に入った写真はreblogするという使い方ができます。
(本当はスマホ用のデザインもしたかったのですが、時間が足りませんでした。。。)

tumblrのAPIとinstagramのAPIを使ったので、非常に作りが簡素化されていますが、自分でもまさか一日で作ることができるとは思いませんでした。
そこで、なぜハッカソンならたった一日でもWEBサービスを作ることができるのか、僕なりに気がついたことを3つにまとめました。

●不必要なハマリがなくなり、必要なことに集中できる
プログラミングを覚えたてのころ、特に独学で勉強している人ほど経験があると思いますが、後から見ると本当にくだらないことではまってしまうことがありますよね?
しかしハッカソンなら、まわりにそのことを経験済みの人がいる可能性が高いため、不必要なハマリがなくなります。
初心者レベルでハマる程度のことだと、言語に依存するような内容は少ないので、自分の同じ言語の人はいるのか?というのはあまり気にする必要がないと思います。
(実際今回は3人でやりましたが、PHP、Ruby、 Object-Cとそれぞれ別の言語でした。それでもお互いのハマリを解決しあえたと思います。)

●自分の持ってない知識を活用できる
今回reblogramを作るにあたりデザインにはtwitterのBootstrapを利用しました。
これがなければデザインまで一日では完成していなかったと思うほど、非常に便利なものでしたが、これは他のメンバーから教えてもらった情報です。
ハッカソンでは、問題の解決という側面だけではなく、サービスを作るまでのショートカットの方法も共有しあえるという面でも効果があったと思いました。

●集中するしかなくなる
これがハッカソンに参加する意味、効果として一番大きいかもしれません。
サボりたくても周りではひたすらコードを書いている人しかいないので、自分もサボるわけにはいきませんw
食事と一緒に海に休憩しにいった時以外は、ひたすらコードを書くか、詰まったところの相談をしていました。
この集中度がなければあったからこその成果だったと思います。

●おまけ
ただコードをひたすら書くだけでしたが、非常に楽しい体験でした。
今回は一旦都内で集合したあと、会場の民宿まで移動したのですが、その移動時間に作るサービスの相談や情報交換などを行えて、移動時間すら有益な時間だったと思います。
お試しとして3人くらいで開催したのですが、いい経験だったのでまたやりたいという話をメンバーの人としました。
次はもう少しメンバーを増やしてやりたいと思いますので、興味ある方は責任者の@masumikawasakiまでご連絡ください。
(僕じゃないですけど・・・)