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): ...@@ -10,10 +10,11 @@ class ModelFormField(fields.FormField):
""" """
Customized ModelFormField for MongoEngine EmbeddedDocuments. Customized ModelFormField for MongoEngine EmbeddedDocuments.
""" """
def __init__(self, model, *args, **kwargs): def __init__(self, model, view, *args, **kwargs):
super(ModelFormField, self).__init__(*args, **kwargs) super(ModelFormField, self).__init__(*args, **kwargs)
self.model = model self.model = model
self.view = view
def populate_obj(self, obj, name): def populate_obj(self, obj, name):
candidate = getattr(obj, name, None) candidate = getattr(obj, name, None)
...@@ -23,6 +24,8 @@ class ModelFormField(fields.FormField): ...@@ -23,6 +24,8 @@ class ModelFormField(fields.FormField):
self.form.populate_obj(candidate) self.form.populate_obj(candidate)
self.view.on_model_change(self.form, candidate)
class MongoFileField(fields.FileField): class MongoFileField(fields.FileField):
widget = widgets.MongoFileInput() widget = widgets.MongoFileInput()
......
...@@ -7,7 +7,7 @@ from wtforms import fields, validators ...@@ -7,7 +7,7 @@ from wtforms import fields, validators
from flask.ext.mongoengine.wtf import orm, fields as mongo_fields from flask.ext.mongoengine.wtf import orm, fields as mongo_fields
from flask.ext.admin import form 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.fields import InlineFieldList
from flask.ext.admin.model.widgets import InlineFormWidget from flask.ext.admin.model.widgets import InlineFormWidget
from flask.ext.admin._compat import iteritems from flask.ext.admin._compat import iteritems
...@@ -36,6 +36,25 @@ class CustomModelConverter(orm.ModelConverter): ...@@ -36,6 +36,25 @@ class CustomModelConverter(orm.ModelConverter):
return None 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): def convert(self, model, field, field_args):
# Check if it is overridden field # Check if it is overridden field
if isinstance(field, FieldPlaceholder): if isinstance(field, FieldPlaceholder):
...@@ -96,11 +115,15 @@ class CustomModelConverter(orm.ModelConverter): ...@@ -96,11 +115,15 @@ class CustomModelConverter(orm.ModelConverter):
doc_type = field.field.document_type doc_type = field.field.document_type
return mongo_fields.ModelSelectMultipleField(model=doc_type, **kwargs) 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: if field.field.choices:
kwargs['multiple'] = True 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 = { kwargs = {
'validators': [], 'validators': [],
'filters': [], 'filters': [],
...@@ -115,9 +138,21 @@ class CustomModelConverter(orm.ModelConverter): ...@@ -115,9 +138,21 @@ class CustomModelConverter(orm.ModelConverter):
'widget': InlineFormWidget() 'widget': InlineFormWidget()
} }
# TODO: Configurable params? view = self._get_subdocument_config(field.name)
form_class = get_form(field.document_type_obj, self, field_args={})
return ModelFormField(field.document_type_obj, form_class, **kwargs) 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') @orm.converts('ReferenceField')
def conv_Reference(self, model, field, kwargs): def conv_Reference(self, model, field, kwargs):
......
...@@ -98,6 +98,92 @@ class ModelView(BaseModelView): ...@@ -98,6 +98,92 @@ class ModelView(BaseModelView):
List of allowed search field types. 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, def __init__(self, model, name=None,
category=None, endpoint=None, url=None): category=None, endpoint=None, url=None):
""" """
......
...@@ -4,7 +4,7 @@ from sqlalchemy import Boolean, Column ...@@ -4,7 +4,7 @@ from sqlalchemy import Boolean, Column
from flask.ext.admin import form from flask.ext.admin import form
from flask.ext.admin.form import Select2Field from flask.ext.admin.form import Select2Field
from flask.ext.admin.model.form import (converts, ModelConverterBase, from flask.ext.admin.model.form import (converts, ModelConverterBase,
InlineFormAdmin, InlineModelConverterBase, InlineModelFormAdmin, InlineModelConverterBase,
FieldPlaceholder) FieldPlaceholder)
from flask.ext.admin.model.helpers import prettify_name from flask.ext.admin.model.helpers import prettify_name
from flask.ext.admin._backwards import get_property from flask.ext.admin._backwards import get_property
...@@ -437,7 +437,7 @@ class InlineModelConverter(InlineModelConverterBase): ...@@ -437,7 +437,7 @@ class InlineModelConverter(InlineModelConverterBase):
Flask-Admin view object Flask-Admin view object
:param model_converter: :param model_converter:
Model converter class. Will be automatically instantiated with Model converter class. Will be automatically instantiated with
appropriate `InlineFormAdmin` instance. appropriate `InlineModelFormAdmin` instance.
""" """
super(InlineModelConverter, self).__init__(view) super(InlineModelConverter, self).__init__(view)
self.session = session self.session = session
...@@ -449,7 +449,7 @@ class InlineModelConverter(InlineModelConverterBase): ...@@ -449,7 +449,7 @@ class InlineModelConverter(InlineModelConverterBase):
# Special case for model instances # Special case for model instances
if info is None: if info is None:
if hasattr(p, '_sa_class_manager'): if hasattr(p, '_sa_class_manager'):
return InlineFormAdmin(p) return InlineModelFormAdmin(p)
else: else:
model = getattr(p, 'model', None) model = getattr(p, 'model', None)
...@@ -461,9 +461,9 @@ class InlineModelConverter(InlineModelConverterBase): ...@@ -461,9 +461,9 @@ class InlineModelConverter(InlineModelConverterBase):
if not attr.startswith('_') and attr != 'model': if not attr.startswith('_') and attr != 'model':
attrs[attr] = getattr(p, attr) attrs[attr] = getattr(p, attr)
return InlineFormAdmin(model, **attrs) return InlineModelFormAdmin(model, **attrs)
info = InlineFormAdmin(model, **attrs) info = InlineModelFormAdmin(model, **attrs)
return info return info
......
...@@ -21,19 +21,15 @@ class InlineFormAdmin(object): ...@@ -21,19 +21,15 @@ class InlineFormAdmin(object):
class MyUserInfoForm(InlineFormAdmin): class MyUserInfoForm(InlineFormAdmin):
form_columns = ('name', 'email') 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 Constructor
:param model:
Target model class
:param kwargs: :param kwargs:
Additional options Additional options
""" """
self.model = model
for k in self._defaults: for k in self._defaults:
if not hasattr(self, k): if not hasattr(self, k):
setattr(self, k, None) setattr(self, k, None)
...@@ -76,6 +72,23 @@ class InlineFormAdmin(object): ...@@ -76,6 +72,23 @@ class InlineFormAdmin(object):
pass 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): class ModelConverterBase(object):
def __init__(self, converters=None, use_mro=True): def __init__(self, converters=None, use_mro=True):
self.use_mro = use_mro self.use_mro = use_mro
...@@ -160,8 +173,8 @@ class InlineModelConverterBase(object): ...@@ -160,8 +173,8 @@ class InlineModelConverterBase(object):
- Model class - Model class
""" """
if isinstance(p, tuple): if isinstance(p, tuple):
return InlineFormAdmin(p[0], **p[1]) return InlineModelFormAdmin(p[0], **p[1])
elif isinstance(p, InlineFormAdmin): elif isinstance(p, InlineModelFormAdmin):
return p return p
return None return None
......
...@@ -212,3 +212,141 @@ def test_custom_form_base(): ...@@ -212,3 +212,141 @@ def test_custom_form_base():
create_form = view.create_form() create_form = view.create_form()
ok_(isinstance(create_form, TestForm)) 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