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までご連絡ください。
(僕じゃないですけど・・・)

2011年9月5日月曜日

Mac ports からhomebrewへ移行

homebrewはmac portsと競合するらしいので、先にmac portsをアンインストールする。

へんなところでつまずくと嫌なので、念のためmac portsでインストールしたものをテキストファイルで保存
#port installed

アンインストール
# sudo port -f uninstall installed
# sudo rm -rf /opt/local/


homebrewのインストール
#ruby -e "$(curl -fsSL https://gist.github.com/raw/323731/install_homebrew.rb)"

homebrewはgitで更新するらしいのでgitをインストール
#brew install git

homebrewのアップデート
#brew update

あとは個別に必要なものをインストール

2011年9月4日日曜日

railsでpaypalを使う方法

rails2.3.5の場合です。
ライブラリが用意してあるので、基本的に簡単なのですが、ちょっとはまったので。

まずはアカウントを用意。
sandboxに登録する。
「Create a preconfigured account」で、BuyerとSellerのアカウントをひとつずつ作る。
Buyerの方には残高を入れておくことを忘れないように。

次にsandbox test siteにsellerのアカウントでログイン。
ProfileからAPI Accessで「API Username」「API Password」「Signature」を取得。
これで準備は完了です。

ライブラリのインストール。
#sudo gem install activemerchant -v 1.10
わざわざ1.10にしているのは、それ以上だとrails2.3.11以上にしか対応していないから。

#app/controller/payments_controller.rb

class PaymentsController < ApplicationController
include ActiveMerchant::Billing
def index

end

def confirm
redirect_to :action => 'index' unless params[:token]

details_response = gateway.details_for(params[:token])

if !details_response.success?
@message = details_response.message
render :action => 'error'
return
end

@address = details_response.address
end

def complete


purchase = gateway.purchase(100,
:ip => request.remote_ip,
:payer_id => params[:payer_id],
:token => params[:token]
)

if !purchase.success?
@message = purchase.message
render :action => 'error'
return
end
end


def checkout
setup_response = gateway.setup_purchase(100,
:ip => request.remote_ip,
:return_url => url_for(:action => 'confirm', :only_path => false),
:cancel_return_url => url_for(:action => 'index', :only_path => false)
)
redirect_to gateway.redirect_url_for(setup_response.token)
end

private
def gateway
@gateway ||= PaypalExpressGateway.new(
:login => 'API Username',
:password => 'API Password',
:signature => 'Signature'
)
end

end


#app/paments/index.html.erb


Purchase Sprockets


Thank you for your decision to purchase our sprockets.


Your order total is 100円



<%= link_to image_tag('https://www.paypal.com/en_US/i/btn/btn_xpressCheckout.gif'), :action => 'checkout' %>




#app/paments/confirm.html.erb

Please Confirm Your Payment Details












Name<%= @address['name'] %>
Company<%= @address['company'] %>
Address 1<%= @address['address1'] %>
Address 2<%= @address['address2'] %>
City<%= @address['city'] %>
State<%= @address['state'] %>
Country<%= @address['country'] %>
Zip<%= @address['zip'] %>


<% form_tag :action => 'complete', :token => params[:token], :payer_id => params[:PayerID] do %>
<%= submit_tag 'Complete Payment' %>
<% end %>


#app/paments/confirm.html.erb

Thank you for your purchase



#app/paments/confirm.html.erb

Error


Unfortunately an error occurred: <%= @message %>


The confirm.html.erb templates displays the customer's PayPal address and asks them if they want to complete the payment. Displaying the review page represents Integration Point 3a from the Integration guide. This is the customer's opportunity to review the details of their order, change shipping methods, etc. It looks like this:


#config/enviroment.rb
config.gem "activemerchant"
を追記

#config/enviroments/development.rb
config.after_initialize do
ActiveMerchant::Billing::Base.mode = :test
ActiveMerchant::Billing::PaypalExpressGateway.default_currency = "JPY"
end
を追記

これでpaypalのテストサイトで課金のテストができます。
本番サーバでやるなら、本番サーバ用にアカウントを用意して、
#config/enviroments/production.rb
config.after_initialize do
ActiveMerchant::Billing::Base.mode = :production
ActiveMerchant::Billing::PaypalExpressGateway.default_currency = "JPY"
end

で、行けるはず。
試してませんが。。。

テスト環境と本番環境でかなり動作が違うらしい。
日本じゃpaypalアカウント持っている人少ないから、クレジットカードの欄を優先させたいんだけど、どうやるんだろう?
本番環境なら優先されるのかな?

あと、定期的な支払い請求もできるらしいけど、それもどれかわからず・・・