123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255 |
- 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._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:
- 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)
|