260 lines
8.8 KiB
Python
260 lines
8.8 KiB
Python
from mongoengine import ReferenceField, ListField
|
|
from mongoengine.base import BaseDocument, DocumentMetaclass, get_document
|
|
|
|
from wtforms import fields, validators
|
|
from flask_mongoengine.wtf import orm, fields as mongo_fields
|
|
|
|
from flask_admin import form
|
|
from flask_admin.model.form import FieldPlaceholder
|
|
from flask_admin.model.fields import InlineFieldList, AjaxSelectField, AjaxSelectMultipleField
|
|
from flask_admin.form.validators import FieldListInputRequired
|
|
from flask_admin._compat import iteritems
|
|
|
|
from .fields import ModelFormField, MongoFileField, MongoImageField
|
|
from .subdoc import EmbeddedForm
|
|
|
|
|
|
class CustomModelConverter(orm.ModelConverter):
|
|
"""
|
|
Customized MongoEngine form conversion class.
|
|
|
|
Injects various Flask-Admin widgets and handles lists with
|
|
customized InlineFieldList field.
|
|
"""
|
|
|
|
def __init__(self, view):
|
|
super(CustomModelConverter, self).__init__()
|
|
|
|
self.view = view
|
|
|
|
def _get_field_override(self, name):
|
|
form_overrides = getattr(self.view, 'form_overrides', None)
|
|
|
|
if form_overrides:
|
|
return form_overrides.get(name)
|
|
|
|
return None
|
|
|
|
def _get_subdocument_config(self, name):
|
|
config = getattr(self.view, '_form_subdocuments', {})
|
|
|
|
p = config.get(name)
|
|
if not p:
|
|
return EmbeddedForm()
|
|
|
|
return p
|
|
|
|
def _convert_choices(self, choices):
|
|
for c in choices:
|
|
if isinstance(c, tuple):
|
|
yield c
|
|
else:
|
|
yield (c, c)
|
|
|
|
def clone_converter(self, view):
|
|
return self.__class__(view)
|
|
|
|
def convert(self, model, field, field_args):
|
|
# Check if it is overridden field
|
|
if isinstance(field, FieldPlaceholder):
|
|
return form.recreate_field(field.field)
|
|
|
|
kwargs = {
|
|
'label': getattr(field, 'verbose_name', None),
|
|
'description': getattr(field, 'help_text', ''),
|
|
'validators': [],
|
|
'filters': [],
|
|
'default': field.default
|
|
}
|
|
|
|
if field_args:
|
|
kwargs.update(field_args)
|
|
|
|
if kwargs['validators']:
|
|
# Create a copy of the list since we will be modifying it.
|
|
kwargs['validators'] = list(kwargs['validators'])
|
|
|
|
if field.required:
|
|
if isinstance(field, ListField):
|
|
kwargs['validators'].append(FieldListInputRequired())
|
|
else:
|
|
kwargs['validators'].append(validators.InputRequired())
|
|
elif not isinstance(field, ListField):
|
|
kwargs['validators'].append(validators.Optional())
|
|
|
|
ftype = type(field).__name__
|
|
|
|
if field.choices:
|
|
kwargs['choices'] = list(self._convert_choices(field.choices))
|
|
|
|
if ftype in self.converters:
|
|
kwargs["coerce"] = self.coerce(ftype)
|
|
if kwargs.pop('multiple', False):
|
|
return fields.SelectMultipleField(**kwargs)
|
|
return fields.SelectField(**kwargs)
|
|
|
|
ftype = type(field).__name__
|
|
|
|
if hasattr(field, 'to_form_field'):
|
|
return field.to_form_field(model, kwargs)
|
|
|
|
override = self._get_field_override(field.name)
|
|
if override:
|
|
return override(**kwargs)
|
|
|
|
if ftype in self.converters:
|
|
return self.converters[ftype](model, field, kwargs)
|
|
|
|
@orm.converts('DateTimeField')
|
|
def conv_DateTime(self, model, field, kwargs):
|
|
kwargs['widget'] = form.DateTimePickerWidget()
|
|
return orm.ModelConverter.conv_DateTime(self, model, field, kwargs)
|
|
|
|
@orm.converts('ListField')
|
|
def conv_List(self, model, field, kwargs):
|
|
if field.field is None:
|
|
raise ValueError('ListField "%s" must have field specified for model %s' % (field.name, model))
|
|
|
|
if isinstance(field.field, ReferenceField):
|
|
loader = getattr(self.view, '_form_ajax_refs', {}).get(field.name)
|
|
if loader:
|
|
return AjaxSelectMultipleField(loader, **kwargs)
|
|
|
|
kwargs['widget'] = form.Select2Widget(multiple=True)
|
|
kwargs.setdefault('validators', []).append(validators.Optional())
|
|
|
|
# TODO: Support AJAX multi-select
|
|
doc_type = field.field.document_type
|
|
return mongo_fields.ModelSelectMultipleField(model=doc_type, **kwargs)
|
|
|
|
# Create converter
|
|
view = self._get_subdocument_config(field.name)
|
|
converter = self.clone_converter(view)
|
|
|
|
if field.field.choices:
|
|
kwargs['multiple'] = True
|
|
return converter.convert(model, field.field, kwargs)
|
|
|
|
unbound_field = converter.convert(model, field.field, {})
|
|
return InlineFieldList(unbound_field, min_entries=0, **kwargs)
|
|
|
|
@orm.converts('EmbeddedDocumentField')
|
|
def conv_EmbeddedDocument(self, model, field, kwargs):
|
|
# FormField does not support validators
|
|
kwargs['validators'] = []
|
|
|
|
view = self._get_subdocument_config(field.name)
|
|
|
|
form_opts = form.FormOpts(widget_args=getattr(view, 'form_widget_args', None),
|
|
form_rules=view._form_rules)
|
|
|
|
form_class = view.get_form()
|
|
if form_class is None:
|
|
converter = self.clone_converter(view)
|
|
form_class = get_form(field.document_type_obj, converter,
|
|
base_class=view.form_base_class or form.BaseForm,
|
|
only=view.form_columns,
|
|
exclude=view.form_excluded_columns,
|
|
field_args=view.form_args,
|
|
extra_fields=view.form_extra_fields)
|
|
|
|
form_class = view.postprocess_form(form_class)
|
|
|
|
return ModelFormField(field.document_type_obj, view, form_class, form_opts=form_opts, **kwargs)
|
|
|
|
@orm.converts('ReferenceField')
|
|
def conv_Reference(self, model, field, kwargs):
|
|
kwargs['allow_blank'] = not field.required
|
|
|
|
loader = getattr(self.view, '_form_ajax_refs', {}).get(field.name)
|
|
if loader:
|
|
return AjaxSelectField(loader, **kwargs)
|
|
|
|
kwargs['widget'] = form.Select2Widget()
|
|
|
|
return orm.ModelConverter.conv_Reference(self, model, field, kwargs)
|
|
|
|
@orm.converts('FileField')
|
|
def conv_File(self, model, field, kwargs):
|
|
return MongoFileField(**kwargs)
|
|
|
|
@orm.converts('ImageField')
|
|
def conv_image(self, model, field, kwargs):
|
|
return MongoImageField(**kwargs)
|
|
|
|
|
|
def get_form(model, converter,
|
|
base_class=form.BaseForm,
|
|
only=None,
|
|
exclude=None,
|
|
field_args=None,
|
|
extra_fields=None):
|
|
"""
|
|
Create a wtforms Form for a given mongoengine Document schema::
|
|
|
|
from flask_mongoengine.wtf import model_form
|
|
from myproject.myapp.schemas import Article
|
|
ArticleForm = model_form(Article)
|
|
|
|
:param model:
|
|
A mongoengine Document schema class
|
|
: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.
|
|
"""
|
|
|
|
if isinstance(model, str):
|
|
model = get_document(model)
|
|
|
|
if not isinstance(model, (BaseDocument, DocumentMetaclass)):
|
|
raise TypeError('Model must be a mongoengine Document schema')
|
|
|
|
field_args = field_args or {}
|
|
|
|
# Find properties
|
|
properties = sorted(((k, v) for k, v in iteritems(model._fields)),
|
|
key=lambda v: v[1].creation_counter)
|
|
|
|
if only:
|
|
props = dict(properties)
|
|
|
|
def find(name):
|
|
if extra_fields and name in extra_fields:
|
|
return FieldPlaceholder(extra_fields[name])
|
|
|
|
p = props.get(name)
|
|
if p is not None:
|
|
return p
|
|
|
|
raise ValueError('Invalid model property name %s.%s' % (model, name))
|
|
|
|
properties = ((p, find(p)) for p in only)
|
|
elif exclude:
|
|
properties = (p for p in properties if p[0] not in exclude)
|
|
|
|
# Create fields
|
|
field_dict = {}
|
|
for name, p in properties:
|
|
field = converter.convert(model, p, field_args.get(name))
|
|
if field is not None:
|
|
field_dict[name] = field
|
|
|
|
# Contribute extra fields
|
|
if not only and extra_fields:
|
|
for name, field in iteritems(extra_fields):
|
|
field_dict[name] = form.recreate_field(field)
|
|
|
|
field_dict['model_class'] = model
|
|
return type(model.__name__ + 'Form', (base_class,), field_dict)
|