thirdparty/google_appengine/google/appengine/ext/db/djangoforms.py
changeset 109 620f9b141567
child 828 f5fd65cc3bf3
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/thirdparty/google_appengine/google/appengine/ext/db/djangoforms.py	Tue Aug 26 21:49:54 2008 +0000
@@ -0,0 +1,886 @@
+#!/usr/bin/env python
+#
+# Copyright 2007 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+"""Support for creating Django (new) forms from Datastore data models.
+
+This is our best shot at supporting as much of Django as possible: you
+won't be able to use Django's db package, but you can use our
+db package instead, and create Django forms from it, either fully
+automatically, or with overrides.
+
+Some of the code here is strongly inspired by Django's own ModelForm
+class (new in Django 0.97).  Our code also supports Django 0.96 (so as
+to be maximally compatible).  Note that our API is always similar to
+Django 0.97's API, even when used with Django 0.96 (which uses a
+different API, chiefly form_for_model()).
+
+Terminology notes:
+  - forms: always refers to the Django newforms subpackage
+  - field: always refers to a Django forms.Field instance
+  - property: always refers to a db.Property instance
+
+Mapping between properties and fields:
+
++====================+===================+==============+====================+
+| Property subclass  | Field subclass    | datatype     | widget; notes      |
++====================+===================+==============+====================+
+| StringProperty     | CharField         | unicode      | Textarea           |
+|                    |                   |              | if multiline       |
++--------------------+-------------------+--------------+--------------------+
+| TextProperty       | CharField         | unicode      | Textarea           |
++--------------------+-------------------+--------------+--------------------+
+| BlobProperty       | FileField         | str          | skipped in v0.96   |
++--------------------+-------------------+--------------+--------------------+
+| DateTimeProperty   | DateTimeField     | datetime     | skipped            |
+|                    |                   |              | if auto_now[_add]  |
++--------------------+-------------------+--------------+--------------------+
+| DateProperty       | DateField         | date         | ditto              |
++--------------------+-------------------+--------------+--------------------+
+| TimeProperty       | TimeField         | time         | ditto              |
++--------------------+-------------------+--------------+--------------------+
+| IntegerProperty    | IntegerField      | int or long  |                    |
++--------------------+-------------------+--------------+--------------------+
+| FloatProperty      | FloatField        | float        | CharField in v0.96 |
++--------------------+-------------------+--------------+--------------------+
+| BooleanProperty    | BooleanField      | bool         |                    |
++--------------------+-------------------+--------------+--------------------+
+| UserProperty       | CharField         | users.User   |                    |
++--------------------+-------------------+--------------+--------------------+
+| StringListProperty | CharField         | list of str  | Textarea           |
++--------------------+-------------------+--------------+--------------------+
+| LinkProperty       | URLField          | str          |                    |
++--------------------+-------------------+--------------+--------------------+
+| ReferenceProperty  | ModelChoiceField* | db.Model     |                    |
++--------------------+-------------------+--------------+--------------------+
+| _ReverseReferenceP.| None              | <iterable>   | always skipped     |
++====================+===================+==============+====================+
+
+Notes:
+*: this Field subclasses is defined by us, not in Django.
+"""
+
+
+
+import itertools
+
+
+import django.core.exceptions
+import django.utils.datastructures
+
+try:
+  from django import newforms as forms
+except ImportError:
+  from django import forms
+
+try:
+  from django.utils.translation import ugettext_lazy as _
+except ImportError:
+  pass
+
+from google.appengine.api import users
+from google.appengine.ext import db
+
+
+
+
+def monkey_patch(name, bases, namespace):
+  """A 'metaclass' for adding new methods to an existing class.
+
+  In this version, existing methods can't be overridden; this is by
+  design, to avoid accidents.
+
+  Usage example:
+
+    class PatchClass(TargetClass):
+      __metaclass__ = monkey_patch
+      def foo(self, ...): ...
+      def bar(self, ...): ...
+
+  This is equivalent to:
+
+    def foo(self, ...): ...
+    def bar(self, ...): ...
+    TargetClass.foo = foo
+    TargetClass.bar = bar
+    PatchClass = TargetClass
+
+  Note that PatchClass becomes an alias for TargetClass; by convention
+  it is recommended to give PatchClass the same name as TargetClass.
+  """
+
+  assert len(bases) == 1, 'Exactly one base class is required'
+  base = bases[0]
+  for name, value in namespace.iteritems():
+    if name not in ('__metaclass__', '__module__'):
+      assert name not in base.__dict__, "Won't override attribute %r" % (name,)
+      setattr(base, name, value)
+  return base
+
+
+
+
+class Property(db.Property):
+  __metaclass__ = monkey_patch
+
+  def get_form_field(self, form_class=forms.CharField, **kwargs):
+    """Return a Django form field appropriate for this property.
+
+    Args:
+      form_class: a forms.Field subclass, default forms.CharField
+
+    Additional keyword arguments are passed to the form_class constructor,
+    with certain defaults:
+      required: self.required
+      label: prettified self.verbose_name, if not None
+      widget: a forms.Select instance if self.choices is non-empty
+      initial: self.default, if not None
+
+    Returns:
+       A fully configured instance of form_class, or None if no form
+       field should be generated for this property.
+    """
+    defaults = {'required': self.required}
+    if self.verbose_name:
+      defaults['label'] = self.verbose_name.capitalize().replace('_', ' ')
+    if self.choices:
+      choices = []
+      if not self.required or (self.default is None and
+                               'initial' not in kwargs):
+        choices.append(('', '---------'))
+      for choice in self.choices:
+        choices.append((str(choice), unicode(choice)))
+      defaults['widget'] = forms.Select(choices=choices)
+    if self.default is not None:
+      defaults['initial'] = self.default
+    defaults.update(kwargs)
+    return form_class(**defaults)
+
+  def get_value_for_form(self, instance):
+    """Extract the property value from the instance for use in a form.
+
+    Override this to do a property- or field-specific type conversion.
+
+    Args:
+      instance: a db.Model instance
+
+    Returns:
+      The property's value extracted from the instance, possibly
+      converted to a type suitable for a form field; possibly None.
+
+    By default this returns the instance attribute's value unchanged.
+    """
+    return getattr(instance, self.name)
+
+  def make_value_from_form(self, value):
+    """Convert a form value to a property value.
+
+    Override this to do a property- or field-specific type conversion.
+
+    Args:
+      value: the cleaned value retrieved from the form field
+
+    Returns:
+      A value suitable for assignment to a model instance's property;
+      possibly None.
+
+    By default this converts the value to self.data_type if it
+    isn't already an instance of that type, except if the value is
+    empty, in which case we return None.
+    """
+    if value in (None, ''):
+      return None
+    if not isinstance(value, self.data_type):
+      value = self.data_type(value)
+    return value
+
+
+class StringProperty(db.StringProperty):
+  __metaclass__ = monkey_patch
+
+  def get_form_field(self, **kwargs):
+    """Return a Django form field appropriate for a string property.
+
+    This sets the widget default to forms.Textarea if the property's
+    multiline attribute is set.
+    """
+    defaults = {}
+    if self.multiline:
+      defaults['widget'] = forms.Textarea
+    defaults.update(kwargs)
+    return super(StringProperty, self).get_form_field(**defaults)
+
+
+class TextProperty(db.TextProperty):
+  __metaclass__ = monkey_patch
+
+  def get_form_field(self, **kwargs):
+    """Return a Django form field appropriate for a text property.
+
+    This sets the widget default to forms.Textarea.
+    """
+    defaults = {'widget': forms.Textarea}
+    defaults.update(kwargs)
+    return super(TextProperty, self).get_form_field(**defaults)
+
+
+class BlobProperty(db.BlobProperty):
+  __metaclass__ = monkey_patch
+
+  def get_form_field(self, **kwargs):
+    """Return a Django form field appropriate for a blob property.
+
+    This defaults to a forms.FileField instance when using Django 0.97
+    or later.  For 0.96 this returns None, as file uploads are not
+    really supported in that version.
+    """
+    if not hasattr(forms, 'FileField'):
+      return None
+    defaults = {'form_class': forms.FileField}
+    defaults.update(kwargs)
+    return super(BlobProperty, self).get_form_field(**defaults)
+
+  def get_value_for_form(self, instance):
+    """Extract the property value from the instance for use in a form.
+
+    There is no way to convert a Blob into an initial value for a file
+    upload, so we always return None.
+    """
+    return None
+
+  def make_value_from_form(self, value):
+    """Convert a form value to a property value.
+
+    This extracts the content from the UploadedFile instance returned
+    by the FileField instance.
+    """
+    if value.__class__.__name__ == 'UploadedFile':
+      return db.Blob(value.content)
+    return super(BlobProperty, self).make_value_from_form(value)
+
+
+class DateTimeProperty(db.DateTimeProperty):
+  __metaclass__ = monkey_patch
+
+  def get_form_field(self, **kwargs):
+    """Return a Django form field appropriate for a date-time property.
+
+    This defaults to a DateTimeField instance, except if auto_now or
+    auto_now_add is set, in which case None is returned, as such
+    'auto' fields should not be rendered as part of the form.
+    """
+    if self.auto_now or self.auto_now_add:
+      return None
+    defaults = {'form_class': forms.DateTimeField}
+    defaults.update(kwargs)
+    return super(DateTimeProperty, self).get_form_field(**defaults)
+
+
+class DateProperty(db.DateProperty):
+  __metaclass__ = monkey_patch
+
+  def get_form_field(self, **kwargs):
+    """Return a Django form field appropriate for a date property.
+
+    This defaults to a DateField instance, except if auto_now or
+    auto_now_add is set, in which case None is returned, as such
+    'auto' fields should not be rendered as part of the form.
+    """
+    if self.auto_now or self.auto_now_add:
+      return None
+    defaults = {'form_class': forms.DateField}
+    defaults.update(kwargs)
+    return super(DateProperty, self).get_form_field(**defaults)
+
+
+class TimeProperty(db.TimeProperty):
+  __metaclass__ = monkey_patch
+
+  def get_form_field(self, **kwargs):
+    """Return a Django form field appropriate for a time property.
+
+    This defaults to a TimeField instance, except if auto_now or
+    auto_now_add is set, in which case None is returned, as such
+    'auto' fields should not be rendered as part of the form.
+    """
+    if self.auto_now or self.auto_now_add:
+      return None
+    defaults = {'form_class': forms.TimeField}
+    defaults.update(kwargs)
+    return super(TimeProperty, self).get_form_field(**defaults)
+
+
+class IntegerProperty(db.IntegerProperty):
+  __metaclass__ = monkey_patch
+
+  def get_form_field(self, **kwargs):
+    """Return a Django form field appropriate for an integer property.
+
+    This defaults to an IntegerField instance.
+    """
+    defaults = {'form_class': forms.IntegerField}
+    defaults.update(kwargs)
+    return super(IntegerProperty, self).get_form_field(**defaults)
+
+
+class FloatProperty(db.FloatProperty):
+  __metaclass__ = monkey_patch
+
+  def get_form_field(self, **kwargs):
+    """Return a Django form field appropriate for an integer property.
+
+    This defaults to a FloatField instance when using Django 0.97 or
+    later.  For 0.96 this defaults to the CharField class.
+    """
+    defaults = {}
+    if hasattr(forms, 'FloatField'):
+      defaults['form_class'] = forms.FloatField
+    defaults.update(kwargs)
+    return super(FloatProperty, self).get_form_field(**defaults)
+
+
+class BooleanProperty(db.BooleanProperty):
+  __metaclass__ = monkey_patch
+
+  def get_form_field(self, **kwargs):
+    """Return a Django form field appropriate for a boolean property.
+
+    This defaults to a BooleanField.
+    """
+    defaults = {'form_class': forms.BooleanField}
+    defaults.update(kwargs)
+    return super(BooleanProperty, self).get_form_field(**defaults)
+
+  def make_value_from_form(self, value):
+    """Convert a form value to a property value.
+
+    This is needed to ensure that False is not replaced with None.
+    """
+    if value is None:
+      return None
+    if isinstance(value, basestring) and value.lower() == 'false':
+      return False
+    return bool(value)
+
+
+class UserProperty(db.UserProperty):
+  __metaclass__ = monkey_patch
+
+  def get_form_field(self, **kwargs):
+    """Return a Django form field appropriate for a user property.
+
+    This defaults to a CharField whose initial value is the current
+    username.
+    """
+    defaults = {'initial': users.GetCurrentUser()}
+    defaults.update(kwargs)
+    return super(UserProperty, self).get_form_field(**defaults)
+
+
+class StringListProperty(db.StringListProperty):
+  __metaclass__ = monkey_patch
+
+
+  def get_form_field(self, **kwargs):
+    """Return a Django form field appropriate for a StringList property.
+
+    This defaults to a Textarea widget with a blank initial value.
+    """
+    defaults = {'widget': forms.Textarea,
+                'initial': ''}
+    defaults.update(kwargs)
+    return super(StringListProperty, self).get_form_field(**defaults)
+
+  def get_value_for_form(self, instance):
+    """Extract the property value from the instance for use in a form.
+
+    This joins a list of strings with newlines.
+    """
+    value = super(StringListProperty, self).get_value_for_form(instance)
+    if not value:
+      return None
+    if isinstance(value, list):
+      value = '\n'.join(value)
+    return value
+
+  def make_value_from_form(self, value):
+    """Convert a form value to a property value.
+
+    This breaks the string into lines.
+    """
+    if not value:
+      return []
+    if isinstance(value, basestring):
+      value = value.splitlines()
+    return value
+
+
+class LinkProperty(db.LinkProperty):
+  __metaclass__ = monkey_patch
+
+  def get_form_field(self, **kwargs):
+    """Return a Django form field appropriate for a URL property.
+
+    This defaults to a URLField instance.
+    """
+    defaults = {'form_class': forms.URLField}
+    defaults.update(kwargs)
+    return super(LinkProperty, self).get_form_field(**defaults)
+
+
+class _WrapIter(object):
+  """Helper class whose iter() calls a given function to get an iterator."""
+
+  def __init__(self, function):
+    self._function = function
+
+  def __iter__(self):
+    return self._function()
+
+
+class ModelChoiceField(forms.Field):
+
+  default_error_messages = {
+      'invalid_choice': _(u'Please select a valid choice. '
+                          u'That choice is not one of the available choices.'),
+    }
+
+  def __init__(self, reference_class, query=None, choices=None,
+               empty_label=u'---------',
+               required=True, widget=forms.Select, label=None, initial=None,
+               help_text=None, *args, **kwargs):
+    """Constructor.
+
+    Args:
+      reference_class: required; the db.Model subclass used in the reference
+      query: optional db.Query; default db.Query(reference_class)
+      choices: optional explicit list of (value, label) pairs representing
+        available choices; defaults to dynamically iterating over the
+        query argument (or its default)
+      empty_label: label to be used for the default selection item in
+        the widget; this is prepended to the choices
+      required, widget, label, initial, help_text, *args, **kwargs:
+        like for forms.Field.__init__(); widget defaults to forms.Select
+    """
+    assert issubclass(reference_class, db.Model)
+    if query is None:
+      query = db.Query(reference_class)
+    assert isinstance(query, db.Query)
+    super(ModelChoiceField, self).__init__(required, widget, label, initial,
+                                           help_text, *args, **kwargs)
+    self.empty_label = empty_label
+    self.reference_class = reference_class
+    self._query = query
+    self._choices = choices
+    self._update_widget_choices()
+
+  def _update_widget_choices(self):
+    """Helper to copy the choices to the widget."""
+    self.widget.choices = self.choices
+
+
+  def _get_query(self):
+    """Getter for the query attribute."""
+    return self._query
+
+  def _set_query(self, query):
+    """Setter for the query attribute.
+
+    As a side effect, the widget's choices are updated.
+    """
+    self._query = query
+    self._update_widget_choices()
+
+  query = property(_get_query, _set_query)
+
+  def _generate_choices(self):
+    """Generator yielding (key, label) pairs from the query results."""
+    yield ('', self.empty_label)
+    for inst in self._query:
+      yield (inst.key(), unicode(inst))
+
+
+  def _get_choices(self):
+    """Getter for the choices attribute.
+
+    This is required to return an object that can be iterated over
+    multiple times.
+    """
+    if self._choices is not None:
+      return self._choices
+    return _WrapIter(self._generate_choices)
+
+  def _set_choices(self, choices):
+    """Setter for the choices attribute.
+
+    As a side effect, the widget's choices are updated.
+    """
+    self._choices = choices
+    self._update_widget_choices()
+
+  choices = property(_get_choices, _set_choices)
+
+  def clean(self, value):
+    """Override Field.clean() to do reference-specific value cleaning.
+
+    This turns a non-empty value into a model instance.
+    """
+    value = super(ModelChoiceField, self).clean(value)
+    if not value:
+      return None
+    instance = db.get(value)
+    if instance is None:
+      raise db.BadValueError(self.error_messages['invalid_choice'])
+    return instance
+
+
+class ReferenceProperty(db.ReferenceProperty):
+  __metaclass__ = monkey_patch
+
+  def get_form_field(self, **kwargs):
+    """Return a Django form field appropriate for a reference property.
+
+    This defaults to a ModelChoiceField instance.
+    """
+    defaults = {'form_class': ModelChoiceField,
+                'reference_class': self.reference_class}
+    defaults.update(kwargs)
+    return super(ReferenceProperty, self).get_form_field(**defaults)
+
+  def get_value_for_form(self, instance):
+    """Extract the property value from the instance for use in a form.
+
+    This return the key object for the referenced object, or None.
+    """
+    value = super(ReferenceProperty, self).get_value_for_form(instance)
+    if value is not None:
+      value = value.key()
+    return value
+
+  def make_value_from_form(self, value):
+    """Convert a form value to a property value.
+
+    This turns a key string or object into a model instance.
+    """
+    if value:
+      if not isinstance(value, db.Model):
+        value = db.get(value)
+    return value
+
+
+class _ReverseReferenceProperty(db._ReverseReferenceProperty):
+  __metaclass__ = monkey_patch
+
+  def get_form_field(self, **kwargs):
+    """Return a Django form field appropriate for a reverse reference.
+
+    This always returns None, since reverse references are always
+    automatic.
+    """
+    return None
+
+
+def property_clean(prop, value):
+  """Apply Property level validation to value.
+
+  Calls .make_value_from_form() and .validate() on the property and catches
+  exceptions generated by either.  The exceptions are converted to
+  forms.ValidationError exceptions.
+
+  Args:
+    prop: The property to validate against.
+    value: The value to validate.
+
+  Raises:
+    forms.ValidationError if the value cannot be validated.
+  """
+  if value is not None:
+    try:
+      prop.validate(prop.make_value_from_form(value))
+    except (db.BadValueError, ValueError), e:
+      raise forms.ValidationError(unicode(e))
+
+
+class ModelFormOptions(object):
+  """A simple class to hold internal options for a ModelForm class.
+
+  Instance attributes:
+    model: a db.Model class, or None
+    fields: list of field names to be defined, or None
+    exclude: list of field names to be skipped, or None
+
+  These instance attributes are copied from the 'Meta' class that is
+  usually present in a ModelForm class, and all default to None.
+  """
+
+
+  def __init__(self, options=None):
+    self.model = getattr(options, 'model', None)
+    self.fields = getattr(options, 'fields', None)
+    self.exclude = getattr(options, 'exclude', None)
+
+
+class ModelFormMetaclass(type):
+  """The metaclass for the ModelForm class defined below.
+
+  This is our analog of Django's own ModelFormMetaclass.  (We
+  can't conveniently subclass that class because there are quite a few
+  differences.)
+
+  See the docs for ModelForm below for a usage example.
+  """
+
+  def __new__(cls, class_name, bases, attrs):
+    """Constructor for a new ModelForm class instance.
+
+    The signature of this method is determined by Python internals.
+
+    All Django Field instances are removed from attrs and added to
+    the base_fields attribute instead.  Additional Field instances
+    are added to this based on the Datastore Model class specified
+    by the Meta attribute.
+    """
+    fields = sorted(((field_name, attrs.pop(field_name))
+                     for field_name, obj in attrs.items()
+                     if isinstance(obj, forms.Field)),
+                    key=lambda obj: obj[1].creation_counter)
+    for base in bases[::-1]:
+      if hasattr(base, 'base_fields'):
+        fields = base.base_fields.items() + fields
+    declared_fields = django.utils.datastructures.SortedDict()
+    for field_name, obj in fields:
+      declared_fields[field_name] = obj
+
+    opts = ModelFormOptions(attrs.get('Meta', None))
+    attrs['_meta'] = opts
+
+    base_models = []
+    for base in bases:
+      base_opts = getattr(base, '_meta', None)
+      base_model = getattr(base_opts, 'model', None)
+      if base_model is not None:
+        base_models.append(base_model)
+    if len(base_models) > 1:
+      raise django.core.exceptions.ImproperlyConfigured(
+          "%s's base classes define more than one model." % class_name)
+
+    if opts.model is not None:
+      if base_models and base_models[0] is not opts.model:
+        raise django.core.exceptions.ImproperlyConfigured(
+            '%s defines a different model than its parent.' % class_name)
+
+      model_fields = django.utils.datastructures.SortedDict()
+      for name, prop in sorted(opts.model.properties().iteritems(),
+                               key=lambda prop: prop[1].creation_counter):
+        if opts.fields and name not in opts.fields:
+          continue
+        if opts.exclude and name in opts.exclude:
+          continue
+        form_field = prop.get_form_field()
+        if form_field is not None:
+          model_fields[name] = form_field
+
+      model_fields.update(declared_fields)
+      attrs['base_fields'] = model_fields
+
+      props = opts.model.properties()
+      for name, field in model_fields.iteritems():
+        prop = props.get(name)
+        if prop:
+          def clean_for_property_field(value, prop=prop, old_clean=field.clean):
+            value = old_clean(value)
+            property_clean(prop, value)
+            return value
+          field.clean = clean_for_property_field
+    else:
+      attrs['base_fields'] = declared_fields
+
+    return super(ModelFormMetaclass, cls).__new__(cls,
+                                                  class_name, bases, attrs)
+
+
+class BaseModelForm(forms.BaseForm):
+  """Base class for ModelForm.
+
+  This overrides the forms.BaseForm constructor and adds a save() method.
+
+  This class does not have a special metaclass; the magic metaclass is
+  added by the subclass ModelForm.
+  """
+
+  def __init__(self, data=None, files=None, auto_id=None, prefix=None,
+               initial=None, error_class=None, label_suffix=None,
+               instance=None):
+    """Constructor.
+
+    Args (all optional and defaulting to None):
+      data: dict of data values, typically from a POST request)
+      files: dict of file upload values; Django 0.97 or later only
+      auto_id, prefix: see Django documentation
+      initial: dict of initial values
+      error_class, label_suffix: see Django 0.97 or later documentation
+      instance: Model instance to be used for additional initial values
+
+    Except for initial and instance, these arguments are passed on to
+    the forms.BaseForm constructor unchanged, but only if not None.
+    Some arguments (files, error_class, label_suffix) are only
+    supported by Django 0.97 or later.  Leave these blank (i.e. None)
+    when using Django 0.96.  Their default values will be used with
+    Django 0.97 or later even when they are explicitly set to None.
+    """
+    opts = self._meta
+    self.instance = instance
+    object_data = {}
+    if instance is not None:
+      for name, prop in instance.properties().iteritems():
+        if opts.fields and name not in opts.fields:
+          continue
+        if opts.exclude and name in opts.exclude:
+          continue
+        object_data[name] = prop.get_value_for_form(instance)
+    if initial is not None:
+      object_data.update(initial)
+    kwargs = dict(data=data, files=files, auto_id=auto_id,
+                  prefix=prefix, initial=object_data,
+                  error_class=error_class, label_suffix=label_suffix)
+    kwargs = dict((name, value)
+                  for name, value in kwargs.iteritems()
+                  if value is not None)
+    super(BaseModelForm, self).__init__(**kwargs)
+
+  def save(self, commit=True):
+    """Save this form's cleaned data into a model instance.
+
+    Args:
+      commit: optional bool, default True; if true, the model instance
+        is also saved to the datastore.
+
+    Returns:
+      A model instance.  If a model instance was already associated
+      with this form instance (either passed to the constructor with
+      instance=...  or by a previous save() call), that same instance
+      is updated and returned; if no instance was associated yet, one
+      is created by this call.
+
+    Raises:
+      ValueError if the data couldn't be validated.
+    """
+    if not self.is_bound:
+      raise ValueError('Cannot save an unbound form')
+    opts = self._meta
+    instance = self.instance
+    if instance is None:
+      fail_message = 'created'
+    else:
+      fail_message = 'updated'
+    if self.errors:
+      raise ValueError("The %s could not be %s because the data didn't "
+                       'validate.' % (opts.model.kind(), fail_message))
+    cleaned_data = self._cleaned_data()
+    converted_data = {}
+    propiter = itertools.chain(
+      opts.model.properties().iteritems(),
+      iter([('key_name', StringProperty(name='key_name'))])
+      )
+    for name, prop in propiter:
+      value = cleaned_data.get(name)
+      if value is not None:
+        converted_data[name] = prop.make_value_from_form(value)
+    try:
+      if instance is None:
+        instance = opts.model(**converted_data)
+        self.instance = instance
+      else:
+        for name, value in converted_data.iteritems():
+          if name == 'key_name':
+            continue
+          setattr(instance, name, value)
+    except db.BadValueError, err:
+      raise ValueError('The %s could not be %s (%s)' %
+                       (opts.model.kind(), fail_message, err))
+    if commit:
+      instance.put()
+    return instance
+
+  def _cleaned_data(self):
+    """Helper to retrieve the cleaned data attribute.
+
+    In Django 0.96 this attribute was called self.clean_data.  In 0.97
+    and later it's been renamed to self.cleaned_data, to avoid a name
+    conflict.  This helper abstracts the difference between the
+    versions away from its caller.
+    """
+    try:
+      return self.cleaned_data
+    except AttributeError:
+      return self.clean_data
+
+
+class ModelForm(BaseModelForm):
+  """A Django form tied to a Datastore model.
+
+  Note that this particular class just sets the metaclass; all other
+  functionality is defined in the base class, BaseModelForm, above.
+
+  Usage example:
+
+    from google.appengine.ext import db
+    from google.appengine.ext.db import djangoforms
+
+    # First, define a model class
+    class MyModel(db.Model):
+      foo = db.StringProperty()
+      bar = db.IntegerProperty(required=True, default=42)
+
+    # Now define a form class
+    class MyForm(djangoforms.ModelForm):
+      class Meta:
+        model = MyModel
+
+  You can now instantiate MyForm without arguments to create an
+  unbound form, or with data from a POST request to create a bound
+  form.  You can also pass a model instance with the instance=...
+  keyword argument to create an unbound (!) form whose initial values
+  are taken from the instance.  For bound forms, use the save() method
+  to return a model instance.
+
+  Like Django's own corresponding ModelForm class, the nested Meta
+  class can have two other attributes:
+
+    fields: if present and non-empty, a list of field names to be
+            included in the form; properties not listed here are
+            excluded from the form
+
+    exclude: if present and non-empty, a list of field names to be
+             excluded from the form
+
+  If exclude and fields are both non-empty, names occurring in both
+  are excluded (i.e. exclude wins).  By default all property in the
+  model have a corresponding form field defined.
+
+  It is also possible to define form fields explicitly.  This gives
+  more control over the widget used, constraints, initial value, and
+  so on.  Such form fields are not affected by the nested Meta class's
+  fields and exclude attributes.
+
+  If you define a form field named 'key_name' it will be treated
+  specially and will be used as the value for the key_name parameter
+  to the Model constructor. This allows you to create instances with
+  named keys. The 'key_name' field will be ignored when updating an
+  instance (although it will still be shown on the form).
+  """
+
+  __metaclass__ = ModelFormMetaclass