Commit f2107232 authored by Serge S. Koval's avatar Serge S. Koval

AJAX ReferenceField in subdocuments support. Fixed #296

parent 0e4f34fa
...@@ -50,6 +50,7 @@ class Tag(db.Document): ...@@ -50,6 +50,7 @@ class Tag(db.Document):
class Comment(db.EmbeddedDocument): class Comment(db.EmbeddedDocument):
name = db.StringField(max_length=20, required=True) name = db.StringField(max_length=20, required=True)
value = db.StringField(max_length=20) value = db.StringField(max_length=20)
tag = db.ReferenceField(Tag)
class Post(db.Document): class Post(db.Document):
......
import mongoengine import mongoengine
from flask.ext.admin._compat import string_types, as_unicode from flask.ext.admin._compat import string_types, as_unicode, iteritems
from flask.ext.admin.model.ajax import AjaxModelLoader, DEFAULT_PAGE_SIZE from flask.ext.admin.model.ajax import AjaxModelLoader, DEFAULT_PAGE_SIZE
...@@ -47,11 +47,11 @@ class QueryAjaxModelLoader(AjaxModelLoader): ...@@ -47,11 +47,11 @@ class QueryAjaxModelLoader(AjaxModelLoader):
return query.limit(limit).all() return query.limit(limit).all()
def create_ajax_loader(model, name, fields): def create_ajax_loader(model, name, field_name, fields):
prop = getattr(model, name, None) prop = getattr(model, field_name, None)
if prop is None: if prop is None:
raise ValueError('Model %s does not have field %s.' % (model, name)) raise ValueError('Model %s does not have field %s.' % (model, field_name))
# TODO: Check for field # TODO: Check for field
...@@ -70,3 +70,58 @@ def create_ajax_loader(model, name, fields): ...@@ -70,3 +70,58 @@ def create_ajax_loader(model, name, fields):
remote_fields.append(field) remote_fields.append(field)
return QueryAjaxModelLoader(name, remote_model, remote_fields) return QueryAjaxModelLoader(name, remote_model, remote_fields)
def process_ajax_references(references, view):
def make_name(base, name):
if base:
return ('%s-%s' % (base, name)).lower()
else:
return as_unicode(name).lower()
def handle_field(field, subdoc, base):
ftype = type(field).__name__
if ftype == 'ListField':
child_doc = getattr(subdoc, '_form_subdocuments', {}).get(None)
if child_doc:
handle_field(field.field, child_doc, base)
elif ftype == 'EmbeddedDocumentField':
result = {}
ajax_refs = getattr(subdoc, 'form_ajax_refs', {})
for field_name, opts in iteritems(ajax_refs):
child_name = make_name(base, field_name)
if isinstance(opts, (list, tuple)):
loader = create_ajax_loader(field.document_type_obj, child_name, field_name, opts)
else:
loader = opts
result[field_name] = loader
references[child_name] = loader
subdoc._form_ajax_refs = result
child_doc = getattr(subdoc, '_form_subdocuments', None)
if child_doc:
handle_subdoc(field.document_type_obj, subdoc, base)
else:
raise ValueError('Failed to process subdocument field %s' % (field,))
def handle_subdoc(model, subdoc, base):
documents = getattr(subdoc, '_form_subdocuments', {})
for name, doc in iteritems(documents):
field = getattr(model, name, None)
if not field:
raise ValueError('Invalid subdocument field %s.%s')
handle_field(field, doc, make_name(base, name))
handle_subdoc(view.model, view, '')
return references
...@@ -18,7 +18,7 @@ from .form import get_form, CustomModelConverter ...@@ -18,7 +18,7 @@ from .form import get_form, CustomModelConverter
from .typefmt import DEFAULT_FORMATTERS from .typefmt import DEFAULT_FORMATTERS
from .tools import parse_like_term from .tools import parse_like_term
from .helpers import format_error from .helpers import format_error
from .ajax import QueryAjaxModelLoader, create_ajax_loader from .ajax import process_ajax_references, create_ajax_loader
from .subdoc import convert_subdocuments from .subdoc import convert_subdocuments
...@@ -213,6 +213,18 @@ class ModelView(BaseModelView): ...@@ -213,6 +213,18 @@ class ModelView(BaseModelView):
# Cache other properties # Cache other properties
super(ModelView, self)._refresh_cache() super(ModelView, self)._refresh_cache()
def _process_ajax_references(self):
"""
AJAX endpoint is exposed by top-level admin view class, but
subdocuments might have AJAX references too.
This method will recursively go over subdocument configuration
and will precompute AJAX references for them ensuring that
subdocuments can also use AJAX to populate their ReferenceFields.
"""
references = super(ModelView, self)._process_ajax_references()
return process_ajax_references(references, self)
def _get_model_fields(self, model=None): def _get_model_fields(self, model=None):
""" """
Inspect model and return list of model fields Inspect model and return list of model fields
...@@ -353,7 +365,7 @@ class ModelView(BaseModelView): ...@@ -353,7 +365,7 @@ class ModelView(BaseModelView):
# AJAX foreignkey support # AJAX foreignkey support
def _create_ajax_loader(self, name, fields): def _create_ajax_loader(self, name, fields):
return create_ajax_loader(self.model, name, fields) return create_ajax_loader(self.model, name, name, fields)
def get_query(self): def get_query(self):
""" """
......
...@@ -495,7 +495,7 @@ class InlineModelConverter(InlineModelConverterBase): ...@@ -495,7 +495,7 @@ class InlineModelConverter(InlineModelConverterBase):
if refs: if refs:
for name, opts in iteritems(refs): for name, opts in iteritems(refs):
new_name = '%s.%s' % (info.model.__name__.lower(), name) new_name = '%s-%s' % (info.model.__name__.lower(), name)
loader = None loader = None
if isinstance(opts, (list, tuple)): if isinstance(opts, (list, tuple)):
......
...@@ -123,7 +123,7 @@ def convert(*args): ...@@ -123,7 +123,7 @@ def convert(*args):
""" """
Decorator for field to filter conversion routine. Decorator for field to filter conversion routine.
See :mod:`flask.ext.admin.contrib.sqlamodel.filters` for usage example. See :mod:`flask.ext.admin.contrib.sqla.filters` for usage example.
""" """
def _inner(func): def _inner(func):
func._converter_for = args func._converter_for = args
......
...@@ -425,3 +425,36 @@ def test_ajax_fk(): ...@@ -425,3 +425,36 @@ def test_ajax_fk():
ok_(mdl.model1 is not None) ok_(mdl.model1 is not None)
eq_(mdl.model1.id, model.id) eq_(mdl.model1.id, model.id)
eq_(mdl.model1.test1, u'first') eq_(mdl.model1.test1, u'first')
def test_nested_ajax_refs():
app, db, admin = setup()
# Check recursive
class Comment(db.Document):
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.ReferenceField(Comment)
class Model1(db.Document):
test1 = db.StringField(max_length=20)
nested = db.EmbeddedDocumentField(Nested)
view1 = CustomModelView(
Model1,
form_subdocuments = {
'nested': {
'form_ajax_refs': {
'comment': ['name']
}
}
}
)
form = view1.create_form()
eq_(type(form.nested.form.comment).__name__, 'AjaxSelectField')
print view1._form_ajax_refs
ok_('nested-comment' in view1._form_ajax_refs)
...@@ -137,7 +137,7 @@ def test_inline_form_ajax_fk(): ...@@ -137,7 +137,7 @@ def test_inline_form_ajax_fk():
form = view.create_form() form = view.create_form()
user_info_form = form.info.unbound_field.args[0] user_info_form = form.info.unbound_field.args[0]
loader = user_info_form.tag.args[0] loader = user_info_form.tag.args[0]
eq_(loader.name, 'userinfo.tag') eq_(loader.name, 'userinfo-tag')
eq_(loader.model, Tag) eq_(loader.model, Tag)
ok_('userinfo.tag' in view._form_ajax_refs) ok_('userinfo-tag' in view._form_ajax_refs)
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