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):
class TodoView(ModelView):
column_filters = ['done']
form_ajax_refs = {
'user': ('name',)
}
# Flask views
@app.route('/')
......
......@@ -66,6 +66,10 @@ class PostAdmin(ModelView):
'date',
User.username)
form_ajax_refs = {
'user': (User.username, 'email')
}
@app.route('/')
def index():
......
......@@ -121,6 +121,11 @@ class PostAdmin(sqla.ModelView):
text=dict(label='Big Text', validators=[validators.required()])
)
form_ajax_refs = {
'user': (User.username, User.email),
'tags': (Tag.name,)
}
def __init__(self, session):
# Just call parent class with predefined model.
super(PostAdmin, self).__init__(Post, session)
......
......@@ -12,7 +12,7 @@ from flask import flash, url_for, redirect, abort, request
from wtforms import fields, validators
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.actions import action, ActionsMixin
from flask.ext.admin.babel import gettext, lazy_gettext
......@@ -173,7 +173,7 @@ class FileAdmin(BaseView, ActionsMixin):
Verify if path exists. If set to `True` and path does not exist
will raise an exception.
"""
self.base_path = base_path
self.base_path = as_unicode(base_path)
self.base_url = base_url
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.base import BaseDocument, DocumentMetaclass
......@@ -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.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._compat import iteritems
......@@ -158,9 +156,14 @@ class CustomModelConverter(orm.ModelConverter):
@orm.converts('ReferenceField')
def conv_Reference(self, model, field, kwargs):
kwargs['widget'] = form.Select2Widget()
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)
@orm.converts('FileField')
......
......@@ -18,6 +18,7 @@ from .form import get_form, CustomModelConverter
from .typefmt import DEFAULT_FORMATTERS
from .tools import parse_like_term
from .helpers import format_error
from .ajax import QueryAjaxModelLoader
SORTABLE_FIELDS = set((
......@@ -336,6 +337,31 @@ class ModelView(BaseModelView):
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):
"""
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
from flask.ext.admin import form
from flask.ext.admin._compat import iteritems, itervalues
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
......@@ -80,13 +80,26 @@ class InlineModelFormList(InlineFieldList):
class CustomModelConverter(ModelConverter):
def __init__(self, additional=None):
def __init__(self, view, additional=None):
super(CustomModelConverter, self).__init__(additional)
self.view = view
self.converters[PrimaryKeyField] = self.handle_pk
self.converters[DateTimeField] = self.handle_datetime
self.converters[DateField] = self.handle_date
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):
kwargs['validators'] = []
return field.name, fields.HiddenField(**kwargs)
......
......@@ -2,7 +2,6 @@ import logging
from flask import flash
from flask.ext.admin import form
from flask.ext.admin._compat import string_types
from flask.ext.admin.babel import gettext, ngettext, lazy_gettext
from flask.ext.admin.model import BaseModelView
......@@ -11,8 +10,10 @@ from peewee import PrimaryKeyField, ForeignKeyField, Field, CharField, TextField
from flask.ext.admin.actions import action
from flask.ext.admin.contrib.peewee import filters
from .form import get_form, CustomModelConverter, InlineModelConverter, save_inline
from .tools import get_primary_key, parse_like_term
from .ajax import QueryAjaxModelLoader
class ModelView(BaseModelView):
......@@ -217,7 +218,7 @@ class ModelView(BaseModelView):
return isinstance(filter, filters.BasePeeweeFilter)
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,
only=self.form_columns,
exclude=self.form_excluded_columns,
......@@ -230,7 +231,7 @@ class ModelView(BaseModelView):
return 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)
for m in self.inline_models:
......@@ -241,6 +242,30 @@ class ModelView(BaseModelView):
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):
if field.model_class != self.model:
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
from flask.ext.admin.model.form import (converts, ModelConverterBase,
InlineFormAdmin, 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
from flask.ext.admin._compat import iteritems
......@@ -70,6 +71,31 @@ class AdminModelConverter(ModelConverterBase):
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):
remote_model = prop.mapper.class_
local_column = prop.local_remote_pairs[0][0]
......@@ -85,16 +111,6 @@ class AdminModelConverter(ModelConverterBase):
# Contribute model-related parameters
if 'allow_blank' not in kwargs:
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 = self._get_field_override(prop.key)
......@@ -102,15 +118,15 @@ class AdminModelConverter(ModelConverterBase):
return override(**kwargs)
if prop.direction.name == 'MANYTOONE':
return QuerySelectField(**kwargs)
return self._model_select_field(prop, False, remote_model, **kwargs)
elif prop.direction.name == 'ONETOMANY':
# Skip backrefs
if not local_column.foreign_keys and getattr(self.view, 'column_hide_backrefs', True):
return None
return QuerySelectMultipleField(**kwargs)
return self._model_select_field(prop, True, remote_model, **kwargs)
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):
# Properly handle forced fields
......
......@@ -16,6 +16,8 @@ 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
class ModelView(BaseModelView):
"""
......@@ -585,6 +587,33 @@ class ModelView(BaseModelView):
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
def get_query(self):
"""
......
......@@ -17,14 +17,25 @@ class Select2Widget(widgets.Select):
def __call__(self, field, **kwargs):
allow_blank = getattr(field, 'allow_blank', False)
kwargs['data-role'] = u'select2'
if allow_blank and not self.multiple:
kwargs['data-role'] = u'select2blank'
else:
kwargs['data-role'] = u'select2'
kwargs['data-allow-blank'] = u'1'
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):
"""
Date picker widget.
......@@ -73,12 +84,3 @@ class RenderTemplateWidget(object):
template = jinja_env.get_template(self.template)
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
from flask import request, url_for, redirect, flash
from flask import request, url_for, redirect, flash, abort, json, Response
from jinja2 import contextfunction
......@@ -15,6 +15,7 @@ from flask.ext.admin.tools import rec_getattr
from flask.ext.admin._backwards import ObsoleteAttr
from flask.ext.admin._compat import iteritems, as_unicode
from .helpers import prettify_name, get_mdict_item_or_list
from .ajax import AjaxModelLoader
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.
"""
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
action_disallowed_list = ObsoleteAttr('action_disallowed_list',
'disallowed_actions',
......@@ -434,12 +450,14 @@ class BaseModelView(BaseView, ActionsMixin):
self.column_labels = {}
# Forms
self._create_form_class = self.get_create_form()
self._edit_form_class = self.get_edit_form()
self._form_ajax_refs = self._process_ajax_references()
if self.form_widget_args is None:
self.form_widget_args = {}
self._create_form_class = self.get_create_form()
self._edit_form_class = self.get_edit_form()
# Search
self._search_supported = self.init_search()
......@@ -971,6 +989,31 @@ class BaseModelView(BaseView, ActionsMixin):
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
@expose('/')
def index_view(self):
......@@ -1157,3 +1200,18 @@ class BaseModelView(BaseView, ActionsMixin):
Mass-model action view.
"""
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
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 .widgets import InlineFieldListWidget, InlineFormWidget
from .widgets import InlineFieldListWidget, InlineFormWidget, AjaxSelect2Widget
class InlineFieldList(FieldList):
......@@ -113,3 +114,99 @@ class InlineFormField(FormField):
Inline version of the ``FormField`` widget.
"""
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
......@@ -9,3 +13,42 @@ class InlineFieldListWidget(RenderTemplateWidget):
class InlineFormWidget(RenderTemplateWidget):
def __init__(self):
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() {
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) {
case 'select2':
$(el).select2({width: 'resolve'});
break;
case 'select2blank':
$(el).select2({allowClear: true, width: 'resolve'});
break;
case 'select2tags':
$(el).select2({tags: [], tokenSeparators: [','], width: 'resolve'});
break;
var opts = {
width: 'resolve'
};
if ($el.attr('data-allow-blank'))
opts['allowClear'] = true;
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':
$(el).datepicker();
break;
$el.datepicker();
return true;
case 'datetimepicker':
$(el).datepicker({displayTime: true});
break;
$el.datepicker({displayTime: true});
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) {
var $el = $(el);
var $template = $($(template).text());
......@@ -60,12 +161,19 @@
this.applyGlobalStyles($template);
};
/**
* Apply global input styles.
*
* @method applyGlobalStyles
* @param {Selector} jQuery element
*/
this.applyGlobalStyles = function(parent) {
$('[data-role=select2]', parent).select2({width: 'resolve'});
$('[data-role=select2blank]', parent).select2({allowClear: true, width: 'resolve'});
$('[data-role=select2tags]', parent).select2({multiple: true, tokenSeparators: [','], width: 'resolve'});
$('[data-role=datepicker]', parent).datepicker();
$('[data-role=datetimepicker]', parent).datepicker({displayTime: true});
var self = this;
$('[data-role]', parent).each(function() {
var $el = $(this);
self.applyStyle($el, $el.attr('data-role'));
});
};
};
......@@ -80,6 +188,8 @@
// Expose faForm globally
var faForm = window.faForm = new AdminForm();
// Apply global styles
faForm.applyGlobalStyles(document);
// Apply global styles for current page after page loaded
$(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 {
margin: 0;
......@@ -14,7 +14,7 @@ Version: 3.4.0 Timestamp: Tue May 14 08:27:33 PDT 2013
.select2-container,
.select2-drop,
.select2-search,
.select2-search input{
.select2-search input {
/*
Force border-box so that % widths fit the parent
container without overlap because of margin/padding.
......@@ -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
*/
-webkit-box-sizing: border-box; /* webkit */
-khtml-box-sizing: border-box; /* konqueror */
-moz-box-sizing: border-box; /* firefox */
-ms-box-sizing: border-box; /* ie */
box-sizing: border-box; /* css3 */
}
......@@ -41,13 +39,9 @@ Version: 3.4.0 Timestamp: Tue May 14 08:27:33 PDT 2013
color: #444;
text-decoration: none;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
border-radius: 4px;
-webkit-background-clip: padding-box;
-moz-background-clip: padding;
background-clip: padding-box;
background-clip: padding-box;
-webkit-touch-callout: none;
-webkit-user-select: none;
......@@ -57,45 +51,41 @@ Version: 3.4.0 Timestamp: Tue May 14 08:27:33 PDT 2013
user-select: none;
background-color: #fff;
background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #eeeeee), color-stop(0.5, white));
background-image: -webkit-linear-gradient(center bottom, #eeeeee 0%, white 50%);
background-image: -moz-linear-gradient(center bottom, #eeeeee 0%, white 50%);
background-image: -o-linear-gradient(bottom, #eeeeee 0%, #ffffff 50%);
background-image: -ms-linear-gradient(top, #ffffff 0%, #eeeeee 50%);
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, #eee 0%, #fff 50%);
background-image: -moz-linear-gradient(center bottom, #eee 0%, #fff 50%);
background-image: -o-linear-gradient(bottom, #eee 0%, #fff 50%);
background-image: -ms-linear-gradient(top, #fff 0%, #eee 50%);
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 {
border-bottom-color: #aaa;
-webkit-border-radius:0 0 4px 4px;
-moz-border-radius:0 0 4px 4px;
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-linear-gradient(center bottom, #eeeeee 0%, white 90%);
background-image: -moz-linear-gradient(center bottom, #eeeeee 0%, white 90%);
background-image: -o-linear-gradient(bottom, #eeeeee 0%, white 90%);
background-image: -ms-linear-gradient(top, #eeeeee 0%,#ffffff 90%);
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#eeeeee',GradientType=0 );
background-image: linear-gradient(top, #eeeeee 0%,#ffffff 90%);
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, #eee 0%, #fff 90%);
background-image: -moz-linear-gradient(center bottom, #eee 0%, #fff 90%);
background-image: -o-linear-gradient(bottom, #eee 0%, #fff 90%);
background-image: -ms-linear-gradient(top, #eee 0%, #fff 90%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#eeeeee', GradientType=0);
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;
}
.select2-container .select2-choice span {
.select2-container .select2-choice > .select2-chosen {
margin-right: 26px;
display: block;
overflow: hidden;
white-space: nowrap;
-ms-text-overflow: ellipsis;
-o-text-overflow: ellipsis;
text-overflow: ellipsis;
text-overflow: ellipsis;
}
.select2-container .select2-choice abbr {
......@@ -125,15 +115,27 @@ Version: 3.4.0 Timestamp: Tue May 14 08:27:33 PDT 2013
}
.select2-drop-mask {
position: absolute;
border: 0;
margin: 0;
padding: 0;
position: fixed;
left: 0;
top: 0;
min-height: 100%;
min-width: 100%;
height: auto;
width: auto;
opacity: 0;
z-index: 9998;
/* styles required for IE to work */
background-color: #fff;
opacity: 0;
filter: alpha(opacity=0);
}
.select2-drop {
width: 100%;
margin-top:-1px;
margin-top: -1px;
position: absolute;
z-index: 9999;
top: 100%;
......@@ -143,12 +145,9 @@ Version: 3.4.0 Timestamp: Tue May 14 08:27:33 PDT 2013
border: 1px solid #aaa;
border-top: 0;
-webkit-border-radius: 0 0 4px 4px;
-moz-border-radius: 0 0 4px 4px;
border-radius: 0 0 4px 4px;
border-radius: 0 0 4px 4px;
-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);
}
......@@ -166,16 +165,22 @@ Version: 3.4.0 Timestamp: Tue May 14 08:27:33 PDT 2013
border-top: 1px solid #aaa;
border-bottom: 0;
-webkit-border-radius: 4px 4px 0 0;
-moz-border-radius: 4px 4px 0 0;
border-radius: 4px 4px 0 0;
border-radius: 4px 4px 0 0;
-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);
}
.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;
width: 18px;
height: 100%;
......@@ -184,25 +189,21 @@ Version: 3.4.0 Timestamp: Tue May 14 08:27:33 PDT 2013
top: 0;
border-left: 1px solid #aaa;
-webkit-border-radius: 0 4px 4px 0;
-moz-border-radius: 0 4px 4px 0;
border-radius: 0 4px 4px 0;
border-radius: 0 4px 4px 0;
-webkit-background-clip: padding-box;
-moz-background-clip: padding;
background-clip: padding-box;
background-clip: padding-box;
background: #ccc;
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: -moz-linear-gradient(center 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);
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;
width: 100%;
height: 100%;
......@@ -235,21 +236,18 @@ Version: 3.4.0 Timestamp: Tue May 14 08:27:33 PDT 2013
font-size: 1em;
border: 1px solid #aaa;
-webkit-border-radius: 0;
-moz-border-radius: 0;
border-radius: 0;
border-radius: 0;
-webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none;
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-linear-gradient(center bottom, white 85%, #eeeeee 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, -o-linear-gradient(bottom, white 85%, #eeeeee 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, linear-gradient(top, #ffffff 85%, #eeeeee 99%);
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, #fff 85%, #eee 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, #fff 85%, #eee 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, #fff 85%, #eee 99%);
}
.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
.select2-search input.select2-active {
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-linear-gradient(center bottom, white 85%, #eeeeee 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%, -o-linear-gradient(bottom, white 85%, #eeeeee 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%, linear-gradient(top, #ffffff 85%, #eeeeee 99%);
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, #fff 85%, #eee 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, #fff 85%, #eee 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, #fff 85%, #eee 99%);
}
.select2-container-active .select2-choice,
......@@ -271,33 +269,26 @@ Version: 3.4.0 Timestamp: Tue May 14 08:27:33 PDT 2013
border: 1px solid #5897fb;
outline: none;
-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);
-webkit-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 {
border-bottom-color: transparent;
-webkit-box-shadow: 0 1px 0 #fff inset;
-moz-box-shadow: 0 1px 0 #fff inset;
box-shadow: 0 1px 0 #fff inset;
-webkit-border-bottom-left-radius: 0;
-moz-border-radius-bottomleft: 0;
border-bottom-left-radius: 0;
-webkit-border-bottom-right-radius: 0;
-moz-border-radius-bottomright: 0;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
background-color: #eee;
background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, white), color-stop(0.5, #eeeeee));
background-image: -webkit-linear-gradient(center bottom, white 0%, #eeeeee 50%);
background-image: -moz-linear-gradient(center bottom, white 0%, #eeeeee 50%);
background-image: -o-linear-gradient(bottom, white 0%, #eeeeee 50%);
background-image: -ms-linear-gradient(top, #ffffff 0%,#eeeeee 50%);
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#eeeeee', endColorstr='#ffffff',GradientType=0 );
background-image: linear-gradient(top, #ffffff 0%,#eeeeee 50%);
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, #fff 0%, #eee 50%);
background-image: -moz-linear-gradient(center bottom, #fff 0%, #eee 50%);
background-image: -o-linear-gradient(bottom, #fff 0%, #eee 50%);
background-image: -ms-linear-gradient(top, #fff 0%, #eee 50%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#eeeeee', endColorstr='#ffffff', GradientType=0);
background-image: linear-gradient(top, #fff 0%, #eee 50%);
}
.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
border: 1px solid #5897fb;
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-linear-gradient(center top, white 0%, #eeeeee 50%);
background-image: -moz-linear-gradient(center top, white 0%, #eeeeee 50%);
background-image: -o-linear-gradient(top, white 0%, #eeeeee 50%);
background-image: -ms-linear-gradient(bottom, #ffffff 0%,#eeeeee 50%);
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#eeeeee', endColorstr='#ffffff',GradientType=0 );
background-image: linear-gradient(bottom, #ffffff 0%,#eeeeee 50%);
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, #fff 0%, #eee 50%);
background-image: -moz-linear-gradient(center top, #fff 0%, #eee 50%);
background-image: -o-linear-gradient(top, #fff 0%, #eee 50%);
background-image: -ms-linear-gradient(bottom, #fff 0%, #eee 50%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#eeeeee', endColorstr='#ffffff', GradientType=0);
background-image: linear-gradient(bottom, #fff 0%, #eee 50%);
}
.select2-dropdown-open .select2-choice div {
.select2-dropdown-open .select2-choice .select2-arrow {
background: transparent;
border-left: none;
filter: none;
}
.select2-dropdown-open .select2-choice div b {
.select2-dropdown-open .select2-choice .select2-arrow b {
background-position: -18px 1px;
}
......@@ -331,7 +322,7 @@ Version: 3.4.0 Timestamp: Tue May 14 08:27:33 PDT 2013
position: relative;
overflow-x: hidden;
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 {
......@@ -387,7 +378,7 @@ Version: 3.4.0 Timestamp: Tue May 14 08:27:33 PDT 2013
}
.select2-results .select2-highlighted ul {
background: white;
background: #fff;
color: #000;
}
......@@ -436,7 +427,7 @@ disabled look for disabled choices in the results dropdown
cursor: default;
}
.select2-container.select2-container-disabled .select2-choice div {
.select2-container.select2-container-disabled .select2-choice .select2-arrow {
background-color: #f4f4f4;
background-image: none;
border-left: 0;
......@@ -461,12 +452,12 @@ disabled look for disabled choices in the results dropdown
overflow: hidden;
background-color: #fff;
background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(1%, #eeeeee), color-stop(15%, #ffffff));
background-image: -webkit-linear-gradient(top, #eeeeee 1%, #ffffff 15%);
background-image: -moz-linear-gradient(top, #eeeeee 1%, #ffffff 15%);
background-image: -o-linear-gradient(top, #eeeeee 1%, #ffffff 15%);
background-image: -ms-linear-gradient(top, #eeeeee 1%, #ffffff 15%);
background-image: linear-gradient(top, #eeeeee 1%, #ffffff 15%);
background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(1%, #eee), color-stop(15%, #fff));
background-image: -webkit-linear-gradient(top, #eee 1%, #fff 15%);
background-image: -moz-linear-gradient(top, #eee 1%, #fff 15%);
background-image: -o-linear-gradient(top, #eee 1%, #fff 15%);
background-image: -ms-linear-gradient(top, #eee 1%, #fff 15%);
background-image: linear-gradient(top, #eee 1%, #fff 15%);
}
.select2-locked {
......@@ -481,9 +472,8 @@ disabled look for disabled choices in the results dropdown
border: 1px solid #5897fb;
outline: none;
-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);
-webkit-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 {
float: left;
......@@ -505,7 +495,6 @@ disabled look for disabled choices in the results dropdown
outline: 0;
border: 0;
-webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none;
background: transparent !important;
}
......@@ -528,17 +517,12 @@ disabled look for disabled choices in the results dropdown
cursor: default;
border: 1px solid #aaaaaa;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
border-radius: 3px;
-webkit-box-shadow: 0 0 2px #ffffff 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 #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);
box-shadow: 0 0 2px #fff inset, 0 1px 0 rgba(0, 0, 0, 0.05);
-webkit-background-clip: padding-box;
-moz-background-clip: padding;
background-clip: padding-box;
background-clip: padding-box;
-webkit-touch-callout: none;
-webkit-user-select: none;
......@@ -548,15 +532,15 @@ disabled look for disabled choices in the results dropdown
user-select: none;
background-color: #e4e4e4;
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-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
background-image: -moz-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
background-image: -o-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
background-image: -ms-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
background-image: linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
}
.select2-container-multi .select2-choices .select2-search-choice span {
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%, #eee));
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%, #eee 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%, #eee 100%);
background-image: linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eee 100%);
}
.select2-container-multi .select2-choices .select2-search-choice .select2-chosen {
cursor: default;
}
.select2-container-multi .select2-choices .select2-search-choice-focus {
......@@ -588,7 +572,7 @@ disabled look for disabled choices in the results dropdown
}
/* disabled styles */
.select2-container-multi.select2-container-disabled .select2-choices{
.select2-container-multi.select2-container-disabled .select2-choices {
background-color: #f4f4f4;
background-image: none;
border: 1px solid #ddd;
......@@ -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;
background:none;
background: none;
}
/* end multiselect */
......@@ -614,16 +598,17 @@ disabled look for disabled choices in the results dropdown
}
.select2-offscreen, .select2-offscreen:focus {
clip: rect(0 0 0 0);
width: 1px;
height: 1px;
border: 0;
margin: 0;
padding: 0;
overflow: hidden;
position: absolute;
outline: 0;
left: 0px;
clip: rect(0 0 0 0) !important;
width: 1px !important;
height: 1px !important;
border: 0 !important;
margin: 0 !important;
padding: 0 !important;
overflow: hidden !important;
position: absolute !important;
outline: 0 !important;
left: 0px !important;
top: 0px !important;
}
.select2-display-none {
......@@ -641,7 +626,7 @@ disabled look for disabled choices in the results dropdown
/* Retina-ize icons */
@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-repeat: no-repeat !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_
from nose.plugins.skip import SkipTest
# Skip test on PY3
from flask.ext.admin._compat import PY2
from flask.ext.admin._compat import PY2, as_unicode
if not PY2:
raise SkipTest('MongoEngine is not Python 3 compatible')
......@@ -350,3 +350,78 @@ def test_nested_list_subdocument():
ok_('name' 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_
from nose.plugins.skip import SkipTest
# Skip test on PY3
from flask.ext.admin._compat import PY2
from flask.ext.admin._compat import PY2, as_unicode
if not PY2:
raise SkipTest('Peewee is not Python 3 compatible')
......@@ -194,3 +194,80 @@ def test_custom_form_base():
create_form = view.create_form()
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():
app.config['SECRET_KEY'] = '1'
app.config['CSRF_ENABLED'] = False
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///'
#app.config['SQLALCHEMY_ECHO'] = True
db = SQLAlchemy(app)
admin = Admin(app)
......
......@@ -3,6 +3,7 @@ from nose.tools import eq_, ok_, raises
from wtforms import fields
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.contrib.sqla import ModelView
......@@ -37,6 +38,9 @@ def create_models(db):
bool_field = db.Column(db.Boolean)
enum_field = db.Column(db.Enum('model1_v1', 'model1_v1'), nullable=True)
def __str__(self):
return self.test1
class Model2(db.Model):
def __init__(self, string_field=None, int_field=None, bool_field=None, model1=None):
self.string_field = string_field
......@@ -675,3 +679,127 @@ def test_custom_form_base():
create_form = view.create_form()
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