--- /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