Commit 2b4bfe35 authored by Serge S. Koval's avatar Serge S. Koval

Merge pull request #756 from pawl/list_editable

Add editable list view
parents 90db8a90 9ba43b5a
...@@ -5,6 +5,7 @@ from flask import request, flash, abort, Response ...@@ -5,6 +5,7 @@ from flask import request, flash, abort, Response
from flask.ext.admin import expose from flask.ext.admin import expose
from flask.ext.admin.babel import gettext, ngettext, lazy_gettext from flask.ext.admin.babel import gettext, ngettext, lazy_gettext
from flask.ext.admin.model import BaseModelView from flask.ext.admin.model import BaseModelView
from flask.ext.admin.model.form import wrap_fields_in_fieldlist
from flask.ext.admin._compat import iteritems, string_types from flask.ext.admin._compat import iteritems, string_types
import mongoengine import mongoengine
...@@ -398,6 +399,18 @@ class ModelView(BaseModelView): ...@@ -398,6 +399,18 @@ class ModelView(BaseModelView):
return form_class return form_class
def scaffold_list_form(self):
"""
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)
return wrap_fields_in_fieldlist(self.form_base_class, form_class)
# AJAX foreignkey support # AJAX foreignkey support
def _create_ajax_loader(self, name, opts): def _create_ajax_loader(self, name, opts):
return create_ajax_loader(self.model, name, name, opts) return create_ajax_loader(self.model, name, name, opts)
......
...@@ -5,6 +5,7 @@ from flask import flash ...@@ -5,6 +5,7 @@ from flask import flash
from flask.ext.admin._compat import string_types from flask.ext.admin._compat import string_types
from flask.ext.admin.babel import gettext, ngettext, lazy_gettext from flask.ext.admin.babel import gettext, ngettext, lazy_gettext
from flask.ext.admin.model import BaseModelView from flask.ext.admin.model import BaseModelView
from flask.ext.admin.model.form import wrap_fields_in_fieldlist
from peewee import PrimaryKeyField, ForeignKeyField, Field, CharField, TextField from peewee import PrimaryKeyField, ForeignKeyField, Field, CharField, TextField
...@@ -237,6 +238,17 @@ class ModelView(BaseModelView): ...@@ -237,6 +238,17 @@ class ModelView(BaseModelView):
return form_class return form_class
def scaffold_list_form(self):
"""
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)
return wrap_fields_in_fieldlist(self.form_base_class, form_class)
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)
inline_converter = self.inline_model_form_converter(self) inline_converter = self.inline_model_form_converter(self)
......
...@@ -11,6 +11,8 @@ from flask import flash ...@@ -11,6 +11,8 @@ from flask import flash
from flask.ext.admin._compat import string_types from flask.ext.admin._compat import string_types
from flask.ext.admin.babel import gettext, ngettext, lazy_gettext from flask.ext.admin.babel import gettext, ngettext, lazy_gettext
from flask.ext.admin.model import BaseModelView from flask.ext.admin.model import BaseModelView
from flask.ext.admin.model.form import wrap_fields_in_fieldlist
from flask.ext.admin.actions import action from flask.ext.admin.actions import action
from flask.ext.admin._backwards import ObsoleteAttr from flask.ext.admin._backwards import ObsoleteAttr
...@@ -19,7 +21,6 @@ from .typefmt import DEFAULT_FORMATTERS ...@@ -19,7 +21,6 @@ from .typefmt import DEFAULT_FORMATTERS
from .tools import get_query_for_ids from .tools import get_query_for_ids
from .ajax import create_ajax_loader from .ajax import create_ajax_loader
# Set up logger # Set up logger
log = logging.getLogger("flask-admin.sqla") log = logging.getLogger("flask-admin.sqla")
...@@ -611,6 +612,18 @@ class ModelView(BaseModelView): ...@@ -611,6 +612,18 @@ class ModelView(BaseModelView):
return form_class return form_class
def scaffold_list_form(self):
"""
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)
return wrap_fields_in_fieldlist(self.form_base_class, form_class)
def scaffold_inline_form_models(self, form_class): def scaffold_inline_form_models(self, form_class):
""" """
Contribute inline models to the form Contribute inline models to the form
......
import warnings import warnings
import re 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 jinja2 import contextfunction
from wtforms.validators import ValidationError from wtforms.validators import ValidationError
...@@ -11,7 +12,8 @@ from flask.ext.admin.base import BaseView, expose ...@@ -11,7 +12,8 @@ from flask.ext.admin.base import BaseView, expose
from flask.ext.admin.form import BaseForm, FormOpts, rules from flask.ext.admin.form import BaseForm, FormOpts, rules
from flask.ext.admin.model import filters, typefmt from flask.ext.admin.model import filters, typefmt
from flask.ext.admin.actions import ActionsMixin 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)
from flask.ext.admin.tools import rec_getattr from flask.ext.admin.tools import rec_getattr
from flask.ext.admin._backwards import ObsoleteAttr from flask.ext.admin._backwards import ObsoleteAttr
from flask.ext.admin._compat import iteritems, OrderedDict, as_unicode from flask.ext.admin._compat import iteritems, OrderedDict, as_unicode
...@@ -264,6 +266,16 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -264,6 +266,16 @@ class BaseModelView(BaseView, ActionsMixin):
column_searchable_list = ('name', 'email') 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 column_choices = None
""" """
Map choices to columns in list view Map choices to columns in list view
...@@ -580,6 +592,12 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -580,6 +592,12 @@ class BaseModelView(BaseView, ActionsMixin):
self._create_form_class = self.get_create_form() self._create_form_class = self.get_create_form()
self._edit_form_class = self.get_edit_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): def _refresh_filters_cache(self):
self._filters = self.get_filters() self._filters = self.get_filters()
...@@ -833,6 +851,13 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -833,6 +851,13 @@ class BaseModelView(BaseView, ActionsMixin):
""" """
raise NotImplementedError('Please implement scaffold_form method') raise NotImplementedError('Please implement scaffold_form method')
def scaffold_list_form(self):
"""
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')
def get_form(self): def get_form(self):
""" """
Get form class. Get form class.
...@@ -879,6 +904,14 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -879,6 +904,14 @@ class BaseModelView(BaseView, ActionsMixin):
""" """
return self._edit_form_class(get_form_data(), obj=obj) 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): def validate_form(self, form):
""" """
Validate the form on submit. Validate the form on submit.
...@@ -898,6 +931,15 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -898,6 +931,15 @@ class BaseModelView(BaseView, ActionsMixin):
""" """
return name in self._sortable_columns 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): def _get_column_by_idx(self, idx):
""" """
Return column index by Return column index by
...@@ -1247,6 +1289,11 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -1247,6 +1289,11 @@ class BaseModelView(BaseView, ActionsMixin):
""" """
List view List view
""" """
if self.column_editable_list:
form = self.list_form()
else:
form = None
# Grab parameters from URL # Grab parameters from URL
view_args = self._get_list_extra_args() view_args = self._get_list_extra_args()
...@@ -1289,37 +1336,47 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -1289,37 +1336,47 @@ class BaseModelView(BaseView, ActionsMixin):
search=None, search=None,
filters=None)) filters=None))
return self.render(self.list_template, return self.render(
data=data, self.list_template,
# List data=data,
list_columns=self._list_columns, form=form,
sortable_columns=self._sortable_columns,
# Stuff # List
enumerate=enumerate, list_columns=self._list_columns,
get_pk_value=self.get_pk_value, sortable_columns=self._sortable_columns,
get_value=self.get_list_value, editable_columns=self.column_editable_list,
return_url=self._get_list_url(view_args),
# Pagination # Pagination
count=count, count=count,
pager_url=pager_url, pager_url=pager_url,
num_pages=num_pages, num_pages=num_pages,
page=view_args.page, page=view_args.page,
# Sorting
sort_column=view_args.sort, # Sorting
sort_desc=view_args.sort_desc, sort_column=view_args.sort,
sort_url=sort_url, sort_desc=view_args.sort_desc,
# Search sort_url=sort_url,
search_supported=self._search_supported,
clear_search_url=clear_search_url, # Search
search=view_args.search, search_supported=self._search_supported,
# Filters clear_search_url=clear_search_url,
filters=self._filters, search=view_args.search,
filter_groups=self._filter_groups,
active_filters=view_args.filters, # Filters
filters=self._filters,
# Actions filter_groups=self._filter_groups,
actions=actions, active_filters=view_args.filters,
actions_confirmation=actions_confirmation)
# Actions
actions=actions,
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')) @expose('/new/', methods=('GET', 'POST'))
def create_view(self): def create_view(self):
...@@ -1433,3 +1490,46 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -1433,3 +1490,46 @@ class BaseModelView(BaseView, ActionsMixin):
data = [loader.format(m) for m in loader.get_list(query, offset, limit)] data = [loader.format(m) for m in loader.get_list(query, offset, limit)]
return Response(json.dumps(data), mimetype='application/json') 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
...@@ -3,8 +3,14 @@ import itertools ...@@ -3,8 +3,14 @@ import itertools
from wtforms.validators import ValidationError from wtforms.validators import ValidationError
from wtforms.fields import FieldList, FormField, SelectFieldBase 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 flask.ext.admin._compat import iteritems
from .widgets import InlineFieldListWidget, InlineFormWidget, AjaxSelect2Widget from .widgets import (InlineFieldListWidget, InlineFormWidget,
AjaxSelect2Widget, XEditableWidget)
class InlineFieldList(FieldList): class InlineFieldList(FieldList):
...@@ -120,6 +126,58 @@ class InlineModelFormField(FormField): ...@@ -120,6 +126,58 @@ 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 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
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,6 +3,9 @@ import inspect ...@@ -3,6 +3,9 @@ import inspect
from flask.ext.admin.form import BaseForm, rules from flask.ext.admin.form import BaseForm, rules
from flask.ext.admin._compat import iteritems from flask.ext.admin._compat import iteritems
from .fields import ListEditableFieldList
from wtforms.fields.core import UnboundField
def converts(*args): def converts(*args):
def _inner(func): def _inner(func):
...@@ -11,6 +14,27 @@ def converts(*args): ...@@ -11,6 +14,27 @@ def converts(*args):
return _inner 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): class InlineBaseFormAdmin(object):
""" """
Settings for inline form administration. Settings for inline form administration.
......
...@@ -61,3 +61,88 @@ class AjaxSelect2Widget(object): ...@@ -61,3 +61,88 @@ class AjaxSelect2Widget(object):
kwargs.setdefault('data-placeholder', placeholder) kwargs.setdefault('data-placeholder', placeholder)
return HTMLString('<input %s>' % html_params(name=field.name, **kwargs)) 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', './ajax/update/')
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", "")
# subfield is the first entry (subfield) from FieldList (field)
subfield = field.entries[0]
kwargs = self.get_kwargs(subfield, kwargs)
return HTMLString(
'<a %s>%s</a>' % (html_params(**kwargs), value)
)
def get_kwargs(self, subfield, kwargs):
"""
Return extra kwargs based on the subfield type.
"""
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 == '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 ['FloatField', 'DecimalField']:
kwargs['data-type'] = 'number'
kwargs['data-step'] = 'any'
elif subfield.type in ['QuerySelectField', 'ModelSelectField']:
kwargs['data-type'] = 'select'
choices = {}
for choice in subfield:
try:
choices[str(choice._value())] = str(choice.label.text)
except TypeError:
choices[str(choice._value())] = ""
kwargs['data-source'] = choices
else:
raise Exception('Unsupported field type: %s' % (type(subfield),))
return kwargs
...@@ -241,6 +241,17 @@ ...@@ -241,6 +241,17 @@
return true; 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) { switch (name) {
case 'select2': case 'select2':
var opts = { var opts = {
...@@ -390,6 +401,32 @@ ...@@ -390,6 +401,32 @@
case 'leaflet': case 'leaflet':
processLeafletWidget($el, name); processLeafletWidget($el, name);
return true; 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 @@ ...@@ -463,7 +500,7 @@
this.applyGlobalStyles = function(parent) { this.applyGlobalStyles = function(parent) {
var self = this; var self = this;
$(':input[data-role]', parent).each(function() { $(':input[data-role], a[data-role]', parent).each(function() {
var $el = $(this); var $el = $(this);
self.applyStyle($el, $el.attr('data-role')); self.applyStyle($el, $el.attr('data-role'));
}); });
......
...@@ -178,6 +178,9 @@ ...@@ -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.css') }}" rel="stylesheet">
<link href="{{ admin_static.url(filename='vendor/leaflet/leaflet.draw.css') }}" rel="stylesheet"> <link href="{{ admin_static.url(filename='vendor/leaflet/leaflet.draw.css') }}" rel="stylesheet">
{% endif %} {% endif %}
{% if editable_columns %}
<link href="{{ admin_static.url(filename='vendor/x-editable/css/bootstrap2-editable-1.5.1.css') }}" rel="stylesheet">
{% endif %}
{% endmacro %} {% endmacro %}
{% macro form_js() %} {% macro form_js() %}
...@@ -189,5 +192,8 @@ ...@@ -189,5 +192,8 @@
<script src="{{ admin_static.url(filename='vendor/leaflet/leaflet.draw.js') }}"></script> <script src="{{ admin_static.url(filename='vendor/leaflet/leaflet.draw.js') }}"></script>
{% endif %} {% endif %}
<script src="{{ admin_static.url(filename='vendor/bootstrap-daterangepicker/daterangepicker.js') }}"></script> <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> <script src="{{ admin_static.url(filename='admin/js/form-1.0.0.js') }}"></script>
{% endmacro %} {% endmacro %}
...@@ -119,8 +119,17 @@ ...@@ -119,8 +119,17 @@
{% endblock %} {% endblock %}
</td> </td>
{% endblock %} {% endblock %}
{% for c, name in list_columns %} {% for c, name in list_columns %}
<td>{{ get_value(row, c) }}</td> {% 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 %} {% endfor %}
{% endblock %} {% endblock %}
</tr> </tr>
...@@ -148,8 +157,8 @@ ...@@ -148,8 +157,8 @@
<script src="{{ admin_static.url(filename='admin/js/filters-1.0.0.js') }}"></script> <script src="{{ admin_static.url(filename='admin/js/filters-1.0.0.js') }}"></script>
{{ actionlib.script(_gettext('Please select at least one record.'), {{ actionlib.script(_gettext('Please select at least one record.'),
actions, actions,
actions_confirmation) }} actions_confirmation) }}
<script language="javascript"> <script language="javascript">
(function($) { (function($) {
......
...@@ -147,13 +147,13 @@ ...@@ -147,13 +147,13 @@
<hr> <hr>
<div class="form-group"> <div class="form-group">
<div class="col-md-offset-2 col-md-10 submit-row"> <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 %} {% if extra %}
{{ extra }} {{ extra }}
{% endif %} {% endif %}
{% if cancel_url %} {% if cancel_url %}
<a href="{{ cancel_url }}" class="btn btn-danger" role="button">{{ _gettext('Cancel') }}</a> <a href="{{ cancel_url }}" class="btn btn-danger" role="button">{{ _gettext('Cancel') }}</a>
{% endif %} {% endif %}
</div> </div>
</div> </div>
{% endmacro %} {% endmacro %}
...@@ -173,6 +173,9 @@ ...@@ -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.css') }}" rel="stylesheet">
<link href="{{ admin_static.url(filename='vendor/leaflet/leaflet.draw.css') }}" rel="stylesheet"> <link href="{{ admin_static.url(filename='vendor/leaflet/leaflet.draw.css') }}" rel="stylesheet">
{% endif %} {% endif %}
{% if editable_columns %}
<link href="{{ admin_static.url(filename='vendor/x-editable/css/bootstrap3-editable-1.5.1.css') }}" rel="stylesheet">
{% endif %}
{% endmacro %} {% endmacro %}
{% macro form_js() %} {% macro form_js() %}
...@@ -184,5 +187,8 @@ ...@@ -184,5 +187,8 @@
<script src="{{ admin_static.url(filename='vendor/leaflet/leaflet.draw.js') }}"></script> <script src="{{ admin_static.url(filename='vendor/leaflet/leaflet.draw.js') }}"></script>
{% endif %} {% endif %}
<script src="{{ admin_static.url(filename='vendor/bootstrap-daterangepicker/daterangepicker.js') }}"></script> <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> <script src="{{ admin_static.url(filename='admin/js/form-1.0.0.js') }}"></script>
{% endmacro %} {% endmacro %}
...@@ -120,7 +120,15 @@ ...@@ -120,7 +120,15 @@
</td> </td>
{% endblock %} {% endblock %}
{% for c, name in list_columns %} {% for c, name in list_columns %}
<td>{{ get_value(row, c) }}</td> {% 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 %} {% endfor %}
{% endblock %} {% endblock %}
</tr> </tr>
...@@ -148,8 +156,8 @@ ...@@ -148,8 +156,8 @@
{{ lib.form_js() }} {{ lib.form_js() }}
{{ actionlib.script(_gettext('Please select at least one record.'), {{ actionlib.script(_gettext('Please select at least one record.'),
actions, actions,
actions_confirmation) }} actions_confirmation) }}
<script language="javascript"> <script language="javascript">
(function($) { (function($) {
......
...@@ -52,6 +52,22 @@ def create_models(db): ...@@ -52,6 +52,22 @@ def create_models(db):
return Model1, Model2 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(): def test_model():
app, db, admin = setup() app, db, admin = setup()
...@@ -124,25 +140,86 @@ def test_model(): ...@@ -124,25 +140,86 @@ def test_model():
eq_(rv.status_code, 302) eq_(rv.status_code, 302)
eq_(Model1.objects.count(), 0) eq_(Model1.objects.count(), 0)
def test_column_editable_list():
app, db, admin = setup()
Model1, Model2 = create_models(db)
view = CustomModelView(Model1,
column_editable_list=[
'test1', 'datetime_field'])
admin.add_view(view)
fill_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
obj1 = Model1.objects.get(test1 = 'test1_val_3')
rv = client.post('/admin/model1/ajax/update/', 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 validation error
obj2 = Model1.objects.get(test1 = 'datetime_obj1')
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)
obj3 = Model2.objects.get(string_field = 'string_field_val_1')
rv = client.post('/admin/model2/ajax/update/', 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(): def test_column_filters():
app, db, admin = setup() app, db, admin = setup()
Model1, Model2 = create_models(db) Model1, Model2 = create_models(db)
# fill DB with values # fill DB with values
Model1('test1_val_1', 'test2_val_1').save() fill_db(Model1, Model2)
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()
# Test string filter # Test string filter
view = CustomModelView(Model1, column_filters=['test1']) view = CustomModelView(Model1, column_filters=['test1'])
......
...@@ -57,9 +57,11 @@ def create_models(db): ...@@ -57,9 +57,11 @@ def create_models(db):
date_field = peewee.DateField(null=True) date_field = peewee.DateField(null=True)
timeonly_field = peewee.TimeField(null=True) timeonly_field = peewee.TimeField(null=True)
datetime_field = peewee.DateTimeField(null=True) datetime_field = peewee.DateTimeField(null=True)
def __str__(self): 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): class Model2(BaseModel):
def __init__(self, char_field=None, int_field=None, float_field=None, def __init__(self, char_field=None, int_field=None, float_field=None,
...@@ -70,18 +72,41 @@ def create_models(db): ...@@ -70,18 +72,41 @@ def create_models(db):
self.int_field = int_field self.int_field = int_field
self.float_field = float_field self.float_field = float_field
self.bool_field = bool_field self.bool_field = bool_field
char_field = peewee.CharField(max_length=20) char_field = peewee.CharField(max_length=20)
int_field = peewee.IntegerField(null=True) int_field = peewee.IntegerField(null=True)
float_field = peewee.FloatField(null=True) float_field = peewee.FloatField(null=True)
bool_field = peewee.BooleanField() bool_field = peewee.BooleanField()
# Relation
model1 = peewee.ForeignKeyField(Model1, null=True)
Model1.create_table() Model1.create_table()
Model2.create_table() Model2.create_table()
return Model1, Model2 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(): def test_model():
app, db, admin = setup() app, db, admin = setup()
Model1, Model2 = create_models(db) Model1, Model2 = create_models(db)
...@@ -153,29 +178,82 @@ def test_model(): ...@@ -153,29 +178,82 @@ def test_model():
eq_(rv.status_code, 302) eq_(rv.status_code, 302)
eq_(Model1.select().count(), 0) eq_(Model1.select().count(), 0)
def test_column_editable_list():
app, db, admin = setup()
Model1, Model2 = create_models(db)
view = CustomModelView(Model1,
column_editable_list=[
'test1', 'enum_field'])
admin.add_view(view)
fill_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/ajax/update/', 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 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)
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(): def test_column_filters():
app, db, admin = setup() app, db, admin = setup()
Model1, Model2 = create_models(db) Model1, Model2 = create_models(db)
# fill DB with values 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()
# Test string filter # Test string filter
view = CustomModelView(Model1, column_filters=['test1']) view = CustomModelView(Model1, column_filters=['test1'])
......
...@@ -81,6 +81,39 @@ def create_models(db): ...@@ -81,6 +81,39 @@ def create_models(db):
return Model1, Model2 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(): def test_model():
app, db, admin = setup() app, db, admin = setup()
Model1, Model2 = create_models(db) Model1, Model2 = create_models(db)
...@@ -286,6 +319,75 @@ def test_complex_searchable_list_missing_children(): ...@@ -286,6 +319,75 @@ def test_complex_searchable_list_missing_children():
ok_('magic string' in data) 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/ajax/update/', 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 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)
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(): def test_column_filters():
app, db, admin = setup() app, db, admin = setup()
...@@ -393,37 +495,7 @@ def test_column_filters(): ...@@ -393,37 +495,7 @@ def test_column_filters():
eq_(list(view._filter_groups.keys()), [u'Test Filter #1', u'Test Filter #2']) eq_(list(view._filter_groups.keys()), [u'Test Filter #1', u'Test Filter #2'])
# Fill DB 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()
client = app.test_client() 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