Commit 1aec2a18 authored by Serge S. Koval's avatar Serge S. Koval

Fixed #258. Recursive subdocument configuration

parent 5d442bc2
......@@ -10,10 +10,11 @@ class ModelFormField(fields.FormField):
"""
Customized ModelFormField for MongoEngine EmbeddedDocuments.
"""
def __init__(self, model, *args, **kwargs):
def __init__(self, model, view, *args, **kwargs):
super(ModelFormField, self).__init__(*args, **kwargs)
self.model = model
self.view = view
def populate_obj(self, obj, name):
candidate = getattr(obj, name, None)
......@@ -23,6 +24,8 @@ class ModelFormField(fields.FormField):
self.form.populate_obj(candidate)
self.view.on_model_change(self.form, candidate)
class MongoFileField(fields.FileField):
widget = widgets.MongoFileInput()
......
......@@ -7,7 +7,7 @@ from wtforms import fields, validators
from flask.ext.mongoengine.wtf import orm, fields as mongo_fields
from flask.ext.admin import form
from flask.ext.admin.model.form import FieldPlaceholder
from flask.ext.admin.model.form import FieldPlaceholder, InlineFormAdmin
from flask.ext.admin.model.fields import InlineFieldList
from flask.ext.admin.model.widgets import InlineFormWidget
from flask.ext.admin._compat import iteritems
......@@ -36,6 +36,25 @@ class CustomModelConverter(orm.ModelConverter):
return None
def _get_subdocument_config(self, name):
config = getattr(self.view, 'form_subdocuments', {})
print 'x', name, config
p = config.get(name)
if not p:
return InlineFormAdmin()
if isinstance(p, dict):
return InlineFormAdmin(**p)
elif isinstance(p, InlineFormAdmin):
return p
raise ValueError('Invalid subdocument type: expecting dict or instance of InlineFormAdmin, got %s' % type(p))
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):
......@@ -96,11 +115,15 @@ class CustomModelConverter(orm.ModelConverter):
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 self.convert(model, field.field, kwargs)
return converter.convert(model, field.field, kwargs)
unbound_field = self.convert(model, field.field, {})
unbound_field = converter.convert(model, field.field, {})
kwargs = {
'validators': [],
'filters': [],
......@@ -115,9 +138,21 @@ class CustomModelConverter(orm.ModelConverter):
'widget': InlineFormWidget()
}
# TODO: Configurable params?
form_class = get_form(field.document_type_obj, self, field_args={})
return ModelFormField(field.document_type_obj, form_class, **kwargs)
view = self._get_subdocument_config(field.name)
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, **kwargs)
@orm.converts('ReferenceField')
def conv_Reference(self, model, field, kwargs):
......
......@@ -98,6 +98,92 @@ class ModelView(BaseModelView):
List of allowed search field types.
"""
form_subdocuments = None
"""
Subdocument configuration options.
This field accepts dictionary, where key is field name and value is either dictionary or instance of the
`InlineFormAdmin`.
Consider following example::
class Comment(db.EmbeddedDocument):
name = db.StringField(max_length=20, required=True)
value = db.StringField(max_length=20)
class Post(db.Document):
text = db.StringField(max_length=30)
data = db.EmbeddedDocumentField(Comment)
class MyAdmin(ModelView):
form_subdocuments = {
'data': {
'form_subdocuments': {
'form_columns': ('name',)
}
}
}
In this example, `Post` model has child `Comment` subdocument. When generating form for `Comment` embedded
document, Flask-Admin will only create `name` field.
It is also possible to use class-based embedded document configuration:
class CommentEmbed(InlineFormAdmin):
form_columns = ('name',)
class MyAdmin(ModelView):
form_subdocuments = {
'data': CommentEmbed()
}
Arbitrary depth nesting is supported::
class SomeEmbed(InlineFormAdmin):
form_excluded_columns = ('test',)
class CommentEmbed(InlineFormAdmin):
form_columns = ('name',)
form_subdocuments = {
'inner': SomeEmbed()
}
class MyAdmin(ModelView):
form_subdocuments = {
'data': CommentEmbed()
}
There's also support for forms embedded into `ListField`. All you have
to do is to create nested rule with `None` as a name. Even though it
is slightly confusing, but that's how Flask-MongoEngine creates
form fields embedded into ListField::
class Comment(db.EmbeddedDocument):
name = db.StringField(max_length=20, required=True)
value = db.StringField(max_length=20)
class Post(db.Document):
text = db.StringField(max_length=30)
data = db.ListField(db.EmbeddedDocumentField(Comment))
class MyAdmin(ModelView):
form_subdocuments = {
'data': {
'form_subdocuments': {
data: {
'form_subdocuments': {
None: {
'form_columns': ('name',)
}
}
}
}
}
}
"""
def __init__(self, model, name=None,
category=None, endpoint=None, url=None):
"""
......
......@@ -4,7 +4,7 @@ from sqlalchemy import Boolean, Column
from flask.ext.admin import form
from flask.ext.admin.form import Select2Field
from flask.ext.admin.model.form import (converts, ModelConverterBase,
InlineFormAdmin, InlineModelConverterBase,
InlineModelFormAdmin, InlineModelConverterBase,
FieldPlaceholder)
from flask.ext.admin.model.helpers import prettify_name
from flask.ext.admin._backwards import get_property
......@@ -437,7 +437,7 @@ class InlineModelConverter(InlineModelConverterBase):
Flask-Admin view object
:param model_converter:
Model converter class. Will be automatically instantiated with
appropriate `InlineFormAdmin` instance.
appropriate `InlineModelFormAdmin` instance.
"""
super(InlineModelConverter, self).__init__(view)
self.session = session
......@@ -449,7 +449,7 @@ class InlineModelConverter(InlineModelConverterBase):
# Special case for model instances
if info is None:
if hasattr(p, '_sa_class_manager'):
return InlineFormAdmin(p)
return InlineModelFormAdmin(p)
else:
model = getattr(p, 'model', None)
......@@ -461,9 +461,9 @@ class InlineModelConverter(InlineModelConverterBase):
if not attr.startswith('_') and attr != 'model':
attrs[attr] = getattr(p, attr)
return InlineFormAdmin(model, **attrs)
return InlineModelFormAdmin(model, **attrs)
info = InlineFormAdmin(model, **attrs)
info = InlineModelFormAdmin(model, **attrs)
return info
......
......@@ -21,19 +21,15 @@ class InlineFormAdmin(object):
class MyUserInfoForm(InlineFormAdmin):
form_columns = ('name', 'email')
"""
_defaults = ['form_columns', 'form_excluded_columns', 'form_args']
_defaults = ['form_base_class', 'form_columns', 'form_excluded_columns', 'form_args', 'form_extra_fields']
def __init__(self, model, **kwargs):
def __init__(self, **kwargs):
"""
Constructor
:param model:
Target model class
:param kwargs:
Additional options
"""
self.model = model
for k in self._defaults:
if not hasattr(self, k):
setattr(self, k, None)
......@@ -76,6 +72,23 @@ class InlineFormAdmin(object):
pass
class InlineModelFormAdmin(InlineFormAdmin):
"""
Settings for inline form administration. Used by relational backends (SQLAlchemy, Peewee), where model
class can not be inherited from the parent model definition.
"""
def __init__(self, model, **kwargs):
"""
Constructor
:param model:
Model class
"""
self.model = model
super(InlineModelFormAdmin, self).__init__(**kwargs)
class ModelConverterBase(object):
def __init__(self, converters=None, use_mro=True):
self.use_mro = use_mro
......@@ -160,8 +173,8 @@ class InlineModelConverterBase(object):
- Model class
"""
if isinstance(p, tuple):
return InlineFormAdmin(p[0], **p[1])
elif isinstance(p, InlineFormAdmin):
return InlineModelFormAdmin(p[0], **p[1])
elif isinstance(p, InlineModelFormAdmin):
return p
return None
......
......@@ -212,3 +212,141 @@ def test_custom_form_base():
create_form = view.create_form()
ok_(isinstance(create_form, TestForm))
def test_subdocument_config():
app, db, admin = setup()
class Comment(db.EmbeddedDocument):
name = db.StringField(max_length=20, required=True)
value = db.StringField(max_length=20)
class Model1(db.Document):
test1 = db.StringField(max_length=20)
subdoc = db.EmbeddedDocumentField(Comment)
# Check only
view1 = CustomModelView(
Model1,
form_subdocuments = {
'subdoc': {
'form_columns': ('name',)
}
}
)
ok_(hasattr(view1._create_form_class, 'subdoc'))
form = view1.create_form()
ok_('name' in dir(form.subdoc.form))
ok_('value' not in dir(form.subdoc.form))
# Check exclude
view2 = CustomModelView(
Model1,
form_subdocuments = {
'subdoc': {
'form_excluded_columns': ('value',)
}
}
)
form = view2.create_form()
ok_('name' in dir(form.subdoc.form))
ok_('value' not in dir(form.subdoc.form))
def test_subdocument_class_config():
app, db, admin = setup()
from flask.ext.admin.model.form import InlineFormAdmin
class Comment(db.EmbeddedDocument):
name = db.StringField(max_length=20, required=True)
value = db.StringField(max_length=20)
class Model1(db.Document):
test1 = db.StringField(max_length=20)
subdoc = db.EmbeddedDocumentField(Comment)
class EmbeddedConfig(InlineFormAdmin):
form_columns = ('name',)
# Check only
view1 = CustomModelView(
Model1,
form_subdocuments = {
'subdoc': EmbeddedConfig()
}
)
form = view1.create_form()
ok_('name' in dir(form.subdoc.form))
ok_('value' not in dir(form.subdoc.form))
def test_nested_subdocument_config():
app, db, admin = setup()
# Check recursive
class Comment(db.EmbeddedDocument):
name = db.StringField(max_length=20, required=True)
value = db.StringField(max_length=20)
class Nested(db.EmbeddedDocument):
name = db.StringField(max_length=20, required=True)
comment = db.EmbeddedDocumentField(Comment)
class Model1(db.Document):
test1 = db.StringField(max_length=20)
nested = db.EmbeddedDocumentField(Nested)
view1 = CustomModelView(
Model1,
form_subdocuments = {
'nested': {
'form_subdocuments': {
'comment': {
'form_columns': ('name',)
}
}
}
}
)
form = view1.create_form()
ok_('name' in dir(form.nested.form.comment.form))
ok_('value' not in dir(form.nested.form.comment.form))
def test_nested_list_subdocument():
app, db, admin = setup()
class Comment(db.EmbeddedDocument):
name = db.StringField(max_length=20, required=True)
value = db.StringField(max_length=20)
class Model1(db.Document):
test1 = db.StringField(max_length=20)
subdoc = db.ListField(db.EmbeddedDocumentField(Comment))
# Check only
view1 = CustomModelView(
Model1,
form_subdocuments = {
'subdoc': {
'form_subdocuments': {
None: {
'form_columns': ('name',)
}
}
}
}
)
form = view1.create_form()
inline_form = form.subdoc.unbound_field.args[2]
ok_('name' in dir(inline_form))
ok_('value' not in dir(inline_form))
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment