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されればなんでもいいのである