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

New data model for form_ajax_refs

parent f2107232
...@@ -122,8 +122,12 @@ class PostAdmin(sqla.ModelView): ...@@ -122,8 +122,12 @@ class PostAdmin(sqla.ModelView):
) )
form_ajax_refs = { form_ajax_refs = {
'user': (User.username, User.email), 'user': {
'tags': (Tag.name,) 'fields': (User.username, User.email)
},
'tags': {
'fields': (Tag.name,)
}
} }
def __init__(self, session): def __init__(self, session):
......
...@@ -5,18 +5,23 @@ from flask.ext.admin.model.ajax import AjaxModelLoader, DEFAULT_PAGE_SIZE ...@@ -5,18 +5,23 @@ from flask.ext.admin.model.ajax import AjaxModelLoader, DEFAULT_PAGE_SIZE
class QueryAjaxModelLoader(AjaxModelLoader): class QueryAjaxModelLoader(AjaxModelLoader):
def __init__(self, name, session, model, fields): def __init__(self, name, session, model, options):
""" """
Constructor. Constructor.
:param fields: :param fields:
Fields to run query against Fields to run query against
""" """
super(QueryAjaxModelLoader, self).__init__(name) super(QueryAjaxModelLoader, self).__init__(name, options)
self.session = session self.session = session
self.model = model self.model = model
self.fields = fields self.fields = options.get('fields')
if not self.fields:
raise ValueError('AJAX loading requires `fields` to be specified for %s.%s' % (model, self.name))
self._cached_fields = self._process_fields()
primary_keys = model._sa_class_manager.mapper.primary_key primary_keys = model._sa_class_manager.mapper.primary_key
if len(primary_keys) > 1: if len(primary_keys) > 1:
...@@ -24,6 +29,23 @@ class QueryAjaxModelLoader(AjaxModelLoader): ...@@ -24,6 +29,23 @@ class QueryAjaxModelLoader(AjaxModelLoader):
self.pk = primary_keys[0].name self.pk = primary_keys[0].name
def _process_fields(self):
remote_fields = []
for field in self.fields:
if isinstance(field, string_types):
attr = getattr(self.model, field, None)
if not attr:
raise ValueError('%s.%s does not exist.' % (self.model, field))
remote_fields.append(attr)
else:
# TODO: Figure out if it is valid SQLAlchemy property?
remote_fields.append(field)
return remote_fields
def format(self, model): def format(self, model):
if not model: if not model:
return None return None
...@@ -36,34 +58,20 @@ class QueryAjaxModelLoader(AjaxModelLoader): ...@@ -36,34 +58,20 @@ class QueryAjaxModelLoader(AjaxModelLoader):
def get_list(self, term, offset=0, limit=DEFAULT_PAGE_SIZE): def get_list(self, term, offset=0, limit=DEFAULT_PAGE_SIZE):
query = self.session.query(self.model) query = self.session.query(self.model)
filters = (field.like(u'%%%s%%' % term) for field in self.fields) filters = (field.like(u'%%%s%%' % term) for field in self._cached_fields)
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): def create_ajax_loader(model, session, name, field_name, options):
attr = getattr(model, field_name, None) 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: if attr is None:
raise ValueError('%s.%s does not exist.' % (remote_model, field)) raise ValueError('Model %s does not have field %s.' % (model, field_name))
remote_fields.append(attr) if not hasattr(attr, 'property') or not hasattr(attr.property, 'direction'):
else: raise ValueError('%s.%s is not a relation.' % (model, field_name))
# TODO: Figure out if it is valid SQLAlchemy property?
remote_fields.append(field)
return QueryAjaxModelLoader(name, session, remote_model, remote_fields) remote_model = attr.prop.mapper.class_
return QueryAjaxModelLoader(name, session, remote_model, options)
...@@ -498,7 +498,7 @@ class InlineModelConverter(InlineModelConverterBase): ...@@ -498,7 +498,7 @@ class InlineModelConverter(InlineModelConverterBase):
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, dict):
loader = create_ajax_loader(info.model, self.session, new_name, name, opts) loader = create_ajax_loader(info.model, self.session, new_name, name, opts)
else: else:
loader = opts loader = opts
......
...@@ -586,8 +586,8 @@ class ModelView(BaseModelView): ...@@ -586,8 +586,8 @@ class ModelView(BaseModelView):
return joined return joined
# AJAX foreignkey support # AJAX foreignkey support
def _create_ajax_loader(self, name, fields): def _create_ajax_loader(self, name, options):
return create_ajax_loader(self.model, self.session, name, name, fields) return create_ajax_loader(self.model, self.session, name, name, options)
# Database-related API # Database-related API
def get_query(self): def get_query(self):
......
...@@ -5,7 +5,7 @@ class AjaxModelLoader(object): ...@@ -5,7 +5,7 @@ class AjaxModelLoader(object):
""" """
Ajax related model loader. Override this to implement custom loading behavior. Ajax related model loader. Override this to implement custom loading behavior.
""" """
def __init__(self, name): def __init__(self, name, options):
""" """
Constructor. Constructor.
...@@ -13,6 +13,7 @@ class AjaxModelLoader(object): ...@@ -13,6 +13,7 @@ class AjaxModelLoader(object):
Field name Field name
""" """
self.name = name self.name = name
self.options = options
def format(self, model): def format(self, model):
""" """
......
...@@ -367,15 +367,28 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -367,15 +367,28 @@ class BaseModelView(BaseView, ActionsMixin):
""" """
Use AJAX for foreign key model loading. Use AJAX for foreign key model loading.
Should contain dictionary, where key is field name and value is enumerable with list to fields Should contain dictionary, where key is field name and value is either a dictionary which
to check against (in remote model). configures AJAX lookups or backend-specific `AjaxModelLoader` class instance.
For example, it can look like:: For example, it can look like::
class MyModelView(BaseModelView): class MyModelView(BaseModelView):
form_ajax_refs = { form_ajax_refs = {
'user': ('first_name', 'last_name', 'email') 'user': {
'fields': ('first_name', 'last_name', 'email')
'page_size': 10
}
} }
Or with SQLAlchemy backend like this::
class MyModelView(BaseModelView):
form_ajax_refs = {
'user': QueryAjaxModelLoader('user', db.session, User, page_size=10)
}
If you need custom loading functionality, you can implement your custom loading behavior
in your `AjaxModelLoader` class.
""" """
# Actions # Actions
...@@ -998,17 +1011,17 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -998,17 +1011,17 @@ class BaseModelView(BaseView, ActionsMixin):
result = {} result = {}
if self.form_ajax_refs: if self.form_ajax_refs:
for name, value in iteritems(self.form_ajax_refs): for name, options in iteritems(self.form_ajax_refs):
if isinstance(value, AjaxModelLoader): if isinstance(options, dict):
result[name] = value result[name] = self._create_ajax_loader(name, options)
elif isinstance(value, (list, tuple)): elif isinstance(options, AjaxModelLoader):
result[name] = self._create_ajax_loader(name, value) result[name] = options
else: else:
raise ValueError('%s.form_ajax_refs can not handle %s types' % (self, type(value))) raise ValueError('%s.form_ajax_refs can not handle %s types' % (self, type(options)))
return result return result
def _create_ajax_loader(self, name, fields): def _create_ajax_loader(self, name, options):
""" """
Model backend will override this to implement AJAX model loading. Model backend will override this to implement AJAX model loading.
""" """
......
...@@ -690,7 +690,9 @@ def test_ajax_fk(): ...@@ -690,7 +690,9 @@ def test_ajax_fk():
Model2, db.session, Model2, db.session,
url='view', url='view',
form_ajax_refs={ form_ajax_refs={
'model1': ('test1', 'test2') 'model1': {
'fields': ('test1', 'test2')
}
} }
) )
admin.add_view(view) admin.add_view(view)
...@@ -774,7 +776,9 @@ def test_ajax_fk_multi(): ...@@ -774,7 +776,9 @@ def test_ajax_fk_multi():
Model2, db.session, Model2, db.session,
url='view', url='view',
form_ajax_refs={ form_ajax_refs={
'model1': ('name',) 'model1': {
'fields': ['name']
}
} }
) )
admin.add_view(view) admin.add_view(view)
......
...@@ -129,7 +129,15 @@ def test_inline_form_ajax_fk(): ...@@ -129,7 +129,15 @@ def test_inline_form_ajax_fk():
# Set up Admin # Set up Admin
class UserModelView(ModelView): class UserModelView(ModelView):
inline_models = [(UserInfo, {'form_ajax_refs': {'tag': ('name',)}})] opts = {
'form_ajax_refs': {
'tag': {
'fields': ['name']
}
}
}
inline_models = [(UserInfo, opts)]
view = UserModelView(User, db.session) view = UserModelView(User, db.session)
admin.add_view(view) admin.add_view(view)
......
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