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 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 from flask.ext.admin.model.ajax import AjaxModelLoader, DEFAULT_PAGE_SIZE
...@@ -40,3 +40,30 @@ class QueryAjaxModelLoader(AjaxModelLoader): ...@@ -40,3 +40,30 @@ class QueryAjaxModelLoader(AjaxModelLoader):
query = query.filter(or_(*filters)) query = query.filter(or_(*filters))
return query.offset(offset).limit(limit).all() 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 ...@@ -4,8 +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, InlineModelConverterBase, FieldPlaceholder)
FieldPlaceholder)
from flask.ext.admin.model.fields import AjaxSelectField, AjaxSelectMultipleField from flask.ext.admin.model.fields import AjaxSelectField, AjaxSelectMultipleField
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
...@@ -14,6 +13,7 @@ from flask.ext.admin._compat import iteritems ...@@ -14,6 +13,7 @@ from flask.ext.admin._compat import iteritems
from .validators import Unique from .validators import Unique
from .fields import QuerySelectField, QuerySelectMultipleField, InlineModelFormList from .fields import QuerySelectField, QuerySelectMultipleField, InlineModelFormList
from .tools import is_inherited_primary_key, get_column_for_current_model, has_multiple_pks from .tools import is_inherited_primary_key, get_column_for_current_model, has_multiple_pks
from .ajax import create_ajax_loader
try: try:
# Field has better input parsing capabilities. # Field has better input parsing capabilities.
...@@ -72,7 +72,7 @@ class AdminModelConverter(ModelConverterBase): ...@@ -72,7 +72,7 @@ class AdminModelConverter(ModelConverterBase):
return None return None
def _model_select_field(self, prop, multiple, remote_model, **kwargs): 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 loader:
if multiple: if multiple:
...@@ -467,7 +467,7 @@ class InlineModelConverter(InlineModelConverterBase): ...@@ -467,7 +467,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 self.form_admin_class(p)
else: else:
model = getattr(p, 'model', None) model = getattr(p, 'model', None)
...@@ -479,12 +479,36 @@ class InlineModelConverter(InlineModelConverterBase): ...@@ -479,12 +479,36 @@ 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 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 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): def contribute(self, model, form_class, inline_model):
""" """
Generate form fields for inline forms and contribute them to Generate form fields for inline forms and contribute them to
......
...@@ -16,7 +16,7 @@ from flask.ext.admin._backwards import ObsoleteAttr ...@@ -16,7 +16,7 @@ from flask.ext.admin._backwards import ObsoleteAttr
from flask.ext.admin.contrib.sqla import form, filters, tools from flask.ext.admin.contrib.sqla import form, filters, tools
from .typefmt import DEFAULT_FORMATTERS from .typefmt import DEFAULT_FORMATTERS
from .tools import is_inherited_primary_key, get_column_for_current_model, get_query_for_ids 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): class ModelView(BaseModelView):
...@@ -554,9 +554,7 @@ class ModelView(BaseModelView): ...@@ -554,9 +554,7 @@ class ModelView(BaseModelView):
self.model_form_converter) self.model_form_converter)
for m in self.inline_models: for m in self.inline_models:
form_class = inline_converter.contribute(self.model, form_class = inline_converter.contribute(self.model, form_class, m)
form_class,
m)
return form_class return form_class
...@@ -589,30 +587,7 @@ class ModelView(BaseModelView): ...@@ -589,30 +587,7 @@ class ModelView(BaseModelView):
# AJAX foreignkey support # AJAX foreignkey support
def _create_ajax_loader(self, name, fields): def _create_ajax_loader(self, name, fields):
attr = getattr(self.model, name, None) return create_ajax_loader(self.model, self.session, name, name, fields)
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)
# Database-related API # Database-related API
def get_query(self): def get_query(self):
......
...@@ -131,6 +131,8 @@ class ModelConverterBase(object): ...@@ -131,6 +131,8 @@ class ModelConverterBase(object):
class InlineModelConverterBase(object): class InlineModelConverterBase(object):
form_admin_class = InlineFormAdmin
def __init__(self, view): def __init__(self, view):
""" """
Base constructor Base constructor
...@@ -173,8 +175,8 @@ class InlineModelConverterBase(object): ...@@ -173,8 +175,8 @@ class InlineModelConverterBase(object):
- Model class - Model class
""" """
if isinstance(p, tuple): if isinstance(p, tuple):
return InlineFormAdmin(p[0], **p[1]) return self.form_admin_class(p[0], **p[1])
elif isinstance(p, InlineFormAdmin): elif isinstance(p, self.form_admin_class):
return p return p
return None return None
......
...@@ -93,3 +93,51 @@ def test_inline_form(): ...@@ -93,3 +93,51 @@ def test_inline_form():
eq_(rv.status_code, 302) eq_(rv.status_code, 302)
eq_(User.query.count(), 0) eq_(User.query.count(), 0)
eq_(UserInfo.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