diff -r 261778de26ff -r 620f9b141567 thirdparty/google_appengine/google/appengine/ext/db/djangoforms.py --- /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 | | 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