Commit 6f4ef689 authored by Paul Brown's avatar Paul Brown

simplify editable list view, fix list view multiple select

parent d7df4fe4
...@@ -5,8 +5,7 @@ from flask import request, flash, abort, Response ...@@ -5,8 +5,7 @@ from flask import request, flash, abort, Response
from flask_admin import expose from flask_admin import expose
from flask_admin.babel import gettext, ngettext, lazy_gettext from flask_admin.babel import gettext, ngettext, lazy_gettext
from flask_admin.model import BaseModelView from flask_admin.model import BaseModelView
from flask_admin.model.form import wrap_fields_in_fieldlist from flask_admin.model.form import create_editable_list_form
from flask_admin.model.fields import ListEditableFieldList
from flask_admin._compat import iteritems, string_types from flask_admin._compat import iteritems, string_types
import mongoengine import mongoengine
...@@ -426,17 +425,16 @@ class ModelView(BaseModelView): ...@@ -426,17 +425,16 @@ class ModelView(BaseModelView):
return form_class return form_class
def scaffold_list_form(self, custom_fieldlist=ListEditableFieldList, def scaffold_list_form(self, widget=None, validators=None):
validators=None):
""" """
Create form for the `index_view` using only the columns from Create form for the `index_view` using only the columns from
`self.column_editable_list`. `self.column_editable_list`.
:param widget:
WTForms widget class. Defaults to `XEditableWidget`.
:param validators: :param validators:
`form_args` dict with only validators `form_args` dict with only validators
{'name': {'validators': [required()]}} {'name': {'validators': [required()]}}
:param custom_fieldlist:
A WTForm FieldList class. By default, `ListEditableFieldList`.
""" """
form_class = get_form(self.model, form_class = get_form(self.model,
self.model_form_converter(self), self.model_form_converter(self),
...@@ -444,9 +442,8 @@ class ModelView(BaseModelView): ...@@ -444,9 +442,8 @@ class ModelView(BaseModelView):
only=self.column_editable_list, only=self.column_editable_list,
field_args=validators) field_args=validators)
return wrap_fields_in_fieldlist(self.form_base_class, return create_editable_list_form(self.form_base_class, form_class,
form_class, widget)
custom_fieldlist)
# AJAX foreignkey support # AJAX foreignkey support
def _create_ajax_loader(self, name, opts): def _create_ajax_loader(self, name, opts):
......
...@@ -5,8 +5,7 @@ from flask import flash ...@@ -5,8 +5,7 @@ from flask import flash
from flask_admin._compat import string_types, iteritems from flask_admin._compat import string_types, iteritems
from flask_admin.babel import gettext, ngettext, lazy_gettext from flask_admin.babel import gettext, ngettext, lazy_gettext
from flask_admin.model import BaseModelView from flask_admin.model import BaseModelView
from flask_admin.model.form import wrap_fields_in_fieldlist from flask_admin.model.form import create_editable_list_form
from flask_admin.model.fields import ListEditableFieldList
from peewee import PrimaryKeyField, ForeignKeyField, Field, CharField, TextField from peewee import PrimaryKeyField, ForeignKeyField, Field, CharField, TextField
...@@ -265,26 +264,24 @@ class ModelView(BaseModelView): ...@@ -265,26 +264,24 @@ class ModelView(BaseModelView):
return form_class return form_class
def scaffold_list_form(self, custom_fieldlist=ListEditableFieldList, def scaffold_list_form(self, widget=None, validators=None):
validators=None):
""" """
Create form for the `index_view` using only the columns from Create form for the `index_view` using only the columns from
`self.column_editable_list`. `self.column_editable_list`.
:param widget:
WTForms widget class. Defaults to `XEditableWidget`.
:param validators: :param validators:
`form_args` dict with only validators `form_args` dict with only validators
{'name': {'validators': [required()]}} {'name': {'validators': [required()]}}
:param custom_fieldlist:
A WTForm FieldList class. By default, `ListEditableFieldList`.
""" """
form_class = get_form(self.model, self.model_form_converter(self), 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.column_editable_list, only=self.column_editable_list,
field_args=validators) field_args=validators)
return wrap_fields_in_fieldlist(self.form_base_class, return create_editable_list_form(self.form_base_class, form_class,
form_class, widget)
custom_fieldlist)
def scaffold_inline_form_models(self, form_class): def scaffold_inline_form_models(self, form_class):
converter = self.model_form_converter(self) converter = self.model_form_converter(self)
......
...@@ -14,7 +14,7 @@ except ImportError: ...@@ -14,7 +14,7 @@ except ImportError:
from .tools import get_primary_key from .tools import get_primary_key
from flask_admin._compat import text_type, string_types, iteritems from flask_admin._compat import text_type, string_types, iteritems
from flask_admin.form import FormOpts, BaseForm from flask_admin.form import FormOpts, BaseForm, Select2Widget
from flask_admin.model.fields import InlineFieldList, InlineModelFormField from flask_admin.model.fields import InlineFieldList, InlineModelFormField
from flask_admin.babel import lazy_gettext from flask_admin.babel import lazy_gettext
...@@ -55,7 +55,7 @@ class QuerySelectField(SelectFieldBase): ...@@ -55,7 +55,7 @@ class QuerySelectField(SelectFieldBase):
being `None`. The label for this blank choice can be set by specifying the being `None`. The label for this blank choice can be set by specifying the
`blank_text` parameter. `blank_text` parameter.
""" """
widget = widgets.Select() widget = Select2Widget()
def __init__(self, label=None, validators=None, query_factory=None, def __init__(self, label=None, validators=None, query_factory=None,
get_pk=None, get_label=None, allow_blank=False, get_pk=None, get_label=None, allow_blank=False,
...@@ -136,7 +136,7 @@ class QuerySelectMultipleField(QuerySelectField): ...@@ -136,7 +136,7 @@ class QuerySelectMultipleField(QuerySelectField):
If any of the items in the data list or submitted form data cannot be If any of the items in the data list or submitted form data cannot be
found in the query, this will result in a validation error. found in the query, this will result in a validation error.
""" """
widget = widgets.Select(multiple=True) widget = Select2Widget(multiple=True)
def __init__(self, label=None, validators=None, default=None, **kwargs): def __init__(self, label=None, validators=None, default=None, **kwargs):
if default is None: if default is None:
......
...@@ -80,12 +80,6 @@ class AdminModelConverter(ModelConverterBase): ...@@ -80,12 +80,6 @@ class AdminModelConverter(ModelConverterBase):
if 'query_factory' not in kwargs: if 'query_factory' not in kwargs:
kwargs['query_factory'] = lambda: self.session.query(remote_model) kwargs['query_factory'] = lambda: self.session.query(remote_model)
if 'widget' not in kwargs:
if multiple:
kwargs['widget'] = form.Select2Widget(multiple=True)
else:
kwargs['widget'] = form.Select2Widget()
if multiple: if multiple:
return QuerySelectMultipleField(**kwargs) return QuerySelectMultipleField(**kwargs)
else: else:
......
...@@ -15,9 +15,7 @@ from flask import flash ...@@ -15,9 +15,7 @@ from flask import flash
from flask_admin._compat import string_types, text_type from flask_admin._compat import string_types, text_type
from flask_admin.babel import gettext, ngettext, lazy_gettext from flask_admin.babel import gettext, ngettext, lazy_gettext
from flask_admin.model import BaseModelView from flask_admin.model import BaseModelView
from flask_admin.model.form import wrap_fields_in_fieldlist from flask_admin.model.form import create_editable_list_form
from flask_admin.model.fields import ListEditableFieldList
from flask_admin.actions import action from flask_admin.actions import action
from flask_admin._backwards import ObsoleteAttr from flask_admin._backwards import ObsoleteAttr
...@@ -675,17 +673,16 @@ class ModelView(BaseModelView): ...@@ -675,17 +673,16 @@ class ModelView(BaseModelView):
return form_class return form_class
def scaffold_list_form(self, custom_fieldlist=ListEditableFieldList, def scaffold_list_form(self, widget=None, validators=None):
validators=None):
""" """
Create form for the `index_view` using only the columns from Create form for the `index_view` using only the columns from
`self.column_editable_list`. `self.column_editable_list`.
:param widget:
WTForms widget class. Defaults to `XEditableWidget`.
:param validators: :param validators:
`form_args` dict with only validators `form_args` dict with only validators
{'name': {'validators': [required()]}} {'name': {'validators': [required()]}}
:param custom_fieldlist:
A WTForm FieldList class. By default, `ListEditableFieldList`.
""" """
converter = self.model_form_converter(self.session, self) converter = self.model_form_converter(self.session, self)
form_class = form.get_form(self.model, converter, form_class = form.get_form(self.model, converter,
...@@ -693,9 +690,8 @@ class ModelView(BaseModelView): ...@@ -693,9 +690,8 @@ class ModelView(BaseModelView):
only=self.column_editable_list, only=self.column_editable_list,
field_args=validators) field_args=validators)
return wrap_fields_in_fieldlist(self.form_base_class, return create_editable_list_form(self.form_base_class, form_class,
form_class, widget)
custom_fieldlist)
def scaffold_inline_form_models(self, form_class): def scaffold_inline_form_models(self, form_class):
""" """
......
...@@ -26,7 +26,6 @@ from flask_admin._compat import (iteritems, itervalues, OrderedDict, ...@@ -26,7 +26,6 @@ from flask_admin._compat import (iteritems, itervalues, OrderedDict,
as_unicode, csv_encode, text_type) as_unicode, csv_encode, text_type)
from .helpers import prettify_name, get_mdict_item_or_list from .helpers import prettify_name, get_mdict_item_or_list
from .ajax import AjaxModelLoader from .ajax import AjaxModelLoader
from .fields import ListEditableFieldList
# Used to generate filter query string name # Used to generate filter query string name
filter_char_re = re.compile('[^a-z0-9 ]') filter_char_re = re.compile('[^a-z0-9 ]')
...@@ -1069,17 +1068,16 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -1069,17 +1068,16 @@ class BaseModelView(BaseView, ActionsMixin):
""" """
raise NotImplementedError('Please implement scaffold_form method') raise NotImplementedError('Please implement scaffold_form method')
def scaffold_list_form(self, custom_fieldlist=ListEditableFieldList, def scaffold_list_form(self, widget=None, validators=None):
validators=None):
""" """
Create form for the `index_view` using only the columns from Create form for the `index_view` using only the columns from
`self.column_editable_list`. `self.column_editable_list`.
:param widget:
WTForms widget class. Defaults to `XEditableWidget`.
:param validators: :param validators:
`form_args` dict with only validators `form_args` dict with only validators
{'name': {'validators': [DataRequired()]}} {'name': {'validators': [DataRequired()]}}
:param custom_fieldlist:
A WTForm FieldList class. By default, `ListEditableFieldList`.
Must be implemented in the child class. Must be implemented in the child class.
""" """
...@@ -1107,7 +1105,6 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -1107,7 +1105,6 @@ class BaseModelView(BaseView, ActionsMixin):
Allows overriding the editable list view field/widget. For example:: Allows overriding the editable list view field/widget. For example::
from flask_admin.model.fields import ListEditableFieldList
from flask_admin.model.widgets import XEditableWidget from flask_admin.model.widgets import XEditableWidget
class CustomWidget(XEditableWidget): class CustomWidget(XEditableWidget):
...@@ -1119,12 +1116,9 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -1119,12 +1116,9 @@ class BaseModelView(BaseView, ActionsMixin):
return kwargs return kwargs
class CustomFieldList(ListEditableFieldList):
widget = CustomWidget()
class MyModelView(BaseModelView): class MyModelView(BaseModelView):
def get_list_form(self): def get_list_form(self):
return self.scaffold_list_form(CustomFieldList) return self.scaffold_list_form(widget=CustomWidget)
""" """
if self.form_args: if self.form_args:
# get only validators, other form_args can break FieldList wrapper # get only validators, other form_args can break FieldList wrapper
...@@ -1716,7 +1710,7 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -1716,7 +1710,7 @@ class BaseModelView(BaseView, ActionsMixin):
List view List view
""" """
if self.column_editable_list: if self.column_editable_list:
form = self.list_form() form = self.list_form
else: else:
form = None form = None
...@@ -2074,24 +2068,23 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -2074,24 +2068,23 @@ class BaseModelView(BaseView, ActionsMixin):
if not self.column_editable_list: if not self.column_editable_list:
abort(404) abort(404)
record = None
form = self.list_form() form = self.list_form()
# prevent validation issues due to submitting a single field # prevent validation issues due to submitting a single field
# delete all fields except the field being submitted # delete all fields except the submitted fields and csrf token
for field in form: for field in form:
# only the submitted field has a positive last_index if (field.name in request.form) or (field.name == 'csrf_token'):
if getattr(field, 'last_index', 0):
record = self.get_one(str(field.last_index))
elif field.name == 'csrf_token':
pass pass
else: else:
form.__delitem__(field.name) form.__delitem__(field.name)
if record is None:
return gettext('Failed to update record. %(error)s', error=''), 500
if self.validate_form(form): if self.validate_form(form):
pk = form.list_form_pk.data
record = self.get_one(pk)
if record is None:
return gettext('Record does not exist.'), 500
if self.update_model(form, record): if self.update_model(form, record):
# Success # Success
return gettext('Record was successfully saved.') return gettext('Record was successfully saved.')
...@@ -2105,6 +2098,8 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -2105,6 +2098,8 @@ class BaseModelView(BaseView, ActionsMixin):
for error in field.errors: for error in field.errors:
# return validation error to x-editable # return validation error to x-editable
if isinstance(error, list): if isinstance(error, list):
return ", ".join(error), 500 return gettext('Failed to update record. %(error)s',
error=", ".join(error)), 500
else: else:
return error, 500 return gettext('Failed to update record. %(error)s',
error=error), 500
...@@ -10,7 +10,7 @@ except ImportError: ...@@ -10,7 +10,7 @@ except ImportError:
from flask_admin._compat import iteritems from flask_admin._compat import iteritems
from .widgets import (InlineFieldListWidget, InlineFormWidget, from .widgets import (InlineFieldListWidget, InlineFormWidget,
AjaxSelect2Widget, XEditableWidget) AjaxSelect2Widget)
class InlineFieldList(FieldList): class InlineFieldList(FieldList):
...@@ -126,58 +126,6 @@ class InlineModelFormField(FormField): ...@@ -126,58 +126,6 @@ class InlineModelFormField(FormField):
field.populate_obj(obj, name) field.populate_obj(obj, name)
class ListEditableFieldList(FieldList):
"""
Modified FieldList to allow for alphanumeric primary keys.
Used in the editable list view.
"""
widget = XEditableWidget()
def __init__(self, *args, **kwargs):
super(ListEditableFieldList, self).__init__(*args, **kwargs)
# min_entries = 1 is required for the widget to determine the type
self.min_entries = 1
def _extract_indices(self, prefix, formdata):
offset = len(prefix) + 1
for name in formdata:
# selects only relevant field (not CSRF, other fields, etc)
if name.startswith(prefix):
# exclude offset (prefix-), remaining text is the index
yield name[offset:]
def _add_entry(self, formdata=None, data=unset_value, index=None):
assert not self.max_entries or len(self.entries) < self.max_entries, \
'You cannot have more than max_entries entries in this FieldList'
if index is None:
index = self.last_index + 1
self.last_index = index
# '%s-%s' instead of '%s-%d' to allow alphanumeric
name = '%s-%s' % (self.short_name, index)
id = '%s-%s' % (self.id, index)
# support both wtforms 1 and 2
meta = getattr(self, 'meta', None)
if meta:
field = self.unbound_field.bind(
form=None, name=name, prefix=self._prefix, id=id, _meta=meta
)
else:
field = self.unbound_field.bind(
form=None, name=name, prefix=self._prefix, id=id
)
field.process(formdata, data)
self.entries.append(field)
return field
def populate_obj(self, obj, name):
# return data from first item, instead of a list of items
setattr(obj, name, self.data.pop())
class AjaxSelectField(SelectFieldBase): class AjaxSelectField(SelectFieldBase):
""" """
Ajax Model Select Field Ajax Model Select Field
......
...@@ -3,7 +3,10 @@ import inspect ...@@ -3,7 +3,10 @@ import inspect
from flask_admin.form import BaseForm, rules from flask_admin.form import BaseForm, rules
from flask_admin._compat import iteritems from flask_admin._compat import iteritems
from wtforms.fields import HiddenField
from wtforms.fields.core import UnboundField from wtforms.fields.core import UnboundField
from wtforms.validators import InputRequired
from .widgets import XEditableWidget
def converts(*args): def converts(*args):
...@@ -13,7 +16,7 @@ def converts(*args): ...@@ -13,7 +16,7 @@ def converts(*args):
return _inner return _inner
def wrap_fields_in_fieldlist(form_base_class, form_class, CustomFieldList): def create_editable_list_form(form_base_class, form_class, widget=None):
""" """
Create a form class with all the fields wrapped in a FieldList. Create a form class with all the fields wrapped in a FieldList.
...@@ -26,20 +29,25 @@ def wrap_fields_in_fieldlist(form_base_class, form_class, CustomFieldList): ...@@ -26,20 +29,25 @@ def wrap_fields_in_fieldlist(form_base_class, form_class, CustomFieldList):
WTForms form class, by default `form_base_class` from base. WTForms form class, by default `form_base_class` from base.
:param form_class: :param form_class:
WTForms form class generated by `form.get_form`. WTForms form class generated by `form.get_form`.
:param CustomFieldList: :param widget:
WTForms FieldList class. WTForms widget class. Defaults to `XEditableWidget`.
By default, `CustomFieldList` is `ListEditableFieldList`.
""" """
class FieldListForm(form_base_class): if widget is None:
pass widget = XEditableWidget
class ListForm(form_base_class):
list_form_pk = HiddenField(validators=[InputRequired()])
# iterate FormMeta to get unbound fields # iterate FormMeta to get unbound fields, replace widget, copy to ListForm
for name, obj in iteritems(form_class.__dict__): for name, obj in iteritems(form_class.__dict__):
if isinstance(obj, UnboundField): if isinstance(obj, UnboundField):
# wrap field in a WTForms FieldList obj.kwargs['widget'] = XEditableWidget()
setattr(FieldListForm, name, CustomFieldList(obj)) setattr(ListForm, name, obj)
if name == "list_form_pk":
raise Exception('Form already has a list_form_pk column.')
return FieldListForm return ListForm
class InlineBaseFormAdmin(object): class InlineBaseFormAdmin(object):
......
...@@ -72,7 +72,7 @@ class XEditableWidget(object): ...@@ -72,7 +72,7 @@ class XEditableWidget(object):
field inside of the FieldList (StringField, IntegerField, etc). field inside of the FieldList (StringField, IntegerField, etc).
""" """
def __call__(self, field, **kwargs): def __call__(self, field, **kwargs):
kwargs.setdefault('data-value', kwargs.pop('value', '')) kwargs.setdefault('data-value', kwargs.pop('display_value', ''))
kwargs.setdefault('data-role', 'x-editable') kwargs.setdefault('data-role', 'x-editable')
kwargs.setdefault('data-url', './ajax/update/') kwargs.setdefault('data-url', './ajax/update/')
...@@ -87,26 +87,23 @@ class XEditableWidget(object): ...@@ -87,26 +87,23 @@ class XEditableWidget(object):
kwargs['data-csrf'] = kwargs.pop("csrf", "") kwargs['data-csrf'] = kwargs.pop("csrf", "")
# subfield is the first entry (subfield) from FieldList (field) kwargs = self.get_kwargs(field, kwargs)
subfield = field.entries[0]
kwargs = self.get_kwargs(subfield, kwargs)
return HTMLString( return HTMLString(
'<a %s>%s</a>' % (html_params(**kwargs), '<a %s>%s</a>' % (html_params(**kwargs),
escape(kwargs['data-value'])) escape(kwargs['data-value']))
) )
def get_kwargs(self, subfield, kwargs): def get_kwargs(self, field, kwargs):
""" """
Return extra kwargs based on the subfield type. Return extra kwargs based on the field type.
""" """
if subfield.type == 'StringField': if field.type == 'StringField':
kwargs['data-type'] = 'text' kwargs['data-type'] = 'text'
elif subfield.type == 'TextAreaField': elif field.type == 'TextAreaField':
kwargs['data-type'] = 'textarea' kwargs['data-type'] = 'textarea'
kwargs['data-rows'] = '5' kwargs['data-rows'] = '5'
elif subfield.type == 'BooleanField': elif field.type == 'BooleanField':
kwargs['data-type'] = 'select' kwargs['data-type'] = 'select'
# data-source = dropdown options # data-source = dropdown options
kwargs['data-source'] = json.dumps([ kwargs['data-source'] = json.dumps([
...@@ -114,53 +111,64 @@ class XEditableWidget(object): ...@@ -114,53 +111,64 @@ class XEditableWidget(object):
{'value': '1', 'text': gettext('Yes')} {'value': '1', 'text': gettext('Yes')}
]) ])
kwargs['data-role'] = 'x-editable-boolean' kwargs['data-role'] = 'x-editable-boolean'
elif subfield.type == 'Select2Field': elif field.type == 'Select2Field':
kwargs['data-type'] = 'select' kwargs['data-type'] = 'select'
choices = [{'value': x, 'text': y} for x, y in subfield.choices] choices = [{'value': x, 'text': y} for x, y in field.choices]
# prepend a blank field to choices if allow_blank = True # prepend a blank field to choices if allow_blank = True
if getattr(subfield, 'allow_blank', False): if getattr(field, 'allow_blank', False):
choices.insert(0, {'value': '__None', 'text': ''}) choices.insert(0, {'value': '__None', 'text': ''})
# json.dumps fixes issue with unicode strings not loading correctly # json.dumps fixes issue with unicode strings not loading correctly
kwargs['data-source'] = json.dumps(choices) kwargs['data-source'] = json.dumps(choices)
elif subfield.type == 'DateField': elif field.type == 'DateField':
kwargs['data-type'] = 'combodate' kwargs['data-type'] = 'combodate'
kwargs['data-format'] = 'YYYY-MM-DD' kwargs['data-format'] = 'YYYY-MM-DD'
kwargs['data-template'] = 'YYYY-MM-DD' kwargs['data-template'] = 'YYYY-MM-DD'
elif subfield.type == 'DateTimeField': elif field.type == 'DateTimeField':
kwargs['data-type'] = 'combodate' kwargs['data-type'] = 'combodate'
kwargs['data-format'] = 'YYYY-MM-DD HH:mm:ss' kwargs['data-format'] = 'YYYY-MM-DD HH:mm:ss'
kwargs['data-template'] = 'YYYY-MM-DD HH:mm:ss' kwargs['data-template'] = 'YYYY-MM-DD HH:mm:ss'
# x-editable-combodate uses 1 minute increments # x-editable-combodate uses 1 minute increments
kwargs['data-role'] = 'x-editable-combodate' kwargs['data-role'] = 'x-editable-combodate'
elif subfield.type == 'TimeField': elif field.type == 'TimeField':
kwargs['data-type'] = 'combodate' kwargs['data-type'] = 'combodate'
kwargs['data-format'] = 'HH:mm:ss' kwargs['data-format'] = 'HH:mm:ss'
kwargs['data-template'] = 'HH:mm:ss' kwargs['data-template'] = 'HH:mm:ss'
kwargs['data-role'] = 'x-editable-combodate' kwargs['data-role'] = 'x-editable-combodate'
elif subfield.type == 'IntegerField': elif field.type == 'IntegerField':
kwargs['data-type'] = 'number' kwargs['data-type'] = 'number'
elif subfield.type in ['FloatField', 'DecimalField']: elif field.type in ['FloatField', 'DecimalField']:
kwargs['data-type'] = 'number' kwargs['data-type'] = 'number'
kwargs['data-step'] = 'any' kwargs['data-step'] = 'any'
elif subfield.type in ['QuerySelectField', 'ModelSelectField']: elif field.type in ['QuerySelectField', 'ModelSelectField',
'QuerySelectMultipleField']:
# QuerySelectField and ModelSelectField are for relations # QuerySelectField and ModelSelectField are for relations
kwargs['data-type'] = 'select' kwargs['data-type'] = 'select'
choices = [] choices = []
for choice in subfield: selected_ids = []
for value, label, selected in field.iter_choices():
try: try:
choices.append({'value': text_type(choice._value()), label = text_type(label)
'text': text_type(choice.label.text)})
except TypeError: except TypeError:
# unable to display text value # unable to display text value
choices.append({'value': text_type(choice._value()), label = ''
'text': ''}) choices.append({'value': text_type(value), 'text': label})
if selected:
selected_ids.append(value)
# blank field is already included if allow_blank # blank field is already included if allow_blank
kwargs['data-source'] = json.dumps(choices) kwargs['data-source'] = json.dumps(choices)
if field.type == 'QuerySelectMultipleField':
kwargs['data-type'] = 'select2'
kwargs['data-role'] = 'x-editable-select2-multiple'
# must use id instead of text or prefilled values won't work
separator = getattr(field, 'separator', ',')
kwargs['data-value'] = separator.join(selected_ids)
else: else:
raise Exception('Unsupported field type: %s' % (type(subfield),)) raise Exception('Unsupported field type: %s' % (type(field),))
return kwargs return kwargs
...@@ -272,11 +272,12 @@ ...@@ -272,11 +272,12 @@
return true; return true;
} }
// make x-editable's POST act like a normal FieldList field // make x-editable's POST compatible with WTForms
// for x-editable, x-editable-combodate, and x-editable-boolean cases // for x-editable, x-editable-combodate, and x-editable-boolean cases
var overrideXeditableParams = function(params) { var overrideXeditableParams = function(params) {
var newParams = {}; var newParams = {};
newParams[params.name + '-' + params.pk] = params.value; newParams['list_form_pk'] = params.pk;
newParams[params.name] = params.value;
if ($(this).data('csrf')) { if ($(this).data('csrf')) {
newParams['csrf_token'] = $(this).data('csrf'); newParams['csrf_token'] = $(this).data('csrf');
} }
...@@ -451,6 +452,30 @@ ...@@ -451,6 +452,30 @@
} }
}); });
return true; return true;
case 'x-editable-select2-multiple':
$el.editable({
params: overrideXeditableParams,
ajaxOptions: {
// prevents keys with the same value from getting converted into arrays
traditional: true
},
select2: {
multiple: true
},
display: function(value) {
// override to display text instead of ids on list view
var html = [];
var data = $.fn.editableutils.itemsByValue(value, $el.data('source'), 'id');
if(data.length) {
$.each(data, function(i, v) { html.push($.fn.editableutils.escape(v.text)); });
$(this).html(html.join(', '));
} else {
$(this).empty();
}
}
});
return true;
case 'x-editable-boolean': case 'x-editable-boolean':
$el.editable({ $el.editable({
params: overrideXeditableParams, params: overrideXeditableParams,
......
...@@ -249,5 +249,5 @@ ...@@ -249,5 +249,5 @@
{% if editable_columns %} {% if editable_columns %}
<script src="{{ admin_static.url(filename='vendor/x-editable/js/bootstrap2-editable.min.js', v='1.5.1') }}"></script> <script src="{{ admin_static.url(filename='vendor/x-editable/js/bootstrap2-editable.min.js', v='1.5.1') }}"></script>
{% endif %} {% endif %}
<script src="{{ admin_static.url(filename='admin/js/form.js', v='1.0.0') }}"></script> <script src="{{ admin_static.url(filename='admin/js/form.js', v='1.0.1') }}"></script>
{% endmacro %} {% endmacro %}
...@@ -158,9 +158,9 @@ ...@@ -158,9 +158,9 @@
<td class="col-{{c}}"> <td class="col-{{c}}">
{% if admin_view.is_editable(c) %} {% if admin_view.is_editable(c) %}
{% if form.csrf_token %} {% if form.csrf_token %}
{{ form[c](pk=get_pk_value(row), value=get_value(row, c), csrf=form.csrf_token._value()) }} {{ form(obj=row)[c](pk=get_pk_value(row), display_value=get_value(row, c), csrf=form.csrf_token._value()) }}
{% else %} {% else %}
{{ form[c](pk=get_pk_value(row), value=get_value(row, c)) }} {{ form(obj=row)[c](pk=get_pk_value(row), display_value=get_value(row, c)) }}
{% endif %} {% endif %}
{% else %} {% else %}
{{ get_value(row, c) }} {{ get_value(row, c) }}
......
...@@ -233,5 +233,5 @@ ...@@ -233,5 +233,5 @@
{% if editable_columns %} {% if editable_columns %}
<script src="{{ admin_static.url(filename='vendor/x-editable/js/bootstrap3-editable.min.js', v='1.5.1') }}"></script> <script src="{{ admin_static.url(filename='vendor/x-editable/js/bootstrap3-editable.min.js', v='1.5.1') }}"></script>
{% endif %} {% endif %}
<script src="{{ admin_static.url(filename='admin/js/form.js', v='1.0.0') }}"></script> <script src="{{ admin_static.url(filename='admin/js/form.js', v='1.0.1') }}"></script>
{% endmacro %} {% endmacro %}
...@@ -157,9 +157,9 @@ ...@@ -157,9 +157,9 @@
<td class="col-{{c}}"> <td class="col-{{c}}">
{% if admin_view.is_editable(c) %} {% if admin_view.is_editable(c) %}
{% if form.csrf_token %} {% if form.csrf_token %}
{{ form[c](pk=get_pk_value(row), value=get_value(row, c), csrf=form.csrf_token._value()) }} {{ form(obj=row)[c](pk=get_pk_value(row), display_value=get_value(row, c), csrf=form.csrf_token._value()) }}
{% else %} {% else %}
{{ form[c](pk=get_pk_value(row), value=get_value(row, c)) }} {{ form(obj=row)[c](pk=get_pk_value(row), display_value=get_value(row, c)) }}
{% endif %} {% endif %}
{% else %} {% else %}
{{ get_value(row, c) }} {{ get_value(row, c) }}
......
...@@ -148,8 +148,7 @@ def test_column_editable_list(): ...@@ -148,8 +148,7 @@ def test_column_editable_list():
Model1, Model2 = create_models(db) Model1, Model2 = create_models(db)
view = CustomModelView(Model1, view = CustomModelView(Model1,
column_editable_list=[ column_editable_list=['test1', 'datetime_field'])
'test1', 'datetime_field'])
admin.add_view(view) admin.add_view(view)
fill_db(Model1, Model2) fill_db(Model1, Model2)
...@@ -164,7 +163,8 @@ def test_column_editable_list(): ...@@ -164,7 +163,8 @@ def test_column_editable_list():
# Form - Test basic in-line edit functionality # Form - Test basic in-line edit functionality
obj1 = Model1.objects.get(test1 = 'test1_val_3') obj1 = Model1.objects.get(test1 = 'test1_val_3')
rv = client.post('/admin/model1/ajax/update/', data={ rv = client.post('/admin/model1/ajax/update/', data={
'test1-' + str(obj1.id): 'change-success-1', 'list_form_pk': str(obj1.id),
'test1': 'change-success-1',
}) })
data = rv.data.decode('utf-8') data = rv.data.decode('utf-8')
ok_('Record was successfully saved.' == data) ok_('Record was successfully saved.' == data)
...@@ -177,33 +177,35 @@ def test_column_editable_list(): ...@@ -177,33 +177,35 @@ def test_column_editable_list():
# Test validation error # Test validation error
obj2 = Model1.objects.get(test1 = 'datetime_obj1') obj2 = Model1.objects.get(test1 = 'datetime_obj1')
rv = client.post('/admin/model1/ajax/update/', data={ rv = client.post('/admin/model1/ajax/update/', data={
'datetime_field-' + str(obj2.id): 'problematic-input', 'list_form_pk': str(obj2.id),
'datetime_field': 'problematic-input',
}) })
eq_(rv.status_code, 500) eq_(rv.status_code, 500)
# Test invalid primary key # Test invalid primary key
rv = client.post('/admin/model1/ajax/update/', data={ rv = client.post('/admin/model1/ajax/update/', data={
'test1-1000': 'problematic-input', 'list_form_pk': '1000',
'test1': 'problematic-input',
}) })
data = rv.data.decode('utf-8') data = rv.data.decode('utf-8')
eq_(rv.status_code, 500) eq_(rv.status_code, 500)
# Test editing column not in column_editable_list # Test editing column not in column_editable_list
rv = client.post('/admin/model1/ajax/update/', data={ rv = client.post('/admin/model1/ajax/update/', data={
'test2-1': 'problematic-input', 'list_form_pk': '1',
'test2': 'problematic-input',
}) })
data = rv.data.decode('utf-8') data = rv.data.decode('utf-8')
eq_(rv.status_code, 500) ok_('problematic-input' not in data)
# Test in-line editing for relations # Test in-line editing for relations
view = CustomModelView(Model2, view = CustomModelView(Model2, column_editable_list=['model1'])
column_editable_list=[
'model1'])
admin.add_view(view) admin.add_view(view)
obj3 = Model2.objects.get(string_field = 'string_field_val_1') obj3 = Model2.objects.get(string_field = 'string_field_val_1')
rv = client.post('/admin/model2/ajax/update/', data={ rv = client.post('/admin/model2/ajax/update/', data={
'model1-' + str(obj3.id): str(obj1.id), 'list_form_pk': str(obj3.id),
'model1': str(obj1.id),
}) })
data = rv.data.decode('utf-8') data = rv.data.decode('utf-8')
ok_('Record was successfully saved.' == data) ok_('Record was successfully saved.' == data)
......
...@@ -185,9 +185,10 @@ def test_column_editable_list(): ...@@ -185,9 +185,10 @@ def test_column_editable_list():
Model1, Model2 = create_models(db) Model1, Model2 = create_models(db)
view = CustomModelView(Model1, # wtf-peewee doesn't automatically add length validators for max_length
column_editable_list=[ form_args = {'test1': {'validators': [validators.Length(max=20)]}}
'test1', 'enum_field']) view = CustomModelView(Model1, column_editable_list=['test1'],
form_args=form_args)
admin.add_view(view) admin.add_view(view)
fill_db(Model1, Model2) fill_db(Model1, Model2)
...@@ -201,7 +202,8 @@ def test_column_editable_list(): ...@@ -201,7 +202,8 @@ def test_column_editable_list():
# Form - Test basic in-line edit functionality # Form - Test basic in-line edit functionality
rv = client.post('/admin/model1/ajax/update/', data={ rv = client.post('/admin/model1/ajax/update/', data={
'test1-1': 'change-success-1', 'list_form_pk': '1',
'test1': 'change-success-1',
}) })
data = rv.data.decode('utf-8') data = rv.data.decode('utf-8')
ok_('Record was successfully saved.' == data) ok_('Record was successfully saved.' == data)
...@@ -213,32 +215,35 @@ def test_column_editable_list(): ...@@ -213,32 +215,35 @@ def test_column_editable_list():
# Test validation error # Test validation error
rv = client.post('/admin/model1/ajax/update/', data={ rv = client.post('/admin/model1/ajax/update/', data={
'enum_field-1': 'problematic-input', 'list_form_pk': '1',
'test1': 'longerthantwentycharacterslongerthantwentycharacterslongerthantwentycharacterslongerthantwentycharacters',
}) })
data = rv.data.decode('utf-8')
eq_(rv.status_code, 500) eq_(rv.status_code, 500)
# Test invalid primary key # Test invalid primary key
rv = client.post('/admin/model1/ajax/update/', data={ rv = client.post('/admin/model1/ajax/update/', data={
'test1-1000': 'problematic-input', 'list_form_pk': '1000',
'test1': 'problematic-input',
}) })
data = rv.data.decode('utf-8') data = rv.data.decode('utf-8')
eq_(rv.status_code, 500) eq_(rv.status_code, 500)
# Test editing column not in column_editable_list # Test editing column not in column_editable_list
rv = client.post('/admin/model1/ajax/update/', data={ rv = client.post('/admin/model1/ajax/update/', data={
'test2-1': 'problematic-input', 'list_form_pk': '1',
'test2': 'problematic-input',
}) })
data = rv.data.decode('utf-8') data = rv.data.decode('utf-8')
eq_(rv.status_code, 500) ok_('problematic-input' not in data)
# Test in-line editing for relations # Test in-line editing for relations
view = CustomModelView(Model2, view = CustomModelView(Model2, column_editable_list=['model1'])
column_editable_list=[
'model1'])
admin.add_view(view) admin.add_view(view)
rv = client.post('/admin/model2/ajax/update/', data={ rv = client.post('/admin/model2/ajax/update/', data={
'model1-1': '3', 'list_form_pk': '1',
'model1': '3',
}) })
data = rv.data.decode('utf-8') data = rv.data.decode('utf-8')
ok_('Record was successfully saved.' == data) ok_('Record was successfully saved.' == data)
......
...@@ -354,8 +354,7 @@ def test_column_editable_list(): ...@@ -354,8 +354,7 @@ def test_column_editable_list():
Model1, Model2 = create_models(db) Model1, Model2 = create_models(db)
view = CustomModelView(Model1, db.session, view = CustomModelView(Model1, db.session,
column_editable_list=[ column_editable_list=['test1', 'enum_field'])
'test1', 'enum_field'])
admin.add_view(view) admin.add_view(view)
fill_db(db, Model1, Model2) fill_db(db, Model1, Model2)
...@@ -369,7 +368,8 @@ def test_column_editable_list(): ...@@ -369,7 +368,8 @@ def test_column_editable_list():
# Form - Test basic in-line edit functionality # Form - Test basic in-line edit functionality
rv = client.post('/admin/model1/ajax/update/', data={ rv = client.post('/admin/model1/ajax/update/', data={
'test1-1': 'change-success-1', 'list_form_pk': '1',
'test1': 'change-success-1',
}) })
data = rv.data.decode('utf-8') data = rv.data.decode('utf-8')
ok_('Record was successfully saved.' == data) ok_('Record was successfully saved.' == data)
...@@ -381,32 +381,34 @@ def test_column_editable_list(): ...@@ -381,32 +381,34 @@ def test_column_editable_list():
# Test validation error # Test validation error
rv = client.post('/admin/model1/ajax/update/', data={ rv = client.post('/admin/model1/ajax/update/', data={
'enum_field-1': 'problematic-input', 'list_form_pk': '1',
'enum_field': 'problematic-input',
}) })
eq_(rv.status_code, 500) eq_(rv.status_code, 500)
# Test invalid primary key # Test invalid primary key
rv = client.post('/admin/model1/ajax/update/', data={ rv = client.post('/admin/model1/ajax/update/', data={
'test1-1000': 'problematic-input', 'list_form_pk': '1000',
'test1': 'problematic-input',
}) })
data = rv.data.decode('utf-8') data = rv.data.decode('utf-8')
eq_(rv.status_code, 500) eq_(rv.status_code, 500)
# Test editing column not in column_editable_list # Test editing column not in column_editable_list
rv = client.post('/admin/model1/ajax/update/', data={ rv = client.post('/admin/model1/ajax/update/', data={
'test2-1': 'problematic-input', 'list_form_pk': '1',
'test2': 'problematic-input',
}) })
data = rv.data.decode('utf-8') data = rv.data.decode('utf-8')
eq_(rv.status_code, 500) ok_('problematic-input' not in data)
# Test in-line editing for relations # Test in-line editing for relations
view = CustomModelView(Model2, db.session, view = CustomModelView(Model2, db.session, column_editable_list=['model1'])
column_editable_list=[
'model1'])
admin.add_view(view) admin.add_view(view)
rv = client.post('/admin/model2/ajax/update/', data={ rv = client.post('/admin/model2/ajax/update/', data={
'model1-1': '3', 'list_form_pk': '1',
'model1': '3',
}) })
data = rv.data.decode('utf-8') data = rv.data.decode('utf-8')
ok_('Record was successfully saved.' == data) ok_('Record was successfully saved.' == data)
...@@ -495,7 +497,8 @@ def test_editable_list_special_pks(): ...@@ -495,7 +497,8 @@ def test_editable_list_special_pks():
# Form - Test basic in-line edit functionality # Form - Test basic in-line edit functionality
rv = client.post('/admin/model1/ajax/update/', data={ rv = client.post('/admin/model1/ajax/update/', data={
'val1-1-1': 'change-success-1', 'list_form_pk': '1-1',
'val1': 'change-success-1',
}) })
data = rv.data.decode('utf-8') data = rv.data.decode('utf-8')
ok_('Record was successfully saved.' == data) ok_('Record was successfully saved.' == data)
...@@ -1511,8 +1514,8 @@ def test_form_onetoone(): ...@@ -1511,8 +1514,8 @@ def test_form_onetoone():
eq_(model1.model2, model2) eq_(model1.model2, model2)
eq_(model2.model1, model1) eq_(model2.model1, model1)
eq_(view1._create_form_class.model2.kwargs['widget'].multiple, False) eq_(view1._create_form_class.model2.field_class.widget.multiple, False)
eq_(view2._create_form_class.model1.kwargs['widget'].multiple, False) eq_(view2._create_form_class.model1.field_class.widget.multiple, False)
def test_relations(): def test_relations():
......
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