kay frameworkでform自動生成

kayの実際のコード例をあまり見かけないので、試用中のコードをここに晒してみたい。あんまりkayの事を良く分かっていない中で作ったので、良い使用例かどうかは判断がつかない。(もう幾らかやってみたらMLで色々聞いてみよう)

下記の例では、入力データの加工(Fieldでのconvert関数)、また、そのフィールドをモデルから自動生成する場合の方法が書いてある。templateは{{ form()|safe }}だけなので、省略。

実はmodelformをimportする時にgoogle.appengine.ext.db.XXXX系は全部googleではなく、kayのものとすり替わっている。そのやや黒い処理を行っている__metaclass__がmonkey_patchという名前の関数(関数だけどmetaclassとはこれ如何に*1 )だった。使えなくなったりしないだろうか(ビクビクッ)

patchちからでdb.Propertyクラスに、

  • get_form_field(self, form_class=forms.TextField, **kwargs)
  • get_value_for_form(self,instance)
  • make_value_from_form(self,instance)という名前から動作の想像に難くないメソが導入される。

ちなみに、datastoreAPIにmake/get_value_for/from_datastoreというものがあるので、対称性の点からこういう名前になっていると思われる。

あと、下記のforms.TextField(validators=[is_unique_fabric_code])のように、ちょっとしたバリデーションも追加しやすいのも嬉しい。

# -*- coding: utf-8 -*-
# fabric.models
from kay.i18n import _
from google.appengine.ext import db

from kay.utils import forms
from kay.utils.forms.modelform import ModelForm
from kay.utils.validators import ValidationError

# rgbの各要素値。#がついてるとHexとして扱う
class ColorUnitField(forms.NumberField):
  def convert(self, value):
    if value and value.startswith('#'):
      value = int('0x%s'%value[1:], 16)
    return super(ColorUnitField, self).convert(value)

# rgbの各要素値のDatastore Property
class ColorUnitProperty(db.IntegerProperty):
  def get_form_field(self, **kwargs):
    defaults = {'form_class': ColorUnitField, 'min_value': 0, 'max_value': 255,}
    defaults.update(kwargs)
    return super(ColorUnitProperty, self).get_form_field(**defaults)

class Fabric(db.Model):
  code = db.StringProperty(required=True)
  r = ColorUnitProperty(required=True)
  g = ColorUnitProperty(required=True)
  b = ColorUnitProperty(required=True)
  stock = db.IntegerProperty(required=True, choices=[0,1], default=1)
  created = db.DateTimeProperty(required=True, auto_now_add=True)
  updated = db.DateTimeProperty(required=True, auto_now=True)

  def color(self, format='rgb'):
    # manage.py test で doctestもできるようにしたい。
    """ return formatted color string
    >>> f = Fabric(code='a', r=255, g=192, b=0, stock=1)
    >>> f.color()
    rgb(255, 192, 0)
    >>> f.color('rgb')
    'rgb(255, 192, 0)'
    >>> f.color('#')
    '#ffc000'
    >>> f.color('list')
    ['255', '192', '0']
    >>> f.color('dict') == {'r': 'ff', 'g': 'c0', 'b': '00'}
    True
    """
    if format.startswith('#'):
      r,g,b = map(lambda v: '%02x'%int(v), (self.r, self.g, self.b))
      format = format[1:]
    else:
      r,g,b = map(str, (self.r, self.g, self.b))
    return 'rgb(%s, %s, %s)'%(r,g,b) if format == 'rgb'  \
      else list((r,g,b))             if format == 'list' \
      else {'r': r, 'g': g, 'b': b}  if format == 'dict' \
      else '#%02s%02s%02s'%(r,g,b)

class FabricForm(ModelForm):
  class Meta:
    model = Fabric
    exclude = ('created', 'updated')

##  def is_unique_fabric_code(form, value):
##    if Fabric.all().filter('code =', value).count(1) > 0:
##      raise ValidationError(_(u"'%(value)s' is already existed")%{'value': value})
##  code = forms.TextField(validators=[is_unique_fabric_code])
  # save済みの場合はスキップ
  def validate(self, *args, **kwds):
    def is_unique_fabric_code(form, value):
      if Fabric.all().filter('code =', value).count(1) > 0:
        raise ValidationError(_(u"'%(value)s' is already existed")%{'value': value})
    if not self.instance or not self.instance.is_saved():
      self.code.validators.append(is_unique_fabric_code)
    return super(FabricForm, self).validate(*args, **kwds)

# これで、フォームの選択肢の表示はできるが、(フォームの関係ない)単なる表示系とかを考えると、
# どういう風に配置するのが一番良いのか悩む
FabricForm.stock.choices = [
  (k, {'0': u'在庫無し', '1': u'在庫あり'}[v]) for k, v in FabricForm.stock.choices]
FabricForm.stock.choices.insert(0, ('', u''))

FabricForm.r.help_text = \
FabricForm.g.help_text = \
FabricForm.b.help_text = _(u'color must be in ther range 0 - 255 (or #00 - #FF by hex)')


--- views.py
def new(request):
  form = FabricForm() # instance=Fabric.get(key)とかやるとupdate系の処理もしてくれる
  errors = {}
  if request.method == 'POST' and form.validate(request.form):
    new = form.save()
    return redirect(reverse('fabric/index', focus=new.key()))
  return render_to_response('fabric/new.html', {'form': form.as_widget()})

*1:別にclassでなくともcallできてinstanceがreturnされればなんでもいいのである

formencode

FormEncode — FormEncode 2.0.0a1 documentationの使い方

フォームバリデーションライブラリ。

ドキュメント見ても今一わかりづらいので、以下エグザンプル。

from formencode import htmlfill
from formencode import Schema
from formencode import validators as vs
from formencode.api import Invalid

from google.appengine.ext import db
class SomeModel(db.Model):
  ...

class UniqueCode(vs.String):
  messages = {'existed': "%(value)s is already existed"}
  def validate_python(self, value, state):
    if SomeModel.all().filter('code =', value).count(1) > 0:
      raise vs.Invalid(self.message('existed', state, value=value), value, state)

class SomeModelForm(Schema):
  filter_extra_fields = allow_extra_fields = True
  code = UniqueCode(not_empty=True)
  color = vs.Int(not_empty=True)
  stock = vs.Int(if_missing=0)

def new(request):
  form = SomeModelForm()
  errors = {}
  if request.method == 'POST':
    try:
      new = Fabric(**form.to_python(request.form))
      new.put()
      return redirect(reverse('index'))
    except Invalid, e:
      errors = e.error_dict
  response = render_to_string('new.html')
  return Response(htmlfill.render(response, request.form or {'selected': True}, errors))

htmlfillはHTMLを簡単にパースしてvalue=""を埋めてくれるというもの。フォームの出力を(複雑でなければ)静的なものにでき、フォームのあるページもキャッシュできる。HTML自体が馬鹿でかいものでなければ、それほど遅くもない。

Google AppEngine + Werkzeug + jinja2 -> kay framework

Djangoを使ってみたのはいいものの、やはり、動かない(あるいは良く分からないので、動かせない)部分が多く、使いづらい。変にDjangoにこだわる必要も無いので、Werkzeug+jinja2というプレーンな構成に変更した。

Djangoは粗結合をうたっているとはいえ、巨大なので部分を動かす時に何が必要条件なのか探し出すのに手間がかかったが、変更した両ライブラリはとも見通しがいい(そのためのライブラリなのだから当たり前か)。

しばらく、その構成をいじくっているうち、「GAE + Werkzeug + jinja2」というほぼそのまんまの構成で、Google Code Archive - Long-term storage for Google Code Project Hosting.というものを発見するに至る。

だいたい目指すところが同じのように見えたので、結局最終的にはkayフレームワークに乗り換えることにした。typoなだけなのにエラーメッセージもなくNotFoundが表示されたりして、Beta感は否めないが、その分、中をのぞいて参考にするには丁度良さそうである。

調子に乗ってアプリケーションを登録しまくると……

話が飛んでしまうが、

デベロッパーは、各自の Google App Engine 管理者アカウントで 10 のアプリケーションを作成できます。現時点では、作成したアプリケーションを削除することはできません。

だそうで、変えられないらしいので、気をつけましょう。

Google AppEngine で外部ライブラリ

Google AppEngine APIは当たり前かもしれないが、データストアやキャッシュやメール等々、インフラ周りしかないため、その他の部分は適当なライブラリを選んで使うことになる。

では、GAEでは一般的にはどうしているかと言えば、http://code.google.com/intl/ja/appengine/docs/python/tools/libraries.htmlを見ると、Django*1が入っているので、TemplateEngineだのなんだのの作法が分らない奴はDjangoで解決せよという事なのだろう。

Djangoが古い

とはいえ、AppEngine標準のDjangoは0.9.6と若干古い*2。正式に1.xが出ているので、こちらを扱いたい。
appspot(production)の環境ではpython標準の.../lib/python2.X/site-packagesは見えないので、外部ライブラリを使うにはappengineプロジェクトディレクトリ内に直接入れておかなければならないが、特に小細工は必要なく、最新のDjangoを取って来て解凍してdjango/ディレクトリとしてプロジェクトルートに置いておけばよい。

のだが「 GAEのプロジェクトディレクトリには3000ファイルまでしかおけない」らしいので、ライブラリはzipで固めて置くのが通例のようだ。Djangoは特にFWという事でファイル数も多いので、無駄にリソースを消費する事になってしまうので、固めておいておきたい。

pip zip --no-pyc django

とコンソールを叩くとpyc抜き*3zipを作ったよとpath付きで教えてくれるのでpathの後ろに.zipをつけてcpすればok。

ただ、zipのimport機構はpython標準からカスタマイズされたGAE仕様らしいので、ものによってはうまくいかないものもあるらしい。(.eggファイルも無理っぽい)

GAEの動作をDjangoへ渡す

当方初学者のため、いきなりDjangoだのwsgi*4だの言われてもよくわからず、GAEからDjangoのつなぎ込みはとかどうすればいいんだ?としばし悩んだ。が解決策はあっさりと見つかった。
Google Code Archive - Long-term storage for Google Code Project Hosting.だ。

(入れてみると分るが、実はエントリポイントのmain.pyではそんなに複雑な事はやっていない。どうも、WSGI用の関数一つ呼んでいるだけみたいだ)

これ以降は特にGAEだから、という事もなくDjangoドキュメントを見ながらテンプレートエンジン等をいじればよいようだ。

注意点としては、GAEのDataStore APIDjangoのDatabase APIが元になっているそうだが、

Django データ モデリング インターフェースは、App Engine データストアとの互換性がありません

と但し書きがある点だ。メインとなるdbAPIに互換性がないので、以外と困る部分も多そうである。*5

今後もつづけてDjango路線で行くかどうか、もう少し軽量のライブラリを組み合わせるべきなのかは検討したい。

*1:python一流のフル装備なweb-framework

*2:Djangoの名が売れだした頃のバージョンだったか

*3:.pycは認識しない

*4:正式?にはウィスキーらしいが、上杉と読んでしまう

*5:勿論、Django目玉商品の一つcontrib.adminも使えないが、一応GAEにもhttp://localhost:8080/_ah/adminに管理機能があるのでそこまでがっかりしなくてもよさそうだ

OS X に Google App Engine (GAE)を入れる

http://code.google.com/intl/ja/appengine/downloads.html#Google_App_Engine_SDK_for_Pythonからdmgを取って来てGoogleAppEngineLauncher.appをどこかへコピー。

これで完了です。

パスを設定

OS Xのシステム標準のPythonを使うのでなければ、メニューから「GoogleAppEngineLauncher」> Preferences... > Python Path」へPythonのパスを通しましょう。

/Library/Frameworks/Python.framework/Versions/2.5/bin/python