Commit 40f3580f authored by Florian Sachs's avatar Florian Sachs

Merge remote-tracking branch 'upstream/master'

parents d2b6f20a 5202791f
...@@ -79,6 +79,10 @@ class UserView(ModelView): ...@@ -79,6 +79,10 @@ class UserView(ModelView):
class TodoView(ModelView): class TodoView(ModelView):
column_filters = ['done'] column_filters = ['done']
form_ajax_refs = {
'user': ('name',)
}
# Flask views # Flask views
@app.route('/') @app.route('/')
......
...@@ -66,6 +66,10 @@ class PostAdmin(ModelView): ...@@ -66,6 +66,10 @@ class PostAdmin(ModelView):
'date', 'date',
User.username) User.username)
form_ajax_refs = {
'user': (User.username, 'email')
}
@app.route('/') @app.route('/')
def index(): def index():
......
...@@ -121,6 +121,11 @@ class PostAdmin(sqla.ModelView): ...@@ -121,6 +121,11 @@ class PostAdmin(sqla.ModelView):
text=dict(label='Big Text', validators=[validators.required()]) text=dict(label='Big Text', validators=[validators.required()])
) )
form_ajax_refs = {
'user': (User.username, User.email),
'tags': (Tag.name,)
}
def __init__(self, session): def __init__(self, session):
# Just call parent class with predefined model. # Just call parent class with predefined model.
super(PostAdmin, self).__init__(Post, session) super(PostAdmin, self).__init__(Post, session)
......
...@@ -12,7 +12,7 @@ from flask import flash, url_for, redirect, abort, request ...@@ -12,7 +12,7 @@ from flask import flash, url_for, redirect, abort, request
from wtforms import fields, validators from wtforms import fields, validators
from flask.ext.admin import form, helpers from flask.ext.admin import form, helpers
from flask.ext.admin._compat import urljoin from flask.ext.admin._compat import urljoin, as_unicode
from flask.ext.admin.base import BaseView, expose from flask.ext.admin.base import BaseView, expose
from flask.ext.admin.actions import action, ActionsMixin from flask.ext.admin.actions import action, ActionsMixin
from flask.ext.admin.babel import gettext, lazy_gettext from flask.ext.admin.babel import gettext, lazy_gettext
...@@ -173,7 +173,7 @@ class FileAdmin(BaseView, ActionsMixin): ...@@ -173,7 +173,7 @@ class FileAdmin(BaseView, ActionsMixin):
Verify if path exists. If set to `True` and path does not exist Verify if path exists. If set to `True` and path does not exist
will raise an exception. will raise an exception.
""" """
self.base_path = base_path self.base_path = as_unicode(base_path)
self.base_url = base_url self.base_url = base_url
self.init_actions() self.init_actions()
......
import mongoengine
from flask.ext.admin._compat import as_unicode
from flask.ext.admin.model.ajax import AjaxModelLoader, DEFAULT_PAGE_SIZE
class QueryAjaxModelLoader(AjaxModelLoader):
def __init__(self, name, model, fields):
"""
Constructor.
:param fields:
Fields to run query against
"""
super(QueryAjaxModelLoader, self).__init__(name)
self.model = model
self.fields = fields
def format(self, model):
if not model:
return None
return (as_unicode(model.id), as_unicode(model))
def get_one(self, pk):
return self.model.objects.filter(id=pk).first()
def get_list(self, term, offset=0, limit=DEFAULT_PAGE_SIZE):
query = self.model.objects
criteria = None
for field in self.fields:
flt = {u'%s__icontains' % field.name: term}
if not criteria:
criteria = mongoengine.Q(**flt)
else:
criteria |= mongoengine.Q(**flt)
query = query.filter(criteria)
if offset:
query = query.skip(offset)
return query.limit(limit).all()
from operator import attrgetter
from mongoengine import ReferenceField from mongoengine import ReferenceField
from mongoengine.base import BaseDocument, DocumentMetaclass from mongoengine.base import BaseDocument, DocumentMetaclass
...@@ -8,7 +6,7 @@ from flask.ext.mongoengine.wtf import orm, fields as mongo_fields ...@@ -8,7 +6,7 @@ from flask.ext.mongoengine.wtf import orm, fields as mongo_fields
from flask.ext.admin import form from flask.ext.admin import form
from flask.ext.admin.model.form import FieldPlaceholder, InlineBaseFormAdmin from flask.ext.admin.model.form import FieldPlaceholder, InlineBaseFormAdmin
from flask.ext.admin.model.fields import InlineFieldList from flask.ext.admin.model.fields import InlineFieldList, AjaxSelectField, AjaxSelectMultipleField
from flask.ext.admin.model.widgets import InlineFormWidget from flask.ext.admin.model.widgets import InlineFormWidget
from flask.ext.admin._compat import iteritems from flask.ext.admin._compat import iteritems
...@@ -158,9 +156,14 @@ class CustomModelConverter(orm.ModelConverter): ...@@ -158,9 +156,14 @@ class CustomModelConverter(orm.ModelConverter):
@orm.converts('ReferenceField') @orm.converts('ReferenceField')
def conv_Reference(self, model, field, kwargs): def conv_Reference(self, model, field, kwargs):
kwargs['widget'] = form.Select2Widget()
kwargs['allow_blank'] = not field.required kwargs['allow_blank'] = not field.required
loader = self.view._form_ajax_refs.get(field.name)
if loader:
return AjaxSelectField(loader, **kwargs)
kwargs['widget'] = form.Select2Widget()
return orm.ModelConverter.conv_Reference(self, model, field, kwargs) return orm.ModelConverter.conv_Reference(self, model, field, kwargs)
@orm.converts('FileField') @orm.converts('FileField')
......
...@@ -18,6 +18,7 @@ from .form import get_form, CustomModelConverter ...@@ -18,6 +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
SORTABLE_FIELDS = set(( SORTABLE_FIELDS = set((
...@@ -336,6 +337,31 @@ class ModelView(BaseModelView): ...@@ -336,6 +337,31 @@ class ModelView(BaseModelView):
return form_class return form_class
# AJAX foreignkey support
def _create_ajax_loader(self, name, fields):
prop = getattr(self.model, name, None)
if prop is None:
raise ValueError('Model %s does not have field %s.' % (self.model, name))
# TODO: Check for field
remote_model = prop.document_type
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:
remote_fields.append(field)
return QueryAjaxModelLoader(name, remote_model, remote_fields)
def get_query(self): def get_query(self):
""" """
Returns the QuerySet for this view. By default, it returns all the Returns the QuerySet for this view. By default, it returns all the
......
import mongoengine
from flask.ext.admin._compat import as_unicode
from flask.ext.admin.model.ajax import AjaxModelLoader, DEFAULT_PAGE_SIZE
from .tools import get_primary_key
class QueryAjaxModelLoader(AjaxModelLoader):
def __init__(self, name, model, fields):
"""
Constructor.
:param fields:
Fields to run query against
"""
super(QueryAjaxModelLoader, self).__init__(name)
self.model = model
self.fields = fields
self.pk = get_primary_key(model)
def format(self, model):
if not model:
return None
return (getattr(model, self.pk), as_unicode(model))
def get_one(self, pk):
return self.model.get(**{self.pk: pk})
def get_list(self, term, offset=0, limit=DEFAULT_PAGE_SIZE):
query = self.model.select()
stmt = None
for field in self.fields:
q = field ** (u'%%%s%%' % term)
if stmt is None:
stmt = q
else:
stmt |= q
query = query.where(stmt)
if offset:
query = query.offset(offset)
return list(query.limit(limit).execute())
...@@ -8,7 +8,7 @@ from wtfpeewee.orm import ModelConverter, model_form ...@@ -8,7 +8,7 @@ from wtfpeewee.orm import ModelConverter, model_form
from flask.ext.admin import form from flask.ext.admin import form
from flask.ext.admin._compat import iteritems, itervalues from flask.ext.admin._compat import iteritems, itervalues
from flask.ext.admin.model.form import InlineFormAdmin, InlineModelConverterBase from flask.ext.admin.model.form import InlineFormAdmin, InlineModelConverterBase
from flask.ext.admin.model.fields import InlineModelFormField, InlineFieldList from flask.ext.admin.model.fields import InlineModelFormField, InlineFieldList, AjaxSelectField
from .tools import get_primary_key from .tools import get_primary_key
...@@ -80,13 +80,26 @@ class InlineModelFormList(InlineFieldList): ...@@ -80,13 +80,26 @@ class InlineModelFormList(InlineFieldList):
class CustomModelConverter(ModelConverter): class CustomModelConverter(ModelConverter):
def __init__(self, additional=None): def __init__(self, view, additional=None):
super(CustomModelConverter, self).__init__(additional) super(CustomModelConverter, self).__init__(additional)
self.view = view
self.converters[PrimaryKeyField] = self.handle_pk self.converters[PrimaryKeyField] = self.handle_pk
self.converters[DateTimeField] = self.handle_datetime self.converters[DateTimeField] = self.handle_datetime
self.converters[DateField] = self.handle_date self.converters[DateField] = self.handle_date
self.converters[TimeField] = self.handle_time self.converters[TimeField] = self.handle_time
def handle_foreign_key(self, model, field, **kwargs):
loader = self.view._form_ajax_refs.get(field.name)
if loader:
if field.null:
kwargs['allow_blank'] = True
return field.name, AjaxSelectField(loader, **kwargs)
return super(CustomModelConverter, self).handle_foreign_key(model, field, **kwargs)
def handle_pk(self, model, field, **kwargs): def handle_pk(self, model, field, **kwargs):
kwargs['validators'] = [] kwargs['validators'] = []
return field.name, fields.HiddenField(**kwargs) return field.name, fields.HiddenField(**kwargs)
......
...@@ -2,7 +2,6 @@ import logging ...@@ -2,7 +2,6 @@ import logging
from flask import flash from flask import flash
from flask.ext.admin import form
from flask.ext.admin._compat import string_types from flask.ext.admin._compat import string_types
from flask.ext.admin.babel import gettext, ngettext, lazy_gettext from flask.ext.admin.babel import gettext, ngettext, lazy_gettext
from flask.ext.admin.model import BaseModelView from flask.ext.admin.model import BaseModelView
...@@ -11,8 +10,10 @@ from peewee import PrimaryKeyField, ForeignKeyField, Field, CharField, TextField ...@@ -11,8 +10,10 @@ from peewee import PrimaryKeyField, ForeignKeyField, Field, CharField, TextField
from flask.ext.admin.actions import action from flask.ext.admin.actions import action
from flask.ext.admin.contrib.peewee import filters from flask.ext.admin.contrib.peewee import filters
from .form import get_form, CustomModelConverter, InlineModelConverter, save_inline from .form import get_form, CustomModelConverter, InlineModelConverter, save_inline
from .tools import get_primary_key, parse_like_term from .tools import get_primary_key, parse_like_term
from .ajax import QueryAjaxModelLoader
class ModelView(BaseModelView): class ModelView(BaseModelView):
...@@ -217,7 +218,7 @@ class ModelView(BaseModelView): ...@@ -217,7 +218,7 @@ class ModelView(BaseModelView):
return isinstance(filter, filters.BasePeeweeFilter) return isinstance(filter, filters.BasePeeweeFilter)
def scaffold_form(self): def scaffold_form(self):
form_class = get_form(self.model, self.model_form_converter(), form_class = get_form(self.model, self.model_form_converter(self),
base_class=self.form_base_class, base_class=self.form_base_class,
only=self.form_columns, only=self.form_columns,
exclude=self.form_excluded_columns, exclude=self.form_excluded_columns,
...@@ -230,7 +231,7 @@ class ModelView(BaseModelView): ...@@ -230,7 +231,7 @@ class ModelView(BaseModelView):
return form_class return form_class
def scaffold_inline_form_models(self, form_class): def scaffold_inline_form_models(self, form_class):
converter = self.model_form_converter() converter = self.model_form_converter(self)
inline_converter = self.inline_model_form_converter(self) inline_converter = self.inline_model_form_converter(self)
for m in self.inline_models: for m in self.inline_models:
...@@ -241,6 +242,30 @@ class ModelView(BaseModelView): ...@@ -241,6 +242,30 @@ class ModelView(BaseModelView):
return form_class return form_class
# AJAX foreignkey support
def _create_ajax_loader(self, name, fields):
prop = getattr(self.model, name, None)
if prop is None:
raise ValueError('Model %s does not have field %s.' % (self.model, name))
# TODO: Check for field
remote_model = prop.rel_model
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:
remote_fields.append(field)
return QueryAjaxModelLoader(name, remote_model, remote_fields)
def _handle_join(self, query, field, joins): def _handle_join(self, query, field, joins):
if field.model_class != self.model: if field.model_class != self.model:
model_name = field.model_class.__name__ model_name = field.model_class.__name__
......
from sqlalchemy import or_
from flask.ext.admin._compat import as_unicode
from flask.ext.admin.model.ajax import AjaxModelLoader, DEFAULT_PAGE_SIZE
class QueryAjaxModelLoader(AjaxModelLoader):
def __init__(self, name, session, model, fields):
"""
Constructor.
:param fields:
Fields to run query against
"""
super(QueryAjaxModelLoader, self).__init__(name)
self.session = session
self.model = model
self.fields = fields
primary_keys = model._sa_class_manager.mapper.primary_key
if len(primary_keys) > 1:
raise NotImplemented('Flask-Admin does not support multi-pk AJAX model loading.')
self.pk = primary_keys[0].name
def format(self, model):
if not model:
return None
return (getattr(model, self.pk), as_unicode(model))
def get_one(self, pk):
return self.session.query(self.model).get(pk)
def get_list(self, term, offset=0, limit=DEFAULT_PAGE_SIZE):
query = self.session.query(self.model)
filters = (field.like(u'%%%s%%' % term) for field in self.fields)
query = query.filter(or_(*filters))
return query.offset(offset).limit(limit).all()
...@@ -6,6 +6,7 @@ from flask.ext.admin.form import Select2Field ...@@ -6,6 +6,7 @@ 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, InlineFormAdmin, InlineModelConverterBase,
FieldPlaceholder) FieldPlaceholder)
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
from flask.ext.admin._compat import iteritems from flask.ext.admin._compat import iteritems
...@@ -70,6 +71,31 @@ class AdminModelConverter(ModelConverterBase): ...@@ -70,6 +71,31 @@ class AdminModelConverter(ModelConverterBase):
return None return None
def _model_select_field(self, prop, multiple, remote_model, **kwargs):
loader = self.view._form_ajax_refs.get(prop.key)
if loader:
if multiple:
return AjaxSelectMultipleField(loader, **kwargs)
else:
return AjaxSelectField(loader, **kwargs)
if 'query_factory' not in kwargs:
kwargs['query_factory'] = lambda: self.session.query(remote_model)
if 'widget' not in kwargs:
if prop.direction.name == 'MANYTOONE':
kwargs['widget'] = form.Select2Widget()
elif prop.direction.name == 'ONETOMANY':
kwargs['widget'] = form.Select2Widget(multiple=True)
elif prop.direction.name == 'MANYTOMANY':
kwargs['widget'] = form.Select2Widget(multiple=True)
if multiple:
return QuerySelectMultipleField(**kwargs)
else:
return QuerySelectField(**kwargs)
def _convert_relation(self, prop, kwargs): def _convert_relation(self, prop, kwargs):
remote_model = prop.mapper.class_ remote_model = prop.mapper.class_
local_column = prop.local_remote_pairs[0][0] local_column = prop.local_remote_pairs[0][0]
...@@ -85,16 +111,6 @@ class AdminModelConverter(ModelConverterBase): ...@@ -85,16 +111,6 @@ class AdminModelConverter(ModelConverterBase):
# Contribute model-related parameters # Contribute model-related parameters
if 'allow_blank' not in kwargs: if 'allow_blank' not in kwargs:
kwargs['allow_blank'] = local_column.nullable kwargs['allow_blank'] = local_column.nullable
if 'query_factory' not in kwargs:
kwargs['query_factory'] = lambda: self.session.query(remote_model)
if 'widget' not in kwargs:
if prop.direction.name == 'MANYTOONE':
kwargs['widget'] = form.Select2Widget()
elif prop.direction.name == 'ONETOMANY':
kwargs['widget'] = form.Select2Widget(multiple=True)
elif prop.direction.name == 'MANYTOMANY':
kwargs['widget'] = form.Select2Widget(multiple=True)
# Override field type if necessary # Override field type if necessary
override = self._get_field_override(prop.key) override = self._get_field_override(prop.key)
...@@ -102,15 +118,15 @@ class AdminModelConverter(ModelConverterBase): ...@@ -102,15 +118,15 @@ class AdminModelConverter(ModelConverterBase):
return override(**kwargs) return override(**kwargs)
if prop.direction.name == 'MANYTOONE': if prop.direction.name == 'MANYTOONE':
return QuerySelectField(**kwargs) return self._model_select_field(prop, False, remote_model, **kwargs)
elif prop.direction.name == 'ONETOMANY': elif prop.direction.name == 'ONETOMANY':
# Skip backrefs # Skip backrefs
if not local_column.foreign_keys and getattr(self.view, 'column_hide_backrefs', True): if not local_column.foreign_keys and getattr(self.view, 'column_hide_backrefs', True):
return None return None
return QuerySelectMultipleField(**kwargs) return self._model_select_field(prop, True, remote_model, **kwargs)
elif prop.direction.name == 'MANYTOMANY': elif prop.direction.name == 'MANYTOMANY':
return QuerySelectMultipleField(**kwargs) return self._model_select_field(prop, True, remote_model, **kwargs)
def convert(self, model, mapper, prop, field_args, hidden_pk): def convert(self, model, mapper, prop, field_args, hidden_pk):
# Properly handle forced fields # Properly handle forced fields
......
...@@ -16,6 +16,8 @@ from flask.ext.admin._backwards import ObsoleteAttr ...@@ -16,6 +16,8 @@ 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
class ModelView(BaseModelView): class ModelView(BaseModelView):
""" """
...@@ -585,6 +587,33 @@ class ModelView(BaseModelView): ...@@ -585,6 +587,33 @@ class ModelView(BaseModelView):
return joined return joined
# 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)
# Database-related API # Database-related API
def get_query(self): def get_query(self):
""" """
......
...@@ -17,14 +17,25 @@ class Select2Widget(widgets.Select): ...@@ -17,14 +17,25 @@ class Select2Widget(widgets.Select):
def __call__(self, field, **kwargs): def __call__(self, field, **kwargs):
allow_blank = getattr(field, 'allow_blank', False) allow_blank = getattr(field, 'allow_blank', False)
kwargs['data-role'] = u'select2'
if allow_blank and not self.multiple: if allow_blank and not self.multiple:
kwargs['data-role'] = u'select2blank' kwargs['data-allow-blank'] = u'1'
else:
kwargs['data-role'] = u'select2'
return super(Select2Widget, self).__call__(field, **kwargs) return super(Select2Widget, self).__call__(field, **kwargs)
class Select2TagsWidget(widgets.TextInput):
"""`Select2 <http://ivaynberg.github.com/select2/#tags>`_ styled text widget.
You must include select2.js, form.js and select2 stylesheet for it to work.
"""
def __call__(self, field, **kwargs):
kwargs['data-role'] = u'select2'
kwargs['data-tags'] = u'1'
return super(Select2TagsWidget, self).__call__(field, **kwargs)
class DatePickerWidget(widgets.TextInput): class DatePickerWidget(widgets.TextInput):
""" """
Date picker widget. Date picker widget.
...@@ -73,12 +84,3 @@ class RenderTemplateWidget(object): ...@@ -73,12 +84,3 @@ class RenderTemplateWidget(object):
template = jinja_env.get_template(self.template) template = jinja_env.get_template(self.template)
return template.render(kwargs) return template.render(kwargs)
class Select2TagsWidget(widgets.TextInput):
"""`Select2 <http://ivaynberg.github.com/select2/#tags>`_ styled text widget.
You must include select2.js, form.js and select2 stylesheet for it to work.
"""
def __call__(self, field, **kwargs):
kwargs['data-role'] = u'select2tags'
return super(Select2TagsWidget, self).__call__(field, **kwargs)
DEFAULT_PAGE_SIZE = 10
class AjaxModelLoader(object):
"""
Ajax related model loader. Override this to implement custom loading behavior.
"""
def __init__(self, name):
"""
Constructor.
:param name:
Field name
"""
self.name = name
def format(self, model):
"""
Return (id, name) tuple from the model.
"""
raise NotImplemented()
def get_one(self, pk):
"""
Find model by its primary key.
:param pk:
Primary key value
"""
raise NotImplemented()
def get_list(self, query, offset=0, limit=DEFAULT_PAGE_SIZE):
"""
Return models that match `query`.
:param view:
Administrative view.
:param query:
Query string
:param offset:
Offset
:param limit:
Limit
"""
raise NotImplemented()
import warnings import warnings
from flask import request, url_for, redirect, flash from flask import request, url_for, redirect, flash, abort, json, Response
from jinja2 import contextfunction from jinja2 import contextfunction
...@@ -15,6 +15,7 @@ from flask.ext.admin.tools import rec_getattr ...@@ -15,6 +15,7 @@ from flask.ext.admin.tools import rec_getattr
from flask.ext.admin._backwards import ObsoleteAttr from flask.ext.admin._backwards import ObsoleteAttr
from flask.ext.admin._compat import iteritems, as_unicode from flask.ext.admin._compat import iteritems, as_unicode
from .helpers import prettify_name, get_mdict_item_or_list from .helpers import prettify_name, get_mdict_item_or_list
from .ajax import AjaxModelLoader
class BaseModelView(BaseView, ActionsMixin): class BaseModelView(BaseView, ActionsMixin):
...@@ -362,6 +363,21 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -362,6 +363,21 @@ class BaseModelView(BaseView, ActionsMixin):
In this case, password field will be put between email and secret fields that are autogenerated. In this case, password field will be put between email and secret fields that are autogenerated.
""" """
form_ajax_refs = None
"""
Use AJAX for foreign key model loading.
Should contain dictionary, where key is field name and value is enumerable with list to fields
to check against (in remote model).
For example, it can look like::
class MyModelView(BaseModelView):
form_ajax_refs = {
'user': ('first_name', 'last_name', 'email')
}
"""
# Actions # Actions
action_disallowed_list = ObsoleteAttr('action_disallowed_list', action_disallowed_list = ObsoleteAttr('action_disallowed_list',
'disallowed_actions', 'disallowed_actions',
...@@ -434,12 +450,14 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -434,12 +450,14 @@ class BaseModelView(BaseView, ActionsMixin):
self.column_labels = {} self.column_labels = {}
# Forms # Forms
self._create_form_class = self.get_create_form() self._form_ajax_refs = self._process_ajax_references()
self._edit_form_class = self.get_edit_form()
if self.form_widget_args is None: if self.form_widget_args is None:
self.form_widget_args = {} self.form_widget_args = {}
self._create_form_class = self.get_create_form()
self._edit_form_class = self.get_edit_form()
# Search # Search
self._search_supported = self.init_search() self._search_supported = self.init_search()
...@@ -971,6 +989,31 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -971,6 +989,31 @@ class BaseModelView(BaseView, ActionsMixin):
return value return value
# AJAX references
def _process_ajax_references(self):
"""
Process `form_ajax_refs` and generate model loaders that
will be used by the `ajax_lookup` view.
"""
result = {}
if self.form_ajax_refs:
for name, value in iteritems(self.form_ajax_refs):
if isinstance(value, AjaxModelLoader):
result[name] = value
elif isinstance(value, (list, tuple)):
result[name] = self._create_ajax_loader(name, value)
else:
raise ValueError('%s.form_ajax_refs can not handle %s types' % (self, type(value)))
return result
def _create_ajax_loader(self, name, fields):
"""
Model backend will override this to implement AJAX model loading.
"""
raise NotImplemented()
# Views # Views
@expose('/') @expose('/')
def index_view(self): def index_view(self):
...@@ -1157,3 +1200,18 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -1157,3 +1200,18 @@ class BaseModelView(BaseView, ActionsMixin):
Mass-model action view. Mass-model action view.
""" """
return self.handle_action() return self.handle_action()
@expose('/ajax/lookup/')
def ajax_lookup(self):
name = request.args.get('name')
query = request.args.get('query')
offset = request.args.get('offset', type=int)
limit = request.args.get('limit', 10, type=int)
loader = self._form_ajax_refs.get(name)
if not loader:
abort(404)
data = [loader.format(m) for m in loader.get_list(query, offset, limit)]
return Response(json.dumps(data), mimetype='application/json')
import itertools import itertools
from wtforms.fields import FieldList, FormField from wtforms.validators import ValidationError
from wtforms.fields import FieldList, FormField, SelectFieldBase
from flask.ext.admin._compat import iteritems from flask.ext.admin._compat import iteritems
from .widgets import InlineFieldListWidget, InlineFormWidget from .widgets import InlineFieldListWidget, InlineFormWidget, AjaxSelect2Widget
class InlineFieldList(FieldList): class InlineFieldList(FieldList):
...@@ -113,3 +114,99 @@ class InlineFormField(FormField): ...@@ -113,3 +114,99 @@ class InlineFormField(FormField):
Inline version of the ``FormField`` widget. Inline version of the ``FormField`` widget.
""" """
widget = InlineFormWidget() widget = InlineFormWidget()
class AjaxSelectField(SelectFieldBase):
"""
Ajax Model Select Field
"""
widget = AjaxSelect2Widget()
separator = ','
def __init__(self, loader, label=None, validators=None, allow_blank=False, blank_text=u'', **kwargs):
super(AjaxSelectField, self).__init__(label, validators, **kwargs)
self.loader = loader
self.allow_blank = allow_blank
self.blank_text = blank_text
def _get_data(self):
if self._formdata is not None:
model = self.loader.get_one(self._formdata)
if model is not None:
self._set_data(model)
return self._data
def _set_data(self, data):
self._data = data
self._formdata = None
data = property(_get_data, _set_data)
def _format_item(self, item):
value = self.loader.format(self.data)
return (value[0], value[1], True)
def process_formdata(self, valuelist):
if valuelist:
if self.allow_blank and valuelist[0] == u'__None':
self.data = None
else:
self._data = None
self._formdata = valuelist[0]
def pre_validate(self, form):
if not self.allow_blank and self.data is None:
raise ValidationError(self.gettext(u'Not a valid choice'))
class AjaxSelectMultipleField(AjaxSelectField):
"""
Ajax-enabled model multi-select field.
"""
widget = AjaxSelect2Widget(multiple=True)
def __init__(self, loader, label=None, validators=None, default=None, **kwargs):
if default is None:
default = []
super(AjaxSelectMultipleField, self).__init__(loader, label, validators, default=default, **kwargs)
self._invalid_formdata = False
def _get_data(self):
formdata = self._formdata
if formdata is not None:
data = []
# TODO: Optimize?
for item in formdata:
model = self.loader.get_one(item)
if model:
data.append(model)
else:
self._invalid_formdata = True
self._set_data(data)
return self._data
def _set_data(self, data):
self._data = data
self._formdata = None
data = property(_get_data, _set_data)
def process_formdata(self, valuelist):
self._formdata = set()
for field in valuelist:
for n in field.split(self.separator):
self._formdata.add(n)
def pre_validate(self, form):
if self._invalid_formdata:
raise ValidationError(self.gettext(u'Not a valid choice'))
from flask import url_for, json
from wtforms.widgets import HTMLString, html_params
from flask.ext.admin._compat import as_unicode
from flask.ext.admin.form import RenderTemplateWidget from flask.ext.admin.form import RenderTemplateWidget
...@@ -9,3 +13,42 @@ class InlineFieldListWidget(RenderTemplateWidget): ...@@ -9,3 +13,42 @@ class InlineFieldListWidget(RenderTemplateWidget):
class InlineFormWidget(RenderTemplateWidget): class InlineFormWidget(RenderTemplateWidget):
def __init__(self): def __init__(self):
super(InlineFormWidget, self).__init__('admin/model/inline_form.html') super(InlineFormWidget, self).__init__('admin/model/inline_form.html')
class AjaxSelect2Widget(object):
def __init__(self, multiple=False):
self.multiple = multiple
def __call__(self, field, **kwargs):
kwargs['data-role'] = u'select2-ajax'
kwargs['data-url'] = url_for('.ajax_lookup', name=field.loader.name)
allow_blank = getattr(field, 'allow_blank', False)
if allow_blank and not self.multiple:
kwargs['data-allow-blank'] = u'1'
kwargs.setdefault('id', field.id)
kwargs.setdefault('type', 'hidden')
if self.multiple:
result = []
ids = []
for value in field.data:
data = field.loader.format(value)
result.append(data)
ids.append(as_unicode(data[0]))
separator = getattr(field, 'separator', ',')
kwargs['value'] = separator.join(ids)
kwargs['data-json'] = json.dumps(result)
kwargs['data-multiple'] = u'1'
else:
data = field.loader.format(field.data)
if data:
kwargs['value'] = data[0]
kwargs['data-json'] = json.dumps(data)
return HTMLString('<input %s>' % html_params(name=field.name, **kwargs))
(function() { (function() {
var AdminForm = function() { var AdminForm = function() {
this.applyStyle = function(el, name) { // Field converters
var fieldConverters = [];
/**
* Process AJAX fk-widget
*/
function processAjaxWidget($el, name) {
var multiple = $el.attr('data-multiple') == '1';
var opts = {
width: 'resolve',
minimumInputLength: 1,
ajax: {
url: $el.attr('data-url'),
data: function(term, page) {
return {
query: term,
offset: (page - 1) * 10,
limit: 10
};
},
results: function(data, page) {
var results = [];
for (var k in data) {
var v = data[k];
results.push({id: v[0], text: v[1]});
}
return {
results: results,
more: results.length == 10
};
}
},
initSelection: function(element, callback) {
$el = $(element);
var value = jQuery.parseJSON($el.attr('data-json'));
var result = null;
if (value) {
if (multiple) {
result = [];
for (var k in value) {
var v = value[k];
result.push({id: v[0], text: v[1]});
}
callback(result);
} else {
result = {id: value[0], text: value[1]};
}
}
callback(result);
}
};
if ($el.attr('data-allow-blank'))
opts['allowClear'] = true;
opts['multiple'] = multiple;
$el.select2(opts);
}
/**
* Process data-role attribute for the given input element. Feel free to override
*
* @param {Selector} $el jQuery selector
* @param {String} name data-role value
*/
this.applyStyle = function($el, name) {
// Process converters first
for (var conv in fieldConverters) {
var fildConv = fieldConverters[conv];
if (fieldConv($el, name))
return true;
}
switch (name) { switch (name) {
case 'select2': case 'select2':
$(el).select2({width: 'resolve'}); var opts = {
break; width: 'resolve'
case 'select2blank': };
$(el).select2({allowClear: true, width: 'resolve'});
break; if ($el.attr('data-allow-blank'))
case 'select2tags': opts['allowClear'] = true;
$(el).select2({tags: [], tokenSeparators: [','], width: 'resolve'});
break; if ($el.attr('data-tags')) {
$.extend(opts, {
multiple: true,
tokenSeparators: [',']
});
}
$el.select2(opts);
return true;
case 'select2-ajax':
processAjaxWidget($el, name);
return true;
case 'datepicker': case 'datepicker':
$(el).datepicker(); $el.datepicker();
break; return true;
case 'datetimepicker': case 'datetimepicker':
$(el).datepicker({displayTime: true}); $el.datepicker({displayTime: true});
break; return true;
} }
}; };
/**
* Add inline form field
*
* @method addInlineField
* @param {String} id Form ID
* @param {Node} el Form element
* @param {String} template Form template
*/
this.addInlineField = function(id, el, template) { this.addInlineField = function(id, el, template) {
var $el = $(el); var $el = $(el);
var $template = $($(template).text()); var $template = $($(template).text());
...@@ -60,12 +161,19 @@ ...@@ -60,12 +161,19 @@
this.applyGlobalStyles($template); this.applyGlobalStyles($template);
}; };
/**
* Apply global input styles.
*
* @method applyGlobalStyles
* @param {Selector} jQuery element
*/
this.applyGlobalStyles = function(parent) { this.applyGlobalStyles = function(parent) {
$('[data-role=select2]', parent).select2({width: 'resolve'}); var self = this;
$('[data-role=select2blank]', parent).select2({allowClear: true, width: 'resolve'});
$('[data-role=select2tags]', parent).select2({multiple: true, tokenSeparators: [','], width: 'resolve'}); $('[data-role]', parent).each(function() {
$('[data-role=datepicker]', parent).datepicker(); var $el = $(this);
$('[data-role=datetimepicker]', parent).datepicker({displayTime: true}); self.applyStyle($el, $el.attr('data-role'));
});
}; };
}; };
...@@ -80,6 +188,8 @@ ...@@ -80,6 +188,8 @@
// Expose faForm globally // Expose faForm globally
var faForm = window.faForm = new AdminForm(); var faForm = window.faForm = new AdminForm();
// Apply global styles // Apply global styles for current page after page loaded
faForm.applyGlobalStyles(document); $(function() {
faForm.applyGlobalStyles(document);
});
})(); })();
File mode changed from 100644 to 100755
/* /*
Version: 3.4.0 Timestamp: Tue May 14 08:27:33 PDT 2013 Version: 3.4.2 Timestamp: Mon Aug 12 15:04:12 PDT 2013
*/ */
.select2-container { .select2-container {
margin: 0; margin: 0;
...@@ -14,7 +14,7 @@ Version: 3.4.0 Timestamp: Tue May 14 08:27:33 PDT 2013 ...@@ -14,7 +14,7 @@ Version: 3.4.0 Timestamp: Tue May 14 08:27:33 PDT 2013
.select2-container, .select2-container,
.select2-drop, .select2-drop,
.select2-search, .select2-search,
.select2-search input{ .select2-search input {
/* /*
Force border-box so that % widths fit the parent Force border-box so that % widths fit the parent
container without overlap because of margin/padding. container without overlap because of margin/padding.
...@@ -22,9 +22,7 @@ Version: 3.4.0 Timestamp: Tue May 14 08:27:33 PDT 2013 ...@@ -22,9 +22,7 @@ Version: 3.4.0 Timestamp: Tue May 14 08:27:33 PDT 2013
More Info : http://www.quirksmode.org/css/box.html More Info : http://www.quirksmode.org/css/box.html
*/ */
-webkit-box-sizing: border-box; /* webkit */ -webkit-box-sizing: border-box; /* webkit */
-khtml-box-sizing: border-box; /* konqueror */
-moz-box-sizing: border-box; /* firefox */ -moz-box-sizing: border-box; /* firefox */
-ms-box-sizing: border-box; /* ie */
box-sizing: border-box; /* css3 */ box-sizing: border-box; /* css3 */
} }
...@@ -41,13 +39,9 @@ Version: 3.4.0 Timestamp: Tue May 14 08:27:33 PDT 2013 ...@@ -41,13 +39,9 @@ Version: 3.4.0 Timestamp: Tue May 14 08:27:33 PDT 2013
color: #444; color: #444;
text-decoration: none; text-decoration: none;
-webkit-border-radius: 4px; border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
-webkit-background-clip: padding-box; background-clip: padding-box;
-moz-background-clip: padding;
background-clip: padding-box;
-webkit-touch-callout: none; -webkit-touch-callout: none;
-webkit-user-select: none; -webkit-user-select: none;
...@@ -57,45 +51,41 @@ Version: 3.4.0 Timestamp: Tue May 14 08:27:33 PDT 2013 ...@@ -57,45 +51,41 @@ Version: 3.4.0 Timestamp: Tue May 14 08:27:33 PDT 2013
user-select: none; user-select: none;
background-color: #fff; background-color: #fff;
background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #eeeeee), color-stop(0.5, white)); background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #eee), color-stop(0.5, #fff));
background-image: -webkit-linear-gradient(center bottom, #eeeeee 0%, white 50%); background-image: -webkit-linear-gradient(center bottom, #eee 0%, #fff 50%);
background-image: -moz-linear-gradient(center bottom, #eeeeee 0%, white 50%); background-image: -moz-linear-gradient(center bottom, #eee 0%, #fff 50%);
background-image: -o-linear-gradient(bottom, #eeeeee 0%, #ffffff 50%); background-image: -o-linear-gradient(bottom, #eee 0%, #fff 50%);
background-image: -ms-linear-gradient(top, #ffffff 0%, #eeeeee 50%); background-image: -ms-linear-gradient(top, #fff 0%, #eee 50%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr = '#ffffff', endColorstr = '#eeeeee', GradientType = 0); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr = '#ffffff', endColorstr = '#eeeeee', GradientType = 0);
background-image: linear-gradient(top, #ffffff 0%, #eeeeee 50%); background-image: linear-gradient(top, #fff 0%, #eee 50%);
} }
.select2-container.select2-drop-above .select2-choice { .select2-container.select2-drop-above .select2-choice {
border-bottom-color: #aaa; border-bottom-color: #aaa;
-webkit-border-radius:0 0 4px 4px; border-radius: 0 0 4px 4px;
-moz-border-radius:0 0 4px 4px;
border-radius:0 0 4px 4px;
background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #eeeeee), color-stop(0.9, white)); background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #eee), color-stop(0.9, #fff));
background-image: -webkit-linear-gradient(center bottom, #eeeeee 0%, white 90%); background-image: -webkit-linear-gradient(center bottom, #eee 0%, #fff 90%);
background-image: -moz-linear-gradient(center bottom, #eeeeee 0%, white 90%); background-image: -moz-linear-gradient(center bottom, #eee 0%, #fff 90%);
background-image: -o-linear-gradient(bottom, #eeeeee 0%, white 90%); background-image: -o-linear-gradient(bottom, #eee 0%, #fff 90%);
background-image: -ms-linear-gradient(top, #eeeeee 0%,#ffffff 90%); background-image: -ms-linear-gradient(top, #eee 0%, #fff 90%);
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#eeeeee',GradientType=0 ); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#eeeeee', GradientType=0);
background-image: linear-gradient(top, #eeeeee 0%,#ffffff 90%); background-image: linear-gradient(top, #eee 0%, #fff 90%);
} }
.select2-container.select2-allowclear .select2-choice span { .select2-container.select2-allowclear .select2-choice .select2-chosen {
margin-right: 42px; margin-right: 42px;
} }
.select2-container .select2-choice span { .select2-container .select2-choice > .select2-chosen {
margin-right: 26px; margin-right: 26px;
display: block; display: block;
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
-ms-text-overflow: ellipsis; text-overflow: ellipsis;
-o-text-overflow: ellipsis;
text-overflow: ellipsis;
} }
.select2-container .select2-choice abbr { .select2-container .select2-choice abbr {
...@@ -125,15 +115,27 @@ Version: 3.4.0 Timestamp: Tue May 14 08:27:33 PDT 2013 ...@@ -125,15 +115,27 @@ Version: 3.4.0 Timestamp: Tue May 14 08:27:33 PDT 2013
} }
.select2-drop-mask { .select2-drop-mask {
position: absolute; border: 0;
margin: 0;
padding: 0;
position: fixed;
left: 0; left: 0;
top: 0; top: 0;
min-height: 100%;
min-width: 100%;
height: auto;
width: auto;
opacity: 0;
z-index: 9998; z-index: 9998;
/* styles required for IE to work */
background-color: #fff;
opacity: 0;
filter: alpha(opacity=0);
} }
.select2-drop { .select2-drop {
width: 100%; width: 100%;
margin-top:-1px; margin-top: -1px;
position: absolute; position: absolute;
z-index: 9999; z-index: 9999;
top: 100%; top: 100%;
...@@ -143,12 +145,9 @@ Version: 3.4.0 Timestamp: Tue May 14 08:27:33 PDT 2013 ...@@ -143,12 +145,9 @@ Version: 3.4.0 Timestamp: Tue May 14 08:27:33 PDT 2013
border: 1px solid #aaa; border: 1px solid #aaa;
border-top: 0; border-top: 0;
-webkit-border-radius: 0 0 4px 4px; border-radius: 0 0 4px 4px;
-moz-border-radius: 0 0 4px 4px;
border-radius: 0 0 4px 4px;
-webkit-box-shadow: 0 4px 5px rgba(0, 0, 0, .15); -webkit-box-shadow: 0 4px 5px rgba(0, 0, 0, .15);
-moz-box-shadow: 0 4px 5px rgba(0, 0, 0, .15);
box-shadow: 0 4px 5px rgba(0, 0, 0, .15); box-shadow: 0 4px 5px rgba(0, 0, 0, .15);
} }
...@@ -166,16 +165,22 @@ Version: 3.4.0 Timestamp: Tue May 14 08:27:33 PDT 2013 ...@@ -166,16 +165,22 @@ Version: 3.4.0 Timestamp: Tue May 14 08:27:33 PDT 2013
border-top: 1px solid #aaa; border-top: 1px solid #aaa;
border-bottom: 0; border-bottom: 0;
-webkit-border-radius: 4px 4px 0 0; border-radius: 4px 4px 0 0;
-moz-border-radius: 4px 4px 0 0;
border-radius: 4px 4px 0 0;
-webkit-box-shadow: 0 -4px 5px rgba(0, 0, 0, .15); -webkit-box-shadow: 0 -4px 5px rgba(0, 0, 0, .15);
-moz-box-shadow: 0 -4px 5px rgba(0, 0, 0, .15);
box-shadow: 0 -4px 5px rgba(0, 0, 0, .15); box-shadow: 0 -4px 5px rgba(0, 0, 0, .15);
} }
.select2-container .select2-choice div { .select2-drop-active {
border: 1px solid #5897fb;
border-top: none;
}
.select2-drop.select2-drop-above.select2-drop-active {
border-top: 1px solid #5897fb;
}
.select2-container .select2-choice .select2-arrow {
display: inline-block; display: inline-block;
width: 18px; width: 18px;
height: 100%; height: 100%;
...@@ -184,25 +189,21 @@ Version: 3.4.0 Timestamp: Tue May 14 08:27:33 PDT 2013 ...@@ -184,25 +189,21 @@ Version: 3.4.0 Timestamp: Tue May 14 08:27:33 PDT 2013
top: 0; top: 0;
border-left: 1px solid #aaa; border-left: 1px solid #aaa;
-webkit-border-radius: 0 4px 4px 0; border-radius: 0 4px 4px 0;
-moz-border-radius: 0 4px 4px 0;
border-radius: 0 4px 4px 0;
-webkit-background-clip: padding-box; background-clip: padding-box;
-moz-background-clip: padding;
background-clip: padding-box;
background: #ccc; background: #ccc;
background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #ccc), color-stop(0.6, #eee)); background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #ccc), color-stop(0.6, #eee));
background-image: -webkit-linear-gradient(center bottom, #ccc 0%, #eee 60%); background-image: -webkit-linear-gradient(center bottom, #ccc 0%, #eee 60%);
background-image: -moz-linear-gradient(center bottom, #ccc 0%, #eee 60%); background-image: -moz-linear-gradient(center bottom, #ccc 0%, #eee 60%);
background-image: -o-linear-gradient(bottom, #ccc 0%, #eee 60%); background-image: -o-linear-gradient(bottom, #ccc 0%, #eee 60%);
background-image: -ms-linear-gradient(top, #cccccc 0%, #eeeeee 60%); background-image: -ms-linear-gradient(top, #ccc 0%, #eee 60%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr = '#eeeeee', endColorstr = '#cccccc', GradientType = 0); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr = '#eeeeee', endColorstr = '#cccccc', GradientType = 0);
background-image: linear-gradient(top, #cccccc 0%, #eeeeee 60%); background-image: linear-gradient(top, #ccc 0%, #eee 60%);
} }
.select2-container .select2-choice div b { .select2-container .select2-choice .select2-arrow b {
display: block; display: block;
width: 100%; width: 100%;
height: 100%; height: 100%;
...@@ -235,21 +236,18 @@ Version: 3.4.0 Timestamp: Tue May 14 08:27:33 PDT 2013 ...@@ -235,21 +236,18 @@ Version: 3.4.0 Timestamp: Tue May 14 08:27:33 PDT 2013
font-size: 1em; font-size: 1em;
border: 1px solid #aaa; border: 1px solid #aaa;
-webkit-border-radius: 0; border-radius: 0;
-moz-border-radius: 0;
border-radius: 0;
-webkit-box-shadow: none; -webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none; box-shadow: none;
background: #fff url('select2.png') no-repeat 100% -22px; background: #fff url('select2.png') no-repeat 100% -22px;
background: url('select2.png') no-repeat 100% -22px, -webkit-gradient(linear, left bottom, left top, color-stop(0.85, white), color-stop(0.99, #eeeeee)); background: url('select2.png') no-repeat 100% -22px, -webkit-gradient(linear, left bottom, left top, color-stop(0.85, #fff), color-stop(0.99, #eee));
background: url('select2.png') no-repeat 100% -22px, -webkit-linear-gradient(center bottom, white 85%, #eeeeee 99%); background: url('select2.png') no-repeat 100% -22px, -webkit-linear-gradient(center bottom, #fff 85%, #eee 99%);
background: url('select2.png') no-repeat 100% -22px, -moz-linear-gradient(center bottom, white 85%, #eeeeee 99%); background: url('select2.png') no-repeat 100% -22px, -moz-linear-gradient(center bottom, #fff 85%, #eee 99%);
background: url('select2.png') no-repeat 100% -22px, -o-linear-gradient(bottom, white 85%, #eeeeee 99%); background: url('select2.png') no-repeat 100% -22px, -o-linear-gradient(bottom, #fff 85%, #eee 99%);
background: url('select2.png') no-repeat 100% -22px, -ms-linear-gradient(top, #ffffff 85%, #eeeeee 99%); background: url('select2.png') no-repeat 100% -22px, -ms-linear-gradient(top, #fff 85%, #eee 99%);
background: url('select2.png') no-repeat 100% -22px, linear-gradient(top, #ffffff 85%, #eeeeee 99%); background: url('select2.png') no-repeat 100% -22px, linear-gradient(top, #fff 85%, #eee 99%);
} }
.select2-drop.select2-drop-above .select2-search input { .select2-drop.select2-drop-above .select2-search input {
...@@ -258,12 +256,12 @@ Version: 3.4.0 Timestamp: Tue May 14 08:27:33 PDT 2013 ...@@ -258,12 +256,12 @@ Version: 3.4.0 Timestamp: Tue May 14 08:27:33 PDT 2013
.select2-search input.select2-active { .select2-search input.select2-active {
background: #fff url('select2-spinner.gif') no-repeat 100%; background: #fff url('select2-spinner.gif') no-repeat 100%;
background: url('select2-spinner.gif') no-repeat 100%, -webkit-gradient(linear, left bottom, left top, color-stop(0.85, white), color-stop(0.99, #eeeeee)); background: url('select2-spinner.gif') no-repeat 100%, -webkit-gradient(linear, left bottom, left top, color-stop(0.85, #fff), color-stop(0.99, #eee));
background: url('select2-spinner.gif') no-repeat 100%, -webkit-linear-gradient(center bottom, white 85%, #eeeeee 99%); background: url('select2-spinner.gif') no-repeat 100%, -webkit-linear-gradient(center bottom, #fff 85%, #eee 99%);
background: url('select2-spinner.gif') no-repeat 100%, -moz-linear-gradient(center bottom, white 85%, #eeeeee 99%); background: url('select2-spinner.gif') no-repeat 100%, -moz-linear-gradient(center bottom, #fff 85%, #eee 99%);
background: url('select2-spinner.gif') no-repeat 100%, -o-linear-gradient(bottom, white 85%, #eeeeee 99%); background: url('select2-spinner.gif') no-repeat 100%, -o-linear-gradient(bottom, #fff 85%, #eee 99%);
background: url('select2-spinner.gif') no-repeat 100%, -ms-linear-gradient(top, #ffffff 85%, #eeeeee 99%); background: url('select2-spinner.gif') no-repeat 100%, -ms-linear-gradient(top, #fff 85%, #eee 99%);
background: url('select2-spinner.gif') no-repeat 100%, linear-gradient(top, #ffffff 85%, #eeeeee 99%); background: url('select2-spinner.gif') no-repeat 100%, linear-gradient(top, #fff 85%, #eee 99%);
} }
.select2-container-active .select2-choice, .select2-container-active .select2-choice,
...@@ -271,33 +269,26 @@ Version: 3.4.0 Timestamp: Tue May 14 08:27:33 PDT 2013 ...@@ -271,33 +269,26 @@ Version: 3.4.0 Timestamp: Tue May 14 08:27:33 PDT 2013
border: 1px solid #5897fb; border: 1px solid #5897fb;
outline: none; outline: none;
-webkit-box-shadow: 0 0 5px rgba(0,0,0,.3); -webkit-box-shadow: 0 0 5px rgba(0, 0, 0, .3);
-moz-box-shadow: 0 0 5px rgba(0,0,0,.3); box-shadow: 0 0 5px rgba(0, 0, 0, .3);
box-shadow: 0 0 5px rgba(0,0,0,.3);
} }
.select2-dropdown-open .select2-choice { .select2-dropdown-open .select2-choice {
border-bottom-color: transparent; border-bottom-color: transparent;
-webkit-box-shadow: 0 1px 0 #fff inset; -webkit-box-shadow: 0 1px 0 #fff inset;
-moz-box-shadow: 0 1px 0 #fff inset;
box-shadow: 0 1px 0 #fff inset; box-shadow: 0 1px 0 #fff inset;
-webkit-border-bottom-left-radius: 0; border-bottom-left-radius: 0;
-moz-border-radius-bottomleft: 0; border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
-webkit-border-bottom-right-radius: 0;
-moz-border-radius-bottomright: 0;
border-bottom-right-radius: 0;
background-color: #eee; background-color: #eee;
background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, white), color-stop(0.5, #eeeeee)); background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #fff), color-stop(0.5, #eee));
background-image: -webkit-linear-gradient(center bottom, white 0%, #eeeeee 50%); background-image: -webkit-linear-gradient(center bottom, #fff 0%, #eee 50%);
background-image: -moz-linear-gradient(center bottom, white 0%, #eeeeee 50%); background-image: -moz-linear-gradient(center bottom, #fff 0%, #eee 50%);
background-image: -o-linear-gradient(bottom, white 0%, #eeeeee 50%); background-image: -o-linear-gradient(bottom, #fff 0%, #eee 50%);
background-image: -ms-linear-gradient(top, #ffffff 0%,#eeeeee 50%); background-image: -ms-linear-gradient(top, #fff 0%, #eee 50%);
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#eeeeee', endColorstr='#ffffff',GradientType=0 ); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#eeeeee', endColorstr='#ffffff', GradientType=0);
background-image: linear-gradient(top, #ffffff 0%,#eeeeee 50%); background-image: linear-gradient(top, #fff 0%, #eee 50%);
} }
.select2-dropdown-open.select2-drop-above .select2-choice, .select2-dropdown-open.select2-drop-above .select2-choice,
...@@ -305,21 +296,21 @@ Version: 3.4.0 Timestamp: Tue May 14 08:27:33 PDT 2013 ...@@ -305,21 +296,21 @@ Version: 3.4.0 Timestamp: Tue May 14 08:27:33 PDT 2013
border: 1px solid #5897fb; border: 1px solid #5897fb;
border-top-color: transparent; border-top-color: transparent;
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, white), color-stop(0.5, #eeeeee)); background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #fff), color-stop(0.5, #eee));
background-image: -webkit-linear-gradient(center top, white 0%, #eeeeee 50%); background-image: -webkit-linear-gradient(center top, #fff 0%, #eee 50%);
background-image: -moz-linear-gradient(center top, white 0%, #eeeeee 50%); background-image: -moz-linear-gradient(center top, #fff 0%, #eee 50%);
background-image: -o-linear-gradient(top, white 0%, #eeeeee 50%); background-image: -o-linear-gradient(top, #fff 0%, #eee 50%);
background-image: -ms-linear-gradient(bottom, #ffffff 0%,#eeeeee 50%); background-image: -ms-linear-gradient(bottom, #fff 0%, #eee 50%);
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#eeeeee', endColorstr='#ffffff',GradientType=0 ); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#eeeeee', endColorstr='#ffffff', GradientType=0);
background-image: linear-gradient(bottom, #ffffff 0%,#eeeeee 50%); background-image: linear-gradient(bottom, #fff 0%, #eee 50%);
} }
.select2-dropdown-open .select2-choice div { .select2-dropdown-open .select2-choice .select2-arrow {
background: transparent; background: transparent;
border-left: none; border-left: none;
filter: none; filter: none;
} }
.select2-dropdown-open .select2-choice div b { .select2-dropdown-open .select2-choice .select2-arrow b {
background-position: -18px 1px; background-position: -18px 1px;
} }
...@@ -331,7 +322,7 @@ Version: 3.4.0 Timestamp: Tue May 14 08:27:33 PDT 2013 ...@@ -331,7 +322,7 @@ Version: 3.4.0 Timestamp: Tue May 14 08:27:33 PDT 2013
position: relative; position: relative;
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
-webkit-tap-highlight-color: rgba(0,0,0,0); -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
} }
.select2-results ul.select2-result-sub { .select2-results ul.select2-result-sub {
...@@ -387,7 +378,7 @@ Version: 3.4.0 Timestamp: Tue May 14 08:27:33 PDT 2013 ...@@ -387,7 +378,7 @@ Version: 3.4.0 Timestamp: Tue May 14 08:27:33 PDT 2013
} }
.select2-results .select2-highlighted ul { .select2-results .select2-highlighted ul {
background: white; background: #fff;
color: #000; color: #000;
} }
...@@ -436,7 +427,7 @@ disabled look for disabled choices in the results dropdown ...@@ -436,7 +427,7 @@ disabled look for disabled choices in the results dropdown
cursor: default; cursor: default;
} }
.select2-container.select2-container-disabled .select2-choice div { .select2-container.select2-container-disabled .select2-choice .select2-arrow {
background-color: #f4f4f4; background-color: #f4f4f4;
background-image: none; background-image: none;
border-left: 0; border-left: 0;
...@@ -461,12 +452,12 @@ disabled look for disabled choices in the results dropdown ...@@ -461,12 +452,12 @@ disabled look for disabled choices in the results dropdown
overflow: hidden; overflow: hidden;
background-color: #fff; background-color: #fff;
background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(1%, #eeeeee), color-stop(15%, #ffffff)); background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(1%, #eee), color-stop(15%, #fff));
background-image: -webkit-linear-gradient(top, #eeeeee 1%, #ffffff 15%); background-image: -webkit-linear-gradient(top, #eee 1%, #fff 15%);
background-image: -moz-linear-gradient(top, #eeeeee 1%, #ffffff 15%); background-image: -moz-linear-gradient(top, #eee 1%, #fff 15%);
background-image: -o-linear-gradient(top, #eeeeee 1%, #ffffff 15%); background-image: -o-linear-gradient(top, #eee 1%, #fff 15%);
background-image: -ms-linear-gradient(top, #eeeeee 1%, #ffffff 15%); background-image: -ms-linear-gradient(top, #eee 1%, #fff 15%);
background-image: linear-gradient(top, #eeeeee 1%, #ffffff 15%); background-image: linear-gradient(top, #eee 1%, #fff 15%);
} }
.select2-locked { .select2-locked {
...@@ -481,9 +472,8 @@ disabled look for disabled choices in the results dropdown ...@@ -481,9 +472,8 @@ disabled look for disabled choices in the results dropdown
border: 1px solid #5897fb; border: 1px solid #5897fb;
outline: none; outline: none;
-webkit-box-shadow: 0 0 5px rgba(0,0,0,.3); -webkit-box-shadow: 0 0 5px rgba(0, 0, 0, .3);
-moz-box-shadow: 0 0 5px rgba(0,0,0,.3); box-shadow: 0 0 5px rgba(0, 0, 0, .3);
box-shadow: 0 0 5px rgba(0,0,0,.3);
} }
.select2-container-multi .select2-choices li { .select2-container-multi .select2-choices li {
float: left; float: left;
...@@ -505,7 +495,6 @@ disabled look for disabled choices in the results dropdown ...@@ -505,7 +495,6 @@ disabled look for disabled choices in the results dropdown
outline: 0; outline: 0;
border: 0; border: 0;
-webkit-box-shadow: none; -webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none; box-shadow: none;
background: transparent !important; background: transparent !important;
} }
...@@ -528,17 +517,12 @@ disabled look for disabled choices in the results dropdown ...@@ -528,17 +517,12 @@ disabled look for disabled choices in the results dropdown
cursor: default; cursor: default;
border: 1px solid #aaaaaa; border: 1px solid #aaaaaa;
-webkit-border-radius: 3px; border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
-webkit-box-shadow: 0 0 2px #ffffff inset, 0 1px 0 rgba(0,0,0,0.05); -webkit-box-shadow: 0 0 2px #fff inset, 0 1px 0 rgba(0, 0, 0, 0.05);
-moz-box-shadow: 0 0 2px #ffffff inset, 0 1px 0 rgba(0,0,0,0.05); box-shadow: 0 0 2px #fff inset, 0 1px 0 rgba(0, 0, 0, 0.05);
box-shadow: 0 0 2px #ffffff inset, 0 1px 0 rgba(0,0,0,0.05);
-webkit-background-clip: padding-box; background-clip: padding-box;
-moz-background-clip: padding;
background-clip: padding-box;
-webkit-touch-callout: none; -webkit-touch-callout: none;
-webkit-user-select: none; -webkit-user-select: none;
...@@ -548,15 +532,15 @@ disabled look for disabled choices in the results dropdown ...@@ -548,15 +532,15 @@ disabled look for disabled choices in the results dropdown
user-select: none; user-select: none;
background-color: #e4e4e4; background-color: #e4e4e4;
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#eeeeee', endColorstr='#f4f4f4', GradientType=0 ); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#eeeeee', endColorstr='#f4f4f4', GradientType=0);
background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(20%, #f4f4f4), color-stop(50%, #f0f0f0), color-stop(52%, #e8e8e8), color-stop(100%, #eeeeee)); background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(20%, #f4f4f4), color-stop(50%, #f0f0f0), color-stop(52%, #e8e8e8), color-stop(100%, #eee));
background-image: -webkit-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%); background-image: -webkit-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eee 100%);
background-image: -moz-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%); background-image: -moz-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eee 100%);
background-image: -o-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%); background-image: -o-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eee 100%);
background-image: -ms-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%); background-image: -ms-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eee 100%);
background-image: linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%); background-image: linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eee 100%);
} }
.select2-container-multi .select2-choices .select2-search-choice span { .select2-container-multi .select2-choices .select2-search-choice .select2-chosen {
cursor: default; cursor: default;
} }
.select2-container-multi .select2-choices .select2-search-choice-focus { .select2-container-multi .select2-choices .select2-search-choice-focus {
...@@ -588,7 +572,7 @@ disabled look for disabled choices in the results dropdown ...@@ -588,7 +572,7 @@ disabled look for disabled choices in the results dropdown
} }
/* disabled styles */ /* disabled styles */
.select2-container-multi.select2-container-disabled .select2-choices{ .select2-container-multi.select2-container-disabled .select2-choices {
background-color: #f4f4f4; background-color: #f4f4f4;
background-image: none; background-image: none;
border: 1px solid #ddd; border: 1px solid #ddd;
...@@ -603,7 +587,7 @@ disabled look for disabled choices in the results dropdown ...@@ -603,7 +587,7 @@ disabled look for disabled choices in the results dropdown
} }
.select2-container-multi.select2-container-disabled .select2-choices .select2-search-choice .select2-search-choice-close { display: none; .select2-container-multi.select2-container-disabled .select2-choices .select2-search-choice .select2-search-choice-close { display: none;
background:none; background: none;
} }
/* end multiselect */ /* end multiselect */
...@@ -614,16 +598,17 @@ disabled look for disabled choices in the results dropdown ...@@ -614,16 +598,17 @@ disabled look for disabled choices in the results dropdown
} }
.select2-offscreen, .select2-offscreen:focus { .select2-offscreen, .select2-offscreen:focus {
clip: rect(0 0 0 0); clip: rect(0 0 0 0) !important;
width: 1px; width: 1px !important;
height: 1px; height: 1px !important;
border: 0; border: 0 !important;
margin: 0; margin: 0 !important;
padding: 0; padding: 0 !important;
overflow: hidden; overflow: hidden !important;
position: absolute; position: absolute !important;
outline: 0; outline: 0 !important;
left: 0px; left: 0px !important;
top: 0px !important;
} }
.select2-display-none { .select2-display-none {
...@@ -641,7 +626,7 @@ disabled look for disabled choices in the results dropdown ...@@ -641,7 +626,7 @@ disabled look for disabled choices in the results dropdown
/* Retina-ize icons */ /* Retina-ize icons */
@media only screen and (-webkit-min-device-pixel-ratio: 1.5), only screen and (min-resolution: 144dpi) { @media only screen and (-webkit-min-device-pixel-ratio: 1.5), only screen and (min-resolution: 144dpi) {
.select2-search input, .select2-search-choice-close, .select2-container .select2-choice abbr, .select2-container .select2-choice div b { .select2-search input, .select2-search-choice-close, .select2-container .select2-choice abbr, .select2-container .select2-choice .select2-arrow b {
background-image: url('select2x2.png') !important; background-image: url('select2x2.png') !important;
background-repeat: no-repeat !important; background-repeat: no-repeat !important;
background-size: 60px 40px !important; background-size: 60px 40px !important;
......
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -2,7 +2,7 @@ from nose.tools import eq_, ok_ ...@@ -2,7 +2,7 @@ from nose.tools import eq_, ok_
from nose.plugins.skip import SkipTest from nose.plugins.skip import SkipTest
# Skip test on PY3 # Skip test on PY3
from flask.ext.admin._compat import PY2 from flask.ext.admin._compat import PY2, as_unicode
if not PY2: if not PY2:
raise SkipTest('MongoEngine is not Python 3 compatible') raise SkipTest('MongoEngine is not Python 3 compatible')
...@@ -350,3 +350,78 @@ def test_nested_list_subdocument(): ...@@ -350,3 +350,78 @@ def test_nested_list_subdocument():
ok_('name' in dir(inline_form)) ok_('name' in dir(inline_form))
ok_('value' not in dir(inline_form)) ok_('value' not in dir(inline_form))
def test_ajax_fk():
app, db, admin = setup()
class Model1(db.Document):
test1 = db.StringField(max_length=20)
test2 = db.StringField(max_length=20)
def __str__(self):
return self.test1
class Model2(db.Document):
int_field = db.IntField()
bool_field = db.BooleanField()
model1 = db.ReferenceField(Model1)
Model1.objects.delete()
Model2.objects.delete()
view = CustomModelView(
Model2,
url='view',
form_ajax_refs={
'model1': ('test1', 'test2')
}
)
admin.add_view(view)
ok_(u'model1' in view._form_ajax_refs)
model = Model1(test1=u'first')
model.save()
model2 = Model1(test1=u'foo', test2=u'bar').save()
# Check loader
loader = view._form_ajax_refs[u'model1']
mdl = loader.get_one(model.id)
eq_(mdl.test1, model.test1)
items = loader.get_list(u'fir')
eq_(len(items), 1)
eq_(items[0].id, model.id)
items = loader.get_list(u'bar')
eq_(len(items), 1)
eq_(items[0].test1, u'foo')
# Check form generation
form = view.create_form()
eq_(form.model1.__class__.__name__, u'AjaxSelectField')
with app.test_request_context('/admin/view/'):
ok_(u'value=""' not in form.model1())
form.model1.data = model
needle = u'data-json="[&quot;%s&quot;, &quot;first&quot;]"' % as_unicode(model.id)
ok_(needle in form.model1())
ok_(u'value="%s"' % as_unicode(model.id) in form.model1())
# Check querying
client = app.test_client()
req = client.get(u'/admin/view/ajax/lookup/?name=model1&query=foo')
eq_(req.data, u'[["%s", "foo"]]' % model2.id)
# Check submitting
client.post('/admin/view/new/', data={u'model1': as_unicode(model.id)})
mdl = Model2.objects.first()
ok_(mdl is not None)
ok_(mdl.model1 is not None)
eq_(mdl.model1.id, model.id)
eq_(mdl.model1.test1, u'first')
...@@ -2,7 +2,7 @@ from nose.tools import eq_, ok_ ...@@ -2,7 +2,7 @@ from nose.tools import eq_, ok_
from nose.plugins.skip import SkipTest from nose.plugins.skip import SkipTest
# Skip test on PY3 # Skip test on PY3
from flask.ext.admin._compat import PY2 from flask.ext.admin._compat import PY2, as_unicode
if not PY2: if not PY2:
raise SkipTest('Peewee is not Python 3 compatible') raise SkipTest('Peewee is not Python 3 compatible')
...@@ -194,3 +194,80 @@ def test_custom_form_base(): ...@@ -194,3 +194,80 @@ def test_custom_form_base():
create_form = view.create_form() create_form = view.create_form()
ok_(isinstance(create_form, TestForm)) ok_(isinstance(create_form, TestForm))
def test_ajax_fk():
app, db, admin = setup()
class BaseModel(peewee.Model):
class Meta:
database = db
class Model1(BaseModel):
test1 = peewee.CharField(max_length=20)
test2 = peewee.CharField(max_length=20)
def __str__(self):
return self.test1
class Model2(BaseModel):
model1 = peewee.ForeignKeyField(Model1)
Model1.create_table()
Model2.create_table()
view = CustomModelView(
Model2,
url='view',
form_ajax_refs={
'model1': ('test1', 'test2')
}
)
admin.add_view(view)
ok_(u'model1' in view._form_ajax_refs)
model = Model1(test1=u'first', test2=u'')
model.save()
model2 = Model1(test1=u'foo', test2=u'bar')
model2.save()
# Check loader
loader = view._form_ajax_refs[u'model1']
mdl = loader.get_one(model.id)
eq_(mdl.test1, model.test1)
items = loader.get_list(u'fir')
eq_(len(items), 1)
eq_(items[0].id, model.id)
items = loader.get_list(u'bar')
eq_(len(items), 1)
eq_(items[0].test1, u'foo')
# Check form generation
form = view.create_form()
eq_(form.model1.__class__.__name__, u'AjaxSelectField')
with app.test_request_context('/admin/view/'):
ok_(u'value=""' not in form.model1())
form.model1.data = model
needle = u'data-json="[%s, &quot;first&quot;]"' % as_unicode(model.id)
ok_(needle in form.model1())
ok_(u'value="%s"' % as_unicode(model.id) in form.model1())
# Check querying
client = app.test_client()
req = client.get(u'/admin/view/ajax/lookup/?name=model1&query=foo')
eq_(req.data, u'[[%s, "foo"]]' % model2.id)
# Check submitting
client.post('/admin/view/new/', data={u'model1': as_unicode(model.id)})
mdl = Model2.select().first()
ok_(mdl is not None)
ok_(mdl.model1 is not None)
eq_(mdl.model1.id, model.id)
eq_(mdl.model1.test1, u'first')
...@@ -8,6 +8,7 @@ def setup(): ...@@ -8,6 +8,7 @@ def setup():
app.config['SECRET_KEY'] = '1' app.config['SECRET_KEY'] = '1'
app.config['CSRF_ENABLED'] = False app.config['CSRF_ENABLED'] = False
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///'
#app.config['SQLALCHEMY_ECHO'] = True
db = SQLAlchemy(app) db = SQLAlchemy(app)
admin = Admin(app) admin = Admin(app)
......
...@@ -3,6 +3,7 @@ from nose.tools import eq_, ok_, raises ...@@ -3,6 +3,7 @@ from nose.tools import eq_, ok_, raises
from wtforms import fields from wtforms import fields
from flask.ext.admin import form from flask.ext.admin import form
from flask.ext.admin._compat import as_unicode
from flask.ext.admin._compat import iteritems from flask.ext.admin._compat import iteritems
from flask.ext.admin.contrib.sqla import ModelView from flask.ext.admin.contrib.sqla import ModelView
...@@ -37,6 +38,9 @@ def create_models(db): ...@@ -37,6 +38,9 @@ def create_models(db):
bool_field = db.Column(db.Boolean) bool_field = db.Column(db.Boolean)
enum_field = db.Column(db.Enum('model1_v1', 'model1_v1'), nullable=True) enum_field = db.Column(db.Enum('model1_v1', 'model1_v1'), nullable=True)
def __str__(self):
return self.test1
class Model2(db.Model): class Model2(db.Model):
def __init__(self, string_field=None, int_field=None, bool_field=None, model1=None): def __init__(self, string_field=None, int_field=None, bool_field=None, model1=None):
self.string_field = string_field self.string_field = string_field
...@@ -675,3 +679,127 @@ def test_custom_form_base(): ...@@ -675,3 +679,127 @@ def test_custom_form_base():
create_form = view.create_form() create_form = view.create_form()
ok_(isinstance(create_form, TestForm)) ok_(isinstance(create_form, TestForm))
def test_ajax_fk():
app, db, admin = setup()
Model1, Model2 = create_models(db)
view = CustomModelView(
Model2, db.session,
url='view',
form_ajax_refs={
'model1': ('test1', 'test2')
}
)
admin.add_view(view)
ok_(u'model1' in view._form_ajax_refs)
model = Model1(u'first')
model2 = Model1(u'foo', u'bar')
db.session.add_all([model, model2])
db.session.commit()
# Check loader
loader = view._form_ajax_refs[u'model1']
mdl = loader.get_one(model.id)
eq_(mdl.test1, model.test1)
items = loader.get_list(u'fir')
eq_(len(items), 1)
eq_(items[0].id, model.id)
items = loader.get_list(u'bar')
eq_(len(items), 1)
eq_(items[0].test1, u'foo')
# Check form generation
form = view.create_form()
eq_(form.model1.__class__.__name__, u'AjaxSelectField')
with app.test_request_context('/admin/view/'):
ok_(u'value=""' not in form.model1())
form.model1.data = model
ok_(u'data-json="[%s, &quot;first&quot;]"' % model.id in form.model1())
ok_(u'value="1"' in form.model1())
# Check querying
client = app.test_client()
req = client.get(u'/admin/view/ajax/lookup/?name=model1&query=foo')
eq_(req.data.decode('utf-8'), u'[[%s, "foo"]]' % model2.id)
# Check submitting
req = client.post('/admin/view/new/', data={u'model1': as_unicode(model.id)})
mdl = db.session.query(Model2).first()
ok_(mdl is not None)
ok_(mdl.model1 is not None)
eq_(mdl.model1.id, model.id)
eq_(mdl.model1.test1, u'first')
def test_ajax_fk_multi():
app, db, admin = setup()
class Model1(db.Model):
__tablename__ = 'model1'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(20))
def __str__(self):
return self.name
table = db.Table('m2m', db.Model.metadata,
db.Column('model1_id', db.Integer, db.ForeignKey('model1.id')),
db.Column('model2_id', db.Integer, db.ForeignKey('model2.id'))
)
class Model2(db.Model):
__tablename__ = 'model2'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(20))
model1_id = db.Column(db.Integer(), db.ForeignKey(Model1.id))
model1 = db.relationship(Model1, backref='models2', secondary=table)
db.create_all()
view = CustomModelView(
Model2, db.session,
url='view',
form_ajax_refs={
'model1': ('name',)
}
)
admin.add_view(view)
ok_(u'model1' in view._form_ajax_refs)
model = Model1(name=u'first')
db.session.add_all([model, Model1(name=u'foo')])
db.session.commit()
# Check form generation
form = view.create_form()
eq_(form.model1.__class__.__name__, u'AjaxSelectMultipleField')
with app.test_request_context('/admin/view/'):
ok_(u'data-json="[]"' in form.model1())
form.model1.data = [model]
ok_(u'data-json="[[1, &quot;first&quot;]]"' in form.model1())
# Check submitting
client = app.test_client()
client.post('/admin/view/new/', data={u'model1': as_unicode(model.id)})
mdl = db.session.query(Model2).first()
ok_(mdl is not None)
ok_(mdl.model1 is not None)
eq_(len(mdl.model1), 1)
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