Jakob Holmelund avatar Jakob Holmelund committed 8bbb53e

Added ndb.py for compat with NDB project

Comments (0)

Files changed (2)

wtforms/ext/appengine/fields.py

                 raise ValueError(self.gettext(u'Not a valid choice'))
 
 
+class KeyPropertyField(fields.SelectFieldBase):
+    """
+    A field for ``db.ReferenceProperty``. The list items are rendered in a
+    select.
+
+    :param reference_class:
+        A db.Model class which will be used to generate the default query
+        to make the list of items. If this is not specified, The `query`
+        property must be overridden before validation.
+    :param get_label:
+        If a string, use this attribute on the model class as the label
+        associated with each option. If a one-argument callable, this callable
+        will be passed model instance and expected to return the label text.
+        Otherwise, the model object's `__str__` or `__unicode__` will be used.
+    :param allow_blank:
+        If set to true, a blank choice will be added to the top of the list
+        to allow `None` to be chosen.
+    :param blank_text:
+        Use this to override the default blank option's label.
+    """
+    widget = widgets.Select()
+
+    def __init__(self, label=None, validators=None, reference_class=None,
+                 label_attr=None, get_label=None, allow_blank=False,
+                 blank_text=u'', **kwargs):
+        super(KeyPropertyField, self).__init__(label, validators,
+                                                     **kwargs)
+        if label_attr is not None:
+            warnings.warn('label_attr= will be removed in WTForms 1.1, use get_label= instead.', DeprecationWarning)
+            self.get_label = operator.attrgetter(label_attr)
+        elif get_label is None:
+            self.get_label = lambda x: x
+        elif isinstance(get_label, basestring):
+            self.get_label = operator.attrgetter(get_label)
+        else:
+            self.get_label = get_label
+
+        self.allow_blank = allow_blank
+        self.blank_text = blank_text
+        self._set_data(None)
+        if reference_class is not None:
+            self.query = reference_class.query()
+
+    def _get_data(self):
+        if self._formdata is not None:
+            for obj in self.query:
+                if str(obj.key.id()) == self._formdata:
+                    self._set_data(obj)
+                    break
+        return self._data
+
+    def _set_data(self, data):
+        self._data = data
+        self._formdata = None
+
+    data = property(_get_data, _set_data)
+
+    def iter_choices(self):
+        if self.allow_blank:
+            yield (u'__None', self.blank_text, self.data is None)
+
+        for obj in self.query:
+            key = str(obj.key.id())
+            label = self.get_label(obj)
+            yield (key, label, self.data and ( self.data.key == obj.key ) )
+
+    def process_formdata(self, valuelist):
+        if valuelist:
+            if valuelist[0] == '__None':
+                self.data = None
+            else:
+                self._data = None
+                self._formdata = valuelist[0]
+
+    def pre_validate(self, form):
+        if not self.allow_blank or self.data is not None:
+            for obj in self.query:
+                if self.data.key == obj.key:
+                    break
+            else:
+                raise ValueError(self.gettext(u'Not a valid choice'))
+
+
 class StringListPropertyField(fields.TextAreaField):
     """
     A field for ``db.StringListProperty``. The list items are rendered in a
                 raise ValueError(self.gettext(u'Not a valid list'))
 
 
+class IntegerListPropertyField(fields.TextAreaField):
+    """
+    A field for ``db.StringListProperty``. The list items are rendered in a
+    textarea.
+    """
+    def _value(self):
+        if self.raw_data:
+            return self.raw_data[0]
+        else:
+            return self.data and unicode("\n".join(self.data)) or u''
+
+    def process_formdata(self, valuelist):
+        if valuelist:
+            try:
+                self.data = [int(value) for value in valuelist[0].splitlines()]
+            except ValueError:
+                raise ValueError(self.gettext(u'Not a valid integer list'))
+
+
 class GeoPtPropertyField(fields.TextField):
 
     def process_formdata(self, valuelist):

wtforms/ext/appengine/ndb.py

+"""
+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.ext.appengine.fields import GeoPtPropertyField, KeyPropertyField, StringListPropertyField, IntegerListPropertyField
+
+
+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._repeated:
+        return StringListPropertyField(**kwargs)
+    kwargs['validators'].append(validators.length(max=500))
+    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``."""
+    if prop._repeated:
+        return IntegerListPropertyField(**kwargs)
+    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
+
+    return f.DateTimeField(format='%Y-%m-%d %H:%M:%S', **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
+
+    return f.DateField(format='%Y-%m-%d', **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
+
+    return f.DateTimeField(format='%H:%M:%S', **kwargs)
+
+
+def convert_RepeatedProperty(model, prop, kwargs):
+    """Returns a form field for a ``db.ListProperty``."""
+    return None
+
+
+def convert_UserProperty(model, prop, kwargs):
+    """Returns a form field for a ``db.UserProperty``."""
+    return None
+
+
+def convert_StructuredProperty(model, prop, kwargs):
+    """Returns a form field for a ``db.ListProperty``."""
+    return None
+
+
+def convert_LocalStructuredProperty(model, prop, kwargs):
+    """Returns a form field for a ``db.ListProperty``."""
+    return None
+
+
+def convert_JsonProperty(model, prop, kwargs):
+    """Returns a form field for a ``db.ListProperty``."""
+    return None
+
+
+def convert_PickleProperty(model, prop, kwargs):
+    """Returns a form field for a ``db.ListProperty``."""
+    return None
+
+
+def convert_GenericProperty(model, prop, kwargs):
+    """Returns a form field for a ``db.ListProperty``."""
+    kwargs['validators'].append(validators.length(max=500))
+    return get_TextField(kwargs)
+
+
+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_ComputedProperty(model, prop, kwargs):
+    """Returns a form field for a ``db.CategoryProperty``."""
+    return None
+
+
+def convert_GeoPtProperty(model, prop, kwargs):
+    """Returns a form field for a ``db.GeoPtProperty``."""
+    return GeoPtPropertyField(**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 KeyPropertyField(**kwargs)
+
+
+def convert_BlobKeyProperty(model, prop, kwargs):
+    """Returns a form field for a ``db.ListProperty``."""
+    return None
+
+
+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         | repeated support
+    |                    |                   |              | if multiline     |
+    +--------------------+-------------------+--------------+------------------+
+    | BooleanProperty    | BooleanField      | bool         |                  |
+    +--------------------+-------------------+--------------+------------------+
+    | IntegerProperty    | IntegerField      | int or long  |                  | repeated support
+    +--------------------+-------------------+--------------+------------------+
+    | 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]   |
+    +--------------------+-------------------+--------------+------------------+
+    | StringRepeatedPrope| TextAreaField     | list of str  |                  |
+    +--------------------+-------------------+--------------+------------------+
+    | IntegerRepeatedProp| TextAreaField     | list of int  |                  |
+    +--------------------+-------------------+--------------+------------------+
+    | BlobProperty       | FileField         | str          |                  |
+    +--------------------+-------------------+--------------+------------------+
+    | TextProperty       | TextAreaField     | unicode      |                  |
+    +--------------------+-------------------+--------------+------------------+
+    | GeoPtProperty      | TextField         | db.GeoPt     |                  |
+    +--------------------+-------------------+--------------+------------------+
+    | KeyProperty        | KeyProperyField   | ndb.Key      |                  |
+    +--------------------+-------------------+--------------+------------------+
+    | BlobKeyProperty    | None              | ndb.BlobKey  | always skipped   |
+    +--------------------+-------------------+--------------+------------------+
+    | UserProperty       | None              | users.User   | always skipped   |
+    +--------------------+-------------------+--------------+------------------+
+    | StructuredProperty | None              | ndb.Model    | always skipped   |
+    +--------------------+-------------------+--------------+------------------+
+    | LocalStructuredPro | None              | ndb.Model    | always skipped   |
+    +--------------------+-------------------+--------------+------------------+
+    | JsonProperty       | TextField         | unicode      |                  |
+    +--------------------+-------------------+--------------+------------------+
+    | PickleProperty     | None              | bytedata     | always skipped   |
+    +--------------------+-------------------+--------------+------------------+
+    | GenericProperty    | None              | generic      | always skipped   |
+    +--------------------+-------------------+--------------+------------------+
+    | ComputedProperty   | none              |              | always skipped   |
+    +====================+===================+==============+==================+
+
+KeyProperty
+    """
+    default_converters = {
+        'StringProperty':        convert_StringProperty,
+        'BooleanProperty':       convert_BooleanProperty,
+        'IntegerProperty':       convert_IntegerProperty,
+        'FloatProperty':         convert_FloatProperty,
+        'DateTimeProperty':      convert_DateTimeProperty,
+        'DateProperty':          convert_DateProperty,
+        'TimeProperty':          convert_TimeProperty,
+        'UserProperty':          convert_UserProperty,
+        'BlobProperty':          convert_BlobProperty,
+        'TextProperty':          convert_TextProperty,
+        'GeoPtProperty':         convert_GeoPtProperty,
+        'BlobKeyProperty':          convert_BlobKeyProperty,
+        'StructuredProperty':          convert_StructuredProperty,
+        'LocalStructuredProperty':          convert_LocalStructuredProperty,
+        'JsonProperty':         convert_JsonProperty,
+        'PickleProperty':          convert_PickleProperty,
+        'GenericProperty':         convert_GenericProperty,
+        'ComputedProperty':         convert_ComputedProperty,
+    }
+
+    # 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__
+
+        #check for generic property
+        if(prop_type_name == "GenericProperty"):
+            #try to get type from field args
+            generic_type = field_args.get("type")
+            if generic_type:
+                prop_type_name = field_args.get("type")
+            #if no type is found, the generic property uses string set in convert_GenericProperty
+
+        kwargs = {
+            'label': prop._code_name.replace('_', ' ').title(),
+            'default': prop._default,
+            '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 kwargs.get('choices', None):
+            # Use choices in a select field.
+            kwargs['choices'] = [(v, v) for v in kwargs.get('choices')]
+            return f.SelectField(**kwargs)
+
+        if prop._choices:
+            # Use choices in a select field.
+            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
+    field_names = model._properties.keys()
+
+    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._get_kind() + 'Form', (base_class,), field_dict)
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.