form.py 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. from mongoengine import ReferenceField, ListField
  2. from mongoengine.base import BaseDocument, DocumentMetaclass, get_document
  3. from wtforms import fields, validators
  4. from flask_mongoengine.wtf import orm, fields as mongo_fields
  5. from flask_admin import form
  6. from flask_admin.model.form import FieldPlaceholder
  7. from flask_admin.model.fields import InlineFieldList, AjaxSelectField, AjaxSelectMultipleField
  8. from flask_admin._compat import iteritems
  9. from .fields import ModelFormField, MongoFileField, MongoImageField
  10. from .subdoc import EmbeddedForm
  11. class CustomModelConverter(orm.ModelConverter):
  12. """
  13. Customized MongoEngine form conversion class.
  14. Injects various Flask-Admin widgets and handles lists with
  15. customized InlineFieldList field.
  16. """
  17. def __init__(self, view):
  18. super(CustomModelConverter, self).__init__()
  19. self.view = view
  20. def _get_field_override(self, name):
  21. form_overrides = getattr(self.view, 'form_overrides', None)
  22. if form_overrides:
  23. return form_overrides.get(name)
  24. return None
  25. def _get_subdocument_config(self, name):
  26. config = getattr(self.view, '_form_subdocuments', {})
  27. p = config.get(name)
  28. if not p:
  29. return EmbeddedForm()
  30. return p
  31. def _convert_choices(self, choices):
  32. for c in choices:
  33. if isinstance(c, tuple):
  34. yield c
  35. else:
  36. yield (c, c)
  37. def clone_converter(self, view):
  38. return self.__class__(view)
  39. def convert(self, model, field, field_args):
  40. # Check if it is overridden field
  41. if isinstance(field, FieldPlaceholder):
  42. return form.recreate_field(field.field)
  43. kwargs = {
  44. 'label': getattr(field, 'verbose_name', None),
  45. 'description': getattr(field, 'help_text', ''),
  46. 'validators': [],
  47. 'filters': [],
  48. 'default': field.default
  49. }
  50. if field_args:
  51. kwargs.update(field_args)
  52. if kwargs['validators']:
  53. # Create a copy of the list since we will be modifying it.
  54. kwargs['validators'] = list(kwargs['validators'])
  55. if field.required:
  56. kwargs['validators'].append(validators.InputRequired())
  57. elif not isinstance(field, ListField):
  58. kwargs['validators'].append(validators.Optional())
  59. ftype = type(field).__name__
  60. if field.choices:
  61. kwargs['choices'] = list(self._convert_choices(field.choices))
  62. if ftype in self.converters:
  63. kwargs["coerce"] = self.coerce(ftype)
  64. if kwargs.pop('multiple', False):
  65. return fields.SelectMultipleField(**kwargs)
  66. return fields.SelectField(**kwargs)
  67. ftype = type(field).__name__
  68. if hasattr(field, 'to_form_field'):
  69. return field.to_form_field(model, kwargs)
  70. override = self._get_field_override(field.name)
  71. if override:
  72. return override(**kwargs)
  73. if ftype in self.converters:
  74. return self.converters[ftype](model, field, kwargs)
  75. @orm.converts('DateTimeField')
  76. def conv_DateTime(self, model, field, kwargs):
  77. kwargs['widget'] = form.DateTimePickerWidget()
  78. return orm.ModelConverter.conv_DateTime(self, model, field, kwargs)
  79. @orm.converts('ListField')
  80. def conv_List(self, model, field, kwargs):
  81. if field.field is None:
  82. raise ValueError('ListField "%s" must have field specified for model %s' % (field.name, model))
  83. if isinstance(field.field, ReferenceField):
  84. loader = getattr(self.view, '_form_ajax_refs', {}).get(field.name)
  85. if loader:
  86. return AjaxSelectMultipleField(loader, **kwargs)
  87. kwargs['widget'] = form.Select2Widget(multiple=True)
  88. kwargs.setdefault('validators', []).append(validators.Optional())
  89. # TODO: Support AJAX multi-select
  90. doc_type = field.field.document_type
  91. return mongo_fields.ModelSelectMultipleField(model=doc_type, **kwargs)
  92. # Create converter
  93. view = self._get_subdocument_config(field.name)
  94. converter = self.clone_converter(view)
  95. if field.field.choices:
  96. kwargs['multiple'] = True
  97. return converter.convert(model, field.field, kwargs)
  98. unbound_field = converter.convert(model, field.field, {})
  99. return InlineFieldList(unbound_field, min_entries=0, **kwargs)
  100. @orm.converts('EmbeddedDocumentField')
  101. def conv_EmbeddedDocument(self, model, field, kwargs):
  102. # FormField does not support validators
  103. kwargs['validators'] = []
  104. view = self._get_subdocument_config(field.name)
  105. form_opts = form.FormOpts(widget_args=getattr(view, 'form_widget_args', None),
  106. form_rules=view._form_rules)
  107. form_class = view.get_form()
  108. if form_class is None:
  109. converter = self.clone_converter(view)
  110. form_class = get_form(field.document_type_obj, converter,
  111. base_class=view.form_base_class or form.BaseForm,
  112. only=view.form_columns,
  113. exclude=view.form_excluded_columns,
  114. field_args=view.form_args,
  115. extra_fields=view.form_extra_fields)
  116. form_class = view.postprocess_form(form_class)
  117. return ModelFormField(field.document_type_obj, view, form_class, form_opts=form_opts, **kwargs)
  118. @orm.converts('ReferenceField')
  119. def conv_Reference(self, model, field, kwargs):
  120. kwargs['allow_blank'] = not field.required
  121. loader = getattr(self.view, '_form_ajax_refs', {}).get(field.name)
  122. if loader:
  123. return AjaxSelectField(loader, **kwargs)
  124. kwargs['widget'] = form.Select2Widget()
  125. return orm.ModelConverter.conv_Reference(self, model, field, kwargs)
  126. @orm.converts('FileField')
  127. def conv_File(self, model, field, kwargs):
  128. return MongoFileField(**kwargs)
  129. @orm.converts('ImageField')
  130. def conv_image(self, model, field, kwargs):
  131. return MongoImageField(**kwargs)
  132. def get_form(model, converter,
  133. base_class=form.BaseForm,
  134. only=None,
  135. exclude=None,
  136. field_args=None,
  137. extra_fields=None):
  138. """
  139. Create a wtforms Form for a given mongoengine Document schema::
  140. from flask_mongoengine.wtf import model_form
  141. from myproject.myapp.schemas import Article
  142. ArticleForm = model_form(Article)
  143. :param model:
  144. A mongoengine Document schema class
  145. :param base_class:
  146. Base form class to extend from. Must be a ``wtforms.Form`` subclass.
  147. :param only:
  148. An optional iterable with the property names that should be included in
  149. the form. Only these properties will have fields.
  150. :param exclude:
  151. An optional iterable with the property names that should be excluded
  152. from the form. All other properties will have fields.
  153. :param field_args:
  154. An optional dictionary of field names mapping to keyword arguments used
  155. to construct each field object.
  156. :param converter:
  157. A converter to generate the fields based on the model properties. If
  158. not set, ``ModelConverter`` is used.
  159. """
  160. if isinstance(model, str):
  161. model = get_document(model)
  162. if not isinstance(model, (BaseDocument, DocumentMetaclass)):
  163. raise TypeError('Model must be a mongoengine Document schema')
  164. field_args = field_args or {}
  165. # Find properties
  166. properties = sorted(((k, v) for k, v in iteritems(model._fields)),
  167. key=lambda v: v[1].creation_counter)
  168. if only:
  169. props = dict(properties)
  170. def find(name):
  171. if extra_fields and name in extra_fields:
  172. return FieldPlaceholder(extra_fields[name])
  173. p = props.get(name)
  174. if p is not None:
  175. return p
  176. raise ValueError('Invalid model property name %s.%s' % (model, name))
  177. properties = ((p, find(p)) for p in only)
  178. elif exclude:
  179. properties = (p for p in properties if p[0] not in exclude)
  180. # Create fields
  181. field_dict = {}
  182. for name, p in properties:
  183. field = converter.convert(model, p, field_args.get(name))
  184. if field is not None:
  185. field_dict[name] = field
  186. # Contribute extra fields
  187. if not only and extra_fields:
  188. for name, field in iteritems(extra_fields):
  189. field_dict[name] = form.recreate_field(field)
  190. field_dict['model_class'] = model
  191. return type(model.__name__ + 'Form', (base_class,), field_dict)