Commit eb37f32c authored by Paul Brown's avatar Paul Brown

add editable list view

parent ba4e10ce
......@@ -6,13 +6,16 @@ from flask.ext.admin import expose
from flask.ext.admin.babel import gettext, ngettext, lazy_gettext
from flask.ext.admin.model import BaseModelView
from flask.ext.admin._compat import iteritems, string_types
from flask.ext.admin.actions import action
from flask.ext.admin.model.fields import ListEditableFieldList
from wtforms.fields.core import UnboundField
import mongoengine
import gridfs
from mongoengine.connection import get_db
from bson.objectid import ObjectId
from flask.ext.admin.actions import action
from .filters import FilterConverter, BaseMongoEngineFilter
from .form import get_form, CustomModelConverter
from .typefmt import DEFAULT_FORMATTERS
......@@ -398,6 +401,23 @@ class ModelView(BaseModelView):
return form_class
def scaffold_list_form(self):
"""
Create form for the list view editable columns.
"""
form_class = get_form(self.model, self.model_form_converter(self),
base_class=self.form_base_class,
only=self.column_editable_list)
# iterate FormMeta to get unbound fields
field_dict = {}
for name, field_object in iteritems(form_class.__dict__):
if not name.startswith('_') and isinstance(field_object, UnboundField):
# wrap each field in the form from get_form in FieldList
field_dict[name] = ListEditableFieldList(field_object)
return type(self.model.__name__ + 'Form', (self.form_base_class, ), field_dict)
# AJAX foreignkey support
def _create_ajax_loader(self, name, opts):
return create_ajax_loader(self.model, name, name, opts)
......@@ -545,6 +565,37 @@ class ModelView(BaseModelView):
return True
def update_list_model(self, form):
"""
Update model from the list view.
Only supports updating a single field at a time.
:param form:
Form instance
"""
try:
model = self.model()
for field in form:
# FieldList's last_index will only be set if a field is submitted
# last_index will be the primary key of the updated record
if getattr(field, 'last_index', None):
record = self.get_one(field.last_index)
setattr(record, field.name, field.data.pop())
self._on_model_change(form, model, False)
record.save()
self.after_model_change(form, model, False)
return True
except Exception as ex:
if not self.handle_view_exception(ex):
log.exception(gettext('Failed to update record. %(error)s', error=str(ex)), 'error')
self.session.rollback()
# Error: Unable to update database or no records were changed.
return False
def delete_model(self, model):
"""
Delete model helper
......
......@@ -2,7 +2,7 @@ import logging
from flask import flash
from flask.ext.admin._compat import string_types
from flask.ext.admin._compat import string_types, iteritems
from flask.ext.admin.babel import gettext, ngettext, lazy_gettext
from flask.ext.admin.model import BaseModelView
......@@ -11,6 +11,9 @@ from peewee import PrimaryKeyField, ForeignKeyField, Field, CharField, TextField
from flask.ext.admin.actions import action
from flask.ext.admin.contrib.peewee import filters
from flask.ext.admin.model.fields import ListEditableFieldList
from wtforms.fields.core import UnboundField
from .form import get_form, CustomModelConverter, InlineModelConverter, save_inline
from .tools import get_primary_key, parse_like_term
from .ajax import create_ajax_loader
......@@ -237,6 +240,23 @@ class ModelView(BaseModelView):
return form_class
def scaffold_list_form(self):
"""
Create form for the list view editable columns.
"""
form_class = get_form(self.model, self.model_form_converter(self),
base_class=self.form_base_class,
only=self.column_editable_list)
# iterate FormMeta to get unbound fields
field_dict = {}
for name, field_object in iteritems(form_class.__dict__):
if not name.startswith('_') and isinstance(field_object, UnboundField):
# wrap each field in the form from get_form in FieldList
field_dict[name] = ListEditableFieldList(field_object)
return type(self.model.__name__ + 'Form', (self.form_base_class, ), field_dict)
def scaffold_inline_form_models(self, form_class):
converter = self.model_form_converter(self)
inline_converter = self.inline_model_form_converter(self)
......@@ -382,6 +402,40 @@ class ModelView(BaseModelView):
return True
def update_list_model(self, form):
"""
Update model from the list view.
Only supports updating a single field at a time.
:param form:
Form instance
"""
try:
model = self.model()
for field in form:
# FieldList's last_index will only be set if a field is submitted
# last_index will be the primary key of the updated record
if getattr(field, 'last_index', None):
record = self.get_one(field.last_index)
setattr(record, field.name, field.data.pop())
self._on_model_change(form, model, False)
record.save()
# For peewee have to save inline forms after model was saved
save_inline(form, model)
self.after_model_change(form, model, False)
return True
except Exception as ex:
if not self.handle_view_exception(ex):
log.exception(gettext('Failed to update record. %(error)s', error=str(ex)), 'error')
self.session.rollback()
# Error: Unable to update database or no records were changed.
return False
def delete_model(self, model):
try:
self.on_model_delete(model)
......
......@@ -8,12 +8,15 @@ from sqlalchemy.exc import IntegrityError
from flask import flash
from flask.ext.admin._compat import string_types
from flask.ext.admin._compat import string_types, iteritems
from flask.ext.admin.babel import gettext, ngettext, lazy_gettext
from flask.ext.admin.model import BaseModelView
from flask.ext.admin.actions import action
from flask.ext.admin._backwards import ObsoleteAttr
from flask.ext.admin.model.fields import ListEditableFieldList
from wtforms.fields.core import UnboundField
from flask.ext.admin.contrib.sqla import form, filters, tools
from .typefmt import DEFAULT_FORMATTERS
from .tools import get_query_for_ids
......@@ -611,6 +614,27 @@ class ModelView(BaseModelView):
return form_class
def scaffold_list_form(self):
"""
Create form for the list view editable columns.
The form is created using the existing get_form(),
but each field is wrapped in a WTForms FieldList.
"""
converter = self.model_form_converter(self.session, self)
form_class = form.get_form(self.model, converter,
base_class=self.form_base_class,
only=self.column_editable_list)
# iterate FormMeta to get unbound fields
field_dict = {}
for name, field_object in iteritems(form_class.__dict__):
if not name.startswith('_') and isinstance(field_object, UnboundField):
# wrap each field in the form from get_form in FieldList
field_dict[name] = ListEditableFieldList(field_object)
return type(self.model.__name__ + 'Form', (self.form_base_class, ), field_dict)
def scaffold_inline_form_models(self, form_class):
"""
Contribute inline models to the form
......@@ -898,6 +922,37 @@ class ModelView(BaseModelView):
return True
def update_list_model(self, form):
"""
Update model from the list view.
Only supports updating a single field at a time.
:param form:
Form instance
"""
try:
model = self.model()
for field in form:
# FieldList's last_index will only be set if a field is submitted
# last_index will be the primary key of the updated record
if getattr(field, 'last_index', None):
record = self.session.query(self.model).get(field.last_index)
setattr(record, field.name, field.data.pop())
self._on_model_change(form, model, False)
self.session.commit()
self.after_model_change(form, model, False)
return True
except Exception as ex:
if not self.handle_view_exception(ex):
log.exception(gettext('Failed to update record. %(error)s', error=str(ex)), 'error')
self.session.rollback()
# Error: Unable to update database or no records were changed.
return False
def delete_model(self, model):
"""
Delete model.
......
......@@ -11,7 +11,8 @@ from flask.ext.admin.base import BaseView, expose
from flask.ext.admin.form import BaseForm, FormOpts, rules
from flask.ext.admin.model import filters, typefmt
from flask.ext.admin.actions import ActionsMixin
from flask.ext.admin.helpers import get_form_data, validate_form_on_submit, get_redirect_target
from flask.ext.admin.helpers import (get_form_data, validate_form_on_submit,
get_redirect_target, is_form_submitted)
from flask.ext.admin.tools import rec_getattr
from flask.ext.admin._backwards import ObsoleteAttr
from flask.ext.admin._compat import iteritems, OrderedDict, as_unicode
......@@ -264,6 +265,16 @@ class BaseModelView(BaseView, ActionsMixin):
column_searchable_list = ('name', 'email')
"""
column_editable_list = None
"""
Collection of the columns which can be edited from the list view.
For example::
class MyModelView(BaseModelView):
column_editable_list = ('name', 'last_name')
"""
column_choices = None
"""
Map choices to columns in list view
......@@ -580,6 +591,12 @@ class BaseModelView(BaseView, ActionsMixin):
self._create_form_class = self.get_create_form()
self._edit_form_class = self.get_edit_form()
# List View In-Line Editing
if self.column_editable_list:
self._list_form_class = self.scaffold_list_form()
else:
self.column_editable_list = {}
def _refresh_filters_cache(self):
self._filters = self.get_filters()
......@@ -833,6 +850,12 @@ class BaseModelView(BaseView, ActionsMixin):
"""
raise NotImplementedError('Please implement scaffold_form method')
def scaffold_list_form(self):
"""
Create form class for list view in-line editing.
"""
raise NotImplementedError('Please implement scaffold_list_form method')
def get_form(self):
"""
Get form class.
......@@ -879,6 +902,14 @@ class BaseModelView(BaseView, ActionsMixin):
"""
return self._edit_form_class(get_form_data(), obj=obj)
def list_form(self, obj=None):
"""
Instantiate model editing form for list view and return it.
Override to implement custom behavior.
"""
return self._list_form_class(get_form_data(), obj=obj)
def validate_form(self, form):
"""
Validate the form on submit.
......@@ -898,6 +929,15 @@ class BaseModelView(BaseView, ActionsMixin):
"""
return name in self._sortable_columns
def is_editable(self, name):
"""
Verify if column is editable.
:param name:
Column name.
"""
return name in self.column_editable_list
def _get_column_by_idx(self, idx):
"""
Return column index by
......@@ -1074,6 +1114,15 @@ class BaseModelView(BaseView, ActionsMixin):
"""
raise NotImplementedError()
def update_list_model(self, form, model):
"""
Update model from the list view.
:param form:
Form instance
"""
raise NotImplementedError()
def delete_model(self, model):
"""
Delete model.
......@@ -1242,11 +1291,45 @@ class BaseModelView(BaseView, ActionsMixin):
raise NotImplementedError()
# Views
@expose('/')
@expose('/', methods=('POST', 'GET'))
def index_view(self):
"""
List view
"""
if self.column_editable_list:
form = self.list_form()
# prevent validation issues due to submitting a single field
# delete all fields except the field being submitted
if is_form_submitted():
for field in form:
# only submitted fields have last_index
if getattr(field, 'last_index', None):
pass
elif field.name == 'csrf_token':
pass
else:
form.__delitem__(field.name)
if self.validate_form(form):
if self.update_list_model(form):
return gettext('Record was successfully saved.')
else:
# No records changed, or error saving to database.
return gettext('Failed to update record. %(error)s',
error=''), 500
if form.errors:
for field in form:
for error in field.errors:
# return error to x-editable
if isinstance(error, list):
return ", ".join(error), 500
else:
return error, 500
else:
form = None
# Grab parameters from URL
view_args = self._get_list_extra_args()
......@@ -1289,29 +1372,32 @@ class BaseModelView(BaseView, ActionsMixin):
search=None,
filters=None))
return self.render(self.list_template,
return self.render(
self.list_template,
data=data,
form=form,
# List
list_columns=self._list_columns,
sortable_columns=self._sortable_columns,
# Stuff
enumerate=enumerate,
get_pk_value=self.get_pk_value,
get_value=self.get_list_value,
return_url=self._get_list_url(view_args),
editable_columns=self.column_editable_list,
# Pagination
count=count,
pager_url=pager_url,
num_pages=num_pages,
page=view_args.page,
# Sorting
sort_column=view_args.sort,
sort_desc=view_args.sort_desc,
sort_url=sort_url,
# Search
search_supported=self._search_supported,
clear_search_url=clear_search_url,
search=view_args.search,
# Filters
filters=self._filters,
filter_groups=self._filter_groups,
......@@ -1319,7 +1405,14 @@ class BaseModelView(BaseView, ActionsMixin):
# Actions
actions=actions,
actions_confirmation=actions_confirmation)
actions_confirmation=actions_confirmation,
# Misc
enumerate=enumerate,
get_pk_value=self.get_pk_value,
get_value=self.get_list_value,
return_url=self._get_list_url(view_args),
)
@expose('/new/', methods=('GET', 'POST'))
def create_view(self):
......
......@@ -3,8 +3,14 @@ import itertools
from wtforms.validators import ValidationError
from wtforms.fields import FieldList, FormField, SelectFieldBase
try:
from wtforms.fields import _unset_value as unset_value
except ImportError:
from wtforms.utils import unset_value
from flask.ext.admin._compat import iteritems
from .widgets import InlineFieldListWidget, InlineFormWidget, AjaxSelect2Widget
from .widgets import (InlineFieldListWidget, InlineFormWidget,
AjaxSelect2Widget, XEditableWidget)
class InlineFieldList(FieldList):
......@@ -120,6 +126,54 @@ class InlineModelFormField(FormField):
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 k in formdata:
if k.startswith(prefix):
k = k[offset:].split('-', 1)[0]
# removed "if k.isdigit():"
yield k
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
class AjaxSelectField(SelectFieldBase):
"""
Ajax Model Select Field
......
......@@ -61,3 +61,81 @@ class AjaxSelect2Widget(object):
kwargs.setdefault('data-placeholder', placeholder)
return HTMLString('<input %s>' % html_params(name=field.name, **kwargs))
class XEditableWidget(object):
"""
WTForms widget that provides in-line editing for the list view.
Determines how to display the x-editable/ajax form based on the
field inside of the FieldList (StringField, IntegerField, etc).
"""
def __call__(self, field, **kwargs):
value = kwargs.pop("value", "")
kwargs.setdefault('data-role', 'x-editable')
kwargs.setdefault('data-url', './')
kwargs.setdefault('id', field.id)
kwargs.setdefault('name', field.name)
kwargs.setdefault('href', '#')
if not kwargs.get('pk'):
raise Exception('pk required')
kwargs['data-pk'] = str(kwargs.pop("pk"))
kwargs['data-csrf'] = kwargs.pop("csrf", "")
# get first entry from FieldList to determine field type
subfield = field.entries[0]
if subfield.type == 'StringField':
kwargs['data-type'] = 'text'
elif subfield.type == 'TextAreaField':
kwargs['data-type'] = 'textarea'
kwargs['data-rows'] = '5'
elif subfield.type == 'BooleanField':
kwargs['data-type'] = 'select'
# data-source = dropdown options
kwargs['data-source'] = {'': 'False', '1': 'True'}
kwargs['data-role'] = 'x-editable-boolean'
elif subfield.type == 'Select2Field':
kwargs['data-type'] = 'select'
kwargs['data-source'] = dict(subfield.choices)
elif subfield.type in ['QuerySelectField', 'ModelSelectField']:
kwargs['data-type'] = 'select'
# MongoEngine throws an error on blank object names
# this prevents "TypeError: cannot create 'NoneType' instances"
choices = {}
for choice in subfield:
try:
choices[str(choice._value())] = str(choice.label.text)
except TypeError:
choices[str(choice._value())] = ""
kwargs['data-source'] = choices
elif subfield.type == 'DateField':
kwargs['data-type'] = 'combodate'
kwargs['data-format'] = 'YYYY-MM-DD'
kwargs['data-template'] = 'YYYY-MM-DD'
elif subfield.type == 'DateTimeField':
kwargs['data-type'] = 'combodate'
kwargs['data-format'] = 'YYYY-MM-DD HH:mm:ss'
kwargs['data-template'] = 'YYYY-MM-DD HH:mm:ss'
# x-editable-combodate uses 1 minute increments
kwargs['data-role'] = 'x-editable-combodate'
elif subfield.type == 'TimeField':
kwargs['data-type'] = 'combodate'
kwargs['data-format'] = 'HH:mm:ss'
kwargs['data-template'] = 'HH:mm:ss'
kwargs['data-role'] = 'x-editable-combodate'
elif subfield.type == 'IntegerField':
kwargs['data-type'] = 'number'
elif subfield.type in ['DecimalField', 'FloatField']:
kwargs['data-type'] = 'number'
kwargs['data-step'] = 'any'
else:
raise Exception('Unsupported field type: %s' % (type(subfield),))
return HTMLString(
'<a %s>%s</a>' % (html_params(**kwargs), value)
)
......@@ -241,6 +241,17 @@
return true;
}
// make x-editable's POST act like a normal FieldList field
// for x-editable, x-editable-combodate, and x-editable-boolean cases
var overrideXeditableParams = function(params) {
var newParams = {};
newParams[params.name + '-' + params.pk] = params.value;
if ($(this).data('csrf')) {
newParams['csrf_token'] = $(this).data('csrf');
}
return newParams;
}
switch (name) {
case 'select2':
var opts = {
......@@ -390,6 +401,32 @@
case 'leaflet':
processLeafletWidget($el, name);
return true;
case 'x-editable':
$el.editable({params: overrideXeditableParams});
return true;
case 'x-editable-combodate':
$el.editable({
params: overrideXeditableParams,
combodate: {
// prevent minutes from showing in 5 minute increments
minuteStep: 1
}
});
return true;
case 'x-editable-boolean':
$el.editable({
params: overrideXeditableParams,
display: function(value, sourceData, response) {
// display new boolean value as an icon
if(response) {
if(value == '1') {
$(this).html('<span class="glyphicon glyphicon-ok-circle icon-ok-circle"></span>');
} else {
$(this).html('<span class="glyphicon glyphicon-minus-sign icon-minus-sign"></span>');
}
}
}
});
}
};
......@@ -463,7 +500,7 @@
this.applyGlobalStyles = function(parent) {
var self = this;
$(':input[data-role]', parent).each(function() {
$(':input[data-role], a[data-role]', parent).each(function() {
var $el = $(this);
self.applyStyle($el, $el.attr('data-role'));
});
......
......@@ -178,6 +178,9 @@
<link href="{{ admin_static.url(filename='vendor/leaflet/leaflet.css') }}" rel="stylesheet">
<link href="{{ admin_static.url(filename='vendor/leaflet/leaflet.draw.css') }}" rel="stylesheet">
{% endif %}
{% if editable_columns %}
<link href="{{ admin_static.url(filename='vendor/x-editable/css/bootstrap2-editable-1.5.1.css') }}" rel="stylesheet">
{% endif %}
{% endmacro %}
{% macro form_js() %}
......@@ -189,5 +192,8 @@
<script src="{{ admin_static.url(filename='vendor/leaflet/leaflet.draw.js') }}"></script>
{% endif %}
<script src="{{ admin_static.url(filename='vendor/bootstrap-daterangepicker/daterangepicker.js') }}"></script>
{% if editable_columns %}
<script src="{{ admin_static.url(filename='vendor/x-editable/js/bootstrap2-editable-1.5.1.min.js') }}"></script>
{% endif %}
<script src="{{ admin_static.url(filename='admin/js/form-1.0.0.js') }}"></script>
{% endmacro %}
......@@ -119,8 +119,17 @@
{% endblock %}
</td>
{% endblock %}
{% for c, name in list_columns %}
{% if admin_view.is_editable(c) %}
{% if form.csrf_token %}
<td>{{ form[c](pk=get_pk_value(row), value=get_value(row, c), csrf=form.csrf_token._value()) }}</td>
{% else %}
<td>{{ form[c](pk=get_pk_value(row), value=get_value(row, c)) }}</td>
{% endif %}
{% else %}
<td>{{ get_value(row, c) }}</td>
{% endif %}
{% endfor %}
{% endblock %}
</tr>
......
......@@ -173,6 +173,9 @@
<link href="{{ admin_static.url(filename='vendor/leaflet/leaflet.css') }}" rel="stylesheet">
<link href="{{ admin_static.url(filename='vendor/leaflet/leaflet.draw.css') }}" rel="stylesheet">
{% endif %}
{% if editable_columns %}
<link href="{{ admin_static.url(filename='vendor/x-editable/css/bootstrap3-editable-1.5.1.css') }}" rel="stylesheet">
{% endif %}
{% endmacro %}
{% macro form_js() %}
......@@ -184,5 +187,8 @@
<script src="{{ admin_static.url(filename='vendor/leaflet/leaflet.draw.js') }}"></script>
{% endif %}
<script src="{{ admin_static.url(filename='vendor/bootstrap-daterangepicker/daterangepicker.js') }}"></script>
{% if editable_columns %}
<script src="{{ admin_static.url(filename='vendor/x-editable/js/bootstrap3-editable-1.5.1.min.js') }}"></script>
{% endif %}
<script src="{{ admin_static.url(filename='admin/js/form-1.0.0.js') }}"></script>
{% endmacro %}
......@@ -120,7 +120,15 @@
</td>
{% endblock %}
{% for c, name in list_columns %}
{% if admin_view.is_editable(c) %}
{% if form.csrf_token %}
<td>{{ form[c](pk=get_pk_value(row), value=get_value(row, c), csrf=form.csrf_token._value()) }}</td>
{% else %}
<td>{{ form[c](pk=get_pk_value(row), value=get_value(row, c)) }}</td>
{% endif %}
{% else %}
<td>{{ get_value(row, c) }}</td>
{% endif %}
{% endfor %}
{% endblock %}
</tr>
......
......@@ -52,6 +52,22 @@ def create_models(db):
return Model1, Model2
def fill_db(Model1, Model2):
Model1('test1_val_1', 'test2_val_1').save()
Model1('test1_val_2', 'test2_val_2').save()
Model1('test1_val_3', 'test2_val_3').save()
Model1('test1_val_4', 'test2_val_4').save()
Model1(None, 'empty_obj').save()
Model2('string_field_val_1', None, None).save()
Model2('string_field_val_2', None, None).save()
Model2('string_field_val_3', 5000, 25.9).save()
Model2('string_field_val_4', 9000, 75.5).save()
Model1('datetime_obj1', datetime_field=datetime(2014,4,3,1,9,0)).save()
Model1('datetime_obj2', datetime_field=datetime(2013,3,2,0,8,0)).save()
def test_model():
app, db, admin = setup()
......@@ -124,25 +140,72 @@ def test_model():
eq_(rv.status_code, 302)
eq_(Model1.objects.count(), 0)
def test_column_filters():
def test_column_editable_list():
app, db, admin = setup()
Model1, Model2 = create_models(db)
# fill DB with values
Model1('test1_val_1', 'test2_val_1').save()
Model1('test1_val_2', 'test2_val_2').save()
Model1('test1_val_3', 'test2_val_3').save()
Model1('test1_val_4', 'test2_val_4').save()
Model1(None, 'empty_obj').save()
view = CustomModelView(Model1,
column_editable_list=[
'test1', 'datetime_field'])
admin.add_view(view)
Model2('string_field_val_1', None, None).save()
Model2('string_field_val_2', None, None).save()
Model2('string_field_val_3', 5000, 25.9).save()
Model2('string_field_val_4', 9000, 75.5).save()
fill_db(Model1, Model2)
Model1('datetime_obj1', datetime_field=datetime(2014,4,3,1,9,0)).save()
Model1('datetime_obj2', datetime_field=datetime(2013,3,2,0,8,0)).save()
client = app.test_client()
# Test in-line edit field rendering
rv = client.get('/admin/model1/')
data = rv.data.decode('utf-8')
ok_('data-role="x-editable"' in data)
# Form - Test basic in-line edit functionality
obj1 = Model1.objects.get(test1 = 'test1_val_3')
rv = client.post('/admin/model1/', data={
'test1-' + str(obj1.id): 'change-success-1',
})
data = rv.data.decode('utf-8')
ok_('Record was successfully saved.' == data)
# confirm the value has changed
rv = client.get('/admin/model1/')
data = rv.data.decode('utf-8')
ok_('change-success-1' in data)
# Test errors
obj2 = Model1.objects.get(test1 = 'datetime_obj1')
rv = client.post('/admin/model1/', data={
'datetime_field-' + str(obj2.id): 'problematic-input',
})
eq_(rv.status_code, 500)
view = CustomModelView(Model2,
column_editable_list=[
'model1'])
admin.add_view(view)
# Test in-line editing for relations
obj3 = Model2.objects.get(string_field = 'string_field_val_1')
rv = client.post('/admin/model2/', data={
'model1-' + str(obj3.id): str(obj1.id),
})
data = rv.data.decode('utf-8')
ok_('Record was successfully saved.' == data)
# confirm the value has changed
rv = client.get('/admin/model2/')
data = rv.data.decode('utf-8')
ok_('test1_val_1' in data)
def test_column_filters():
app, db, admin = setup()
Model1, Model2 = create_models(db)
# fill DB with values
fill_db(Model1, Model2)
# Test string filter
view = CustomModelView(Model1, column_filters=['test1'])
......
......@@ -63,25 +63,49 @@ def create_models(db):
class Model2(BaseModel):
def __init__(self, char_field=None, int_field=None, float_field=None,
bool_field=0):
bool_field=0, model1=None):
super(Model2, self).__init__()
self.char_field = char_field
self.int_field = int_field
self.float_field = float_field
self.bool_field = bool_field
self.model1 = model1
char_field = peewee.CharField(max_length=20)
int_field = peewee.IntegerField(null=True)
float_field = peewee.FloatField(null=True)
bool_field = peewee.BooleanField()
# Relation
model1 = peewee.ForeignKeyField(Model1, null=True)
Model1.create_table()
Model2.create_table()
return Model1, Model2
def fill_db(Model1, Model2):
Model1('test1_val_1', 'test2_val_1').save()
Model1('test1_val_2', 'test2_val_2').save()
Model1('test1_val_3', 'test2_val_3').save()
Model1('test1_val_4', 'test2_val_4').save()
Model1(None, 'empty_obj').save()
Model2('char_field_val_1', None, None).save()
Model2('char_field_val_2', None, None).save()
Model2('char_field_val_3', 5000, 25.9).save()
Model2('char_field_val_4', 9000, 75.5).save()
Model1('date_obj1', date_field=date(2014,11,17)).save()
Model1('date_obj2', date_field=date(2013,10,16)).save()
Model1('timeonly_obj1', timeonly_field=time(11,10,9)).save()
Model1('timeonly_obj2', timeonly_field=time(10,9,8)).save()
Model1('datetime_obj1', datetime_field=datetime(2014,4,3,1,9,0)).save()
Model1('datetime_obj2', datetime_field=datetime(2013,3,2,0,8,0)).save()
def test_model():
app, db, admin = setup()
Model1, Model2 = create_models(db)
......@@ -153,29 +177,63 @@ def test_model():
eq_(rv.status_code, 302)
eq_(Model1.select().count(), 0)
def test_column_filters():
def test_column_editable_list():
app, db, admin = setup()
Model1, Model2 = create_models(db)
# fill DB with values
Model1('test1_val_1', 'test2_val_1').save()
Model1('test1_val_2', 'test2_val_2').save()
Model1('test1_val_3', 'test2_val_3').save()
Model1('test1_val_4', 'test2_val_4').save()
Model1(None, 'empty_obj').save()
view = CustomModelView(Model1,
column_editable_list=[
'test1', 'enum_field'])
admin.add_view(view)
Model2('char_field_val_1', None, None).save()
Model2('char_field_val_2', None, None).save()
Model2('char_field_val_3', 5000, 25.9).save()
Model2('char_field_val_4', 9000, 75.5).save()
fill_db(Model1, Model2)
Model1('date_obj1', date_field=date(2014,11,17)).save()
Model1('date_obj2', date_field=date(2013,10,16)).save()
Model1('timeonly_obj1', timeonly_field=time(11,10,9)).save()
Model1('timeonly_obj2', timeonly_field=time(10,9,8)).save()
Model1('datetime_obj1', datetime_field=datetime(2014,4,3,1,9,0)).save()
Model1('datetime_obj2', datetime_field=datetime(2013,3,2,0,8,0)).save()
client = app.test_client()
# Test in-line edit field rendering
rv = client.get('/admin/model1/')
data = rv.data.decode('utf-8')
ok_('data-role="x-editable"' in data)
# Form - Test basic in-line edit functionality
rv = client.post('/admin/model1/', data={
'test1-1': 'change-success-1',
})
data = rv.data.decode('utf-8')
ok_('Record was successfully saved.' == data)
# ensure the value has changed
rv = client.get('/admin/model1/')
data = rv.data.decode('utf-8')
ok_('change-success-1' in data)
# Test errors
rv = client.post('/admin/model1/', data={
'enum_field-1': 'problematic-input',
})
eq_(rv.status_code, 500)
view = CustomModelView(Model2,
column_editable_list=[
'model1'])
admin.add_view(view)
# Test in-line editing for relations
rv = client.post('/admin/model2/', data={
'model1-1': '3',
})
data = rv.data.decode('utf-8')
ok_('Record was successfully saved.' == data)
def test_column_filters():
app, db, admin = setup()
Model1, Model2 = create_models(db)
fill_db(Model1, Model2)
# Test string filter
view = CustomModelView(Model1, column_filters=['test1'])
......
......@@ -81,6 +81,39 @@ def create_models(db):
return Model1, Model2
def fill_db(db, Model1, Model2):
model1_obj1 = Model1('test1_val_1', 'test2_val_1', bool_field=True)
model1_obj2 = Model1('test1_val_2', 'test2_val_2')
model1_obj3 = Model1('test1_val_3', 'test2_val_3')
model1_obj4 = Model1('test1_val_4', 'test2_val_4')
model2_obj1 = Model2('test2_val_1', model1=model1_obj1, float_field=None)
model2_obj2 = Model2('test2_val_2', model1=model1_obj2, float_field=None)
model2_obj3 = Model2('test2_val_3', int_field=5000, float_field=25.9)
model2_obj4 = Model2('test2_val_4', int_field=9000, float_field=75.5)
date_obj1 = Model1('date_obj1', date_field=date(2014,11,17))
date_obj2 = Model1('date_obj2', date_field=date(2013,10,16))
timeonly_obj1 = Model1('timeonly_obj1', time_field=time(11,10,9))
timeonly_obj2 = Model1('timeonly_obj2', time_field=time(10,9,8))
datetime_obj1 = Model1('datetime_obj1', datetime_field=datetime(2014,4,3,1,9,0))
datetime_obj2 = Model1('datetime_obj2', datetime_field=datetime(2013,3,2,0,8,0))
enum_obj1 = Model1('enum_obj1', enum_field="model1_v1")
enum_obj2 = Model1('enum_obj2', enum_field="model1_v2")
empty_obj = Model1(test2="empty_obj")
db.session.add_all([
model1_obj1, model1_obj2, model1_obj3, model1_obj4,
model2_obj1, model2_obj2, model2_obj3, model2_obj4,
date_obj1, timeonly_obj1, datetime_obj1,
date_obj2, timeonly_obj2, datetime_obj2,
enum_obj1, enum_obj2, empty_obj
])
db.session.commit()
def test_model():
app, db, admin = setup()
Model1, Model2 = create_models(db)
......@@ -286,6 +319,56 @@ def test_complex_searchable_list_missing_children():
ok_('magic string' in data)
def test_column_editable_list():
app, db, admin = setup()
Model1, Model2 = create_models(db)
view = CustomModelView(Model1, db.session,
column_editable_list=[
'test1', 'enum_field'])
admin.add_view(view)
fill_db(db, Model1, Model2)
client = app.test_client()
# Test in-line edit field rendering
rv = client.get('/admin/model1/')
data = rv.data.decode('utf-8')
ok_('data-role="x-editable"' in data)
# Form - Test basic in-line edit functionality
rv = client.post('/admin/model1/', data={
'test1-1': 'change-success-1',
})
data = rv.data.decode('utf-8')
ok_('Record was successfully saved.' == data)
# ensure the value has changed
rv = client.get('/admin/model1/')
data = rv.data.decode('utf-8')
ok_('change-success-1' in data)
# Test errors
rv = client.post('/admin/model1/', data={
'enum_field-1': 'problematic-input',
})
eq_(rv.status_code, 500)
view = CustomModelView(Model2, db.session,
column_editable_list=[
'model1'])
admin.add_view(view)
# Test in-line editing for relations
rv = client.post('/admin/model2/', data={
'model1-1': '3',
})
data = rv.data.decode('utf-8')
ok_('Record was successfully saved.' == data)
def test_column_filters():
app, db, admin = setup()
......@@ -393,37 +476,7 @@ def test_column_filters():
eq_(list(view._filter_groups.keys()), [u'Test Filter #1', u'Test Filter #2'])
# Fill DB
model1_obj1 = Model1('test1_val_1', 'test2_val_1', bool_field=True)
model1_obj2 = Model1('test1_val_2', 'test2_val_2')
model1_obj3 = Model1('test1_val_3', 'test2_val_3')
model1_obj4 = Model1('test1_val_4', 'test2_val_4')
model2_obj1 = Model2('test2_val_1', model1=model1_obj1, float_field=None)
model2_obj2 = Model2('test2_val_2', model1=model1_obj2, float_field=None)
model2_obj3 = Model2('test2_val_3', int_field=5000, float_field=25.9)
model2_obj4 = Model2('test2_val_4', int_field=9000, float_field=75.5)
date_obj1 = Model1('date_obj1', date_field=date(2014,11,17))
date_obj2 = Model1('date_obj2', date_field=date(2013,10,16))
timeonly_obj1 = Model1('timeonly_obj1', time_field=time(11,10,9))
timeonly_obj2 = Model1('timeonly_obj2', time_field=time(10,9,8))
datetime_obj1 = Model1('datetime_obj1', datetime_field=datetime(2014,4,3,1,9,0))
datetime_obj2 = Model1('datetime_obj2', datetime_field=datetime(2013,3,2,0,8,0))
enum_obj1 = Model1('enum_obj1', enum_field="model1_v1")
enum_obj2 = Model1('enum_obj2', enum_field="model1_v2")
empty_obj = Model1(test2="empty_obj")
db.session.add_all([
model1_obj1, model1_obj2, model1_obj3, model1_obj4,
model2_obj1, model2_obj2, model2_obj3, model2_obj4,
date_obj1, timeonly_obj1, datetime_obj1,
date_obj2, timeonly_obj2, datetime_obj2,
enum_obj1, enum_obj2, empty_obj
])
db.session.commit()
fill_db(db, Model1, Model2)
client = app.test_client()
......
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