db.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464
  1. """
  2. Form generation utilities for App Engine's ``db.Model`` class.
  3. The goal of ``model_form()`` is to provide a clean, explicit and predictable
  4. way to create forms based on ``db.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 db
  12. from tipfy.ext.model.form import model_form
  13. # Define an example model and add a record.
  14. class Contact(db.Model):
  15. name = db.StringProperty(required=True)
  16. city = db.StringProperty()
  17. age = db.IntegerProperty(required=True)
  18. is_admin = db.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, widgets, fields as f
  68. from wtforms.compat import iteritems
  69. from wtforms.ext.appengine.fields import GeoPtPropertyField, ReferencePropertyField, StringListPropertyField
  70. def get_TextField(kwargs):
  71. """
  72. Returns a ``TextField``, applying the ``db.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 ``db.IntegerProperty`` range
  80. limits.
  81. """
  82. v = validators.NumberRange(min=-0x8000000000000000, max=0x7fffffffffffffff)
  83. kwargs['validators'].append(v)
  84. return f.IntegerField(**kwargs)
  85. def convert_StringProperty(model, prop, kwargs):
  86. """Returns a form field for a ``db.StringProperty``."""
  87. if prop.multiline:
  88. kwargs['validators'].append(validators.length(max=500))
  89. return f.TextAreaField(**kwargs)
  90. else:
  91. return get_TextField(kwargs)
  92. def convert_ByteStringProperty(model, prop, kwargs):
  93. """Returns a form field for a ``db.ByteStringProperty``."""
  94. return get_TextField(kwargs)
  95. def convert_BooleanProperty(model, prop, kwargs):
  96. """Returns a form field for a ``db.BooleanProperty``."""
  97. return f.BooleanField(**kwargs)
  98. def convert_IntegerProperty(model, prop, kwargs):
  99. """Returns a form field for a ``db.IntegerProperty``."""
  100. return get_IntegerField(kwargs)
  101. def convert_FloatProperty(model, prop, kwargs):
  102. """Returns a form field for a ``db.FloatProperty``."""
  103. return f.FloatField(**kwargs)
  104. def convert_DateTimeProperty(model, prop, kwargs):
  105. """Returns a form field for a ``db.DateTimeProperty``."""
  106. if prop.auto_now or prop.auto_now_add:
  107. return None
  108. kwargs.setdefault('format', '%Y-%m-%d %H:%M:%S')
  109. return f.DateTimeField(**kwargs)
  110. def convert_DateProperty(model, prop, kwargs):
  111. """Returns a form field for a ``db.DateProperty``."""
  112. if prop.auto_now or prop.auto_now_add:
  113. return None
  114. kwargs.setdefault('format', '%Y-%m-%d')
  115. return f.DateField(**kwargs)
  116. def convert_TimeProperty(model, prop, kwargs):
  117. """Returns a form field for a ``db.TimeProperty``."""
  118. if prop.auto_now or prop.auto_now_add:
  119. return None
  120. kwargs.setdefault('format', '%H:%M:%S')
  121. return f.DateTimeField(**kwargs)
  122. def convert_ListProperty(model, prop, kwargs):
  123. """Returns a form field for a ``db.ListProperty``."""
  124. return None
  125. def convert_StringListProperty(model, prop, kwargs):
  126. """Returns a form field for a ``db.StringListProperty``."""
  127. return StringListPropertyField(**kwargs)
  128. def convert_ReferenceProperty(model, prop, kwargs):
  129. """Returns a form field for a ``db.ReferenceProperty``."""
  130. kwargs['reference_class'] = prop.reference_class
  131. kwargs.setdefault('allow_blank', not prop.required)
  132. return ReferencePropertyField(**kwargs)
  133. def convert_SelfReferenceProperty(model, prop, kwargs):
  134. """Returns a form field for a ``db.SelfReferenceProperty``."""
  135. return None
  136. def convert_UserProperty(model, prop, kwargs):
  137. """Returns a form field for a ``db.UserProperty``."""
  138. return None
  139. def convert_BlobProperty(model, prop, kwargs):
  140. """Returns a form field for a ``db.BlobProperty``."""
  141. return f.FileField(**kwargs)
  142. def convert_TextProperty(model, prop, kwargs):
  143. """Returns a form field for a ``db.TextProperty``."""
  144. return f.TextAreaField(**kwargs)
  145. def convert_CategoryProperty(model, prop, kwargs):
  146. """Returns a form field for a ``db.CategoryProperty``."""
  147. return get_TextField(kwargs)
  148. def convert_LinkProperty(model, prop, kwargs):
  149. """Returns a form field for a ``db.LinkProperty``."""
  150. kwargs['validators'].append(validators.url())
  151. return get_TextField(kwargs)
  152. def convert_EmailProperty(model, prop, kwargs):
  153. """Returns a form field for a ``db.EmailProperty``."""
  154. kwargs['validators'].append(validators.email())
  155. return get_TextField(kwargs)
  156. def convert_GeoPtProperty(model, prop, kwargs):
  157. """Returns a form field for a ``db.GeoPtProperty``."""
  158. return GeoPtPropertyField(**kwargs)
  159. def convert_IMProperty(model, prop, kwargs):
  160. """Returns a form field for a ``db.IMProperty``."""
  161. return None
  162. def convert_PhoneNumberProperty(model, prop, kwargs):
  163. """Returns a form field for a ``db.PhoneNumberProperty``."""
  164. return get_TextField(kwargs)
  165. def convert_PostalAddressProperty(model, prop, kwargs):
  166. """Returns a form field for a ``db.PostalAddressProperty``."""
  167. return get_TextField(kwargs)
  168. def convert_RatingProperty(model, prop, kwargs):
  169. """Returns a form field for a ``db.RatingProperty``."""
  170. kwargs['validators'].append(validators.NumberRange(min=0, max=100))
  171. return f.IntegerField(**kwargs)
  172. class ModelConverter(object):
  173. """
  174. Converts properties from a ``db.Model`` class to form fields.
  175. Default conversions between properties and fields:
  176. +====================+===================+==============+==================+
  177. | Property subclass | Field subclass | datatype | notes |
  178. +====================+===================+==============+==================+
  179. | StringProperty | TextField | unicode | TextArea |
  180. | | | | if multiline |
  181. +--------------------+-------------------+--------------+------------------+
  182. | ByteStringProperty | TextField | str | |
  183. +--------------------+-------------------+--------------+------------------+
  184. | BooleanProperty | BooleanField | bool | |
  185. +--------------------+-------------------+--------------+------------------+
  186. | IntegerProperty | IntegerField | int or long | |
  187. +--------------------+-------------------+--------------+------------------+
  188. | FloatProperty | TextField | float | |
  189. +--------------------+-------------------+--------------+------------------+
  190. | DateTimeProperty | DateTimeField | datetime | skipped if |
  191. | | | | auto_now[_add] |
  192. +--------------------+-------------------+--------------+------------------+
  193. | DateProperty | DateField | date | skipped if |
  194. | | | | auto_now[_add] |
  195. +--------------------+-------------------+--------------+------------------+
  196. | TimeProperty | DateTimeField | time | skipped if |
  197. | | | | auto_now[_add] |
  198. +--------------------+-------------------+--------------+------------------+
  199. | ListProperty | None | list | always skipped |
  200. +--------------------+-------------------+--------------+------------------+
  201. | StringListProperty | TextAreaField | list of str | |
  202. +--------------------+-------------------+--------------+------------------+
  203. | ReferenceProperty | ReferencePropertyF| db.Model | |
  204. +--------------------+-------------------+--------------+------------------+
  205. | SelfReferenceP. | ReferencePropertyF| db.Model | |
  206. +--------------------+-------------------+--------------+------------------+
  207. | UserProperty | None | users.User | always skipped |
  208. +--------------------+-------------------+--------------+------------------+
  209. | BlobProperty | FileField | str | |
  210. +--------------------+-------------------+--------------+------------------+
  211. | TextProperty | TextAreaField | unicode | |
  212. +--------------------+-------------------+--------------+------------------+
  213. | CategoryProperty | TextField | unicode | |
  214. +--------------------+-------------------+--------------+------------------+
  215. | LinkProperty | TextField | unicode | |
  216. +--------------------+-------------------+--------------+------------------+
  217. | EmailProperty | TextField | unicode | |
  218. +--------------------+-------------------+--------------+------------------+
  219. | GeoPtProperty | TextField | db.GeoPt | |
  220. +--------------------+-------------------+--------------+------------------+
  221. | IMProperty | None | db.IM | always skipped |
  222. +--------------------+-------------------+--------------+------------------+
  223. | PhoneNumberProperty| TextField | unicode | |
  224. +--------------------+-------------------+--------------+------------------+
  225. | PostalAddressP. | TextField | unicode | |
  226. +--------------------+-------------------+--------------+------------------+
  227. | RatingProperty | IntegerField | int or long | |
  228. +--------------------+-------------------+--------------+------------------+
  229. | _ReverseReferenceP.| None | <iterable> | always skipped |
  230. +====================+===================+==============+==================+
  231. """
  232. default_converters = {
  233. 'StringProperty': convert_StringProperty,
  234. 'ByteStringProperty': convert_ByteStringProperty,
  235. 'BooleanProperty': convert_BooleanProperty,
  236. 'IntegerProperty': convert_IntegerProperty,
  237. 'FloatProperty': convert_FloatProperty,
  238. 'DateTimeProperty': convert_DateTimeProperty,
  239. 'DateProperty': convert_DateProperty,
  240. 'TimeProperty': convert_TimeProperty,
  241. 'ListProperty': convert_ListProperty,
  242. 'StringListProperty': convert_StringListProperty,
  243. 'ReferenceProperty': convert_ReferenceProperty,
  244. 'SelfReferenceProperty': convert_SelfReferenceProperty,
  245. 'UserProperty': convert_UserProperty,
  246. 'BlobProperty': convert_BlobProperty,
  247. 'TextProperty': convert_TextProperty,
  248. 'CategoryProperty': convert_CategoryProperty,
  249. 'LinkProperty': convert_LinkProperty,
  250. 'EmailProperty': convert_EmailProperty,
  251. 'GeoPtProperty': convert_GeoPtProperty,
  252. 'IMProperty': convert_IMProperty,
  253. 'PhoneNumberProperty': convert_PhoneNumberProperty,
  254. 'PostalAddressProperty': convert_PostalAddressProperty,
  255. 'RatingProperty': convert_RatingProperty,
  256. }
  257. # Don't automatically add a required validator for these properties
  258. NO_AUTO_REQUIRED = frozenset(['ListProperty', 'StringListProperty', 'BooleanProperty'])
  259. def __init__(self, converters=None):
  260. """
  261. Constructs the converter, setting the converter callables.
  262. :param converters:
  263. A dictionary of converter callables for each property type. The
  264. callable must accept the arguments (model, prop, kwargs).
  265. """
  266. self.converters = converters or self.default_converters
  267. def convert(self, model, prop, field_args):
  268. """
  269. Returns a form field for a single model property.
  270. :param model:
  271. The ``db.Model`` class that contains the property.
  272. :param prop:
  273. The model property: a ``db.Property`` instance.
  274. :param field_args:
  275. Optional keyword arguments to construct the field.
  276. """
  277. prop_type_name = type(prop).__name__
  278. kwargs = {
  279. 'label': prop.name.replace('_', ' ').title(),
  280. 'default': prop.default_value(),
  281. 'validators': [],
  282. }
  283. if field_args:
  284. kwargs.update(field_args)
  285. if prop.required and prop_type_name not in self.NO_AUTO_REQUIRED:
  286. kwargs['validators'].append(validators.required())
  287. if prop.choices:
  288. # Use choices in a select field if it was not provided in field_args
  289. if 'choices' not in kwargs:
  290. kwargs['choices'] = [(v, v) for v in prop.choices]
  291. return f.SelectField(**kwargs)
  292. else:
  293. converter = self.converters.get(prop_type_name, None)
  294. if converter is not None:
  295. return converter(model, prop, kwargs)
  296. def model_fields(model, only=None, exclude=None, field_args=None,
  297. converter=None):
  298. """
  299. Extracts and returns a dictionary of form fields for a given
  300. ``db.Model`` class.
  301. :param model:
  302. The ``db.Model`` class to extract fields from.
  303. :param only:
  304. An optional iterable with the property names that should be included in
  305. the form. Only these properties will have fields.
  306. :param exclude:
  307. An optional iterable with the property names that should be excluded
  308. from the form. All other properties will have fields.
  309. :param field_args:
  310. An optional dictionary of field names mapping to a keyword arguments
  311. used to construct each field object.
  312. :param converter:
  313. A converter to generate the fields based on the model properties. If
  314. not set, ``ModelConverter`` is used.
  315. """
  316. converter = converter or ModelConverter()
  317. field_args = field_args or {}
  318. # Get the field names we want to include or exclude, starting with the
  319. # full list of model properties.
  320. props = model.properties()
  321. sorted_props = sorted(iteritems(props), key=lambda prop: prop[1].creation_counter)
  322. field_names = list(x[0] for x in sorted_props)
  323. if only:
  324. field_names = list(f for f in only if f in field_names)
  325. elif exclude:
  326. field_names = list(f for f in field_names if f not in exclude)
  327. # Create all fields.
  328. field_dict = {}
  329. for name in field_names:
  330. field = converter.convert(model, props[name], field_args.get(name))
  331. if field is not None:
  332. field_dict[name] = field
  333. return field_dict
  334. def model_form(model, base_class=Form, only=None, exclude=None, field_args=None,
  335. converter=None):
  336. """
  337. Creates and returns a dynamic ``wtforms.Form`` class for a given
  338. ``db.Model`` class. The form class can be used as it is or serve as a base
  339. for extended form classes, which can then mix non-model related fields,
  340. subforms with other model forms, among other possibilities.
  341. :param model:
  342. The ``db.Model`` class to generate a form for.
  343. :param base_class:
  344. Base form class to extend from. Must be a ``wtforms.Form`` subclass.
  345. :param only:
  346. An optional iterable with the property names that should be included in
  347. the form. Only these properties will have fields.
  348. :param exclude:
  349. An optional iterable with the property names that should be excluded
  350. from the form. All other properties will have fields.
  351. :param field_args:
  352. An optional dictionary of field names mapping to keyword arguments
  353. used to construct each field object.
  354. :param converter:
  355. A converter to generate the fields based on the model properties. If
  356. not set, ``ModelConverter`` is used.
  357. """
  358. # Extract the fields from the model.
  359. field_dict = model_fields(model, only, exclude, field_args, converter)
  360. # Return a dynamically created form class, extending from base_class and
  361. # including the created fields as properties.
  362. return type(model.kind() + 'Form', (base_class,), field_dict)