ndb.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418
  1. """
  2. Form generation utilities for App Engine's new ``ndb.Model`` class.
  3. The goal of ``model_form()`` is to provide a clean, explicit and predictable
  4. way to create forms based on ``ndb.Model`` classes. No malabarism or black
  5. magic should be necessary to generate a form for models, and to add custom
  6. non-model related fields: ``model_form()`` simply generates a form class
  7. that can be used as it is, or that can be extended directly or even be used
  8. to create other forms using ``model_form()``.
  9. Example usage:
  10. .. code-block:: python
  11. from google.appengine.ext import ndb
  12. from wtforms.ext.appengine.ndb import model_form
  13. # Define an example model and add a record.
  14. class Contact(ndb.Model):
  15. name = ndb.StringProperty(required=True)
  16. city = ndb.StringProperty()
  17. age = ndb.IntegerProperty(required=True)
  18. is_admin = ndb.BooleanProperty(default=False)
  19. new_entity = Contact(key_name='test', name='Test Name', age=17)
  20. new_entity.put()
  21. # Generate a form based on the model.
  22. ContactForm = model_form(Contact)
  23. # Get a form populated with entity data.
  24. entity = Contact.get_by_key_name('test')
  25. form = ContactForm(obj=entity)
  26. Properties from the model can be excluded from the generated form, or it can
  27. include just a set of properties. For example:
  28. .. code-block:: python
  29. # Generate a form based on the model, excluding 'city' and 'is_admin'.
  30. ContactForm = model_form(Contact, exclude=('city', 'is_admin'))
  31. # or...
  32. # Generate a form based on the model, only including 'name' and 'age'.
  33. ContactForm = model_form(Contact, only=('name', 'age'))
  34. The form can be generated setting field arguments:
  35. .. code-block:: python
  36. ContactForm = model_form(Contact, only=('name', 'age'), field_args={
  37. 'name': {
  38. 'label': 'Full name',
  39. 'description': 'Your name',
  40. },
  41. 'age': {
  42. 'label': 'Age',
  43. 'validators': [validators.NumberRange(min=14, max=99)],
  44. }
  45. })
  46. The class returned by ``model_form()`` can be used as a base class for forms
  47. mixing non-model fields and/or other model forms. For example:
  48. .. code-block:: python
  49. # Generate a form based on the model.
  50. BaseContactForm = model_form(Contact)
  51. # Generate a form based on other model.
  52. ExtraContactForm = model_form(MyOtherModel)
  53. class ContactForm(BaseContactForm):
  54. # Add an extra, non-model related field.
  55. subscribe_to_news = f.BooleanField()
  56. # Add the other model form as a subform.
  57. extra = f.FormField(ExtraContactForm)
  58. The class returned by ``model_form()`` can also extend an existing form
  59. class:
  60. .. code-block:: python
  61. class BaseContactForm(Form):
  62. # Add an extra, non-model related field.
  63. subscribe_to_news = f.BooleanField()
  64. # Generate a form based on the model.
  65. ContactForm = model_form(Contact, base_class=BaseContactForm)
  66. """
  67. from wtforms import Form, validators, fields as f
  68. from wtforms.compat import string_types
  69. from wtforms.ext.appengine.fields import GeoPtPropertyField, KeyPropertyField, StringListPropertyField, IntegerListPropertyField
  70. def get_TextField(kwargs):
  71. """
  72. Returns a ``TextField``, applying the ``ndb.StringProperty`` length limit
  73. of 500 bytes.
  74. """
  75. kwargs['validators'].append(validators.length(max=500))
  76. return f.TextField(**kwargs)
  77. def get_IntegerField(kwargs):
  78. """
  79. Returns an ``IntegerField``, applying the ``ndb.IntegerProperty`` range
  80. limits.
  81. """
  82. v = validators.NumberRange(min=-0x8000000000000000, max=0x7fffffffffffffff)
  83. kwargs['validators'].append(v)
  84. return f.IntegerField(**kwargs)
  85. class ModelConverterBase(object):
  86. def __init__(self, converters=None):
  87. """
  88. Constructs the converter, setting the converter callables.
  89. :param converters:
  90. A dictionary of converter callables for each property type. The
  91. callable must accept the arguments (model, prop, kwargs).
  92. """
  93. self.converters = {}
  94. for name in dir(self):
  95. if not name.startswith('convert_'):
  96. continue
  97. self.converters[name[8:]] = getattr(self, name)
  98. def convert(self, model, prop, field_args):
  99. """
  100. Returns a form field for a single model property.
  101. :param model:
  102. The ``db.Model`` class that contains the property.
  103. :param prop:
  104. The model property: a ``db.Property`` instance.
  105. :param field_args:
  106. Optional keyword arguments to construct the field.
  107. """
  108. prop_type_name = type(prop).__name__
  109. # Check for generic property
  110. if(prop_type_name == "GenericProperty"):
  111. # Try to get type from field args
  112. generic_type = field_args.get("type")
  113. if generic_type:
  114. prop_type_name = field_args.get("type")
  115. # If no type is found, the generic property uses string set in convert_GenericProperty
  116. kwargs = {
  117. 'label': prop._code_name.replace('_', ' ').title(),
  118. 'default': prop._default,
  119. 'validators': [],
  120. }
  121. if field_args:
  122. kwargs.update(field_args)
  123. if prop._required and prop_type_name not in self.NO_AUTO_REQUIRED:
  124. kwargs['validators'].append(validators.required())
  125. if kwargs.get('choices', None):
  126. # Use choices in a select field.
  127. kwargs['choices'] = [(v, v) for v in kwargs.get('choices')]
  128. return f.SelectField(**kwargs)
  129. if prop._choices:
  130. # Use choices in a select field.
  131. kwargs['choices'] = [(v, v) for v in prop._choices]
  132. return f.SelectField(**kwargs)
  133. else:
  134. converter = self.converters.get(prop_type_name, None)
  135. if converter is not None:
  136. return converter(model, prop, kwargs)
  137. else:
  138. return self.fallback_converter(model, prop, kwargs)
  139. class ModelConverter(ModelConverterBase):
  140. """
  141. Converts properties from a ``ndb.Model`` class to form fields.
  142. Default conversions between properties and fields:
  143. +====================+===================+==============+==================+
  144. | Property subclass | Field subclass | datatype | notes |
  145. +====================+===================+==============+==================+
  146. | StringProperty | TextField | unicode | TextArea | repeated support
  147. | | | | if multiline |
  148. +--------------------+-------------------+--------------+------------------+
  149. | BooleanProperty | BooleanField | bool | |
  150. +--------------------+-------------------+--------------+------------------+
  151. | IntegerProperty | IntegerField | int or long | | repeated support
  152. +--------------------+-------------------+--------------+------------------+
  153. | FloatProperty | TextField | float | |
  154. +--------------------+-------------------+--------------+------------------+
  155. | DateTimeProperty | DateTimeField | datetime | skipped if |
  156. | | | | auto_now[_add] |
  157. +--------------------+-------------------+--------------+------------------+
  158. | DateProperty | DateField | date | skipped if |
  159. | | | | auto_now[_add] |
  160. +--------------------+-------------------+--------------+------------------+
  161. | TimeProperty | DateTimeField | time | skipped if |
  162. | | | | auto_now[_add] |
  163. +--------------------+-------------------+--------------+------------------+
  164. | TextProperty | TextAreaField | unicode | |
  165. +--------------------+-------------------+--------------+------------------+
  166. | GeoPtProperty | TextField | db.GeoPt | |
  167. +--------------------+-------------------+--------------+------------------+
  168. | KeyProperty | KeyProperyField | ndb.Key | |
  169. +--------------------+-------------------+--------------+------------------+
  170. | BlobKeyProperty | None | ndb.BlobKey | always skipped |
  171. +--------------------+-------------------+--------------+------------------+
  172. | UserProperty | None | users.User | always skipped |
  173. +--------------------+-------------------+--------------+------------------+
  174. | StructuredProperty | None | ndb.Model | always skipped |
  175. +--------------------+-------------------+--------------+------------------+
  176. | LocalStructuredPro | None | ndb.Model | always skipped |
  177. +--------------------+-------------------+--------------+------------------+
  178. | JsonProperty | TextField | unicode | |
  179. +--------------------+-------------------+--------------+------------------+
  180. | PickleProperty | None | bytedata | always skipped |
  181. +--------------------+-------------------+--------------+------------------+
  182. | GenericProperty | None | generic | always skipped |
  183. +--------------------+-------------------+--------------+------------------+
  184. | ComputedProperty | none | | always skipped |
  185. +====================+===================+==============+==================+
  186. """
  187. # Don't automatically add a required validator for these properties
  188. NO_AUTO_REQUIRED = frozenset(['ListProperty', 'StringListProperty', 'BooleanProperty'])
  189. def convert_StringProperty(self, model, prop, kwargs):
  190. """Returns a form field for a ``ndb.StringProperty``."""
  191. if prop._repeated:
  192. return StringListPropertyField(**kwargs)
  193. kwargs['validators'].append(validators.length(max=500))
  194. return get_TextField(kwargs)
  195. def convert_BooleanProperty(self, model, prop, kwargs):
  196. """Returns a form field for a ``ndb.BooleanProperty``."""
  197. return f.BooleanField(**kwargs)
  198. def convert_IntegerProperty(self, model, prop, kwargs):
  199. """Returns a form field for a ``ndb.IntegerProperty``."""
  200. if prop._repeated:
  201. return IntegerListPropertyField(**kwargs)
  202. return get_IntegerField(kwargs)
  203. def convert_FloatProperty(self, model, prop, kwargs):
  204. """Returns a form field for a ``ndb.FloatProperty``."""
  205. return f.FloatField(**kwargs)
  206. def convert_DateTimeProperty(self, model, prop, kwargs):
  207. """Returns a form field for a ``ndb.DateTimeProperty``."""
  208. if prop._auto_now or prop._auto_now_add:
  209. return None
  210. return f.DateTimeField(format='%Y-%m-%d %H:%M:%S', **kwargs)
  211. def convert_DateProperty(self, model, prop, kwargs):
  212. """Returns a form field for a ``ndb.DateProperty``."""
  213. if prop._auto_now or prop._auto_now_add:
  214. return None
  215. return f.DateField(format='%Y-%m-%d', **kwargs)
  216. def convert_TimeProperty(self, model, prop, kwargs):
  217. """Returns a form field for a ``ndb.TimeProperty``."""
  218. if prop._auto_now or prop._auto_now_add:
  219. return None
  220. return f.DateTimeField(format='%H:%M:%S', **kwargs)
  221. def convert_RepeatedProperty(self, model, prop, kwargs):
  222. """Returns a form field for a ``ndb.ListProperty``."""
  223. return None
  224. def convert_UserProperty(self, model, prop, kwargs):
  225. """Returns a form field for a ``ndb.UserProperty``."""
  226. return None
  227. def convert_StructuredProperty(self, model, prop, kwargs):
  228. """Returns a form field for a ``ndb.ListProperty``."""
  229. return None
  230. def convert_LocalStructuredProperty(self, model, prop, kwargs):
  231. """Returns a form field for a ``ndb.ListProperty``."""
  232. return None
  233. def convert_JsonProperty(self, model, prop, kwargs):
  234. """Returns a form field for a ``ndb.ListProperty``."""
  235. return None
  236. def convert_PickleProperty(self, model, prop, kwargs):
  237. """Returns a form field for a ``ndb.ListProperty``."""
  238. return None
  239. def convert_GenericProperty(self, model, prop, kwargs):
  240. """Returns a form field for a ``ndb.ListProperty``."""
  241. kwargs['validators'].append(validators.length(max=500))
  242. return get_TextField(kwargs)
  243. def convert_BlobKeyProperty(self, model, prop, kwargs):
  244. """Returns a form field for a ``ndb.BlobKeyProperty``."""
  245. return f.FileField(**kwargs)
  246. def convert_TextProperty(self, model, prop, kwargs):
  247. """Returns a form field for a ``ndb.TextProperty``."""
  248. return f.TextAreaField(**kwargs)
  249. def convert_ComputedProperty(self, model, prop, kwargs):
  250. """Returns a form field for a ``ndb.ComputedProperty``."""
  251. return None
  252. def convert_GeoPtProperty(self, model, prop, kwargs):
  253. """Returns a form field for a ``ndb.GeoPtProperty``."""
  254. return GeoPtPropertyField(**kwargs)
  255. def convert_KeyProperty(self, model, prop, kwargs):
  256. """Returns a form field for a ``ndb.KeyProperty``."""
  257. if 'reference_class' not in kwargs:
  258. try:
  259. reference_class = prop._kind
  260. except AttributeError:
  261. reference_class = prop._reference_class
  262. if isinstance(reference_class, string_types):
  263. # reference class is a string, try to retrieve the model object.
  264. mod = __import__(model.__module__, None, None, [reference_class], 0)
  265. reference_class = getattr(mod, reference_class)
  266. kwargs['reference_class'] = reference_class
  267. kwargs.setdefault('allow_blank', not prop._required)
  268. return KeyPropertyField(**kwargs)
  269. def model_fields(model, only=None, exclude=None, field_args=None,
  270. converter=None):
  271. """
  272. Extracts and returns a dictionary of form fields for a given
  273. ``db.Model`` class.
  274. :param model:
  275. The ``db.Model`` class to extract fields from.
  276. :param only:
  277. An optional iterable with the property names that should be included in
  278. the form. Only these properties will have fields.
  279. :param exclude:
  280. An optional iterable with the property names that should be excluded
  281. from the form. All other properties will have fields.
  282. :param field_args:
  283. An optional dictionary of field names mapping to a keyword arguments
  284. used to construct each field object.
  285. :param converter:
  286. A converter to generate the fields based on the model properties. If
  287. not set, ``ModelConverter`` is used.
  288. """
  289. converter = converter or ModelConverter()
  290. field_args = field_args or {}
  291. # Get the field names we want to include or exclude, starting with the
  292. # full list of model properties.
  293. props = model._properties
  294. field_names = list(x[0] for x in sorted(props.items(), key=lambda x: x[1]._creation_counter))
  295. if only:
  296. field_names = list(f for f in only if f in field_names)
  297. elif exclude:
  298. field_names = list(f for f in field_names if f not in exclude)
  299. # Create all fields.
  300. field_dict = {}
  301. for name in field_names:
  302. field = converter.convert(model, props[name], field_args.get(name))
  303. if field is not None:
  304. field_dict[name] = field
  305. return field_dict
  306. def model_form(model, base_class=Form, only=None, exclude=None, field_args=None,
  307. converter=None):
  308. """
  309. Creates and returns a dynamic ``wtforms.Form`` class for a given
  310. ``ndb.Model`` class. The form class can be used as it is or serve as a base
  311. for extended form classes, which can then mix non-model related fields,
  312. subforms with other model forms, among other possibilities.
  313. :param model:
  314. The ``ndb.Model`` class to generate a form for.
  315. :param base_class:
  316. Base form class to extend from. Must be a ``wtforms.Form`` subclass.
  317. :param only:
  318. An optional iterable with the property names that should be included in
  319. the form. Only these properties will have fields.
  320. :param exclude:
  321. An optional iterable with the property names that should be excluded
  322. from the form. All other properties will have fields.
  323. :param field_args:
  324. An optional dictionary of field names mapping to keyword arguments
  325. used to construct each field object.
  326. :param converter:
  327. A converter to generate the fields based on the model properties. If
  328. not set, ``ModelConverter`` is used.
  329. """
  330. # Extract the fields from the model.
  331. field_dict = model_fields(model, only, exclude, field_args, converter)
  332. # Return a dynamically created form class, extending from base_class and
  333. # including the created fields as properties.
  334. return type(model._get_kind() + 'Form', (base_class,), field_dict)