Flask+jQuery UIでAutocomplete

Flaskはjsonで返すメソッドがあるのでjQueryとの親和性は高そうな感じ。Autocomplete(suggest機能)が欲しかったので色々調べた結果、jQuery UIのがよさそうだったのでこれで。

GoogleのAJAX Libraries APIを使えばローカルにライブラリ置いておかなくてもよいので楽ちん。タグのリストはあえてJSONで取得するようなサンプルにしてみた。ちなみに実際に作っているモノはSQLiteでデータベースからselectした結果を返すようにしている。

下のコードだけで動く。

from flask import Flask, jsonify

app = Flask(__name__)

@app.route("/")
def hello():
    response = """<html><head>
<link rel="stylesheet" href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.3/themes/base/jquery-ui.css" type="text/css" media="all" />
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js" type="text/javascript"></script>
<script src="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.3/jquery-ui.min.js" type="text/javascript"></script>         
<script type="text/javascript">
$(function() {$.getJSON("/tags", function(json){$("#tags").autocomplete({ source: json.tags});});});
</script></head>
<body><div class="demo"><div class="ui-widget"><label for="tags">Tags: </label><input id="tags"></div></div></body></html>
"""
    return response

@app.route("/tags")
def show_tags():
    tags = ["ActionScript", "AppleScript", "Asp", "BASIC", "C", "C++", "Clojure", "COBOL",
            "ColdFusion", "Erlang", "Fortran", "Groovy", "Haskell", "Java", "JavaScript", 
            "Lisp", "Perl", "PHP", "Python", "Ruby", "Scala", "Scheme"];
    return jsonify(tags = tags)

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

実際に動かしてみたらタグを複数選択することが出来なくて、自分の欲しいものとはちょっと違うので、AutoSuggest使うかも。

あとautocompleteはリストじゃなくてURLを指定することもできるんだけど、Flask側でJSONのリストだけを返す方法がわからなくて困った。必ずハッシュで返ってきちゃうのかな?

FlaskでJSON出力

jsonifyっていう関数が用意されているので楽ちん

@app.route('/json/tags')
def show_tags():
    tags = db_session.query(Tag).all()
    return jsonify(tags = [tag.name for tag in tags if len(tag.entries) > 0])

ProductName JavaScript: The Good Parts ―「良いパーツ」によるベストプラクティス
Douglas Crockford
オライリージャパン / ¥ 1,890 ()
在庫あり。

FlaskでRSSを出力する

make_responseでresponseオブジェクトを作って、Content-Typeを設定する。

from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello():
    response = app.make_response(rss)
    response.headers['Content-Type'] = 'application/rss+xml'
    return response

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

RSSの生成には、ElementTreeを使うかエキスパートPythonプログラミングにあるようにテンプレートを使う。

ProductName エキスパートPythonプログラミング
Tarek Ziade
アスキー・メディアワークス / ¥ 3,780 ()
在庫あり。

SQLAlchemyのmany-to-manyの検索のやりかたがわからない

なぜかFlaskではなくSQLAlchemyではまる

よくあるタグとエントリーの多対多のテーブル。

entry_tags = Table('entry_tags', Base.metadata,
    Column('entry_id', Integer, ForeignKey('entries.id')),
    Column('tag_id', Integer, ForeignKey('tags.id'))
)

class Tag(Base):
    __tablename__ = 'tags'
    id      = Column(Integer, primary_key=True)
    name    = Column(String(128), unique=True)
    entries = relation("Entry", secondary=entry_tags)

class Entry(Base):
    __tablename__    = 'entries'
    id               = Column(Integer, primary_key=True)
    title            = Column(String(128), unique=True)
    content          = Column(Text())
    pubdate          = Column(DateTime)
    tags             = relation("Tag", secondary=entry_tags)

このとき、タグをもつエントリを検索したい。単にエントリ最新10件をとってきたい場合には

entries = db_session.query(Entry).order_by(Entry.pubdate.desc()).limit(10)

でいい。

続いて、あるタグがふられているエントリの最新10件をとってきたい場合にどう書いていいか悩んだ挙句、結局わからなかったのでINを使うことにした。

tag_ids = [t.id for t in db_session.query(Tag).filter(Tag.name == tagname).first().entries]
entries = db_session.query(Entry).filter(Entry.id.in_(tag_ids)).order_by(Entry.pubdate.desc()).limit(10)

filterかfilter_byに何入れればいいんだろう?

ProductName Essential Sqlalchemy
Rick Copeland
Oreilly & Associates Inc / ¥ 3,370 ()
通常1~3週間以内に発送

2010.08.01 追記

relationでorder_byを設定すればよかった。

entries = relation("Entry", secondary=entry_tags, order_by=Entry.pubdate.desc)

でもって

entries = db_session.query(Tag).filter(Tag.name == tagname).first().entries

とすれば、あるタグを含むエントリを日付の最近の順に取ってくる。

Flaskでコードをハイライトするサービス

pythonのMarkdownにはPygmentsをつかったコードハイライト用のエクステンションがあるので、それを使えば簡単にかける

flaskmd

スクリーンショット撮ってから「セッションとか使ってないからsessionとかkeyとかいらないじゃん」と思って少し綺麗にしたので最終的には15行くらい。

flaskmd.py

from flask import Flask, request, url_for, render_template
from markdown import markdown

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

@app.route('/',methods=['POST', 'GET'])
def show_code(code = ""):
    if request.method == 'POST':
        code = markdown(request.form['code'], ['codehilite'],safe_mode=False)
    return render_template('show_code.html', code=code)

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

show_code.html

markdownで生成したhtmlをそのままテンプレートに渡すとエスケープされてしまうので、autoescapeをFalseにした。今回ここが一番悩んだがドキュメントをちゃんと読めば書いてあった。

<!doctype html>
<title>CodeHilite</title>
<link rel=stylesheet type=text/css href="{{ url_for('static', filename='style.css') }}">
<body>
<form action="{{ url_for('show_code') }}" method=post class=add-entry>
<dl>
<dt>code:
<dd><textarea name=code rows=5 cols=40></textarea>
<dd><input type=submit value=hilite>
</dl>
</form>
<div class=page>
  <h1>CodeHilite</h1>
{% autoescape false %}
{{ code }}
{% endautoescape %}
</div>
</body>
</html>

style.css

cssはpygmentsであらかじめファイルに出力しておいた

from pygments.formatters import HtmlFormatter
with open("style.css","a") as f:
    f.write(HtmlFormatter().get_style_defs('.codehilitetable'))

Flask楽しい。

ProductName Lonely Planet Discover Japan
Chris Rowthorn,Andrew Bender,Matthew D. Firestone,Timothy N. Hornyak
Lonely Planet / ¥ 2,326 ()
在庫あり。

Flaskでtwitter名刺みたいなやつをつくるやつ

OAuth使えば簡単だろうと。

twitter_card

あとは名刺型にhtmlをいじればよいんだけど飽きた。

twittercard.py

from flask import Flask, request, session, g, redirect, url_for, abort, render_template, flash
from flaskext.oauth import OAuth
import qrencode

DEBUG      = True
SECRET_KEY = 'development key'
TWITTER_URL = "http://twitter.com/"

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

oauth = OAuth()
twitter = oauth.remote_app('twitter',
                           base_url='http://api.twitter.com/1/',
                           request_token_url='http://api.twitter.com/oauth/request_token',
                           access_token_url='http://api.twitter.com/oauth/access_token',
                           authorize_url='http://api.twitter.com/oauth/authorize',
                           consumer_key='XXXXXXXXXXXXX',
                           consumer_secret='XXXXXXXXXXXXX'
)

@twitter.tokengetter
def get_twitter_token():
    return session.get('twitter_token')

@app.route('/')
def show_entries():
    screen_name = session.get('screen_name',None)
    name = session.get('name',None)
    profile_image_url = session.get('profile_image_url',None)
    entry = {}
    if screen_name != None: 
        imgurl = "http://twitter.com/%s" % screen_name
        imgfile   = "%s.png" % screen_name
        qrencode.encode_scaled(imgurl,50)[2].save("static/"+imgfile)
        resp = twitter.get('http://twitter.com/status/user_timeline/'+screen_name+".json")
        if resp.status == 200:
            user = resp.data[0]['user']
            name = user['name']
            url = user['url']
            profile_image_url = user['profile_image_url']
        else:
            name = None
            profile_image_url = None

        entry = dict(screen_name=screen_name,qrimg=imgfile,name=name, profile_image_url=profile_image_url, url=url)
    return render_template('show_entries.html', entry=entry)

@app.route('/login')
def login():
    return twitter.authorize(callback=url_for("oauth_authorized"))

@app.route('/logout')
def logout():
    session.pop('screen_name', None)
    session.pop('logged_in', None)
    session.pop('twitter_token', None)
    flash('You were logged out')
    return redirect(url_for('show_entries'))

@app.route('/oauth-authorized')
@twitter.authorized_handler
def oauth_authorized(resp):
    next_url = url_for('show_entries')
    if resp is None:
        flash(u'You denied the request to sign in.')
        return redirect(next_url)

    session['logged_in'] = True
    session['screen_name'] = resp['screen_name']
    session['twitter_token'] = (
        resp['oauth_token'],
        resp['oauth_token_secret']
        )

    flash(resp['screen_name'] + ' were signed in')
    return redirect(next_url)

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

テンプレート(やる気レス)

<!doctype html>
<title>TwitterCard</title>
<link rel=stylesheet type=text/css href="{{ url_for('static', filename='style.css') }}">
<div class=page>
  <h1>TwitterCard</h1>
  <div class=metanav>
    {% if not session.logged_in %}
    <a href="{{ url_for('login') }}">log in</a>
    {% else %}
    <a href="{{ url_for('logout') }}">log out</a>
    {% endif %}
  </div>
  {% for message in get_flashed_messages() %}
  <div class=flash>{{ message }}</div>
  {% endfor %}
  <img src="{{ entry.profile_image_url }}" />
  <ul class=entry>
    <li>{{ entry.screen_name }}</li>
    <li>{{ entry.name }}</li>
    <li>{{ entry.url }}</li>
  </ul>
  <img src={{ url_for('static',filename=entry.qrimg) }} />
</div>

ついったー名刺ってのがあってQRコードはblogとかのURLなんだけど、ツイッターオフとかいくと、簡単にフォローしたいからQRコードはtwitterアカウントのほうがいいかな?なんて思っていたので作ってみようとした(がもう寝る時間なので眠くなってしまい、ヤル気が急速に失われた)。


エントリ書いてて思ったんだが、ツイッターオフ参加者のIDのリストをQRコードにしといて、そいつを食わすとまとめてフォローするサービスのほうがいいような気がしてきた。そんなのないかな。

名刺見ながらポチポチ登録作業すんのもめんどいよね。

FlaskrのモデルをSQLAlchemyで

FlaskのTutorialのモデル部分をSQLAlchemyにしてみた(Declarative)。

database.py

from sqlalchemy import create_engine
from sqlalchemy.orm import scoped_session, sessionmaker
from sqlalchemy.ext.declarative import declarative_base

engine = create_engine('sqlite:////Users/kzfm/flask/flaskr/flaskr.db', convert_unicode=True)
db_session = scoped_session(sessionmaker(autocommit=False,
                                         autoflush=False,
                                         bind=engine))
Base = declarative_base()
Base.query = db_session.query_property()

def init_db():
    Base.metadata.create_all(bind=engine)

models.py

from sqlalchemy import Column, Integer, String, Text
from database import Base

class Entry(Base):
    __tablename__ = 'entries'
    id = Column(Integer, primary_key=True)
    title = Column(String(500), unique=True)
    text = Column(Text(), unique=True)

    def __init__(self, title=None, text=None):
        self.title = title
        self.text = text

    def __repr__(self):
        return '<Title %r>' % (self.title)

これで、まだSQLiteのデータベースができてない場合にはpythonのシェルから

>>> from database import init.db
>>> from models import Entry
>>> init_db()

でデータベースが出来上がる。あとはflaskr部分。login,logoutはチュートリアルと一緒なので省略

import sqlite3
from flask import Flask, request, session, g, redirect, url_for, abort, render_template, flash
from contextlib import closing
from database import db_session
from models import Entry

DATABASE   = '/Users/kzfm/flask/flaskr/flaskr.db'
DEBUG      = True
SECRET_KEY = 'development key'
USERNAME   = 'admin'
PASSWORD   = 'default'

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

@app.after_request
def after_request(response):
    db_session.remove()
    return response

@app.route('/')
def show_entries():
    entries = Entry.query.all()
    return render_template('show_entries.html', entries=entries)

@app.route('/add', methods=['POST'])
def add_entry():
    if not session.get('logged_in'):
        abort(401)
    entry = Entry(request.form['title'], request.form['text'])
    db_session.add(entry)
    db_session.commit()
    return redirect(url_for('show_entries'))

簡単だ。

参考

ProductName Essential Sqlalchemy
Rick Copeland
Oreilly & Associates Inc / ¥ 3,370 ()
通常1~3週間以内に発送

Flaskrの認証をOAuthで

Flaskのチュートリアルの認証をFlask-OAuthを使ったものに変えてみた。

flaskr oauth

loginのとこを変えるのと、新しくoauth-authorized,get_twitter_tokenというメソッドを追加してoauthの認証に対応させただけ。

import sqlite3
from flask import Flask, request, session, g, redirect, url_for, abort, render_template, flash
from contextlib import closing
from flaskext.oauth import OAuth

DATABASE   = '/Users/kzfm/flask/flaskr/flaskr.db'
DEBUG      = True
SECRET_KEY = 'development key'

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

oauth = OAuth()
twitter = oauth.remote_app('twitter',
                           base_url='http://api.twitter.com/1/',
                           request_token_url='http://api.twitter.com/oauth/request_token',
                           access_token_url='http://api.twitter.com/oauth/access_token',
                           authorize_url='http://api.twitter.com/oauth/authorize',
                           consumer_key='XXXXXXXXXXXXXXXXX',
                           consumer_secret='XXXXXXXXXXXXX'
)

def connect_db():
    return sqlite3.connect(app.config['DATABASE'])

def init_db():
    with closing(connect_db()) as db:
        with app.open_resource('schema.sql') as f:
            db.cursor().executescript(f.read())
        db.commit()

@app.before_request
def before_request():
    g.db = connect_db()

@app.after_request
def after_request(response):
    g.db.close()
    return response

@twitter.tokengetter
def get_twitter_token():
    return session.get('twitter_token')

@app.route('/')
def show_entries():
    cur = g.db.execute('select title, text from entries order by id desc')
    entries = [dict(title=row[0], text=row[1]) for row in cur.fetchall()]
    return render_template('show_entries.html', entries=entries)

@app.route('/add', methods=['POST'])
def add_entry():
    if not session.get('logged_in'):
        abort(401)
    g.db.execute('insert into entries (title, text) values (?, ?)',
                 [request.form['title'], request.form['text']])
    g.db.commit()
    return redirect(url_for('show_entries'))

@app.route('/login')
def login():
    sys.stderr.write(url_for("oauth_authorized"))
    return twitter.authorize(callback=url_for("oauth_authorized"))

@app.route('/logout')
def logout():
    session.pop('logged_in', None)
    flash('You were logged out')
    return redirect(url_for('show_entries'))

@app.route('/oauth-authorized')
@twitter.authorized_handler
def oauth_authorized(resp):
    next_url = url_for('show_entries')
    if resp is None:
        flash(u'You denied the request to sign in.')
        return redirect(next_url)

    session['logged_in'] = True
    session['username'] = resp['screen_name']
    flash(resp['screen_name'] + ' were signed in')
    return redirect(next_url)

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

手元のmacbookで書いてたんだけど、localhostだとcallbackでエラー吐くので、sunshine.private.jpとか適当なホストネームを127.0.0.1にあてといた。

ProductName ハートキャッチプリキュア! シャイニータンバリン

バンダイ / ¥ 3,990 (2010-07-17)
在庫あり。

Flaskrのデモ

Flaskのチュートリアルをやってみた

flask sample

全部で70行くらい。モデルのとこはSQLAlchemy使ったほうが楽かな(またはMongo)

jQueryを使いたい場合はコレを読む