123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331 |
- import warnings
- import re
- import csv
- import mimetypes
- import time
- from math import ceil
- from werkzeug import secure_filename
- from flask import (current_app, request, redirect, flash, abort, json,
- Response, get_flashed_messages, stream_with_context)
- from jinja2 import contextfunction
- 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
- 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)
- 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_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_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'))
- 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)
- """
- 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'),
- 'page_size': 10
- }
- }
- 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:
- View category
- :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
- # 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_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
- # 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
- 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, 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:
- 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)
- 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.')
- # URL generation helpers
- def _get_list_filter_args(self):
- if self._filters:
- filters = []
- for n in request.args:
- if not n.startswith('flt'):
- continue
- if '_' not in n:
- continue
- pos, key = n[3:].split('_', 1)
- if key in self._filter_args:
- idx, flt = self._filter_args[key]
- value = request.args[n]
- if flt.validate(value):
- filters.append((pos, (idx, as_unicode(flt.name), value)))
- else:
- flash(gettext('Invalid Filter Value: %(value)s', value=value), '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())
- 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:
- value = type_fmt(self, value)
- return value
- @contextfunction
- 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,
- )
- 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,
- # 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),
- )
- @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(request.url)
- 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_list_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):
- flash(gettext('Record was successfully deleted.'), '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=[c[1] for c in self._export_columns])
- count, data = self._export_data()
- for row in data:
- vals = [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
|