Commit 0c2f226a authored by Serge S. Koval's avatar Serge S. Koval

Allow AJAX FKs usage in inline models

parent f6b400ee
from sqlalchemy import or_
from flask.ext.admin._compat import as_unicode
from flask.ext.admin._compat import as_unicode, string_types
from flask.ext.admin.model.ajax import AjaxModelLoader, DEFAULT_PAGE_SIZE
......@@ -40,3 +40,30 @@ class QueryAjaxModelLoader(AjaxModelLoader):
query = query.filter(or_(*filters))
return query.offset(offset).limit(limit).all()
def create_ajax_loader(model, session, name, field_name, fields):
attr = getattr(model, field_name, None)
if attr is None:
raise ValueError('Model %s does not have field %s.' % (model, field_name))
if not hasattr(attr, 'property') or not hasattr(attr.property, 'direction'):
raise ValueError('%s.%s is not a relation.' % (model, field_name))
remote_model = attr.prop.mapper.class_
remote_fields = []
for field in fields:
if isinstance(field, string_types):
attr = getattr(remote_model, field, None)
if not attr:
raise ValueError('%s.%s does not exist.' % (remote_model, field))
remote_fields.append(attr)
else:
# TODO: Figure out if it is valid SQLAlchemy property?
remote_fields.append(field)
return QueryAjaxModelLoader(name, session, remote_model, remote_fields)
......@@ -4,8 +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,
FieldPlaceholder)
InlineModelConverterBase, FieldPlaceholder)
from flask.ext.admin.model.fields import AjaxSelectField, AjaxSelectMultipleField
from flask.ext.admin.model.helpers import prettify_name
from flask.ext.admin._backwards import get_property
......@@ -14,6 +13,7 @@ from flask.ext.admin._compat import iteritems
from .validators import Unique
from .fields import QuerySelectField, QuerySelectMultipleField, InlineModelFormList
from .tools import is_inherited_primary_key, get_column_for_current_model, has_multiple_pks
from .ajax import create_ajax_loader
try:
# Field has better input parsing capabilities.
......@@ -72,7 +72,7 @@ class AdminModelConverter(ModelConverterBase):
return None
def _model_select_field(self, prop, multiple, remote_model, **kwargs):
loader = self.view._form_ajax_refs.get(prop.key)
loader = getattr(self.view, '_form_ajax_refs', {}).get(prop.key)
if loader:
if multiple:
......@@ -467,7 +467,7 @@ class InlineModelConverter(InlineModelConverterBase):
# Special case for model instances
if info is None:
if hasattr(p, '_sa_class_manager'):
return InlineFormAdmin(p)
return self.form_admin_class(p)
else:
model = getattr(p, 'model', None)
......@@ -479,12 +479,36 @@ class InlineModelConverter(InlineModelConverterBase):
if not attr.startswith('_') and attr != 'model':
attrs[attr] = getattr(p, attr)
return InlineFormAdmin(model, **attrs)
return self.form_admin_class(model, **attrs)
info = InlineFormAdmin(model, **attrs)
info = self.form_admin_class(model, **attrs)
# Resolve AJAX FKs
info._form_ajax_refs = self.process_ajax_refs(info)
return info
def process_ajax_refs(self, info):
refs = getattr(info, 'form_ajax_refs', None)
result = {}
if refs:
for name, opts in iteritems(refs):
new_name = '%s.%s' % (info.model.__name__.lower(), name)
loader = None
if isinstance(opts, (list, tuple)):
loader = create_ajax_loader(info.model, self.session, new_name, name, opts)
else:
loader = opts
result[name] = loader
self.view._form_ajax_refs[new_name] = loader
return result
def contribute(self, model, form_class, inline_model):
"""
Generate form fields for inline forms and contribute them to
......
......@@ -16,7 +16,7 @@ from flask.ext.admin._backwards import ObsoleteAttr
from flask.ext.admin.contrib.sqla import form, filters, tools
from .typefmt import DEFAULT_FORMATTERS
from .tools import is_inherited_primary_key, get_column_for_current_model, get_query_for_ids
from .ajax import QueryAjaxModelLoader
from .ajax import create_ajax_loader
class ModelView(BaseModelView):
......@@ -554,9 +554,7 @@ class ModelView(BaseModelView):
self.model_form_converter)
for m in self.inline_models:
form_class = inline_converter.contribute(self.model,
form_class,
m)
form_class = inline_converter.contribute(self.model, form_class, m)
return form_class
......@@ -589,30 +587,7 @@ class ModelView(BaseModelView):
# AJAX foreignkey support
def _create_ajax_loader(self, name, fields):
attr = getattr(self.model, name, None)
if attr is None:
raise ValueError('Model %s does not have field %s.' % (self.model, name))
if not hasattr(attr, 'property') or not hasattr(attr.property, 'direction'):
raise ValueError('%s.%s is not a relation.' % (self.model, name))
remote_model = attr.prop.mapper.class_
remote_fields = []
for field in fields:
if isinstance(field, string_types):
attr = getattr(remote_model, field, None)
if not attr:
raise ValueError('%s.%s does not exist.' % (remote_model, field))
remote_fields.append(attr)
else:
# TODO: Figure out if it is valid SQLAlchemy property?
remote_fields.append(field)
return QueryAjaxModelLoader(name, self.session, remote_model, remote_fields)
return create_ajax_loader(self.model, self.session, name, name, fields)
# Database-related API
def get_query(self):
......
......@@ -131,6 +131,8 @@ class ModelConverterBase(object):
class InlineModelConverterBase(object):
form_admin_class = InlineFormAdmin
def __init__(self, view):
"""
Base constructor
......@@ -173,8 +175,8 @@ class InlineModelConverterBase(object):
- Model class
"""
if isinstance(p, tuple):
return InlineFormAdmin(p[0], **p[1])
elif isinstance(p, InlineFormAdmin):
return self.form_admin_class(p[0], **p[1])
elif isinstance(p, self.form_admin_class):
return p
return None
......
......@@ -93,3 +93,51 @@ def test_inline_form():
eq_(rv.status_code, 302)
eq_(User.query.count(), 0)
eq_(UserInfo.query.count(), 0)
def test_inline_form_ajax_fk():
app, db, admin = setup()
# Set up models and database
class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String, unique=True)
def __init__(self, name=None):
self.name = name
class Tag(db.Model):
__tablename__ = 'tags'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String, unique=True)
class UserInfo(db.Model):
__tablename__ = 'user_info'
id = db.Column(db.Integer, primary_key=True)
key = db.Column(db.String, nullable=False)
val = db.Column(db.String)
user_id = db.Column(db.Integer, db.ForeignKey(User.id))
user = db.relationship(User, backref=db.backref('info', cascade="all, delete-orphan", single_parent=True))
tag_id = db.Column(db.Integer, db.ForeignKey(Tag.id))
tag = db.relationship(Tag, backref='user_info')
db.create_all()
# Set up Admin
class UserModelView(ModelView):
inline_models = [(UserInfo, {'form_ajax_refs': {'tag': ('name',)}})]
view = UserModelView(User, db.session)
admin.add_view(view)
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.model, Tag)
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