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):
class Comment(db.EmbeddedDocument):
name = db.StringField(max_length=20, required=True)
value = db.StringField(max_length=20)
tag = db.ReferenceField(Tag)
class Post(db.Document):
......
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
......@@ -47,11 +47,11 @@ class QueryAjaxModelLoader(AjaxModelLoader):
return query.limit(limit).all()
def create_ajax_loader(model, name, fields):
prop = getattr(model, name, None)
def create_ajax_loader(model, name, field_name, fields):
prop = getattr(model, field_name, 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
......@@ -70,3 +70,58 @@ def create_ajax_loader(model, name, fields):
remote_fields.append(field)
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
from .typefmt import DEFAULT_FORMATTERS
from .tools import parse_like_term
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
......@@ -213,6 +213,18 @@ class ModelView(BaseModelView):
# Cache other properties
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):
"""
Inspect model and return list of model fields
......@@ -353,7 +365,7 @@ class ModelView(BaseModelView):
# AJAX foreignkey support
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):
"""
......
......@@ -495,7 +495,7 @@ class InlineModelConverter(InlineModelConverterBase):
if 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
if isinstance(opts, (list, tuple)):
......
......@@ -123,7 +123,7 @@ def convert(*args):
"""
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):
func._converter_for = args
......
......@@ -425,3 +425,36 @@ def test_ajax_fk():
ok_(mdl.model1 is not None)
eq_(mdl.model1.id, model.id)
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():
form = view.create_form()
user_info_form = form.info.unbound_field.args[0]
loader = user_info_form.tag.args[0]
eq_(loader.name, 'userinfo.tag')
eq_(loader.name, 'userinfo-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