FlaskでDrag and Drop APIとXHR2で画像をポストする

HTMLガイドブックはphpなのでFlaskでやってみた。この本は良書ですね、オススメです(最後のほうにNode.jsのサンプルも載ってるし)。

ProductName 徹底解説 HTML5 APIガイドブック コミュニケーション系API編
小松 健作
秀和システム / 2730円 ( 2010-12 )


青い囲みの中にドラッグドロップするとアップロードされるが、ドラッグドロップAPIのためにスペースを用意するっていうのはなんかいまいちだなぁ。もうちょっと洗練されたドラッグドロップのインターフェースはないものかね。

file_upload

ディレクトリ構成

$ tree
.
├── static
│   └── uploads
├── templates
│   └── index.html
└── uploader.py

サーバー側

from flask import Flask, request, url_for, render_template, make_response
import os

DEBUG = True
SECRET_KEY = 'development key'
UPLOAD_FOLDER = 'static/uploads'
ALLOWED_EXTENSIONS = set(['txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif'])

app = Flask(__name__)
app.config.from_object(__name__)

def allowed_file(filename):
    return '.' in filename and \
           filename.rsplit('.', 1)[1] in ALLOWED_EXTENSIONS

@app.route('/')
def show_index():
    return render_template('index.html')

@app.route('/upload', methods=['POST'])
def do_upload():
    file = request.files['xhr2upload']
    if file and allowed_file(file.filename):
        filename = file.filename
        file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
    response = make_response(url_for('static', filename='uploads/'+filename, _external=True))
    response.headers['Access-Control-Allow-Origin'] = '*'
    return response

if __name__ == '__main__':
    app.run()

クライアント側(templates/index.html)

<!doctype html>
<html>
  <head>
  <meta charset="utf-8">
  <title>File Uploader</title>
  <script src="//ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.js"></script>
  <style>
  #dropbox {
    width: 500px;
    height: 200px;
    border: 1px solid blue;
    background: #eee;
  }
  #urllists {
    margin-top: 30px;
    width: 400px;
    height: 300px;
    overflow: auto;
    floar: left;
    boader: 1px solid blue;
  }
  #currentimage {
    margin-left: 420px;
    margin-top: 30px;
  }
  </style>
  </head>
<body>
<h1>File Uploader</h1>
<div id=dropbox></div>
<div id=urllists></div>
<div id=currentimage></div>
<script>
var DnDUploader = function (base_id) {
  if(typeof(base_id) != "string" || base_id.length == 0 || document.getElementById(base_id) == null)
    return false;

  var __body = document.getElementsByTagName('body')[0];
  var parent = document.getElementById(base_id);
  __body.addEventListener("drop", function(e){e.stopPropagation();e.preventDefault();}, false);
  __body.addEventListener("dragenter", function(e){e.stopPropagation();e.preventDefault();}, false);
  __body.addEventListener("dragover", function(e){e.stopPropagation();e.preventDefault();}, false);
  parent.addEventListener("drop", function(e){e.stopPropagation();e.preventDefault();_handleDrop(e);}, false);
  parent.addEventListener("dragenter", function(e){e.stopPropagation();e.preventDefault();}, false);
  parent.addEventListener("dragover", function(e){e.stopPropagation();e.preventDefault();}, false);

  var _handleDrop = function(e) {
    var x = e.layerX, y = e.layerY;
    var dt = e.dataTransfer, files = dt.files, count = files.length;

    var types = [
            'image/png',
            'image/gif',
            'image/jpeg'
    ];

    for (var i=0; i < count; i++) {
    if (files[i].fileSize < 1048576) {
        var file = files[i];
        var type = file.type;
        var filename = file.fileName;

        if($.inArray(file.type, types) == -1) {
        alert(file.type + 'はサポート外です。');
        continue;
        }

        var reader = new FileReader();
        reader.readAsDataURL(file);
        _upload(file);

        reader.onload = function(e) {
        var fileData = e.target.result;
        _drawImage(x, y, fileData);
        }
    } else {
        alert('ファイルが大きすぎます');
    }
    }
  };

    var _drawImage = function(x, y, file) {
    var imgElement = document.createElement('img');
    imgElement.src = file;
    imgElement.style.position = 'absolute';
    imgElement.style.display = 'none';
    parent.appendChild(imgElement);

    setTimeout(function(e) {
        var o_w = imgElement.width;
        var o_h = imgElement.height;
        imgElement.width = o_w > 100 ? 100 : o_w;
        imgElement.height = parseInt( o_h * imgElement.width / o_w);

        var w = imgElement.width;
        var h = imgElement.height;
        imgElement.style.left = (x-w / 2)+'px';
        imgElement.style.top = (y-h / 2)+'px';
        imgElement.style.display = 'block';
    },1);
    };

    var _upload = function(file) {
    var fd = new FormData();
    fd.append("xhr2upload", file);
    var xhr = new XMLHttpRequest()
    xhr.open("POST", "http://www.kzfmix.com:5000/upload");
    xhr.send(fd);

    xhr.onload = function(e) {
        var url = e.target.responseText;
        $('#urllists').prepend('<p><a href="' + url + '">'+url+'</a></p>');
        $('#currentimage').html('<img src="' +url+ '">');
    }
    }
}

DnDUploader('dropbox');
</script>
</body>
</html>

FlaskでXHR2を試す

XHR2はJSONPと比べてどんなメリットがあるんだろうか?

JSONPで行っている事は、外部のJavascriptを読み込んでいることに他ならず、不用意に利用することは大変危険な行為です。

ProductName 徹底解説 HTML5 APIガイドブック コミュニケーション系API編
小松 健作
秀和システム / 2730円 ( 2010-12 )


まぁ、JSONPはハック色が強いですからね。

というわけで、クロスドメイン間で通信ができるというXHR2を試してみた。

サーバーのコード

ログ見てわかったんだがPOSTメソッドの時はOPTIONSで問い合わせないっぽい。なのでコメントアウトしても動いた。Access-Control-Allow-Originは必須で、コレがないと動かない。

from flask import Flask, make_response, request

app = Flask(__name__)
app.debug = True

#@app.route('/events', methods=['OPTIONS'])
#def view_events():
#   response = make_response()
#   response.headers['Access-Control-Allow-Origin'] = '*'
#   return response

@app.route('/events', methods=['POST'])
def show_events():
   u = request.form['username']
   p = request.form['password']
   response = make_response("user: %s, pass: %s" % (u, p))
   response.headers['Access-Control-Allow-Origin'] = '*'
   return response

if __name__ == '__main__':
   app.run(host='www.kzfmix.com')

クライアントのコード

普通にXMLHttpRequestをnewしてPOSTメソッドでsendする

<!DOCTYPE html>
<html>
<head>
<title>xhr2 test</title>
</head>
<body>
<script>
document.addEventListener("DOMContentLoaded", function(){
 var formData = new FormData();
 formData.append('username', 'myuser');
 formData.append('password', 'mypass');

 var xhr = new XMLHttpRequest();
 xhr.open("POST", "http://www.kzfmix.com:5000/events");
 xhr.send(formData);
 xhr.onerror = function(e) {
   console.log("ERROR");
   console.log(e);
 }

 xhr.onload = function(e) {
   console.log("LOAD");
   console.log(e);
   console.log(xhr.status);
   console.log(xhr.statusText);
   console.log(xhr.responseText);
   alert(xhr.responseText);
 }
}, false);
</script>
</body>
</html>

実行結果

xhr2_test

異なるドメイン(オリジン)間でデータの受け渡しが出来てる

$ python xhr2.py 
 * Running on http://www.kzfmix.com:5000/
 * Restarting with reloader...
124.41.xx.xxx - - [26/Jul/2011 19:23:51] "POST /events HTTP/1.1" 200 -
124.41.xx.xxx - - [26/Jul/2011 19:24:04] "POST /events HTTP/1.1" 200 -

本譲ります的なサイトをGAE+Flaskで作ってみた

昨日作ったものだけど、成果発表をしていないのでこのエントリでしてみたいと思う。

こんな感じのサイトで、認証はTwitterのOAuth、本の管理はbooklogの本棚APIを使ってます。

譲れる本の一覧を表示して、欲しい本があったらreplyしてもらって、手渡しで渡すと。

そんだけ。

まぁ、譲るぜオーラを漂わせるわけではなくて、あくまでイベントのついでに、物々交換的な機能があれば便利なんじゃないかと常々考えてたのでShizuDevつくる会で作ってみたと。

それからGAEも触りたかったしね。

xooqってどっからつけたかというと、はじめに本の交換だからbooxって名づけようと思っていたのだけど、やはりいありがちすぎて既に存在していたので、ひっくり返したらまぁOKだったという(一意性を担保するにはひっくり返せという教えが役に立った)。

前回ちょっと書いて放置してたら半年ほど経過してしまったが、今回みんなで集まったおかげで集中して作業できてよかった。

以下、自分で考えたオススメポイント。

住所バレしない

郵送だと住所書かないといけないのでちょっと嫌ですよね。なので会って渡す、コレ基本。イベントに行くついでにちょっと渡してくるというお手軽感。

Androidのbooklogアプリ便利

読み終わったらAndroidのアプリでスキャンして既読管理しているので、ついでにもう読まないであろう本は「交換してもいい本」カテゴリに入れてしまう、そんだけでOK。パソコン開いて作業しなくてよいので楽ちんですね。

ATNDでどのイベントに参加するかわかるようになってる

とりあえず表示するようにしただけ。やっつけ仕事っぽいがそのうちどうにかする。

Flask + GAE + Twitter OAuth

18日にやろっかなーと思っていたのだけど、今日時間があったのでOAuth認証を実装してみた。

ProductName プログラミング Google App Engine
Dan Sanderson
オライリージャパン / 3570円 ( 2011-01-24 )


というわけで、18日はなにしようかなぁ。jQMobileで血圧管理システムをつくってみようか、中断してる文献管理システムの構築再開しようか。

RESTfulでお悩み中

Flaskにはjsonifyって関数があるから、いま作ってるサービスをRESTにしようと思ったんだけど細かいところでうまい実装が思いつかなかったり、そもそもきちんと理解してないことも発覚してRESTful Webサービスを読み直している。

ProductName RESTful Webサービス
Leonard Richardson
オライリー・ジャパン / 3990円 ( 2007-12-21 )


とりあえずJSON用のRESTを実装して、クライアントとしてのwebはそっちをアクセスするようにしたほうがいいのかなぁと。

ただ、そうするとURIがぶつかるからJSON用のAPIのほうは/v1/をpathの先頭につけたけど。

あと、FlaskのjsonifyってSqlalchemyの結果をそのまま渡すと駄目で、dictionaryを組み立てて渡さないとあかんのね。これがちょっとめんどくさい

7章のブックマークサービスをRoRで実装する章をFlaskで再実装するという修行をしないとあかんのかなぁ、、、

Flaskでメソッドで振り分けるときどっちがいいの?

こんなふうにif-elseで振り分けるのと

@app.route('/some_path', methods=['GET,POST'])
def some_method():
    if request.method == 'POST':
        // POST用の処理
    else:
        // GET用の処理

こんな風に明示的に分けるの

@app.route('/some_path', methods=['GET'])
def get_some_path():
   // GET用の処理

@app.route('/some_path', methods=['POST'])
def post_some_path():
    // POST用の処理

前者はインデントが深くなっちゃうので後者のほうが読みやすいと思うんだけど。

追記 110614

MLのアーカイブにあった

Flaskでone-to-manyのデータの数を表示する

FlaskというよりはSQLAlchemy+Jinja2の話なのだけど。

one-to-manyのスキーマがあったとして(例えばあるレビューに付いたコメント数)、countメソッド使うと怒られるんだろうなぁと思いつつ使ってみる

{{ bookmark.comments.count() }}

これはやはり怒られた。

TypeError: count() takes exactly one argument (0 given)

で、Jinja2でlenが使えるかなとふと思ったのでやってみたら使えなかったが、builtin filtersにlengthがあるそうなので、それで解決。

onetomany

ところで、サービスは一気に動くところまで仕上げないと、だれるっていうかモチベーションが下がってきて生産性が落ちるなぁ。みんなはどうやってやる気を保ち続けているんだろうか?こつが知りたい。

FlaskでURLのパスにスラッシュを含めたい時

現在ちょっとした文献管理システムを作っています(doiで管理できるやつ)。今使っているのはlast.fmや、deliciousみたいなSBSにインスパイアされて5,6年前に作ったのだけど。

last.fmは曲だけどmendeleyは文献だからなストリームとしての文献ってなんだろかね?

Mendeleyのメリット

  • 世界中のMendeleyを使う研究者とつながることができ、同じ分野の研究者が何を読んでいるかを知ることができること

他の研究者の読んでる文献がわかったからどうなの?ということです。先っぽにいれば、重要な論文は大体みんな読んでるからなぁ。

自分でサービス作って運用してまわりの反応みてみたけど、設計としてもう少しいい方向を模索すべきだなという結論になった。

あとでもうちょっと丁寧に書くかも。

で、doiは

10.[出版社コード]/[出版社で勝手に決めた文字列]

というようにスラッシュが含まれているのでURL中にdoiを含めたい場合には/をどうにかしないといけないですね。

Flaskの場合はpathっていうconverterを書いておけばよろしくやってくれます。

@app.route('/entry/<path:doi>')
def show_entry(doi=None):
    if doi == None: abort(404)

    entry = db_session.query(Entry).filter(Entry.doi == doi).first()

    if entry == None: 
        abort(404)
    else:
        return render_template('show_entry.html', entry=entry)

それから、pdfからdoiを抜き出したい時にも[出版社で勝手に決めた文字列]のところでうまい正規表現がつくれなかったり、pdfからテキストを抽出する段階で文字がつながってしまったりしてdoiがきちんと取れてこれなかったりして残念度が高いです。

ていうか、pdfなんだからメタデータにきちんとdoiいれとけよなーと。

jQuery Form Pluginを使えば簡単にAjaxなformを導入できる

今、欲しい物はこういうモノです。

Real World Haskellのように、文書のパラグラフ毎にコメントが入れられるようなシステム。それをSphinxでやりたい。つまりSphinxで文書を書いてmake htmlをするとパラグラフ毎にtwitterのOAuthかGoogleの認証使ってコメントが入れられるようなHTMLが出力されるナイスなドキュメント

実際にReal World Haskellのソースを追いかけてみると、jQuery Form Pluginを使っていてAjax化が簡単にできそうなので、別の題材で試してみる。もちろんflaskr

  • statisticsにjqueryとjquery.form.jsを配置
  • show_entries.htmlのformにid属性をつける(id='flaskr_form'っていう属性をつけた)
  • layout.htmlにjsの設定を追加

layout.html

<script type="text/javascript" src="{{ url_for('static', filename='jquery-1.6.min.js') }}"></script> 
<script type="text/javascript" src="{{ url_for('static', filename='jquery.form.js') }}"></script> 
<script type="text/javascript"> 
        // wait for the DOM to be loaded 
        $(document).ready(function() { 
            // bind 'myForm' and provide a simple callback function 
            $('#flaskr_form').ajaxForm(function() { 
            $(document.body).load("/");
                alert("Thank you for your comment!"); 
            }); 
        }); 
    </script>

これでsubmitしたときにアラート画面が出るようになります。今回書いたコードはここからダウンロードできます。

flaskr_form

というわけで、flaskかGAEでパラグラフのidをクエリとしてコメントをjsonで返すサービスを用意しておいて、SphinxにjQuery埋め込んでコメントサービスと連携させれば望みのものはできそうな気がするんだけど、Sphinxのパラグラフに固有のIDを付加する方法がわからない。

どうやるのがいいのかなぁ

ProductName エキスパートPythonプログラミング
Tarek Ziade
アスキー・メディアワークス / 3780円 ( 2010-05-28 )


Flaskでsoaplibを使う

githubから

あとでWerkzeugのドキュメントをちゃんと読まないと何やってるかよくわからんが/soapにアクセスすればsoapサービス使えるということでいいのかな。