Commit e9aba295 authored by Paul Brown's avatar Paul Brown

make suggested changes to editable list view implementation

parent eb37f32c
......@@ -5,17 +5,15 @@ from flask import request, flash, abort, Response
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.model.form import wrap_fields_in_fieldlist
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
......@@ -403,20 +401,15 @@ class ModelView(BaseModelView):
def scaffold_list_form(self):
"""
Create form for the list view editable columns.
Create form for the `index_view` using only the columns from
`self.column_editable_list`.
"""
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,
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)
return wrap_fields_in_fieldlist(self.form_base_class, form_class)
# AJAX foreignkey support
def _create_ajax_loader(self, name, opts):
......@@ -565,37 +558,6 @@ 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,18 +2,16 @@ import logging
from flask import flash
from flask.ext.admin._compat import string_types, iteritems
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
from flask.ext.admin.model.form import wrap_fields_in_fieldlist
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
......@@ -242,20 +240,14 @@ class ModelView(BaseModelView):
def scaffold_list_form(self):
"""
Create form for the list view editable columns.
Create form for the `index_view` using only the columns from
`self.column_editable_list`.
"""
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)
return wrap_fields_in_fieldlist(self.form_base_class, form_class)
def scaffold_inline_form_models(self, form_class):
converter = self.model_form_converter(self)
......@@ -402,40 +394,6 @@ 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,21 +8,19 @@ from sqlalchemy.exc import IntegrityError
from flask import flash
from flask.ext.admin._compat import string_types, iteritems
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
from flask.ext.admin.model.form import wrap_fields_in_fieldlist
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
from .ajax import create_ajax_loader
# Set up logger
log = logging.getLogger("flask-admin.sqla")
......@@ -616,24 +614,15 @@ class ModelView(BaseModelView):
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.
Create form for the `index_view` using only the columns from
`self.column_editable_list`.
"""
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)
return wrap_fields_in_fieldlist(self.form_base_class, form_class)
def scaffold_inline_form_models(self, form_class):
"""
......@@ -922,37 +911,6 @@ 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.
......
import warnings
import re
from flask import request, redirect, flash, abort, json, Response
from flask import (request, redirect, flash, abort, json, Response,
get_flashed_messages)
from jinja2 import contextfunction
from wtforms.validators import ValidationError
......@@ -11,8 +12,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, is_form_submitted)
from flask.ext.admin.helpers import (get_form_data, validate_form_on_submit,
get_redirect_target)
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
......@@ -274,7 +275,7 @@ class BaseModelView(BaseView, ActionsMixin):
class MyModelView(BaseModelView):
column_editable_list = ('name', 'last_name')
"""
column_choices = None
"""
Map choices to columns in list view
......@@ -590,9 +591,9 @@ 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:
if self.column_editable_list:
self._list_form_class = self.scaffold_list_form()
else:
self.column_editable_list = {}
......@@ -852,7 +853,8 @@ class BaseModelView(BaseView, ActionsMixin):
def scaffold_list_form(self):
"""
Create form class for list view in-line editing.
Create form for the `index_view` using only the columns from
`self.column_editable_list`. Must be implemented in the child class.
"""
raise NotImplementedError('Please implement scaffold_list_form method')
......@@ -937,7 +939,7 @@ class BaseModelView(BaseView, ActionsMixin):
Column name.
"""
return name in self.column_editable_list
def _get_column_by_idx(self, idx):
"""
Return column index by
......@@ -1114,15 +1116,6 @@ 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.
......@@ -1291,42 +1284,13 @@ class BaseModelView(BaseView, ActionsMixin):
raise NotImplementedError()
# Views
@expose('/', methods=('POST', 'GET'))
@expose('/')
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
......@@ -1376,7 +1340,7 @@ class BaseModelView(BaseView, ActionsMixin):
self.list_template,
data=data,
form=form,
# List
list_columns=self._list_columns,
sortable_columns=self._sortable_columns,
......@@ -1526,3 +1490,46 @@ class BaseModelView(BaseView, ActionsMixin):
data = [loader.format(m) for m in loader.get_list(query, offset, limit)]
return Response(json.dumps(data), mimetype='application/json')
@expose('/ajax/update/', methods=('POST',))
def ajax_update(self):
"""
Edits a single column of a record in list view.
"""
if not self.column_editable_list:
abort(404)
record = None
form = self.list_form()
# prevent validation issues due to submitting a single field
# delete all fields except the field being submitted
for field in form:
# only the submitted field has a positive last_index
if getattr(field, 'last_index', 0):
record = self.get_one(str(field.last_index))
elif field.name == 'csrf_token':
pass
else:
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.update_model(form, record):
# Success
return gettext('Record was successfully saved.')
else:
# Error: No records changed, or problem saving to database.
msgs = ", ".join([msg for msg in get_flashed_messages()])
return gettext('Failed to update record. %(error)s',
error=msgs), 500
else:
for field in form:
for error in field.errors:
# return validation error to x-editable
if isinstance(error, list):
return ", ".join(error), 500
else:
return error, 500
......@@ -9,7 +9,7 @@ except ImportError:
from wtforms.utils import unset_value
from flask.ext.admin._compat import iteritems
from .widgets import (InlineFieldListWidget, InlineFormWidget,
from .widgets import (InlineFieldListWidget, InlineFormWidget,
AjaxSelect2Widget, XEditableWidget)
......@@ -129,7 +129,7 @@ class InlineModelFormField(FormField):
class ListEditableFieldList(FieldList):
"""
Modified FieldList to allow for alphanumeric primary keys.
Used in the editable list view.
"""
widget = XEditableWidget()
......@@ -173,6 +173,10 @@ class ListEditableFieldList(FieldList):
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):
"""
......
......@@ -3,6 +3,9 @@ import inspect
from flask.ext.admin.form import BaseForm, rules
from flask.ext.admin._compat import iteritems
from .fields import ListEditableFieldList
from wtforms.fields.core import UnboundField
def converts(*args):
def _inner(func):
......@@ -11,6 +14,27 @@ def converts(*args):
return _inner
def wrap_fields_in_fieldlist(form_base_class, form_class):
"""
Create a form class with all the fields wrapped in a FieldList.
Wrapping each field in FieldList allows submitting POST requests
in this format: ('<field_name>-<primary_key>', '<value>')
Used in the editable list view.
"""
class FieldListForm(form_base_class):
pass
# iterate FormMeta to get unbound fields
for name, obj in iteritems(form_class.__dict__):
if isinstance(obj, UnboundField):
# wrap field in a WTForms FieldList
setattr(FieldListForm, name, ListEditableFieldList(obj))
return FieldListForm
class InlineBaseFormAdmin(object):
"""
Settings for inline form administration.
......
......@@ -74,7 +74,7 @@ class XEditableWidget(object):
value = kwargs.pop("value", "")
kwargs.setdefault('data-role', 'x-editable')
kwargs.setdefault('data-url', './')
kwargs.setdefault('data-url', './ajax/update/')
kwargs.setdefault('id', field.id)
kwargs.setdefault('name', field.name)
......
......@@ -147,7 +147,7 @@
<hr>
<div class="form-group">
<div class="col-md-offset-2 col-md-10 submit-row">
<input type="submit" class="btn btn-primary" value="{{ _gettext('Submit') }}" />
<input type="submit" class="btn btn-primary" value="{{ _gettext('Submit') }}" />
{% if extra %}
{{ extra }}
{% endif %}
......
......@@ -162,7 +162,7 @@ def test_column_editable_list():
# Form - Test basic in-line edit functionality
obj1 = Model1.objects.get(test1 = 'test1_val_3')
rv = client.post('/admin/model1/', data={
rv = client.post('/admin/model1/ajax/update/', data={
'test1-' + str(obj1.id): 'change-success-1',
})
data = rv.data.decode('utf-8')
......@@ -173,21 +173,35 @@ def test_column_editable_list():
data = rv.data.decode('utf-8')
ok_('change-success-1' in data)
# Test errors
# Test validation error
obj2 = Model1.objects.get(test1 = 'datetime_obj1')
rv = client.post('/admin/model1/', data={
rv = client.post('/admin/model1/ajax/update/', data={
'datetime_field-' + str(obj2.id): 'problematic-input',
})
eq_(rv.status_code, 500)
# Test invalid primary key
rv = client.post('/admin/model1/ajax/update/', data={
'test1-1000': 'problematic-input',
})
data = rv.data.decode('utf-8')
eq_(rv.status_code, 500)
# Test editing column not in column_editable_list
rv = client.post('/admin/model1/ajax/update/', data={
'test2-1': 'problematic-input',
})
data = rv.data.decode('utf-8')
eq_(rv.status_code, 500)
# Test in-line editing for relations
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={
rv = client.post('/admin/model2/ajax/update/', data={
'model1-' + str(obj3.id): str(obj1.id),
})
data = rv.data.decode('utf-8')
......
......@@ -57,29 +57,30 @@ def create_models(db):
date_field = peewee.DateField(null=True)
timeonly_field = peewee.TimeField(null=True)
datetime_field = peewee.DateTimeField(null=True)
def __str__(self):
return self.test1
# "or ''" fixes error when loading choices for relation field:
# TypeError: coercing to Unicode: need string or buffer, NoneType found
return self.test1 or ''
class Model2(BaseModel):
def __init__(self, char_field=None, int_field=None, float_field=None,
bool_field=0, model1=None):
bool_field=0):
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()
......@@ -198,7 +199,7 @@ def test_column_editable_list():
ok_('data-role="x-editable"' in data)
# Form - Test basic in-line edit functionality
rv = client.post('/admin/model1/', data={
rv = client.post('/admin/model1/ajax/update/', data={
'test1-1': 'change-success-1',
})
data = rv.data.decode('utf-8')
......@@ -209,24 +210,43 @@ def test_column_editable_list():
data = rv.data.decode('utf-8')
ok_('change-success-1' in data)
# Test errors
rv = client.post('/admin/model1/', data={
# Test validation error
rv = client.post('/admin/model1/ajax/update/', data={
'enum_field-1': 'problematic-input',
})
eq_(rv.status_code, 500)
# Test invalid primary key
rv = client.post('/admin/model1/ajax/update/', data={
'test1-1000': 'problematic-input',
})
data = rv.data.decode('utf-8')
eq_(rv.status_code, 500)
# Test editing column not in column_editable_list
rv = client.post('/admin/model1/ajax/update/', data={
'test2-1': 'problematic-input',
})
data = rv.data.decode('utf-8')
eq_(rv.status_code, 500)
# Test in-line editing for relations
view = CustomModelView(Model2,
column_editable_list=[
'model1'])
admin.add_view(view)
# Test in-line editing for relations
rv = client.post('/admin/model2/', data={
rv = client.post('/admin/model2/ajax/update/', data={
'model1-1': '3',
})
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_3' in data)
def test_column_filters():
app, db, admin = setup()
......
......@@ -339,7 +339,7 @@ def test_column_editable_list():
ok_('data-role="x-editable"' in data)
# Form - Test basic in-line edit functionality
rv = client.post('/admin/model1/', data={
rv = client.post('/admin/model1/ajax/update/', data={
'test1-1': 'change-success-1',
})
data = rv.data.decode('utf-8')
......@@ -350,24 +350,43 @@ def test_column_editable_list():
data = rv.data.decode('utf-8')
ok_('change-success-1' in data)
# Test errors
rv = client.post('/admin/model1/', data={
# Test validation error
rv = client.post('/admin/model1/ajax/update/', data={
'enum_field-1': 'problematic-input',
})
eq_(rv.status_code, 500)
# Test invalid primary key
rv = client.post('/admin/model1/ajax/update/', data={
'test1-1000': 'problematic-input',
})
data = rv.data.decode('utf-8')
eq_(rv.status_code, 500)
# Test editing column not in column_editable_list
rv = client.post('/admin/model1/ajax/update/', data={
'test2-1': 'problematic-input',
})
data = rv.data.decode('utf-8')
eq_(rv.status_code, 500)
# Test in-line editing for relations
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={
rv = client.post('/admin/model2/ajax/update/', data={
'model1-1': '3',
})
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_3' in data)
def test_column_filters():
app, db, admin = setup()
......
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