Skip to content

Instantly share code, notes, and snippets.

@mentat
Created May 18, 2012 16:18
Show Gist options
  • Save mentat/2726179 to your computer and use it in GitHub Desktop.
Save mentat/2726179 to your computer and use it in GitHub Desktop.
WTForm protorpc.Message converter/creator.
from protorpc import messages
from wtforms import Form, validators, widgets, fields as f
__all__ = ['pb_form']
class ListPropertyField(f.TextAreaField):
"""
A field for any repeated property. The list items are rendered in a
textarea.
"""
def __init__(self, type_, **kwargs):
super(ListPropertyField, self).__init__(**kwargs)
self._type = type_
def _value(self):
if self.raw_data:
return self.raw_data[0]
else:
return self.data and unicode(u"\n".join(filter(unicode, self.data))) or u''
def process_formdata(self, valuelist):
if valuelist:
try:
self.data = [self._type(x) for x in valuelist[0].splitlines()]
except ValueError:
raise ValueError(self.gettext(u'Not a valid list'))
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(pb, prop, kwargs):
"""Returns a form field for a ``protorpc.StringField``."""
if prop.repeated:
return ListPropertyField(unicode, **kwargs)
else:
return get_TextField(kwargs)
def convert_BooleanProperty(pb, prop, kwargs):
"""Returns a form field for a ``protorpc.BooleanField``."""
if prop.repeated:
return ListPropertyField(bool, **kwargs)
return f.BooleanField(**kwargs)
def convert_IntegerProperty(pb, prop, kwargs):
"""Returns a form field for a ``protorpc.IntegerField``."""
if prop.repeated:
return ListPropertyField(long, **kwargs)
return f.IntegerField(**kwargs)
def convert_FloatProperty(pb, prop, kwargs):
"""Returns a form field for a ``protorpc.FloatField``."""
if prop.repeated:
return ListPropertyField(float, **kwargs)
return f.FloatField(**kwargs)
def convert_MessageProperty(pb, prop, kwargs):
"""Returns a form field for a ``protorpc.MesssageField``."""
if prop.repeated:
return f.FieldList(f.FormField(pb_form(prop.type), **kwargs), min_entries=1)
return f.FormField(pb_form(prop.type), **kwargs)
def convert_BlobProperty(pb, prop, kwargs):
"""Returns a form field for a ``protorpc.BytesField``."""
return f.FileField(**kwargs)
class PBConverter(object):
"""
Converts properties from a ``protorpc.Message`` class to form fields.
Default conversions between properties and fields:
+====================+===================+==============+==================+
| Property subclass | Field subclass | datatype | notes |
+====================+===================+==============+==================+
| StringField | TextField | unicode | |
+--------------------+-------------------+--------------+------------------+
| BytesField | FileField | byte | |
+--------------------+-------------------+--------------+------------------+
| BooleanField | BooleanField | bool | |
+--------------------+-------------------+--------------+------------------+
| IntegerField | IntegerField | int or long | |
+--------------------+-------------------+--------------+------------------+
| FloatField | TextField | float | |
+--------------------+-------------------+--------------+------------------+
| MessageField | FormField | dict | Use DictLSP |
+====================+===================+==============+==================+
* https://gist.github.com/40a5ce996aed99a49432
"""
default_converters = {
'StringField': convert_StringProperty,
'BytesField': convert_BlobProperty,
'BooleanField': convert_BooleanProperty,
'IntegerField': convert_IntegerProperty,
'FloatField': convert_FloatProperty,
'MessageField': convert_MessageProperty,
'EnumField': convert_IntegerProperty,
}
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 (pb, prop, kwargs).
"""
self.converters = converters or self.default_converters
def convert(self, pb, field, field_args):
"""
Returns a form field for a single model property.
:param pb:
The ``protorpc.Message`` class that contains the property.
:param field:
The message field: a ``protorpc.Field`` instance.
:param field_args:
Optional keyword arguments to construct the field.
"""
kwargs = {
'label': field.name.replace('_', ' ').title(),
'default': field.default,
'validators': [],
}
if field_args:
kwargs.update(field_args)
if field.required:
kwargs['validators'].append(validators.required())
if isinstance(field, messages.EnumField):
# Use choices in a select field.
kwargs['choices'] = [(v, k.replace('_', ' ')) for k,v in field.type.to_dict().iteritems()]
kwargs['coerce'] = int
kwargs['default'] = 1
return f.SelectField(**kwargs)
else:
converter = self.converters.get(type(field).__name__, None)
if converter is not None:
return converter(pb, field, kwargs)
def pb_fields(pb, only=None, exclude=None, field_args=None,
converter=None):
"""
Extracts and returns a dictionary of form fields for a given
``protorpc.Message`` class.
:param pb:
The ``protorpc.Message`` 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, ``PBConverter`` is used.
"""
converter = converter or PBConverter()
field_args = field_args or {}
# Get the field names we want to include or exclude, starting with the
# full list of model properties.
props = pb._Message__by_name
field_names = list(pb._Message__by_name.iterkeys())
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(pb, props[name], field_args.get(name))
if field is not None:
field_dict[name] = field
return field_dict
def pb_form(pb, base_class=Form, only=None, exclude=None, field_args=None,
converter=None):
"""
Creates and returns a dynamic ``wtforms.Form`` class for a given
``protorpc.Message`` 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 pb:
The ``protorpc.Message`` 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, ``PBConverter`` is used.
"""
# Extract the fields from the model.
field_dict = pb_fields(pb, only, exclude, field_args, converter)
# Return a dynamically created form class, extending from base_class and
# including the created fields as properties.
return type(type(pb).__name__ + 'Form', (base_class,), field_dict)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment