2439 lines
78 KiB
Python
2439 lines
78 KiB
Python
import warnings
|
|
import re
|
|
import csv
|
|
import mimetypes
|
|
import time
|
|
from math import ceil
|
|
import inspect
|
|
|
|
from werkzeug.utils import secure_filename
|
|
|
|
from flask import (current_app, request, redirect, flash, abort, json,
|
|
Response, get_flashed_messages, stream_with_context)
|
|
try:
|
|
import tablib
|
|
except ImportError:
|
|
tablib = None
|
|
from wtforms.fields import HiddenField
|
|
from wtforms.fields.core import UnboundField
|
|
from wtforms.validators import ValidationError, InputRequired
|
|
|
|
from flask_admin.babel import gettext, ngettext
|
|
|
|
from flask_admin.base import BaseView, expose
|
|
from flask_admin.form import BaseForm, FormOpts, rules
|
|
from flask_admin.model import filters, typefmt, template
|
|
from flask_admin.actions import ActionsMixin
|
|
from flask_admin.helpers import (get_form_data, validate_form_on_submit,
|
|
get_redirect_target, flash_errors)
|
|
from flask_admin.tools import rec_getattr
|
|
from flask_admin._backwards import ObsoleteAttr
|
|
from flask_admin._compat import (iteritems, itervalues, OrderedDict,
|
|
as_unicode, csv_encode, text_type, pass_context)
|
|
from .helpers import prettify_name, get_mdict_item_or_list
|
|
from .ajax import AjaxModelLoader
|
|
|
|
# Used to generate filter query string name
|
|
filter_char_re = re.compile('[^a-z0-9 ]')
|
|
filter_compact_re = re.compile(' +')
|
|
|
|
|
|
class ViewArgs(object):
|
|
"""
|
|
List view arguments.
|
|
"""
|
|
def __init__(self, page=None, page_size=None, sort=None, sort_desc=None,
|
|
search=None, filters=None, extra_args=None):
|
|
self.page = page
|
|
self.page_size = page_size
|
|
self.sort = sort
|
|
self.sort_desc = bool(sort_desc)
|
|
self.search = search
|
|
self.filters = filters
|
|
|
|
if not self.search:
|
|
self.search = None
|
|
|
|
self.extra_args = extra_args or dict()
|
|
|
|
def clone(self, **kwargs):
|
|
if self.filters:
|
|
flt = list(self.filters)
|
|
else:
|
|
flt = None
|
|
|
|
kwargs.setdefault('page', self.page)
|
|
kwargs.setdefault('page_size', self.page_size)
|
|
kwargs.setdefault('sort', self.sort)
|
|
kwargs.setdefault('sort_desc', self.sort_desc)
|
|
kwargs.setdefault('search', self.search)
|
|
kwargs.setdefault('filters', flt)
|
|
kwargs.setdefault('extra_args', dict(self.extra_args))
|
|
|
|
return ViewArgs(**kwargs)
|
|
|
|
|
|
class FilterGroup(object):
|
|
def __init__(self, label):
|
|
self.label = label
|
|
self.filters = []
|
|
|
|
def append(self, filter):
|
|
self.filters.append(filter)
|
|
|
|
def non_lazy(self):
|
|
filters = []
|
|
for item in self.filters:
|
|
copy = dict(item)
|
|
copy['operation'] = as_unicode(copy['operation'])
|
|
options = copy['options']
|
|
if options:
|
|
copy['options'] = [(k, text_type(v)) for k, v in options]
|
|
|
|
filters.append(copy)
|
|
return as_unicode(self.label), filters
|
|
|
|
def __iter__(self):
|
|
return iter(self.filters)
|
|
|
|
|
|
class BaseModelView(BaseView, ActionsMixin):
|
|
"""
|
|
Base model view.
|
|
|
|
This view does not make any assumptions on how models are stored or managed, but expects the following:
|
|
|
|
1. The provided model is an object
|
|
2. The model contains properties
|
|
3. Each model contains an attribute which uniquely identifies it (i.e. a primary key for a database model)
|
|
4. It is possible to retrieve a list of sorted models with pagination applied from a data source
|
|
5. You can get one model by its identifier from the data source
|
|
|
|
Essentially, if you want to support a new data store, all you have to do is:
|
|
|
|
1. Derive from the `BaseModelView` class
|
|
2. Implement various data-related methods (`get_list`, `get_one`, `create_model`, etc)
|
|
3. Implement automatic form generation from the model representation (`scaffold_form`)
|
|
"""
|
|
# Permissions
|
|
can_create = True
|
|
"""Is model creation allowed"""
|
|
|
|
can_edit = True
|
|
"""Is model editing allowed"""
|
|
|
|
can_delete = True
|
|
"""Is model deletion allowed"""
|
|
|
|
can_view_details = False
|
|
"""
|
|
Setting this to true will enable the details view. This is recommended
|
|
when there are too many columns to display in the list_view.
|
|
"""
|
|
|
|
can_export = False
|
|
"""Is model list export allowed"""
|
|
|
|
# Templates
|
|
list_template = 'admin/model/list.html'
|
|
"""Default list view template"""
|
|
|
|
edit_template = 'admin/model/edit.html'
|
|
"""Default edit template"""
|
|
|
|
create_template = 'admin/model/create.html'
|
|
"""Default create template"""
|
|
|
|
details_template = 'admin/model/details.html'
|
|
"""Default details view template"""
|
|
|
|
# Modal Templates
|
|
edit_modal_template = 'admin/model/modals/edit.html'
|
|
"""Default edit modal template"""
|
|
|
|
create_modal_template = 'admin/model/modals/create.html'
|
|
"""Default create modal template"""
|
|
|
|
details_modal_template = 'admin/model/modals/details.html'
|
|
"""Default details modal view template"""
|
|
|
|
# Modals
|
|
edit_modal = False
|
|
"""Setting this to true will display the edit_view as a modal dialog."""
|
|
|
|
create_modal = False
|
|
"""Setting this to true will display the create_view as a modal dialog."""
|
|
|
|
details_modal = False
|
|
"""Setting this to true will display the details_view as a modal dialog."""
|
|
|
|
# Customizations
|
|
column_list = ObsoleteAttr('column_list', 'list_columns', None)
|
|
"""
|
|
Collection of the model field names for the list view.
|
|
If set to `None`, will get them from the model.
|
|
|
|
For example::
|
|
|
|
class MyModelView(BaseModelView):
|
|
column_list = ('name', 'last_name', 'email')
|
|
|
|
(Added in 1.4.0) SQLAlchemy model attributes can be used instead of strings::
|
|
|
|
class MyModelView(BaseModelView):
|
|
column_list = ('name', User.last_name)
|
|
|
|
When using SQLAlchemy models, you can reference related columns like this::
|
|
class MyModelView(BaseModelView):
|
|
column_list = ('<relationship>.<related column name>',)
|
|
"""
|
|
|
|
column_exclude_list = ObsoleteAttr('column_exclude_list',
|
|
'excluded_list_columns', None)
|
|
"""
|
|
Collection of excluded list column names.
|
|
|
|
For example::
|
|
|
|
class MyModelView(BaseModelView):
|
|
column_exclude_list = ('last_name', 'email')
|
|
"""
|
|
|
|
column_details_list = None
|
|
"""
|
|
Collection of the field names included in the details view.
|
|
If set to `None`, will get them from the model.
|
|
"""
|
|
|
|
column_details_exclude_list = None
|
|
"""
|
|
Collection of fields excluded from the details view.
|
|
"""
|
|
|
|
column_export_list = None
|
|
"""
|
|
Collection of the field names included in the export.
|
|
If set to `None`, will get them from the model.
|
|
"""
|
|
|
|
column_export_exclude_list = None
|
|
"""
|
|
Collection of fields excluded from the export.
|
|
"""
|
|
|
|
column_formatters = ObsoleteAttr('column_formatters', 'list_formatters', dict())
|
|
"""
|
|
Dictionary of list view column formatters.
|
|
|
|
For example, if you want to show price multiplied by
|
|
two, you can do something like this::
|
|
|
|
class MyModelView(BaseModelView):
|
|
column_formatters = dict(price=lambda v, c, m, p: m.price*2)
|
|
|
|
or using Jinja2 `macro` in template::
|
|
|
|
from flask_admin.model.template import macro
|
|
|
|
class MyModelView(BaseModelView):
|
|
column_formatters = dict(price=macro('render_price'))
|
|
|
|
# in template
|
|
{% macro render_price(model, column) %}
|
|
{{ model.price * 2 }}
|
|
{% endmacro %}
|
|
|
|
The Callback function has the prototype::
|
|
|
|
def formatter(view, context, model, name):
|
|
# `view` is current administrative view
|
|
# `context` is instance of jinja2.runtime.Context
|
|
# `model` is model instance
|
|
# `name` is property name
|
|
pass
|
|
"""
|
|
|
|
column_formatters_export = None
|
|
"""
|
|
Dictionary of list view column formatters to be used for export.
|
|
|
|
Defaults to column_formatters when set to None.
|
|
|
|
Functions the same way as column_formatters except
|
|
that macros are not supported.
|
|
"""
|
|
|
|
column_formatters_detail = None
|
|
"""
|
|
Dictionary of list view column formatters to be used for the detail view.
|
|
|
|
Defaults to column_formatters when set to None.
|
|
|
|
Functions the same way as column_formatters except
|
|
that macros are not supported.
|
|
"""
|
|
|
|
column_type_formatters = ObsoleteAttr('column_type_formatters', 'list_type_formatters', None)
|
|
"""
|
|
Dictionary of value type formatters to be used in the list view.
|
|
|
|
By default, three types are formatted:
|
|
|
|
1. ``None`` will be displayed as an empty string
|
|
2. ``bool`` will be displayed as a checkmark if it is ``True``
|
|
3. ``list`` will be joined using ', '
|
|
|
|
If you don't like the default behavior and don't want any type formatters
|
|
applied, just override this property with an empty dictionary::
|
|
|
|
class MyModelView(BaseModelView):
|
|
column_type_formatters = dict()
|
|
|
|
If you want to display `NULL` instead of an empty string, you can do
|
|
something like this. Also comes with bonus `date` formatter::
|
|
|
|
from datetime import date
|
|
from flask_admin.model import typefmt
|
|
|
|
def date_format(view, value):
|
|
return value.strftime('%d.%m.%Y')
|
|
|
|
MY_DEFAULT_FORMATTERS = dict(typefmt.BASE_FORMATTERS)
|
|
MY_DEFAULT_FORMATTERS.update({
|
|
type(None): typefmt.null_formatter,
|
|
date: date_format
|
|
})
|
|
|
|
class MyModelView(BaseModelView):
|
|
column_type_formatters = MY_DEFAULT_FORMATTERS
|
|
|
|
Type formatters have lower priority than list column formatters.
|
|
|
|
The callback function has following prototype::
|
|
|
|
def type_formatter(view, value):
|
|
# `view` is current administrative view
|
|
# `value` value to format
|
|
pass
|
|
"""
|
|
|
|
column_type_formatters_export = None
|
|
"""
|
|
Dictionary of value type formatters to be used in the export.
|
|
|
|
By default, two types are formatted:
|
|
|
|
1. ``None`` will be displayed as an empty string
|
|
2. ``list`` will be joined using ', '
|
|
|
|
Functions the same way as column_type_formatters.
|
|
"""
|
|
|
|
column_type_formatters_detail = None
|
|
"""
|
|
Dictionary of value type formatters to be used in the detail view.
|
|
|
|
By default, two types are formatted:
|
|
|
|
1. ``None`` will be displayed as an empty string
|
|
2. ``list`` will be joined using ', '
|
|
|
|
Functions the same way as column_type_formatters.
|
|
"""
|
|
|
|
column_labels = ObsoleteAttr('column_labels', 'rename_columns', None)
|
|
"""
|
|
Dictionary where key is column name and value is string to display.
|
|
|
|
For example::
|
|
|
|
class MyModelView(BaseModelView):
|
|
column_labels = dict(name='Name', last_name='Last Name')
|
|
"""
|
|
|
|
column_descriptions = None
|
|
"""
|
|
Dictionary where key is column name and
|
|
value is description for `list view` column or add/edit form field.
|
|
|
|
For example::
|
|
|
|
class MyModelView(BaseModelView):
|
|
column_descriptions = dict(
|
|
full_name='First and Last name'
|
|
)
|
|
"""
|
|
|
|
column_sortable_list = ObsoleteAttr('column_sortable_list',
|
|
'sortable_columns',
|
|
None)
|
|
"""
|
|
Collection of the sortable columns for the list view.
|
|
If set to `None`, will get them from the model.
|
|
|
|
For example::
|
|
|
|
class MyModelView(BaseModelView):
|
|
column_sortable_list = ('name', 'last_name')
|
|
|
|
If you want to explicitly specify field/column to be used while
|
|
sorting, you can use a tuple::
|
|
|
|
class MyModelView(BaseModelView):
|
|
column_sortable_list = ('name', ('user', 'user.username'))
|
|
|
|
You can also specify multiple fields to be used while sorting::
|
|
|
|
class MyModelView(BaseModelView):
|
|
column_sortable_list = (
|
|
'name', ('user', ('user.first_name', 'user.last_name')))
|
|
|
|
When using SQLAlchemy models, model attributes can be used instead
|
|
of strings::
|
|
|
|
class MyModelView(BaseModelView):
|
|
column_sortable_list = ('name', ('user', User.username))
|
|
"""
|
|
|
|
column_default_sort = None
|
|
"""
|
|
Default sort column if no sorting is applied.
|
|
|
|
Example::
|
|
|
|
class MyModelView(BaseModelView):
|
|
column_default_sort = 'user'
|
|
|
|
You can use tuple to control ascending descending order. In following example, items
|
|
will be sorted in descending order::
|
|
|
|
class MyModelView(BaseModelView):
|
|
column_default_sort = ('user', True)
|
|
|
|
If you want to sort by more than one column,
|
|
you can pass a list of tuples::
|
|
|
|
class MyModelView(BaseModelView):
|
|
column_default_sort = [('name', True), ('last_name', True)]
|
|
"""
|
|
|
|
column_searchable_list = ObsoleteAttr('column_searchable_list',
|
|
'searchable_columns',
|
|
None)
|
|
"""
|
|
A collection of the searchable columns. It is assumed that only
|
|
text-only fields are searchable, but it is up to the model
|
|
implementation to decide.
|
|
|
|
Example::
|
|
|
|
class MyModelView(BaseModelView):
|
|
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
|
|
|
|
Example::
|
|
|
|
class MyModelView(BaseModelView):
|
|
column_choices = {
|
|
'my_column': [
|
|
('db_value', 'display_value'),
|
|
]
|
|
}
|
|
"""
|
|
|
|
column_filters = None
|
|
"""
|
|
Collection of the column filters.
|
|
|
|
Can contain either field names or instances of :class:`~flask_admin.model.filters.BaseFilter` classes.
|
|
|
|
Example::
|
|
|
|
class MyModelView(BaseModelView):
|
|
column_filters = ('user', 'email')
|
|
"""
|
|
|
|
named_filter_urls = False
|
|
"""
|
|
Set to True to use human-readable names for filters in URL parameters.
|
|
|
|
False by default so as to be robust across translations.
|
|
|
|
Changing this parameter will break any existing URLs that have filters.
|
|
"""
|
|
|
|
column_display_pk = ObsoleteAttr('column_display_pk',
|
|
'list_display_pk',
|
|
False)
|
|
"""
|
|
Controls if the primary key should be displayed in the list view.
|
|
"""
|
|
|
|
column_display_actions = True
|
|
"""
|
|
Controls the display of the row actions (edit, delete, details, etc.)
|
|
column in the list view.
|
|
|
|
Useful for preventing a blank column from displaying if your view does
|
|
not use any build-in or custom row actions.
|
|
|
|
This column is not hidden automatically due to backwards compatibility.
|
|
|
|
Note: This only affects display and does not control whether the row
|
|
actions endpoints are accessible.
|
|
"""
|
|
|
|
column_extra_row_actions = None
|
|
"""
|
|
List of row actions (instances of :class:`~flask_admin.model.template.BaseListRowAction`).
|
|
|
|
Flask-Admin will generate standard per-row actions (edit, delete, etc)
|
|
and will append custom actions from this list right after them.
|
|
|
|
For example::
|
|
|
|
from flask_admin.model.template import EndpointLinkRowAction, LinkRowAction
|
|
|
|
class MyModelView(BaseModelView):
|
|
column_extra_row_actions = [
|
|
LinkRowAction('glyphicon glyphicon-off', 'http://direct.link/?id={row_id}'),
|
|
EndpointLinkRowAction('glyphicon glyphicon-test', 'my_view.index_view')
|
|
]
|
|
"""
|
|
|
|
simple_list_pager = False
|
|
"""
|
|
Enable or disable simple list pager.
|
|
If enabled, model interface would not run count query and will only show prev/next pager buttons.
|
|
"""
|
|
|
|
form = None
|
|
"""
|
|
Form class. Override if you want to use custom form for your model.
|
|
Will completely disable form scaffolding functionality.
|
|
|
|
For example::
|
|
|
|
class MyForm(Form):
|
|
name = StringField('Name')
|
|
|
|
class MyModelView(BaseModelView):
|
|
form = MyForm
|
|
"""
|
|
|
|
form_base_class = BaseForm
|
|
"""
|
|
Base form class. Will be used by form scaffolding function when creating model form.
|
|
|
|
Useful if you want to have custom constructor or override some fields.
|
|
|
|
Example::
|
|
|
|
class MyBaseForm(Form):
|
|
def do_something(self):
|
|
pass
|
|
|
|
class MyModelView(BaseModelView):
|
|
form_base_class = MyBaseForm
|
|
|
|
"""
|
|
|
|
form_args = None
|
|
"""
|
|
Dictionary of form field arguments. Refer to WTForms documentation for
|
|
list of possible options.
|
|
|
|
Example::
|
|
|
|
from wtforms.validators import DataRequired
|
|
class MyModelView(BaseModelView):
|
|
form_args = dict(
|
|
name=dict(label='First Name', validators=[DataRequired()])
|
|
)
|
|
"""
|
|
|
|
form_columns = None
|
|
"""
|
|
Collection of the model field names for the form. If set to `None` will
|
|
get them from the model.
|
|
|
|
Example::
|
|
|
|
class MyModelView(BaseModelView):
|
|
form_columns = ('name', 'email')
|
|
|
|
(Added in 1.4.0) SQLAlchemy model attributes can be used instead of
|
|
strings::
|
|
|
|
class MyModelView(BaseModelView):
|
|
form_columns = ('name', User.last_name)
|
|
|
|
SQLA Note: Model attributes must be on the same model as your ModelView
|
|
or you will need to use `inline_models`.
|
|
"""
|
|
|
|
form_excluded_columns = ObsoleteAttr('form_excluded_columns',
|
|
'excluded_form_columns',
|
|
None)
|
|
"""
|
|
Collection of excluded form field names.
|
|
|
|
For example::
|
|
|
|
class MyModelView(BaseModelView):
|
|
form_excluded_columns = ('last_name', 'email')
|
|
"""
|
|
|
|
form_overrides = None
|
|
"""
|
|
Dictionary of form column overrides.
|
|
|
|
Example::
|
|
|
|
class MyModelView(BaseModelView):
|
|
form_overrides = dict(name=wtf.FileField)
|
|
"""
|
|
|
|
form_widget_args = None
|
|
"""
|
|
Dictionary of form widget rendering arguments.
|
|
Use this to customize how widget is rendered without using custom template.
|
|
|
|
Example::
|
|
|
|
class MyModelView(BaseModelView):
|
|
form_widget_args = {
|
|
'description': {
|
|
'rows': 10,
|
|
'style': 'color: black'
|
|
},
|
|
'other_field': {
|
|
'disabled': True
|
|
}
|
|
}
|
|
|
|
Changing the format of a DateTimeField will require changes to both form_widget_args and form_args.
|
|
|
|
Example::
|
|
|
|
form_args = dict(
|
|
start=dict(format='%Y-%m-%d %I:%M %p') # changes how the input is parsed by strptime (12 hour time)
|
|
)
|
|
form_widget_args = dict(
|
|
start={
|
|
'data-date-format': u'yyyy-mm-dd HH:ii P',
|
|
'data-show-meridian': 'True'
|
|
} # changes how the DateTimeField displays the time
|
|
)
|
|
"""
|
|
|
|
form_extra_fields = None
|
|
"""
|
|
Dictionary of additional fields.
|
|
|
|
Example::
|
|
|
|
class MyModelView(BaseModelView):
|
|
form_extra_fields = {
|
|
'password': PasswordField('Password')
|
|
}
|
|
|
|
You can control order of form fields using ``form_columns`` property. For example::
|
|
|
|
class MyModelView(BaseModelView):
|
|
form_columns = ('name', 'email', 'password', 'secret')
|
|
|
|
form_extra_fields = {
|
|
'password': PasswordField('Password')
|
|
}
|
|
|
|
In this case, password field will be put between email and secret fields that are autogenerated.
|
|
"""
|
|
|
|
form_ajax_refs = None
|
|
"""
|
|
Use AJAX for foreign key model loading.
|
|
|
|
Should contain dictionary, where key is field name and value is either a dictionary which
|
|
configures AJAX lookups or backend-specific `AjaxModelLoader` class instance.
|
|
|
|
For example, it can look like::
|
|
|
|
class MyModelView(BaseModelView):
|
|
form_ajax_refs = {
|
|
'user': {
|
|
'fields': ('first_name', 'last_name', 'email'),
|
|
'placeholder': 'Please select',
|
|
'page_size': 10,
|
|
'minimum_input_length': 0,
|
|
}
|
|
}
|
|
|
|
Or with SQLAlchemy backend like this::
|
|
|
|
class MyModelView(BaseModelView):
|
|
form_ajax_refs = {
|
|
'user': QueryAjaxModelLoader('user', db.session, User, fields=['email'], page_size=10)
|
|
}
|
|
|
|
If you need custom loading functionality, you can implement your custom loading behavior
|
|
in your `AjaxModelLoader` class.
|
|
"""
|
|
|
|
form_rules = None
|
|
"""
|
|
List of rendering rules for model creation form.
|
|
|
|
This property changed default form rendering behavior and makes possible to rearrange order
|
|
of rendered fields, add some text between fields, group them, etc. If not set, will use
|
|
default Flask-Admin form rendering logic.
|
|
|
|
Here's simple example which illustrates how to use::
|
|
|
|
from flask_admin.form import rules
|
|
|
|
class MyModelView(ModelView):
|
|
form_rules = [
|
|
# Define field set with header text and four fields
|
|
rules.FieldSet(('first_name', 'last_name', 'email', 'phone'), 'User'),
|
|
# ... and it is just shortcut for:
|
|
rules.Header('User'),
|
|
rules.Field('first_name'),
|
|
rules.Field('last_name'),
|
|
# ...
|
|
# It is possible to create custom rule blocks:
|
|
MyBlock('Hello World'),
|
|
# It is possible to call macros from current context
|
|
rules.Macro('my_macro', foobar='baz')
|
|
]
|
|
"""
|
|
|
|
form_edit_rules = None
|
|
"""
|
|
Customized rules for the edit form. Override `form_rules` if present.
|
|
"""
|
|
|
|
form_create_rules = None
|
|
"""
|
|
Customized rules for the create form. Override `form_rules` if present.
|
|
"""
|
|
|
|
# Actions
|
|
action_disallowed_list = ObsoleteAttr('action_disallowed_list',
|
|
'disallowed_actions',
|
|
[])
|
|
"""
|
|
Set of disallowed action names. For example, if you want to disable
|
|
mass model deletion, do something like this:
|
|
|
|
class MyModelView(BaseModelView):
|
|
action_disallowed_list = ['delete']
|
|
"""
|
|
|
|
# Export settings
|
|
export_max_rows = 0
|
|
"""
|
|
Maximum number of rows allowed for export.
|
|
|
|
Unlimited by default. Uses `page_size` if set to `None`.
|
|
"""
|
|
|
|
export_types = ['csv']
|
|
"""
|
|
A list of available export filetypes. `csv` only is default, but any
|
|
filetypes supported by tablib can be used.
|
|
|
|
Check tablib for https://github.com/kennethreitz/tablib/blob/master/README.rst
|
|
for supported types.
|
|
"""
|
|
|
|
# Pagination settings
|
|
page_size = 20
|
|
"""
|
|
Default page size for pagination.
|
|
"""
|
|
|
|
can_set_page_size = False
|
|
"""
|
|
Allows to select page size via dropdown list
|
|
"""
|
|
|
|
def __init__(self, model,
|
|
name=None, category=None, endpoint=None, url=None, static_folder=None,
|
|
menu_class_name=None, menu_icon_type=None, menu_icon_value=None):
|
|
"""
|
|
Constructor.
|
|
|
|
:param model:
|
|
Model class
|
|
:param name:
|
|
View name. If not provided, will use the model class name
|
|
:param category:
|
|
Optional category name, for grouping views in the menu
|
|
:param endpoint:
|
|
Base endpoint. If not provided, will use the model name.
|
|
:param url:
|
|
Base URL. If not provided, will use endpoint as a URL.
|
|
:param menu_class_name:
|
|
Optional class name for the menu item.
|
|
:param menu_icon_type:
|
|
Optional icon. Possible icon types:
|
|
|
|
- `flask_admin.consts.ICON_TYPE_GLYPH` - Bootstrap glyph icon
|
|
- `flask_admin.consts.ICON_TYPE_FONT_AWESOME` - Font Awesome icon
|
|
- `flask_admin.consts.ICON_TYPE_IMAGE` - Image relative to Flask static directory
|
|
- `flask_admin.consts.ICON_TYPE_IMAGE_URL` - Image with full URL
|
|
:param menu_icon_value:
|
|
Icon glyph name or URL, depending on `menu_icon_type` setting
|
|
"""
|
|
self.model = model
|
|
|
|
# If name not provided, it is model name
|
|
if name is None:
|
|
name = '%s' % self._prettify_class_name(model.__name__)
|
|
|
|
super(BaseModelView, self).__init__(name, category, endpoint, url, static_folder,
|
|
menu_class_name=menu_class_name,
|
|
menu_icon_type=menu_icon_type,
|
|
menu_icon_value=menu_icon_value)
|
|
|
|
# Actions
|
|
self.init_actions()
|
|
|
|
# Scaffolding
|
|
self._refresh_cache()
|
|
|
|
# Endpoint
|
|
def _get_endpoint(self, endpoint):
|
|
if endpoint:
|
|
return super(BaseModelView, self)._get_endpoint(endpoint)
|
|
|
|
return self.model.__name__.lower()
|
|
|
|
# Caching
|
|
def _refresh_forms_cache(self):
|
|
# Forms
|
|
self._form_ajax_refs = self._process_ajax_references()
|
|
|
|
if self.form_widget_args is None:
|
|
self.form_widget_args = {}
|
|
|
|
self._create_form_class = self.get_create_form()
|
|
self._edit_form_class = self.get_edit_form()
|
|
self._delete_form_class = self.get_delete_form()
|
|
self._action_form_class = self.get_action_form()
|
|
|
|
# List View In-Line Editing
|
|
if self.column_editable_list:
|
|
self._list_form_class = self.get_list_form()
|
|
else:
|
|
self.column_editable_list = {}
|
|
|
|
def _refresh_filters_cache(self):
|
|
self._filters = self.get_filters()
|
|
|
|
if self._filters:
|
|
self._filter_groups = OrderedDict()
|
|
self._filter_args = {}
|
|
|
|
for i, flt in enumerate(self._filters):
|
|
key = as_unicode(flt.name)
|
|
if key not in self._filter_groups:
|
|
self._filter_groups[key] = FilterGroup(flt.name)
|
|
self._filter_groups[key].append({
|
|
'index': i,
|
|
'arg': self.get_filter_arg(i, flt),
|
|
'operation': flt.operation(),
|
|
'options': flt.get_options(self) or None,
|
|
'type': flt.data_type
|
|
})
|
|
|
|
self._filter_args[self.get_filter_arg(i, flt)] = (i, flt)
|
|
else:
|
|
self._filter_groups = None
|
|
self._filter_args = None
|
|
|
|
def _refresh_form_rules_cache(self):
|
|
if self.form_create_rules:
|
|
self._form_create_rules = rules.RuleSet(self, self.form_create_rules)
|
|
else:
|
|
self._form_create_rules = None
|
|
|
|
if self.form_edit_rules:
|
|
self._form_edit_rules = rules.RuleSet(self, self.form_edit_rules)
|
|
else:
|
|
self._form_edit_rules = None
|
|
|
|
if self.form_rules:
|
|
form_rules = rules.RuleSet(self, self.form_rules)
|
|
|
|
if not self._form_create_rules:
|
|
self._form_create_rules = form_rules
|
|
|
|
if not self._form_edit_rules:
|
|
self._form_edit_rules = form_rules
|
|
|
|
def _refresh_cache(self):
|
|
"""
|
|
Refresh various cached variables.
|
|
"""
|
|
# List view
|
|
self._list_columns = self.get_list_columns()
|
|
self._sortable_columns = self.get_sortable_columns()
|
|
|
|
# Details view
|
|
if self.can_view_details:
|
|
self._details_columns = self.get_details_columns()
|
|
|
|
# Export view
|
|
self._export_columns = self.get_export_columns()
|
|
|
|
# Labels
|
|
if self.column_labels is None:
|
|
self.column_labels = {}
|
|
|
|
# Forms
|
|
self._refresh_forms_cache()
|
|
|
|
# Search
|
|
self._search_supported = self.init_search()
|
|
|
|
# Choices
|
|
if self.column_choices:
|
|
self._column_choices_map = dict([
|
|
(column, dict(choices))
|
|
for column, choices in self.column_choices.items()
|
|
])
|
|
else:
|
|
self.column_choices = self._column_choices_map = dict()
|
|
|
|
# Column formatters
|
|
if self.column_formatters_export is None:
|
|
self.column_formatters_export = self.column_formatters
|
|
|
|
if self.column_formatters_detail is None:
|
|
self.column_formatters_detail = self.column_formatters
|
|
|
|
# Type formatters
|
|
if self.column_type_formatters is None:
|
|
self.column_type_formatters = dict(typefmt.BASE_FORMATTERS)
|
|
|
|
if self.column_type_formatters_export is None:
|
|
self.column_type_formatters_export = dict(typefmt.EXPORT_FORMATTERS)
|
|
|
|
if self.column_type_formatters_detail is None:
|
|
self.column_type_formatters_detail = dict(typefmt.DETAIL_FORMATTERS)
|
|
|
|
if self.column_descriptions is None:
|
|
self.column_descriptions = dict()
|
|
|
|
# Filters
|
|
self._refresh_filters_cache()
|
|
|
|
# Form rendering rules
|
|
self._refresh_form_rules_cache()
|
|
|
|
# Process form rules
|
|
self._validate_form_class(self._form_edit_rules, self._edit_form_class)
|
|
self._validate_form_class(self._form_create_rules, self._create_form_class)
|
|
|
|
# Primary key
|
|
def get_pk_value(self, model):
|
|
"""
|
|
Return PK value from a model object.
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
# List view
|
|
def scaffold_list_columns(self):
|
|
"""
|
|
Return list of the model field names. Must be implemented in
|
|
the child class.
|
|
|
|
Expected return format is list of tuples with field name and
|
|
display text. For example::
|
|
|
|
['name', 'first_name', 'last_name']
|
|
"""
|
|
raise NotImplementedError('Please implement scaffold_list_columns method')
|
|
|
|
def get_column_name(self, field):
|
|
"""
|
|
Return a human-readable column name.
|
|
|
|
:param field:
|
|
Model field name.
|
|
"""
|
|
if self.column_labels and field in self.column_labels:
|
|
return self.column_labels[field]
|
|
else:
|
|
return self._prettify_name(field)
|
|
|
|
def get_list_row_actions(self):
|
|
"""
|
|
Return list of row action objects, each is instance of
|
|
:class:`~flask_admin.model.template.BaseListRowAction`
|
|
"""
|
|
actions = []
|
|
|
|
if self.can_view_details:
|
|
if self.details_modal:
|
|
actions.append(template.ViewPopupRowAction())
|
|
else:
|
|
actions.append(template.ViewRowAction())
|
|
|
|
if self.can_edit:
|
|
if self.edit_modal:
|
|
actions.append(template.EditPopupRowAction())
|
|
else:
|
|
actions.append(template.EditRowAction())
|
|
|
|
if self.can_delete:
|
|
actions.append(template.DeleteRowAction())
|
|
|
|
return actions + (self.column_extra_row_actions or [])
|
|
|
|
def get_column_names(self, only_columns, excluded_columns):
|
|
"""
|
|
Returns a list of tuples with the model field name and formatted
|
|
field name.
|
|
|
|
:param only_columns:
|
|
List of columns to include in the results. If not set,
|
|
`scaffold_list_columns` will generate the list from the model.
|
|
:param excluded_columns:
|
|
List of columns to exclude from the results if `only_columns`
|
|
is not set.
|
|
"""
|
|
if excluded_columns:
|
|
only_columns = [c for c in only_columns if c not in excluded_columns]
|
|
|
|
return [(c, self.get_column_name(c)) for c in only_columns]
|
|
|
|
def get_list_columns(self):
|
|
"""
|
|
Uses `get_column_names` to get a list of tuples with the model
|
|
field name and formatted name for the columns in `column_list`
|
|
and not in `column_exclude_list`. If `column_list` is not set,
|
|
the columns from `scaffold_list_columns` will be used.
|
|
"""
|
|
return self.get_column_names(
|
|
only_columns=self.column_list or self.scaffold_list_columns(),
|
|
excluded_columns=self.column_exclude_list,
|
|
)
|
|
|
|
def get_details_columns(self):
|
|
"""
|
|
Uses `get_column_names` to get a list of tuples with the model
|
|
field name and formatted name for the columns in `column_details_list`
|
|
and not in `column_details_exclude_list`. If `column_details_list`
|
|
is not set, the columns from `scaffold_list_columns` will be used.
|
|
"""
|
|
try:
|
|
only_columns = self.column_details_list or self.scaffold_list_columns()
|
|
except NotImplementedError:
|
|
raise Exception('Please define column_details_list')
|
|
|
|
return self.get_column_names(
|
|
only_columns=only_columns,
|
|
excluded_columns=self.column_details_exclude_list,
|
|
)
|
|
|
|
def get_export_columns(self):
|
|
"""
|
|
Uses `get_column_names` to get a list of tuples with the model
|
|
field name and formatted name for the columns in `column_export_list`
|
|
and not in `column_export_exclude_list`. If `column_export_list` is
|
|
not set, it will attempt to use the columns from `column_list`
|
|
or finally the columns from `scaffold_list_columns` will be used.
|
|
"""
|
|
only_columns = (self.column_export_list or self.column_list or
|
|
self.scaffold_list_columns())
|
|
|
|
return self.get_column_names(
|
|
only_columns=only_columns,
|
|
excluded_columns=self.column_export_exclude_list,
|
|
)
|
|
|
|
def scaffold_sortable_columns(self):
|
|
"""
|
|
Returns dictionary of sortable columns. Must be implemented in
|
|
the child class.
|
|
|
|
Expected return format is a dictionary, where keys are field names and
|
|
values are property names.
|
|
"""
|
|
raise NotImplementedError('Please implement scaffold_sortable_columns method')
|
|
|
|
def get_sortable_columns(self):
|
|
"""
|
|
Returns a dictionary of the sortable columns. Key is a model
|
|
field name and value is sort column (for example - attribute).
|
|
|
|
If `column_sortable_list` is set, will use it. Otherwise, will call
|
|
`scaffold_sortable_columns` to get them from the model.
|
|
"""
|
|
if self.column_sortable_list is None:
|
|
return self.scaffold_sortable_columns() or dict()
|
|
else:
|
|
result = dict()
|
|
|
|
for c in self.column_sortable_list:
|
|
if isinstance(c, tuple):
|
|
result[c[0]] = c[1]
|
|
else:
|
|
result[c] = c
|
|
|
|
return result
|
|
|
|
def init_search(self):
|
|
"""
|
|
Initialize search. If data provider does not support search,
|
|
`init_search` will return `False`.
|
|
"""
|
|
return False
|
|
|
|
def search_placeholder(self):
|
|
"""
|
|
Return search placeholder text.
|
|
"""
|
|
return None
|
|
|
|
# Filter helpers
|
|
def scaffold_filters(self, name):
|
|
"""
|
|
Generate filter object for the given name
|
|
|
|
:param name:
|
|
Name of the field
|
|
"""
|
|
return None
|
|
|
|
def is_valid_filter(self, filter):
|
|
"""
|
|
Verify that the provided filter object is valid.
|
|
|
|
Override in model backend implementation to verify if
|
|
the provided filter type is allowed.
|
|
|
|
:param filter:
|
|
Filter object to verify.
|
|
"""
|
|
return isinstance(filter, filters.BaseFilter)
|
|
|
|
def handle_filter(self, filter):
|
|
"""
|
|
Postprocess (add joins, etc) for a filter.
|
|
|
|
:param filter:
|
|
Filter object to postprocess
|
|
"""
|
|
return filter
|
|
|
|
def get_filters(self):
|
|
"""
|
|
Return a list of filter objects.
|
|
|
|
If your model backend implementation does not support filters,
|
|
override this method and return `None`.
|
|
"""
|
|
if self.column_filters:
|
|
collection = []
|
|
|
|
for n in self.column_filters:
|
|
if self.is_valid_filter(n):
|
|
collection.append(self.handle_filter(n))
|
|
else:
|
|
flt = self.scaffold_filters(n)
|
|
if flt:
|
|
collection.extend(flt)
|
|
else:
|
|
raise Exception('Unsupported filter type %s' % n)
|
|
return collection
|
|
else:
|
|
return None
|
|
|
|
def get_filter_arg(self, index, flt):
|
|
"""
|
|
Given a filter `flt`, return a unique name for that filter in
|
|
this view.
|
|
|
|
Does not include the `flt[n]_` portion of the filter name.
|
|
|
|
:param index:
|
|
Filter index in _filters array
|
|
:param flt:
|
|
Filter instance
|
|
"""
|
|
if self.named_filter_urls:
|
|
operation = flt.operation()
|
|
|
|
try:
|
|
# get lazy string original value
|
|
operation = operation._args[0]
|
|
except AttributeError:
|
|
pass
|
|
|
|
name = ('%s %s' % (flt.name, as_unicode(operation))).lower()
|
|
name = filter_char_re.sub('', name)
|
|
name = filter_compact_re.sub('_', name)
|
|
return name
|
|
else:
|
|
return str(index)
|
|
|
|
def _get_filter_groups(self):
|
|
"""
|
|
Returns non-lazy version of filter strings
|
|
"""
|
|
if self._filter_groups:
|
|
results = OrderedDict()
|
|
|
|
for group in itervalues(self._filter_groups):
|
|
key, items = group.non_lazy()
|
|
results[key] = items
|
|
|
|
return results
|
|
|
|
return None
|
|
|
|
# Form helpers
|
|
def scaffold_form(self):
|
|
"""
|
|
Create `form.BaseForm` inherited class from the model. Must be
|
|
implemented in the child class.
|
|
"""
|
|
raise NotImplementedError('Please implement scaffold_form method')
|
|
|
|
def scaffold_list_form(self, widget=None, validators=None):
|
|
"""
|
|
Create form for the `index_view` using only the columns from
|
|
`self.column_editable_list`.
|
|
|
|
:param widget:
|
|
WTForms widget class. Defaults to `XEditableWidget`.
|
|
:param validators:
|
|
`form_args` dict with only validators
|
|
{'name': {'validators': [DataRequired()]}}
|
|
|
|
Must be implemented in the child class.
|
|
"""
|
|
raise NotImplementedError('Please implement scaffold_list_form method')
|
|
|
|
def get_form(self):
|
|
"""
|
|
Get form class.
|
|
|
|
If ``self.form`` is set, will return it and will call
|
|
``self.scaffold_form`` otherwise.
|
|
|
|
Override to implement customized behavior.
|
|
"""
|
|
if self.form is not None:
|
|
return self.form
|
|
|
|
return self.scaffold_form()
|
|
|
|
def get_list_form(self):
|
|
"""
|
|
Get form class for the editable list view.
|
|
|
|
Uses only validators from `form_args` to build the form class.
|
|
|
|
Allows overriding the editable list view field/widget. For example::
|
|
|
|
from flask_admin.model.widgets import XEditableWidget
|
|
|
|
class CustomWidget(XEditableWidget):
|
|
def get_kwargs(self, subfield, kwargs):
|
|
if subfield.type == 'TextAreaField':
|
|
kwargs['data-type'] = 'textarea'
|
|
kwargs['data-rows'] = '20'
|
|
# elif: kwargs for other fields
|
|
|
|
return kwargs
|
|
|
|
class MyModelView(BaseModelView):
|
|
def get_list_form(self):
|
|
return self.scaffold_list_form(widget=CustomWidget)
|
|
"""
|
|
if self.form_args:
|
|
# get only validators, other form_args can break FieldList wrapper
|
|
validators = dict(
|
|
(key, {'validators': value["validators"]})
|
|
for key, value in iteritems(self.form_args)
|
|
if value.get("validators")
|
|
)
|
|
else:
|
|
validators = None
|
|
|
|
return self.scaffold_list_form(validators=validators)
|
|
|
|
def get_create_form(self):
|
|
"""
|
|
Create form class for model creation view.
|
|
|
|
Override to implement customized behavior.
|
|
"""
|
|
return self.get_form()
|
|
|
|
def get_edit_form(self):
|
|
"""
|
|
Create form class for model editing view.
|
|
|
|
Override to implement customized behavior.
|
|
"""
|
|
return self.get_form()
|
|
|
|
def get_delete_form(self):
|
|
"""
|
|
Create form class for model delete view.
|
|
|
|
Override to implement customized behavior.
|
|
"""
|
|
class DeleteForm(self.form_base_class):
|
|
id = HiddenField(validators=[InputRequired()])
|
|
url = HiddenField()
|
|
|
|
return DeleteForm
|
|
|
|
def get_action_form(self):
|
|
"""
|
|
Create form class for a model action.
|
|
|
|
Override to implement customized behavior.
|
|
"""
|
|
class ActionForm(self.form_base_class):
|
|
action = HiddenField()
|
|
url = HiddenField()
|
|
# rowid is retrieved using getlist, for backward compatibility
|
|
|
|
return ActionForm
|
|
|
|
def create_form(self, obj=None):
|
|
"""
|
|
Instantiate model creation form and return it.
|
|
|
|
Override to implement custom behavior.
|
|
"""
|
|
return self._create_form_class(get_form_data(), obj=obj)
|
|
|
|
def edit_form(self, obj=None):
|
|
"""
|
|
Instantiate model editing form and return it.
|
|
|
|
Override to implement custom behavior.
|
|
"""
|
|
return self._edit_form_class(get_form_data(), obj=obj)
|
|
|
|
def delete_form(self):
|
|
"""
|
|
Instantiate model delete form and return it.
|
|
|
|
Override to implement custom behavior.
|
|
|
|
The delete form originally used a GET request, so delete_form
|
|
accepts both GET and POST request for backwards compatibility.
|
|
"""
|
|
if request.form:
|
|
return self._delete_form_class(request.form)
|
|
elif request.args:
|
|
# allow request.args for backward compatibility
|
|
return self._delete_form_class(request.args)
|
|
else:
|
|
return self._delete_form_class()
|
|
|
|
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 action_form(self, obj=None):
|
|
"""
|
|
Instantiate model action form and return it.
|
|
|
|
Override to implement custom behavior.
|
|
"""
|
|
return self._action_form_class(get_form_data(), obj=obj)
|
|
|
|
def validate_form(self, form):
|
|
"""
|
|
Validate the form on submit.
|
|
|
|
:param form:
|
|
Form to validate
|
|
"""
|
|
return validate_form_on_submit(form)
|
|
|
|
def get_save_return_url(self, model, is_created=False):
|
|
"""
|
|
Return url where user is redirected after successful form save.
|
|
|
|
:param model:
|
|
Saved object
|
|
:param is_created:
|
|
Whether new object was created or existing one was updated
|
|
|
|
For example, redirect use to object details view after form save::
|
|
|
|
class MyModelView(ModelView):
|
|
can_view_details = True
|
|
|
|
def get_save_return_url(self, model, is_created):
|
|
return self.get_url('.details_view', id=model.id)
|
|
|
|
"""
|
|
return get_redirect_target() or self.get_url('.index_view')
|
|
|
|
def _get_ruleset_missing_fields(self, ruleset, form):
|
|
missing_fields = []
|
|
|
|
if ruleset:
|
|
visible_fields = ruleset.visible_fields
|
|
for field in form:
|
|
if field.name not in visible_fields:
|
|
missing_fields.append(field.name)
|
|
|
|
return missing_fields
|
|
|
|
def _show_missing_fields_warning(self, text):
|
|
warnings.warn(text)
|
|
|
|
def _validate_form_class(self, ruleset, form_class, remove_missing=True):
|
|
form_fields = []
|
|
for name, obj in iteritems(form_class.__dict__):
|
|
if isinstance(obj, UnboundField):
|
|
form_fields.append(name)
|
|
|
|
missing_fields = []
|
|
if ruleset:
|
|
visible_fields = ruleset.visible_fields
|
|
for field_name in form_fields:
|
|
if field_name not in visible_fields:
|
|
missing_fields.append(field_name)
|
|
|
|
if missing_fields:
|
|
self._show_missing_fields_warning('Fields missing from ruleset: %s' % (','.join(missing_fields)))
|
|
if remove_missing:
|
|
self._remove_fields_from_form_class(missing_fields, form_class)
|
|
|
|
def _validate_form_instance(self, ruleset, form, remove_missing=True):
|
|
missing_fields = self._get_ruleset_missing_fields(ruleset=ruleset, form=form)
|
|
if missing_fields:
|
|
self._show_missing_fields_warning('Fields missing from ruleset: %s' % (','.join(missing_fields)))
|
|
if remove_missing:
|
|
self._remove_fields_from_form_instance(missing_fields, form)
|
|
|
|
def _remove_fields_from_form_instance(self, field_names, form):
|
|
for field_name in field_names:
|
|
form.__delitem__(field_name)
|
|
|
|
def _remove_fields_from_form_class(self, field_names, form_class):
|
|
for field_name in field_names:
|
|
delattr(form_class, field_name)
|
|
|
|
# Helpers
|
|
def is_sortable(self, name):
|
|
"""
|
|
Verify if column is sortable.
|
|
|
|
Not case-sensitive.
|
|
|
|
:param name:
|
|
Column name.
|
|
"""
|
|
return name.lower() in (x.lower() for x 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 and self.can_edit
|
|
|
|
def _get_column_by_idx(self, idx):
|
|
"""
|
|
Return column index by
|
|
"""
|
|
if idx is None or idx < 0 or idx >= len(self._list_columns):
|
|
return None
|
|
|
|
return self._list_columns[idx]
|
|
|
|
def _get_default_order(self):
|
|
"""
|
|
Return default sort order
|
|
"""
|
|
if self.column_default_sort:
|
|
if isinstance(self.column_default_sort, list):
|
|
return self.column_default_sort
|
|
if isinstance(self.column_default_sort, tuple):
|
|
return [self.column_default_sort]
|
|
else:
|
|
return [(self.column_default_sort, False)]
|
|
|
|
return None
|
|
|
|
# Database-related API
|
|
def get_list(self, page, sort_field, sort_desc, search, filters,
|
|
page_size=None):
|
|
"""
|
|
Return a paginated and sorted list of models from the data source.
|
|
|
|
Must be implemented in the child class.
|
|
|
|
:param page:
|
|
Page number, 0 based. Can be set to None if it is first page.
|
|
:param sort_field:
|
|
Sort column name or None.
|
|
:param sort_desc:
|
|
If set to True, sorting is in descending order.
|
|
:param search:
|
|
Search query
|
|
:param filters:
|
|
List of filter tuples. First value in a tuple is a search
|
|
index, second value is a search value.
|
|
:param page_size:
|
|
Number of results. Defaults to ModelView's page_size. Can be
|
|
overriden to change the page_size limit. Removing the page_size
|
|
limit requires setting page_size to 0 or False.
|
|
"""
|
|
raise NotImplementedError('Please implement get_list method')
|
|
|
|
def get_one(self, id):
|
|
"""
|
|
Return one model by its id.
|
|
|
|
Must be implemented in the child class.
|
|
|
|
:param id:
|
|
Model id
|
|
"""
|
|
raise NotImplementedError('Please implement get_one method')
|
|
|
|
# Exception handler
|
|
def handle_view_exception(self, exc):
|
|
if isinstance(exc, ValidationError):
|
|
flash(as_unicode(exc), 'error')
|
|
return True
|
|
|
|
if current_app.config.get('ADMIN_RAISE_ON_VIEW_EXCEPTION'):
|
|
raise
|
|
|
|
if self._debug:
|
|
raise
|
|
|
|
return False
|
|
|
|
# Model event handlers
|
|
def on_model_change(self, form, model, is_created):
|
|
"""
|
|
Perform some actions before a model is created or updated.
|
|
|
|
Called from create_model and update_model in the same transaction
|
|
(if it has any meaning for a store backend).
|
|
|
|
By default does nothing.
|
|
|
|
:param form:
|
|
Form used to create/update model
|
|
:param model:
|
|
Model that will be created/updated
|
|
:param is_created:
|
|
Will be set to True if model was created and to False if edited
|
|
"""
|
|
pass
|
|
|
|
def _on_model_change(self, form, model, is_created):
|
|
"""
|
|
Compatibility helper.
|
|
"""
|
|
try:
|
|
self.on_model_change(form, model, is_created)
|
|
except TypeError as e:
|
|
if re.match(r'on_model_change\(\) takes .* 3 .* arguments .* 4 .* given .*', str(e)):
|
|
msg = ('%s.on_model_change() now accepts third ' +
|
|
'parameter is_created. Please update your code') % self.model
|
|
warnings.warn(msg)
|
|
|
|
self.on_model_change(form, model)
|
|
else:
|
|
raise
|
|
|
|
def after_model_change(self, form, model, is_created):
|
|
"""
|
|
Perform some actions after a model was created or updated and
|
|
committed to the database.
|
|
|
|
Called from create_model after successful database commit.
|
|
|
|
By default does nothing.
|
|
|
|
:param form:
|
|
Form used to create/update model
|
|
:param model:
|
|
Model that was created/updated
|
|
:param is_created:
|
|
True if model was created, False if model was updated
|
|
"""
|
|
pass
|
|
|
|
def on_model_delete(self, model):
|
|
"""
|
|
Perform some actions before a model is deleted.
|
|
|
|
Called from delete_model in the same transaction
|
|
(if it has any meaning for a store backend).
|
|
|
|
By default do nothing.
|
|
"""
|
|
pass
|
|
|
|
def after_model_delete(self, model):
|
|
"""
|
|
Perform some actions after a model was deleted and
|
|
committed to the database.
|
|
|
|
Called from delete_model after successful database commit
|
|
(if it has any meaning for a store backend).
|
|
|
|
By default does nothing.
|
|
|
|
:param model:
|
|
Model that was deleted
|
|
"""
|
|
pass
|
|
|
|
def on_form_prefill(self, form, id):
|
|
"""
|
|
Perform additional actions to pre-fill the edit form.
|
|
|
|
Called from edit_view, if the current action is rendering
|
|
the form rather than receiving client side input, after
|
|
default pre-filling has been performed.
|
|
|
|
By default does nothing.
|
|
|
|
You only need to override this if you have added custom
|
|
fields that depend on the database contents in a way that
|
|
Flask-admin can't figure out by itself. Fields that were
|
|
added by name of a normal column or relationship should
|
|
work out of the box.
|
|
|
|
:param form:
|
|
Form instance
|
|
:param id:
|
|
id of the object that is going to be edited
|
|
"""
|
|
pass
|
|
|
|
def create_model(self, form):
|
|
"""
|
|
Create model from the form.
|
|
|
|
Returns the model instance if operation succeeded.
|
|
|
|
Must be implemented in the child class.
|
|
|
|
:param form:
|
|
Form instance
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
def update_model(self, form, model):
|
|
"""
|
|
Update model from the form.
|
|
|
|
Returns `True` if operation succeeded.
|
|
|
|
Must be implemented in the child class.
|
|
|
|
:param form:
|
|
Form instance
|
|
:param model:
|
|
Model instance
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
def delete_model(self, model):
|
|
"""
|
|
Delete model.
|
|
|
|
Returns `True` if operation succeeded.
|
|
|
|
Must be implemented in the child class.
|
|
|
|
:param model:
|
|
Model instance
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
# Various helpers
|
|
def _prettify_name(self, name):
|
|
"""
|
|
Prettify pythonic variable name.
|
|
|
|
For example, 'hello_world' will be converted to 'Hello World'
|
|
|
|
:param name:
|
|
Name to prettify
|
|
"""
|
|
return prettify_name(name)
|
|
|
|
def get_empty_list_message(self):
|
|
return gettext('There are no items in the table.')
|
|
|
|
def get_invalid_value_msg(self, value, filter):
|
|
"""
|
|
Returns message, which should be printed in case of failed validation.
|
|
:param value: Invalid value
|
|
:param filter: Filter
|
|
:return: string
|
|
"""
|
|
return gettext('Invalid Filter Value: %(value)s', value=value)
|
|
|
|
# URL generation helpers
|
|
def _get_list_filter_args(self):
|
|
if self._filters:
|
|
filters = []
|
|
|
|
for arg in request.args:
|
|
if not arg.startswith('flt'):
|
|
continue
|
|
|
|
if '_' not in arg:
|
|
continue
|
|
|
|
pos, key = arg[3:].split('_', 1)
|
|
|
|
if key in self._filter_args:
|
|
idx, flt = self._filter_args[key]
|
|
|
|
value = request.args[arg]
|
|
|
|
if flt.validate(value):
|
|
data = (pos, (idx, as_unicode(flt.name), value))
|
|
filters.append(data)
|
|
else:
|
|
flash(self.get_invalid_value_msg(value, flt), 'error')
|
|
|
|
# Sort filters
|
|
return [v[1] for v in sorted(filters, key=lambda n: n[0])]
|
|
|
|
return None
|
|
|
|
def _get_list_extra_args(self):
|
|
"""
|
|
Return arguments from query string.
|
|
"""
|
|
return ViewArgs(page=request.args.get('page', 0, type=int),
|
|
page_size=request.args.get('page_size', 0, type=int),
|
|
sort=request.args.get('sort', None, type=int),
|
|
sort_desc=request.args.get('desc', None, type=int),
|
|
search=request.args.get('search', None),
|
|
filters=self._get_list_filter_args(),
|
|
extra_args=dict([
|
|
(k, v) for k, v in request.args.items()
|
|
if k not in ('page', 'page_size', 'sort', 'desc', 'search', ) and
|
|
not k.startswith('flt')
|
|
]))
|
|
|
|
def _get_filters(self, filters):
|
|
"""
|
|
Get active filters as dictionary of URL arguments and values
|
|
|
|
:param filters:
|
|
List of filters from ViewArgs object
|
|
"""
|
|
kwargs = {}
|
|
|
|
if filters:
|
|
for i, pair in enumerate(filters):
|
|
idx, flt_name, value = pair
|
|
|
|
key = 'flt%d_%s' % (i, self.get_filter_arg(idx, self._filters[idx]))
|
|
kwargs[key] = value
|
|
|
|
return kwargs
|
|
|
|
# URL generation helpers
|
|
def _get_list_url(self, view_args):
|
|
"""
|
|
Generate page URL with current page, sort column and
|
|
other parameters.
|
|
|
|
:param view:
|
|
View name
|
|
:param view_args:
|
|
ViewArgs object with page number, filters, etc.
|
|
"""
|
|
page = view_args.page or None
|
|
desc = 1 if view_args.sort_desc else None
|
|
|
|
kwargs = dict(page=page, sort=view_args.sort, desc=desc, search=view_args.search)
|
|
kwargs.update(view_args.extra_args)
|
|
|
|
if view_args.page_size:
|
|
kwargs['page_size'] = view_args.page_size
|
|
|
|
kwargs.update(self._get_filters(view_args.filters))
|
|
|
|
return self.get_url('.index_view', **kwargs)
|
|
|
|
# Actions
|
|
def is_action_allowed(self, name):
|
|
"""
|
|
Override this method to allow or disallow actions based
|
|
on some condition.
|
|
|
|
The default implementation only checks if the particular action
|
|
is not in `action_disallowed_list`.
|
|
"""
|
|
return name not in self.action_disallowed_list
|
|
|
|
def _get_field_value(self, model, name):
|
|
"""
|
|
Get unformatted field value from the model
|
|
"""
|
|
return rec_getattr(model, name)
|
|
|
|
def _get_list_value(self, context, model, name, column_formatters,
|
|
column_type_formatters):
|
|
"""
|
|
Returns the value to be displayed.
|
|
|
|
:param context:
|
|
:py:class:`jinja2.runtime.Context` if available
|
|
:param model:
|
|
Model instance
|
|
:param name:
|
|
Field name
|
|
:param column_formatters:
|
|
column_formatters to be used.
|
|
:param column_type_formatters:
|
|
column_type_formatters to be used.
|
|
"""
|
|
column_fmt = column_formatters.get(name)
|
|
if column_fmt is not None:
|
|
value = column_fmt(self, context, model, name)
|
|
else:
|
|
value = self._get_field_value(model, name)
|
|
|
|
choices_map = self._column_choices_map.get(name, {})
|
|
if choices_map:
|
|
return choices_map.get(value) or value
|
|
|
|
type_fmt = None
|
|
for typeobj, formatter in column_type_formatters.items():
|
|
if isinstance(value, typeobj):
|
|
type_fmt = formatter
|
|
break
|
|
if type_fmt is not None:
|
|
try:
|
|
value = type_fmt(self, value, name)
|
|
except TypeError:
|
|
spec = inspect.getfullargspec(type_fmt)
|
|
|
|
if len(spec.args) == 2:
|
|
warnings.warn(f'Please update your type formatter {type_fmt} to '
|
|
'include additional `name` parameter.')
|
|
else:
|
|
raise
|
|
|
|
value = type_fmt(self, value)
|
|
|
|
return value
|
|
|
|
@pass_context
|
|
def get_list_value(self, context, model, name):
|
|
"""
|
|
Returns the value to be displayed in the list view
|
|
|
|
:param context:
|
|
:py:class:`jinja2.runtime.Context`
|
|
:param model:
|
|
Model instance
|
|
:param name:
|
|
Field name
|
|
"""
|
|
return self._get_list_value(
|
|
context,
|
|
model,
|
|
name,
|
|
self.column_formatters,
|
|
self.column_type_formatters,
|
|
)
|
|
|
|
@pass_context
|
|
def get_detail_value(self, context, model, name):
|
|
"""
|
|
Returns the value to be displayed in the detail view
|
|
|
|
:param context:
|
|
:py:class:`jinja2.runtime.Context`
|
|
:param model:
|
|
Model instance
|
|
:param name:
|
|
Field name
|
|
"""
|
|
return self._get_list_value(
|
|
context,
|
|
model,
|
|
name,
|
|
self.column_formatters_detail,
|
|
self.column_type_formatters_detail,
|
|
)
|
|
|
|
def get_export_value(self, model, name):
|
|
"""
|
|
Returns the value to be displayed in export.
|
|
Allows export to use different (non HTML) formatters.
|
|
|
|
:param model:
|
|
Model instance
|
|
:param name:
|
|
Field name
|
|
"""
|
|
return self._get_list_value(
|
|
None,
|
|
model,
|
|
name,
|
|
self.column_formatters_export,
|
|
self.column_type_formatters_export,
|
|
)
|
|
|
|
def get_export_name(self, export_type='csv'):
|
|
"""
|
|
:return: The exported csv file name.
|
|
"""
|
|
filename = '%s_%s.%s' % (self.name,
|
|
time.strftime("%Y-%m-%d_%H-%M-%S"),
|
|
export_type)
|
|
return filename
|
|
|
|
# AJAX references
|
|
def _process_ajax_references(self):
|
|
"""
|
|
Process `form_ajax_refs` and generate model loaders that
|
|
will be used by the `ajax_lookup` view.
|
|
"""
|
|
result = {}
|
|
|
|
if self.form_ajax_refs:
|
|
for name, options in iteritems(self.form_ajax_refs):
|
|
if isinstance(options, dict):
|
|
result[name] = self._create_ajax_loader(name, options)
|
|
elif isinstance(options, AjaxModelLoader):
|
|
result[name] = options
|
|
else:
|
|
raise ValueError('%s.form_ajax_refs can not handle %s types' % (self, type(options)))
|
|
|
|
return result
|
|
|
|
def _create_ajax_loader(self, name, options):
|
|
"""
|
|
Model backend will override this to implement AJAX model loading.
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
# Views
|
|
@expose('/')
|
|
def index_view(self):
|
|
"""
|
|
List view
|
|
"""
|
|
if self.can_delete:
|
|
delete_form = self.delete_form()
|
|
else:
|
|
delete_form = None
|
|
|
|
# Grab parameters from URL
|
|
view_args = self._get_list_extra_args()
|
|
|
|
# Map column index to column name
|
|
sort_column = self._get_column_by_idx(view_args.sort)
|
|
if sort_column is not None:
|
|
sort_column = sort_column[0]
|
|
|
|
# Get page size
|
|
page_size = view_args.page_size or self.page_size
|
|
|
|
# Get count and data
|
|
count, data = self.get_list(view_args.page, sort_column, view_args.sort_desc,
|
|
view_args.search, view_args.filters, page_size=page_size)
|
|
|
|
list_forms = {}
|
|
if self.column_editable_list:
|
|
for row in data:
|
|
list_forms[self.get_pk_value(row)] = self.list_form(obj=row)
|
|
|
|
# Calculate number of pages
|
|
if count is not None and page_size:
|
|
num_pages = int(ceil(count / float(page_size)))
|
|
elif not page_size:
|
|
num_pages = 0 # hide pager for unlimited page_size
|
|
else:
|
|
num_pages = None # use simple pager
|
|
|
|
# Various URL generation helpers
|
|
def pager_url(p):
|
|
# Do not add page number if it is first page
|
|
if p == 0:
|
|
p = None
|
|
|
|
return self._get_list_url(view_args.clone(page=p))
|
|
|
|
def sort_url(column, invert=False, desc=None):
|
|
if not desc and invert and not view_args.sort_desc:
|
|
desc = 1
|
|
|
|
return self._get_list_url(view_args.clone(sort=column, sort_desc=desc))
|
|
|
|
def page_size_url(s):
|
|
if not s:
|
|
s = self.page_size
|
|
|
|
return self._get_list_url(view_args.clone(page_size=s))
|
|
|
|
# Actions
|
|
actions, actions_confirmation = self.get_actions_list()
|
|
if actions:
|
|
action_form = self.action_form()
|
|
else:
|
|
action_form = None
|
|
|
|
clear_search_url = self._get_list_url(view_args.clone(page=0,
|
|
sort=view_args.sort,
|
|
sort_desc=view_args.sort_desc,
|
|
search=None,
|
|
filters=None))
|
|
|
|
return self.render(
|
|
self.list_template,
|
|
data=data,
|
|
list_forms=list_forms,
|
|
delete_form=delete_form,
|
|
action_form=action_form,
|
|
|
|
# List
|
|
list_columns=self._list_columns,
|
|
sortable_columns=self._sortable_columns,
|
|
editable_columns=self.column_editable_list,
|
|
list_row_actions=self.get_list_row_actions(),
|
|
|
|
# Pagination
|
|
count=count,
|
|
pager_url=pager_url,
|
|
num_pages=num_pages,
|
|
can_set_page_size=self.can_set_page_size,
|
|
page_size_url=page_size_url,
|
|
page=view_args.page,
|
|
page_size=page_size,
|
|
default_page_size=self.page_size,
|
|
|
|
# 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,
|
|
search_placeholder=self.search_placeholder(),
|
|
|
|
# Filters
|
|
filters=self._filters,
|
|
filter_groups=self._get_filter_groups(),
|
|
active_filters=view_args.filters,
|
|
filter_args=self._get_filters(view_args.filters),
|
|
|
|
# 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),
|
|
|
|
# Extras
|
|
extra_args=view_args.extra_args,
|
|
)
|
|
|
|
@expose('/new/', methods=('GET', 'POST'))
|
|
def create_view(self):
|
|
"""
|
|
Create model view
|
|
"""
|
|
return_url = get_redirect_target() or self.get_url('.index_view')
|
|
|
|
if not self.can_create:
|
|
return redirect(return_url)
|
|
|
|
form = self.create_form()
|
|
if not hasattr(form, '_validated_ruleset') or not form._validated_ruleset:
|
|
self._validate_form_instance(ruleset=self._form_create_rules, form=form)
|
|
|
|
if self.validate_form(form):
|
|
# in versions 1.1.0 and before, this returns a boolean
|
|
# in later versions, this is the model itself
|
|
model = self.create_model(form)
|
|
if model:
|
|
flash(gettext('Record was successfully created.'), 'success')
|
|
if '_add_another' in request.form:
|
|
return redirect(request.url)
|
|
elif '_continue_editing' in request.form:
|
|
# if we have a valid model, try to go to the edit view
|
|
if model is not True:
|
|
url = self.get_url('.edit_view', id=self.get_pk_value(model), url=return_url)
|
|
else:
|
|
url = return_url
|
|
return redirect(url)
|
|
else:
|
|
# save button
|
|
return redirect(self.get_save_return_url(model, is_created=True))
|
|
|
|
form_opts = FormOpts(widget_args=self.form_widget_args,
|
|
form_rules=self._form_create_rules)
|
|
|
|
if self.create_modal and request.args.get('modal'):
|
|
template = self.create_modal_template
|
|
else:
|
|
template = self.create_template
|
|
|
|
return self.render(template,
|
|
form=form,
|
|
form_opts=form_opts,
|
|
return_url=return_url)
|
|
|
|
@expose('/edit/', methods=('GET', 'POST'))
|
|
def edit_view(self):
|
|
"""
|
|
Edit model view
|
|
"""
|
|
return_url = get_redirect_target() or self.get_url('.index_view')
|
|
|
|
if not self.can_edit:
|
|
return redirect(return_url)
|
|
|
|
id = get_mdict_item_or_list(request.args, 'id')
|
|
if id is None:
|
|
return redirect(return_url)
|
|
|
|
model = self.get_one(id)
|
|
|
|
if model is None:
|
|
flash(gettext('Record does not exist.'), 'error')
|
|
return redirect(return_url)
|
|
|
|
form = self.edit_form(obj=model)
|
|
if not hasattr(form, '_validated_ruleset') or not form._validated_ruleset:
|
|
self._validate_form_instance(ruleset=self._form_edit_rules, form=form)
|
|
|
|
if self.validate_form(form):
|
|
if self.update_model(form, model):
|
|
flash(gettext('Record was successfully saved.'), 'success')
|
|
if '_add_another' in request.form:
|
|
return redirect(self.get_url('.create_view', url=return_url))
|
|
elif '_continue_editing' in request.form:
|
|
return redirect(self.get_url('.edit_view', id=self.get_pk_value(model)))
|
|
else:
|
|
# save button
|
|
return redirect(self.get_save_return_url(model, is_created=False))
|
|
|
|
if request.method == 'GET' or form.errors:
|
|
self.on_form_prefill(form, id)
|
|
|
|
form_opts = FormOpts(widget_args=self.form_widget_args,
|
|
form_rules=self._form_edit_rules)
|
|
|
|
if self.edit_modal and request.args.get('modal'):
|
|
template = self.edit_modal_template
|
|
else:
|
|
template = self.edit_template
|
|
|
|
return self.render(template,
|
|
model=model,
|
|
form=form,
|
|
form_opts=form_opts,
|
|
return_url=return_url)
|
|
|
|
@expose('/details/')
|
|
def details_view(self):
|
|
"""
|
|
Details model view
|
|
"""
|
|
return_url = get_redirect_target() or self.get_url('.index_view')
|
|
|
|
if not self.can_view_details:
|
|
return redirect(return_url)
|
|
|
|
id = get_mdict_item_or_list(request.args, 'id')
|
|
if id is None:
|
|
return redirect(return_url)
|
|
|
|
model = self.get_one(id)
|
|
|
|
if model is None:
|
|
flash(gettext('Record does not exist.'), 'error')
|
|
return redirect(return_url)
|
|
|
|
if self.details_modal and request.args.get('modal'):
|
|
template = self.details_modal_template
|
|
else:
|
|
template = self.details_template
|
|
|
|
return self.render(template,
|
|
model=model,
|
|
details_columns=self._details_columns,
|
|
get_value=self.get_detail_value,
|
|
return_url=return_url)
|
|
|
|
@expose('/delete/', methods=('POST',))
|
|
def delete_view(self):
|
|
"""
|
|
Delete model view. Only POST method is allowed.
|
|
"""
|
|
return_url = get_redirect_target() or self.get_url('.index_view')
|
|
|
|
if not self.can_delete:
|
|
return redirect(return_url)
|
|
|
|
form = self.delete_form()
|
|
|
|
if self.validate_form(form):
|
|
# id is InputRequired()
|
|
id = form.id.data
|
|
|
|
model = self.get_one(id)
|
|
|
|
if model is None:
|
|
flash(gettext('Record does not exist.'), 'error')
|
|
return redirect(return_url)
|
|
|
|
# message is flashed from within delete_model if it fails
|
|
if self.delete_model(model):
|
|
count = 1
|
|
flash(
|
|
ngettext('Record was successfully deleted.',
|
|
'%(count)s records were successfully deleted.',
|
|
count, count=count), 'success')
|
|
return redirect(return_url)
|
|
else:
|
|
flash_errors(form, message='Failed to delete record. %(error)s')
|
|
|
|
return redirect(return_url)
|
|
|
|
@expose('/action/', methods=('POST',))
|
|
def action_view(self):
|
|
"""
|
|
Mass-model action view.
|
|
"""
|
|
return self.handle_action()
|
|
|
|
def _export_data(self):
|
|
# Macros in column_formatters are not supported.
|
|
# Macros will have a function name 'inner'
|
|
# This causes non-macro functions named 'inner' not work.
|
|
for col, func in iteritems(self.column_formatters_export):
|
|
# skip checking columns not being exported
|
|
if col not in [col for col, _ in self._export_columns]:
|
|
continue
|
|
|
|
if func.__name__ == 'inner':
|
|
raise NotImplementedError(
|
|
'Macros are not implemented in export. Exclude column in'
|
|
' column_formatters_export, column_export_list, or '
|
|
' column_export_exclude_list. Column: %s' % (col,)
|
|
)
|
|
|
|
# Grab parameters from URL
|
|
view_args = self._get_list_extra_args()
|
|
|
|
# Map column index to column name
|
|
sort_column = self._get_column_by_idx(view_args.sort)
|
|
if sort_column is not None:
|
|
sort_column = sort_column[0]
|
|
|
|
# Get count and data
|
|
count, data = self.get_list(0, sort_column, view_args.sort_desc,
|
|
view_args.search, view_args.filters,
|
|
page_size=self.export_max_rows)
|
|
|
|
return count, data
|
|
|
|
@expose('/export/<export_type>/')
|
|
def export(self, export_type):
|
|
return_url = get_redirect_target() or self.get_url('.index_view')
|
|
|
|
if not self.can_export or (export_type not in self.export_types):
|
|
flash(gettext('Permission denied.'), 'error')
|
|
return redirect(return_url)
|
|
|
|
if export_type == 'csv':
|
|
return self._export_csv(return_url)
|
|
else:
|
|
return self._export_tablib(export_type, return_url)
|
|
|
|
def _export_csv(self, return_url):
|
|
"""
|
|
Export a CSV of records as a stream.
|
|
"""
|
|
count, data = self._export_data()
|
|
|
|
# https://docs.djangoproject.com/en/1.8/howto/outputting-csv/
|
|
class Echo(object):
|
|
"""
|
|
An object that implements just the write method of the file-like
|
|
interface.
|
|
"""
|
|
def write(self, value):
|
|
"""
|
|
Write the value by returning it, instead of storing
|
|
in a buffer.
|
|
"""
|
|
return value
|
|
|
|
writer = csv.writer(Echo())
|
|
|
|
def generate():
|
|
# Append the column titles at the beginning
|
|
titles = [csv_encode(c[1]) for c in self._export_columns]
|
|
yield writer.writerow(titles)
|
|
|
|
for row in data:
|
|
vals = [csv_encode(self.get_export_value(row, c[0]))
|
|
for c in self._export_columns]
|
|
yield writer.writerow(vals)
|
|
|
|
filename = self.get_export_name(export_type='csv')
|
|
|
|
disposition = 'attachment;filename=%s' % (secure_filename(filename),)
|
|
|
|
return Response(
|
|
stream_with_context(generate()),
|
|
headers={'Content-Disposition': disposition},
|
|
mimetype='text/csv'
|
|
)
|
|
|
|
def _export_tablib(self, export_type, return_url):
|
|
"""
|
|
Exports a variety of formats using the tablib library.
|
|
"""
|
|
if tablib is None:
|
|
flash(gettext('Tablib dependency not installed.'), 'error')
|
|
return redirect(return_url)
|
|
|
|
filename = self.get_export_name(export_type)
|
|
|
|
disposition = 'attachment;filename=%s' % (secure_filename(filename),)
|
|
|
|
mimetype, encoding = mimetypes.guess_type(filename)
|
|
if not mimetype:
|
|
mimetype = 'application/octet-stream'
|
|
if encoding:
|
|
mimetype = '%s; charset=%s' % (mimetype, encoding)
|
|
|
|
ds = tablib.Dataset(headers=[csv_encode(c[1]) for c in self._export_columns])
|
|
|
|
count, data = self._export_data()
|
|
|
|
for row in data:
|
|
vals = [csv_encode(self.get_export_value(row, c[0])) for c in self._export_columns]
|
|
ds.append(vals)
|
|
|
|
try:
|
|
try:
|
|
response_data = ds.export(format=export_type)
|
|
except AttributeError:
|
|
response_data = getattr(ds, export_type)
|
|
except (AttributeError, tablib.UnsupportedFormat):
|
|
flash(gettext('Export type "%(type)s not supported.',
|
|
type=export_type), 'error')
|
|
return redirect(return_url)
|
|
|
|
return Response(
|
|
response_data,
|
|
headers={'Content-Disposition': disposition},
|
|
mimetype=mimetype,
|
|
)
|
|
|
|
@expose('/ajax/lookup/')
|
|
def ajax_lookup(self):
|
|
name = request.args.get('name')
|
|
query = request.args.get('query')
|
|
offset = request.args.get('offset', type=int)
|
|
limit = request.args.get('limit', 10, type=int)
|
|
|
|
loader = self._form_ajax_refs.get(name)
|
|
|
|
if not loader:
|
|
abort(404)
|
|
|
|
data = [loader.format(m) for m in loader.get_list(query, offset, limit)]
|
|
return Response(json.dumps(data), mimetype='application/json')
|
|
|
|
@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)
|
|
|
|
form = self.list_form()
|
|
|
|
# prevent validation issues due to submitting a single field
|
|
# delete all fields except the submitted fields and csrf token
|
|
for field in list(form):
|
|
if (field.name in request.form) or (field.name == 'csrf_token'):
|
|
pass
|
|
else:
|
|
form.__delitem__(field.name)
|
|
|
|
if self.validate_form(form):
|
|
pk = form.list_form_pk.data
|
|
record = self.get_one(pk)
|
|
|
|
if record is None:
|
|
return gettext('Record does not exist.'), 500
|
|
|
|
if self.update_model(form, record):
|
|
# 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 gettext('Failed to update record. %(error)s',
|
|
error=", ".join(error)), 500
|
|
else:
|
|
return gettext('Failed to update record. %(error)s',
|
|
error=error), 500
|