2009年5月31日日曜日

Ajax + jQueryで超簡単にファイルをアップロードするためのメモ

jQueryはAJAX関連のメソッドが充実しているものの、デフォルトではファイルのアップロードをAJAXで行うことはできません。

AJAXでファイルアップロードを行うためのプラグインは数あれど AjaxFileUpload がよさげなので使ってみることにしました。

AjaxFileUpload
http://www.phpletter.com/Our-Projects/AjaxFileUpload

上のリンクのHow to use it? を参考にすれば簡単にAjaxでファイルのアップロードを行うことが可能なのですが、今回はちょっとひと工夫を。

通常は...
1. ファイルを選択
2. 「アップロードボタン(submitボタン)」をクリック
という流れでファイルのアップロードを行います。

ボタンをクリックするの面倒じゃない?という方のために、ファイルを選択した時点でアップロードを実行するようにしてみます。


HTML

<form action="/profile/icon/upload/" enctype="multipart/form-data" id="upload_form" method="post">
<input type="file" name="icon" id="icon_field"/>
</form>

<div id="icon_container">
<img src="default_icon.gif" alt="icon" />
</div>

Javascript
<script src="/javascripts/ajaxfileupload.js" type="text/javascript"></script>

<script type="text/javascirpt">
$(document).ready(function() {
$('#upload_form').submit(function(){
var $this = $(this);

$.ajaxFileUpload({
url: $this.attr('action'),
secureuri:false,
fileElementId: 'icon_field',
dataType: 'json',
success: function (obj, status) {
if (obj.success) {
$('#icon_container').find('img').attr('src', '/icon/' + obj.key);
} else if(obj.error && obj.message){
alert(obj.message);
}
$this.find('input:file').bind('change', triggerUpload);
},
error: function (data, s, e) {
alert('Sorry, an error occured.');
}
});
return false;
});

$('#icon_field').change(triggerUpload);
});

function triggerUpload() {
$('#upload_form').trigger('submit');
}
</script>

アップロードが成功したら動的に画像を変更します。

ポイントはtriggerUploadという関数を用意しておいて、Ajax成功後にsuccessの中で再バインドしているところでしょうか。

AjaxFileUploadはAjax通信時にファイルフィールド(fileElementIdで指定した要素)のIDを動的に変更するようです。
ですので、$(document).readyで最初にchangeイベントをバインドしていても、一度Ajaxリクエストを行うとイベントが効かなくなりますのでご注意を。

Serverサイド (ここではPython(Django0.96)を使用)
from app import models
from django.http import HttpResponse, Http404
from django.utils import simplejson
from google.appengine.api import images

def upload_icon(request):
res = {}
if request.method != 'POST' or not request.FILES.has_key('icon') or 'content-type' not in request.FILES['icon']:
raise Http404

main, sub = request.FILES['icon']['content-type'].split('/')
if not (main == 'image' and sub in ['jpeg', 'pjpeg', 'gif', 'png']):
res = {'error': 1, 'message': 'JPEG, PNG, GIF only.'}
else:
try:
prof = request.user.get_profile()
except:
prof = models.Profile()
prof.user = request.user

content = request.FILES['icon']['content']

if sub == 'jpeg' or sub == 'pjpeg':
output_encoding = images.JPEG
content_type = 'jpeg'
else:
output_encoding = images.PNG
content_type = 'png'

prof.icon = images.resize(content, 50, 50, output_encoding)
prof.content_type = content_type
prof.save()
res = {'success': 1, 'key': str(prof.key())}

response = simplejson.dumps(res, ensure_ascii=False)
return HttpResponse(response)

ここではアップロードされた画像を50×50にリサイズしてストレージ(Datastore)に保存しています。
(Google App Engineで試したので通常のDjangoにはないモジュールを使用しています)

ポイントは、JSONを返すといってもレスポンスのヘッダーをtext/javascirptなどにしてしまわないこと。
(上の例ではtext/htmlで返しています。)

ヘッダーをtext/javascirptとしてしまうと、JSONが<pre>タグで囲まれてしまいます。
(ファイルをAjaxでアップロードするためにiframeを利用しているため!?)
<pre>{'success': 1, 'key': 'abcdefg'}</pre>



専用のサンプルページは用意していませんが、LibroSpotのプロフィール編集ページが上のようになっています。
LibroSpotにログインした後、「My本ログ」の「プロフィールを編集する」リンクに行ってiconのアップロードをしてみてください。

[LibroSpot] 読んでる本/読みたい本を表示するガジェットを作りました

LibroSpot × Google Gadgetsを利用しています。

サンプルとしてこのブログに設置してみました。(右側のLIBROSPOTってやつです)


本の登録方法:

LibroSpotにログインして好きな本を登録してください。
LibroSpotはGoogleアカウントを利用してアカウントの管理を行っているので、Googleのアカウントをお持ちの方はユーザー登録の手間はありません。
Googleアカウントをお持ちでない方はこちらから作成してください。


ガジェットを取得する:

こちらのページでガジェットを取得することができます。

UsernameにあなたのLibroSpotのユーザー名を入力してください。
画像あり/なし、著者名あり/なし、ノートあり/なしを選択することができます。


LibroSpot
http://jp.librospot.com

2009年5月24日日曜日

PythonでX日前/X日後を求める方法

Pythonで日付の差を求める方法メモ。
「この記事は3日前に投稿されました」みたいなメッセージを表示する時に使えるかも。

date/datetimeオブジェクトとtimedeltaオブジェクトを利用します。

from datetime import date, datetime, timedelta

# 今日
today = date.today()

# 3日前
three_days_ago = today - timedelta(3)

# 3日後
three_days_since = today + timedelta(3)


# 現在
now = datetime.now()

# 6時間前
six_hours_ago = now - timedelta(0, 3600*6)

# 6時間後
six_hours_since = now + timedelta(0, 3600*6)

datetime オブジェクト
date オブジェクト
timedelta オブジェクト

2009年5月21日木曜日

WindowsマシンでRailsアプリを開発する時のハマりどころ - gem install でハマる

個人用メモ

アンチウィルスソフトが入ってたりするとタイムアウト(Timeout::Error)してgem installできないことがある...
ウィルスソフトを切りたくないときは手動でインストール

gem install rails --local

依存ライブラリのgemファイルをすべてかき集める必要あり!

Ruby on Rails本体
Rake
ActiveSupport
ActiveRecord
ActiveResource
ActionPack
ActionMailer
ActionWebService
mysql-win (MySQLを使う場合)
gem install hoge --local

ひとつひとつコマンドを叩く!
プラグインやその他gemを利用する時も同様に。
正気の沙汰じゃない...

2009年5月16日土曜日

Amazon Product Advertising APIの署名認証をPHPでやってみる

Pythonに続いて(Amazon Product Advertising APIの署名認証をPythonでやってみる) アマゾンのProduct Advertising APIの署名認証をPHPで行うためのメモ。
PHP5を前提としています。

require_once 'Zend/Http/Client.php';

class ProductAdvertising
{
private $_access_key;
private $_secret_key;
private $_version;
private $_tracking_id;
private $_amazon_base_url;
private $_uri;
private $_end_point;
private $_base_url;

public function __construct($access_key, $secret_key, $tracking_id="", $version='2009-03-31')
{
$this->_service = 'AWSECommerceService';
$this->_access_key_id = $access_key;
$this->_secret_key = $secret_key;
$this->_version = $version;
$this->_tracking_id = $tracking_id;
$this->_uri = "webservices.amazon.co.jp";
$this->_end_point = "/onca/xml";
$this->_base_url = 'http://' . $this->_uri . $this->_end_point;
}

/**
* アイテムを返す
* @param string $asin アマゾンのASIN
* @param array $options ItemLookupのオプション
* @return mixed
*/
public function itemLookup($asin, $options=array())
{
$options['Operation'] = "ItemLookup";
$options['ItemId'] = $asin;

$item = $this->_sendRequest($options);

if(isset($item->Items->Item)) {
return $item->Items->Item;
}
return false;
}

/**
* リクエストを送信する
* @param array $options
* @return mixed
*/
private function _sendRequest($options=array())
{
$options['Service'] = $this->_service;
$options['AWSAccessKeyId'] = $this->_access_key;
$options['AssociateTag'] = $this->_tracking_id;
$options['Version'] = $this->_version;

// タイムスタンプをセットする
$options['Timestamp'] = gmdate('c');

// オプションをアルファベット順にソートする
ksort($options);

// 署名作成
// (2009/05/17修正) $canonical_string = http_build_query($options);
// http_build_queryは内部でurlencode()を使用しているようです。
// urlencode()は空白文字をプラス(+)に変換し、そのままだとアマゾンが受け付けてくれないようです。
// ですので、rawurlencode()でURLエンコードを行うように修正しました
foreach($options as $key => $val) {
$canonical_string .= "&" . $key . "=" . rawurlencode($val);
}
$canonical_string = substr($canonical_string, 1);

$string_to_sign = "GET\n{$this->_uri}\n{$this->_end_point}\n{$canonical_string}";
$signature = base64_encode(
hash_hmac('sha256', $string_to_sign, $this->_secret_key, true)
);

$url = $this->_amazon_base_url . '?' . $canonical_string . '&Signature=' . rawurlencode($signature);
$client = new Zend_Http_Client($url);
$response = $client->request();

if($response->isError()) {
// エラー処理
}

$body = $response->getBody();
$results = simplexml_load_string( $body );

if(isset( $results->Error)) {
throw new Exception('CODE :: ' . $results->Error->Code . '\n Message :: ' . $results->Error->Message);
}

return $results;
}
}


使い方はこんな感じ
$access_key = "Your Access Key";
$secret_key = "Your Secret Key";
$asin = "ASIN";

$pa = new ProductAdvertising($access_key, $secret_key);
$item = $pa->itemLookup($asin);

2009年5月11日月曜日

gitでpushできないエラー

gitでこんなエラーが発生してpushできなくなった。

git push
To git@github.com:user/repo.git
! [rejected] master-> master(non-fast forward)
error: failed to push some refs to 'git@github.com:user/repo.git'

git pull したら一発解決。
あらら。。。

2009年5月10日日曜日

Amazon Product Advertising APIの署名認証をPythonでやってみる

AmazonアソシエイトWebサービスの名称が新しくProduct Advertising APIに変わるみたいです。

それだけなら、ふぅんという話なのですが、合わせて署名認証のお知らせがありました。

Amazon アソシエイト Web サービスの名称変更および署名認証についてのお知らせ

署名認証というのは正直初めて耳にしましたが、要するに自分とamazonだけが知っているパスワード(Secret Key)を利用して他人が不正利用できないようにすること、と理解しました。
あぁ、そういえばAmazonWebサービスに登録した時にSecret Keyというのがあったなと思い出しました。


Product Advertising APIを利用しているWebアプリはいくつかあるので、とりあえずPythonで作ったものから手をつけることに。


import urllib, urllib2, hmac, hashlib, base64
from datetime import datetime

class ProductAdvertising:

def __init__(self, access_key_id, secret_key, associate_tag=None, version='2008-08-19'):
self.access_key_id = access_key_id
self.secret_key = secret_key
self.associate_tag = associate_tag
self.version = version
self.uri = 'webservices.amazon.co.jp'
self.end_point = '/onca/xml'

def item_lookup(self, asin, options={}):
options['Operation'] = 'ItemLookup'
options['ItemId'] = asin
return self.send_request(options)

def send_request(self, options):
options['Service'] = 'AWSECommerceService'
options['AWSAccessKeyId'] = self.access_key_id
options['Version'] = self.version

# Timestampをセットしないとエラーになる。
# タイムスタンプはGMT(≒UTC)
options['Timestamp'] = datetime.utcnow().isoformat()

if self.associate_tag:
options['AssociateTag'] = self.associate_tag

# パラメーターをソートしてURLエンコード
# (修正 2009/05/17) payload = urllib.urlencode(sorted(options.items()))
payload = ""
for v in sorted(options.items()):
payload += '&%s=%s' % (v[0], urllib.quote(str(v[1])))
payload = payload[1:]

# 署名用の文字列
strings = ['GET', self.uri, self.end_point, payload]

# 署名の作成
digest = hmac.new(self.secret_key, '\n'.join(strings), hashlib.sha256).digest()
signature = base64.b64encode(digest)

url = "http://%s%s?%s&Signature=%s" % (self.uri, self.end_point, payload, urllib.quote_plus(signature))
return urllib2.urlopen(url).read()


使い方はこんな感じ
pa = ProductAdvertising(access_key_id='your_access_key_id', secret_key='your_secret_key')
item = pa.item_lookup('ASIN')


基本的には Product Advertising API: Example REST Requests ←こちらの例を参考にすれば問題ないはず。


ちなみに自分のSecret KeyはAmazon Web ServicesのYour AccountのAccess Identifiersで確認することができます。

PHPでもやってみました
Amazon Product Advertising APIの署名認証をPHPでやってみる


2009/05/17 修正

URLエンコード部分を下記のようにやっていましたが
payload = urllib.urlencode(sorted(options.items()))

urllib.urlencodeは内部でquote_plus使用しているようです。
quote_plusは空白文字をプラス(+)に変換します。
hmacの文字列にプラスが含まれているとアマゾンから
The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details.

という、署名が間違っているぞというエラーが返ってきますのでurllib.quoteでURLエンコードをおこなうように修正しました。

payload = ""
for v in sorted(options.items()):
payload += '&%s=%s' % (v[0], urllib.quote(str(v[1])))
payload = payload[1:]