forked from s434650/CatOrNot
465 lines
18 KiB
Python
465 lines
18 KiB
Python
|
"""
|
||
|
Form generation utilities for App Engine's ``db.Model`` class.
|
||
|
|
||
|
The goal of ``model_form()`` is to provide a clean, explicit and predictable
|
||
|
way to create forms based on ``db.Model`` classes. No malabarism or black
|
||
|
magic should be necessary to generate a form for models, and to add custom
|
||
|
non-model related fields: ``model_form()`` simply generates a form class
|
||
|
that can be used as it is, or that can be extended directly or even be used
|
||
|
to create other forms using ``model_form()``.
|
||
|
|
||
|
Example usage:
|
||
|
|
||
|
.. code-block:: python
|
||
|
|
||
|
from google.appengine.ext import db
|
||
|
from tipfy.ext.model.form import model_form
|
||
|
|
||
|
# Define an example model and add a record.
|
||
|
class Contact(db.Model):
|
||
|
name = db.StringProperty(required=True)
|
||
|
city = db.StringProperty()
|
||
|
age = db.IntegerProperty(required=True)
|
||
|
is_admin = db.BooleanProperty(default=False)
|
||
|
|
||
|
new_entity = Contact(key_name='test', name='Test Name', age=17)
|
||
|
new_entity.put()
|
||
|
|
||
|
# Generate a form based on the model.
|
||
|
ContactForm = model_form(Contact)
|
||
|
|
||
|
# Get a form populated with entity data.
|
||
|
entity = Contact.get_by_key_name('test')
|
||
|
form = ContactForm(obj=entity)
|
||
|
|
||
|
Properties from the model can be excluded from the generated form, or it can
|
||
|
include just a set of properties. For example:
|
||
|
|
||
|
.. code-block:: python
|
||
|
|
||
|
# Generate a form based on the model, excluding 'city' and 'is_admin'.
|
||
|
ContactForm = model_form(Contact, exclude=('city', 'is_admin'))
|
||
|
|
||
|
# or...
|
||
|
|
||
|
# Generate a form based on the model, only including 'name' and 'age'.
|
||
|
ContactForm = model_form(Contact, only=('name', 'age'))
|
||
|
|
||
|
The form can be generated setting field arguments:
|
||
|
|
||
|
.. code-block:: python
|
||
|
|
||
|
ContactForm = model_form(Contact, only=('name', 'age'), field_args={
|
||
|
'name': {
|
||
|
'label': 'Full name',
|
||
|
'description': 'Your name',
|
||
|
},
|
||
|
'age': {
|
||
|
'label': 'Age',
|
||
|
'validators': [validators.NumberRange(min=14, max=99)],
|
||
|
}
|
||
|
})
|
||
|
|
||
|
The class returned by ``model_form()`` can be used as a base class for forms
|
||
|
mixing non-model fields and/or other model forms. For example:
|
||
|
|
||
|
.. code-block:: python
|
||
|
|
||
|
# Generate a form based on the model.
|
||
|
BaseContactForm = model_form(Contact)
|
||
|
|
||
|
# Generate a form based on other model.
|
||
|
ExtraContactForm = model_form(MyOtherModel)
|
||
|
|
||
|
class ContactForm(BaseContactForm):
|
||
|
# Add an extra, non-model related field.
|
||
|
subscribe_to_news = f.BooleanField()
|
||
|
|
||
|
# Add the other model form as a subform.
|
||
|
extra = f.FormField(ExtraContactForm)
|
||
|
|
||
|
The class returned by ``model_form()`` can also extend an existing form
|
||
|
class:
|
||
|
|
||
|
.. code-block:: python
|
||
|
|
||
|
class BaseContactForm(Form):
|
||
|
# Add an extra, non-model related field.
|
||
|
subscribe_to_news = f.BooleanField()
|
||
|
|
||
|
# Generate a form based on the model.
|
||
|
ContactForm = model_form(Contact, base_class=BaseContactForm)
|
||
|
|
||
|
"""
|
||
|
from wtforms import Form, validators, widgets, fields as f
|
||
|
from wtforms.compat import iteritems
|
||
|
from wtforms.ext.appengine.fields import GeoPtPropertyField, ReferencePropertyField, StringListPropertyField
|
||
|
|
||
|
|
||
|
def get_TextField(kwargs):
|
||
|
"""
|
||
|
Returns a ``TextField``, applying the ``db.StringProperty`` length limit
|
||
|
of 500 bytes.
|
||
|
"""
|
||
|
kwargs['validators'].append(validators.length(max=500))
|
||
|
return f.TextField(**kwargs)
|
||
|
|
||
|
|
||
|
def get_IntegerField(kwargs):
|
||
|
"""
|
||
|
Returns an ``IntegerField``, applying the ``db.IntegerProperty`` range
|
||
|
limits.
|
||
|
"""
|
||
|
v = validators.NumberRange(min=-0x8000000000000000, max=0x7fffffffffffffff)
|
||
|
kwargs['validators'].append(v)
|
||
|
return f.IntegerField(**kwargs)
|
||
|
|
||
|
|
||
|
def convert_StringProperty(model, prop, kwargs):
|
||
|
"""Returns a form field for a ``db.StringProperty``."""
|
||
|
if prop.multiline:
|
||
|
kwargs['validators'].append(validators.length(max=500))
|
||
|
return f.TextAreaField(**kwargs)
|
||
|
else:
|
||
|
return get_TextField(kwargs)
|
||
|
|
||
|
|
||
|
def convert_ByteStringProperty(model, prop, kwargs):
|
||
|
"""Returns a form field for a ``db.ByteStringProperty``."""
|
||
|
return get_TextField(kwargs)
|
||
|
|
||
|
|
||
|
def convert_BooleanProperty(model, prop, kwargs):
|
||
|
"""Returns a form field for a ``db.BooleanProperty``."""
|
||
|
return f.BooleanField(**kwargs)
|
||
|
|
||
|
|
||
|
def convert_IntegerProperty(model, prop, kwargs):
|
||
|
"""Returns a form field for a ``db.IntegerProperty``."""
|
||
|
return get_IntegerField(kwargs)
|
||
|
|
||
|
|
||
|
def convert_FloatProperty(model, prop, kwargs):
|
||
|
"""Returns a form field for a ``db.FloatProperty``."""
|
||
|
return f.FloatField(**kwargs)
|
||
|
|
||
|
|
||
|
def convert_DateTimeProperty(model, prop, kwargs):
|
||
|
"""Returns a form field for a ``db.DateTimeProperty``."""
|
||
|
if prop.auto_now or prop.auto_now_add:
|
||
|
return None
|
||
|
|
||
|
kwargs.setdefault('format', '%Y-%m-%d %H:%M:%S')
|
||
|
return f.DateTimeField(**kwargs)
|
||
|
|
||
|
|
||
|
def convert_DateProperty(model, prop, kwargs):
|
||
|
"""Returns a form field for a ``db.DateProperty``."""
|
||
|
if prop.auto_now or prop.auto_now_add:
|
||
|
return None
|
||
|
|
||
|
kwargs.setdefault('format', '%Y-%m-%d')
|
||
|
return f.DateField(**kwargs)
|
||
|
|
||
|
|
||
|
def convert_TimeProperty(model, prop, kwargs):
|
||
|
"""Returns a form field for a ``db.TimeProperty``."""
|
||
|
if prop.auto_now or prop.auto_now_add:
|
||
|
return None
|
||
|
|
||
|
kwargs.setdefault('format', '%H:%M:%S')
|
||
|
return f.DateTimeField(**kwargs)
|
||
|
|
||
|
|
||
|
def convert_ListProperty(model, prop, kwargs):
|
||
|
"""Returns a form field for a ``db.ListProperty``."""
|
||
|
return None
|
||
|
|
||
|
|
||
|
def convert_StringListProperty(model, prop, kwargs):
|
||
|
"""Returns a form field for a ``db.StringListProperty``."""
|
||
|
return StringListPropertyField(**kwargs)
|
||
|
|
||
|
|
||
|
def convert_ReferenceProperty(model, prop, kwargs):
|
||
|
"""Returns a form field for a ``db.ReferenceProperty``."""
|
||
|
kwargs['reference_class'] = prop.reference_class
|
||
|
kwargs.setdefault('allow_blank', not prop.required)
|
||
|
return ReferencePropertyField(**kwargs)
|
||
|
|
||
|
|
||
|
def convert_SelfReferenceProperty(model, prop, kwargs):
|
||
|
"""Returns a form field for a ``db.SelfReferenceProperty``."""
|
||
|
return None
|
||
|
|
||
|
|
||
|
def convert_UserProperty(model, prop, kwargs):
|
||
|
"""Returns a form field for a ``db.UserProperty``."""
|
||
|
return None
|
||
|
|
||
|
|
||
|
def convert_BlobProperty(model, prop, kwargs):
|
||
|
"""Returns a form field for a ``db.BlobProperty``."""
|
||
|
return f.FileField(**kwargs)
|
||
|
|
||
|
|
||
|
def convert_TextProperty(model, prop, kwargs):
|
||
|
"""Returns a form field for a ``db.TextProperty``."""
|
||
|
return f.TextAreaField(**kwargs)
|
||
|
|
||
|
|
||
|
def convert_CategoryProperty(model, prop, kwargs):
|
||
|
"""Returns a form field for a ``db.CategoryProperty``."""
|
||
|
return get_TextField(kwargs)
|
||
|
|
||
|
|
||
|
def convert_LinkProperty(model, prop, kwargs):
|
||
|
"""Returns a form field for a ``db.LinkProperty``."""
|
||
|
kwargs['validators'].append(validators.url())
|
||
|
return get_TextField(kwargs)
|
||
|
|
||
|
|
||
|
def convert_EmailProperty(model, prop, kwargs):
|
||
|
"""Returns a form field for a ``db.EmailProperty``."""
|
||
|
kwargs['validators'].append(validators.email())
|
||
|
return get_TextField(kwargs)
|
||
|
|
||
|
|
||
|
def convert_GeoPtProperty(model, prop, kwargs):
|
||
|
"""Returns a form field for a ``db.GeoPtProperty``."""
|
||
|
return GeoPtPropertyField(**kwargs)
|
||
|
|
||
|
|
||
|
def convert_IMProperty(model, prop, kwargs):
|
||
|
"""Returns a form field for a ``db.IMProperty``."""
|
||
|
return None
|
||
|
|
||
|
|
||
|
def convert_PhoneNumberProperty(model, prop, kwargs):
|
||
|
"""Returns a form field for a ``db.PhoneNumberProperty``."""
|
||
|
return get_TextField(kwargs)
|
||
|
|
||
|
|
||
|
def convert_PostalAddressProperty(model, prop, kwargs):
|
||
|
"""Returns a form field for a ``db.PostalAddressProperty``."""
|
||
|
return get_TextField(kwargs)
|
||
|
|
||
|
|
||
|
def convert_RatingProperty(model, prop, kwargs):
|
||
|
"""Returns a form field for a ``db.RatingProperty``."""
|
||
|
kwargs['validators'].append(validators.NumberRange(min=0, max=100))
|
||
|
return f.IntegerField(**kwargs)
|
||
|
|
||
|
|
||
|
class ModelConverter(object):
|
||
|
"""
|
||
|
Converts properties from a ``db.Model`` class to form fields.
|
||
|
|
||
|
Default conversions between properties and fields:
|
||
|
|
||
|
+====================+===================+==============+==================+
|
||
|
| Property subclass | Field subclass | datatype | notes |
|
||
|
+====================+===================+==============+==================+
|
||
|
| StringProperty | TextField | unicode | TextArea |
|
||
|
| | | | if multiline |
|
||
|
+--------------------+-------------------+--------------+------------------+
|
||
|
| ByteStringProperty | TextField | str | |
|
||
|
+--------------------+-------------------+--------------+------------------+
|
||
|
| BooleanProperty | BooleanField | bool | |
|
||
|
+--------------------+-------------------+--------------+------------------+
|
||
|
| IntegerProperty | IntegerField | int or long | |
|
||
|
+--------------------+-------------------+--------------+------------------+
|
||
|
| FloatProperty | TextField | float | |
|
||
|
+--------------------+-------------------+--------------+------------------+
|
||
|
| DateTimeProperty | DateTimeField | datetime | skipped if |
|
||
|
| | | | auto_now[_add] |
|
||
|
+--------------------+-------------------+--------------+------------------+
|
||
|
| DateProperty | DateField | date | skipped if |
|
||
|
| | | | auto_now[_add] |
|
||
|
+--------------------+-------------------+--------------+------------------+
|
||
|
| TimeProperty | DateTimeField | time | skipped if |
|
||
|
| | | | auto_now[_add] |
|
||
|
+--------------------+-------------------+--------------+------------------+
|
||
|
| ListProperty | None | list | always skipped |
|
||
|
+--------------------+-------------------+--------------+------------------+
|
||
|
| StringListProperty | TextAreaField | list of str | |
|
||
|
+--------------------+-------------------+--------------+------------------+
|
||
|
| ReferenceProperty | ReferencePropertyF| db.Model | |
|
||
|
+--------------------+-------------------+--------------+------------------+
|
||
|
| SelfReferenceP. | ReferencePropertyF| db.Model | |
|
||
|
+--------------------+-------------------+--------------+------------------+
|
||
|
| UserProperty | None | users.User | always skipped |
|
||
|
+--------------------+-------------------+--------------+------------------+
|
||
|
| BlobProperty | FileField | str | |
|
||
|
+--------------------+-------------------+--------------+------------------+
|
||
|
| TextProperty | TextAreaField | unicode | |
|
||
|
+--------------------+-------------------+--------------+------------------+
|
||
|
| CategoryProperty | TextField | unicode | |
|
||
|
+--------------------+-------------------+--------------+------------------+
|
||
|
| LinkProperty | TextField | unicode | |
|
||
|
+--------------------+-------------------+--------------+------------------+
|
||
|
| EmailProperty | TextField | unicode | |
|
||
|
+--------------------+-------------------+--------------+------------------+
|
||
|
| GeoPtProperty | TextField | db.GeoPt | |
|
||
|
+--------------------+-------------------+--------------+------------------+
|
||
|
| IMProperty | None | db.IM | always skipped |
|
||
|
+--------------------+-------------------+--------------+------------------+
|
||
|
| PhoneNumberProperty| TextField | unicode | |
|
||
|
+--------------------+-------------------+--------------+------------------+
|
||
|
| PostalAddressP. | TextField | unicode | |
|
||
|
+--------------------+-------------------+--------------+------------------+
|
||
|
| RatingProperty | IntegerField | int or long | |
|
||
|
+--------------------+-------------------+--------------+------------------+
|
||
|
| _ReverseReferenceP.| None | <iterable> | always skipped |
|
||
|
+====================+===================+==============+==================+
|
||
|
"""
|
||
|
default_converters = {
|
||
|
'StringProperty': convert_StringProperty,
|
||
|
'ByteStringProperty': convert_ByteStringProperty,
|
||
|
'BooleanProperty': convert_BooleanProperty,
|
||
|
'IntegerProperty': convert_IntegerProperty,
|
||
|
'FloatProperty': convert_FloatProperty,
|
||
|
'DateTimeProperty': convert_DateTimeProperty,
|
||
|
'DateProperty': convert_DateProperty,
|
||
|
'TimeProperty': convert_TimeProperty,
|
||
|
'ListProperty': convert_ListProperty,
|
||
|
'StringListProperty': convert_StringListProperty,
|
||
|
'ReferenceProperty': convert_ReferenceProperty,
|
||
|
'SelfReferenceProperty': convert_SelfReferenceProperty,
|
||
|
'UserProperty': convert_UserProperty,
|
||
|
'BlobProperty': convert_BlobProperty,
|
||
|
'TextProperty': convert_TextProperty,
|
||
|
'CategoryProperty': convert_CategoryProperty,
|
||
|
'LinkProperty': convert_LinkProperty,
|
||
|
'EmailProperty': convert_EmailProperty,
|
||
|
'GeoPtProperty': convert_GeoPtProperty,
|
||
|
'IMProperty': convert_IMProperty,
|
||
|
'PhoneNumberProperty': convert_PhoneNumberProperty,
|
||
|
'PostalAddressProperty': convert_PostalAddressProperty,
|
||
|
'RatingProperty': convert_RatingProperty,
|
||
|
}
|
||
|
|
||
|
# Don't automatically add a required validator for these properties
|
||
|
NO_AUTO_REQUIRED = frozenset(['ListProperty', 'StringListProperty', 'BooleanProperty'])
|
||
|
|
||
|
def __init__(self, converters=None):
|
||
|
"""
|
||
|
Constructs the converter, setting the converter callables.
|
||
|
|
||
|
:param converters:
|
||
|
A dictionary of converter callables for each property type. The
|
||
|
callable must accept the arguments (model, prop, kwargs).
|
||
|
"""
|
||
|
self.converters = converters or self.default_converters
|
||
|
|
||
|
def convert(self, model, prop, field_args):
|
||
|
"""
|
||
|
Returns a form field for a single model property.
|
||
|
|
||
|
:param model:
|
||
|
The ``db.Model`` class that contains the property.
|
||
|
:param prop:
|
||
|
The model property: a ``db.Property`` instance.
|
||
|
:param field_args:
|
||
|
Optional keyword arguments to construct the field.
|
||
|
"""
|
||
|
prop_type_name = type(prop).__name__
|
||
|
kwargs = {
|
||
|
'label': prop.name.replace('_', ' ').title(),
|
||
|
'default': prop.default_value(),
|
||
|
'validators': [],
|
||
|
}
|
||
|
if field_args:
|
||
|
kwargs.update(field_args)
|
||
|
|
||
|
if prop.required and prop_type_name not in self.NO_AUTO_REQUIRED:
|
||
|
kwargs['validators'].append(validators.required())
|
||
|
|
||
|
if prop.choices:
|
||
|
# Use choices in a select field if it was not provided in field_args
|
||
|
if 'choices' not in kwargs:
|
||
|
kwargs['choices'] = [(v, v) for v in prop.choices]
|
||
|
return f.SelectField(**kwargs)
|
||
|
else:
|
||
|
converter = self.converters.get(prop_type_name, None)
|
||
|
if converter is not None:
|
||
|
return converter(model, prop, kwargs)
|
||
|
|
||
|
|
||
|
def model_fields(model, only=None, exclude=None, field_args=None,
|
||
|
converter=None):
|
||
|
"""
|
||
|
Extracts and returns a dictionary of form fields for a given
|
||
|
``db.Model`` class.
|
||
|
|
||
|
:param model:
|
||
|
The ``db.Model`` class to extract fields from.
|
||
|
:param only:
|
||
|
An optional iterable with the property names that should be included in
|
||
|
the form. Only these properties will have fields.
|
||
|
:param exclude:
|
||
|
An optional iterable with the property names that should be excluded
|
||
|
from the form. All other properties will have fields.
|
||
|
:param field_args:
|
||
|
An optional dictionary of field names mapping to a keyword arguments
|
||
|
used to construct each field object.
|
||
|
:param converter:
|
||
|
A converter to generate the fields based on the model properties. If
|
||
|
not set, ``ModelConverter`` is used.
|
||
|
"""
|
||
|
converter = converter or ModelConverter()
|
||
|
field_args = field_args or {}
|
||
|
|
||
|
# Get the field names we want to include or exclude, starting with the
|
||
|
# full list of model properties.
|
||
|
props = model.properties()
|
||
|
sorted_props = sorted(iteritems(props), key=lambda prop: prop[1].creation_counter)
|
||
|
field_names = list(x[0] for x in sorted_props)
|
||
|
|
||
|
if only:
|
||
|
field_names = list(f for f in only if f in field_names)
|
||
|
elif exclude:
|
||
|
field_names = list(f for f in field_names if f not in exclude)
|
||
|
|
||
|
# Create all fields.
|
||
|
field_dict = {}
|
||
|
for name in field_names:
|
||
|
field = converter.convert(model, props[name], field_args.get(name))
|
||
|
if field is not None:
|
||
|
field_dict[name] = field
|
||
|
|
||
|
return field_dict
|
||
|
|
||
|
|
||
|
def model_form(model, base_class=Form, only=None, exclude=None, field_args=None,
|
||
|
converter=None):
|
||
|
"""
|
||
|
Creates and returns a dynamic ``wtforms.Form`` class for a given
|
||
|
``db.Model`` class. The form class can be used as it is or serve as a base
|
||
|
for extended form classes, which can then mix non-model related fields,
|
||
|
subforms with other model forms, among other possibilities.
|
||
|
|
||
|
:param model:
|
||
|
The ``db.Model`` class to generate a form for.
|
||
|
:param base_class:
|
||
|
Base form class to extend from. Must be a ``wtforms.Form`` subclass.
|
||
|
:param only:
|
||
|
An optional iterable with the property names that should be included in
|
||
|
the form. Only these properties will have fields.
|
||
|
:param exclude:
|
||
|
An optional iterable with the property names that should be excluded
|
||
|
from the form. All other properties will have fields.
|
||
|
:param field_args:
|
||
|
An optional dictionary of field names mapping to keyword arguments
|
||
|
used to construct each field object.
|
||
|
:param converter:
|
||
|
A converter to generate the fields based on the model properties. If
|
||
|
not set, ``ModelConverter`` is used.
|
||
|
"""
|
||
|
# Extract the fields from the model.
|
||
|
field_dict = model_fields(model, only, exclude, field_args, converter)
|
||
|
|
||
|
# Return a dynamically created form class, extending from base_class and
|
||
|
# including the created fields as properties.
|
||
|
return type(model.kind() + 'Form', (base_class,), field_dict)
|