base.py 77 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331
  1. import warnings
  2. import re
  3. import csv
  4. import mimetypes
  5. import time
  6. from math import ceil
  7. from werkzeug import secure_filename
  8. from flask import (current_app, request, redirect, flash, abort, json,
  9. Response, get_flashed_messages, stream_with_context)
  10. from jinja2 import contextfunction
  11. try:
  12. import tablib
  13. except ImportError:
  14. tablib = None
  15. from wtforms.fields import HiddenField
  16. from wtforms.fields.core import UnboundField
  17. from wtforms.validators import ValidationError, InputRequired
  18. from flask_admin.babel import gettext
  19. from flask_admin.base import BaseView, expose
  20. from flask_admin.form import BaseForm, FormOpts, rules
  21. from flask_admin.model import filters, typefmt, template
  22. from flask_admin.actions import ActionsMixin
  23. from flask_admin.helpers import (get_form_data, validate_form_on_submit,
  24. get_redirect_target, flash_errors)
  25. from flask_admin.tools import rec_getattr
  26. from flask_admin._backwards import ObsoleteAttr
  27. from flask_admin._compat import (iteritems, itervalues, OrderedDict,
  28. as_unicode, csv_encode, text_type)
  29. from .helpers import prettify_name, get_mdict_item_or_list
  30. from .ajax import AjaxModelLoader
  31. # Used to generate filter query string name
  32. filter_char_re = re.compile('[^a-z0-9 ]')
  33. filter_compact_re = re.compile(' +')
  34. class ViewArgs(object):
  35. """
  36. List view arguments.
  37. """
  38. def __init__(self, page=None, page_size=None, sort=None, sort_desc=None,
  39. search=None, filters=None, extra_args=None):
  40. self.page = page
  41. self.page_size = page_size
  42. self.sort = sort
  43. self.sort_desc = bool(sort_desc)
  44. self.search = search
  45. self.filters = filters
  46. if not self.search:
  47. self.search = None
  48. self.extra_args = extra_args or dict()
  49. def clone(self, **kwargs):
  50. if self.filters:
  51. flt = list(self.filters)
  52. else:
  53. flt = None
  54. kwargs.setdefault('page', self.page)
  55. kwargs.setdefault('page_size', self.page_size)
  56. kwargs.setdefault('sort', self.sort)
  57. kwargs.setdefault('sort_desc', self.sort_desc)
  58. kwargs.setdefault('search', self.search)
  59. kwargs.setdefault('filters', flt)
  60. kwargs.setdefault('extra_args', dict(self.extra_args))
  61. return ViewArgs(**kwargs)
  62. class FilterGroup(object):
  63. def __init__(self, label):
  64. self.label = label
  65. self.filters = []
  66. def append(self, filter):
  67. self.filters.append(filter)
  68. def non_lazy(self):
  69. filters = []
  70. for item in self.filters:
  71. copy = dict(item)
  72. copy['operation'] = as_unicode(copy['operation'])
  73. options = copy['options']
  74. if options:
  75. copy['options'] = [(k, text_type(v)) for k, v in options]
  76. filters.append(copy)
  77. return as_unicode(self.label), filters
  78. def __iter__(self):
  79. return iter(self.filters)
  80. class BaseModelView(BaseView, ActionsMixin):
  81. """
  82. Base model view.
  83. This view does not make any assumptions on how models are stored or managed, but expects the following:
  84. 1. The provided model is an object
  85. 2. The model contains properties
  86. 3. Each model contains an attribute which uniquely identifies it (i.e. a primary key for a database model)
  87. 4. It is possible to retrieve a list of sorted models with pagination applied from a data source
  88. 5. You can get one model by its identifier from the data source
  89. Essentially, if you want to support a new data store, all you have to do is:
  90. 1. Derive from the `BaseModelView` class
  91. 2. Implement various data-related methods (`get_list`, `get_one`, `create_model`, etc)
  92. 3. Implement automatic form generation from the model representation (`scaffold_form`)
  93. """
  94. # Permissions
  95. can_create = True
  96. """Is model creation allowed"""
  97. can_edit = True
  98. """Is model editing allowed"""
  99. can_delete = True
  100. """Is model deletion allowed"""
  101. can_view_details = False
  102. """
  103. Setting this to true will enable the details view. This is recommended
  104. when there are too many columns to display in the list_view.
  105. """
  106. can_export = False
  107. """Is model list export allowed"""
  108. # Templates
  109. list_template = 'admin/model/list.html'
  110. """Default list view template"""
  111. edit_template = 'admin/model/edit.html'
  112. """Default edit template"""
  113. create_template = 'admin/model/create.html'
  114. """Default create template"""
  115. details_template = 'admin/model/details.html'
  116. """Default details view template"""
  117. # Modal Templates
  118. edit_modal_template = 'admin/model/modals/edit.html'
  119. """Default edit modal template"""
  120. create_modal_template = 'admin/model/modals/create.html'
  121. """Default create modal template"""
  122. details_modal_template = 'admin/model/modals/details.html'
  123. """Default details modal view template"""
  124. # Modals
  125. edit_modal = False
  126. """Setting this to true will display the edit_view as a modal dialog."""
  127. create_modal = False
  128. """Setting this to true will display the create_view as a modal dialog."""
  129. details_modal = False
  130. """Setting this to true will display the details_view as a modal dialog."""
  131. # Customizations
  132. column_list = ObsoleteAttr('column_list', 'list_columns', None)
  133. """
  134. Collection of the model field names for the list view.
  135. If set to `None`, will get them from the model.
  136. For example::
  137. class MyModelView(BaseModelView):
  138. column_list = ('name', 'last_name', 'email')
  139. (Added in 1.4.0) SQLAlchemy model attributes can be used instead of strings::
  140. class MyModelView(BaseModelView):
  141. column_list = ('name', User.last_name)
  142. When using SQLAlchemy models, you can reference related columns like this::
  143. class MyModelView(BaseModelView):
  144. column_list = ('<relationship>.<related column name>',)
  145. """
  146. column_exclude_list = ObsoleteAttr('column_exclude_list',
  147. 'excluded_list_columns', None)
  148. """
  149. Collection of excluded list column names.
  150. For example::
  151. class MyModelView(BaseModelView):
  152. column_exclude_list = ('last_name', 'email')
  153. """
  154. column_details_list = None
  155. """
  156. Collection of the field names included in the details view.
  157. If set to `None`, will get them from the model.
  158. """
  159. column_details_exclude_list = None
  160. """
  161. Collection of fields excluded from the details view.
  162. """
  163. column_export_list = None
  164. """
  165. Collection of the field names included in the export.
  166. If set to `None`, will get them from the model.
  167. """
  168. column_export_exclude_list = None
  169. """
  170. Collection of fields excluded from the export.
  171. """
  172. column_formatters = ObsoleteAttr('column_formatters', 'list_formatters', dict())
  173. """
  174. Dictionary of list view column formatters.
  175. For example, if you want to show price multiplied by
  176. two, you can do something like this::
  177. class MyModelView(BaseModelView):
  178. column_formatters = dict(price=lambda v, c, m, p: m.price*2)
  179. or using Jinja2 `macro` in template::
  180. from flask_admin.model.template import macro
  181. class MyModelView(BaseModelView):
  182. column_formatters = dict(price=macro('render_price'))
  183. # in template
  184. {% macro render_price(model, column) %}
  185. {{ model.price * 2 }}
  186. {% endmacro %}
  187. The Callback function has the prototype::
  188. def formatter(view, context, model, name):
  189. # `view` is current administrative view
  190. # `context` is instance of jinja2.runtime.Context
  191. # `model` is model instance
  192. # `name` is property name
  193. pass
  194. """
  195. column_formatters_export = None
  196. """
  197. Dictionary of list view column formatters to be used for export.
  198. Defaults to column_formatters when set to None.
  199. Functions the same way as column_formatters except
  200. that macros are not supported.
  201. """
  202. column_type_formatters = ObsoleteAttr('column_type_formatters', 'list_type_formatters', None)
  203. """
  204. Dictionary of value type formatters to be used in the list view.
  205. By default, three types are formatted:
  206. 1. ``None`` will be displayed as an empty string
  207. 2. ``bool`` will be displayed as a checkmark if it is ``True``
  208. 3. ``list`` will be joined using ', '
  209. If you don't like the default behavior and don't want any type formatters
  210. applied, just override this property with an empty dictionary::
  211. class MyModelView(BaseModelView):
  212. column_type_formatters = dict()
  213. If you want to display `NULL` instead of an empty string, you can do
  214. something like this. Also comes with bonus `date` formatter::
  215. from datetime import date
  216. from flask_admin.model import typefmt
  217. def date_format(view, value):
  218. return value.strftime('%d.%m.%Y')
  219. MY_DEFAULT_FORMATTERS = dict(typefmt.BASE_FORMATTERS)
  220. MY_DEFAULT_FORMATTERS.update({
  221. type(None): typefmt.null_formatter,
  222. date: date_format
  223. })
  224. class MyModelView(BaseModelView):
  225. column_type_formatters = MY_DEFAULT_FORMATTERS
  226. Type formatters have lower priority than list column formatters.
  227. The callback function has following prototype::
  228. def type_formatter(view, value):
  229. # `view` is current administrative view
  230. # `value` value to format
  231. pass
  232. """
  233. column_type_formatters_export = None
  234. """
  235. Dictionary of value type formatters to be used in the export.
  236. By default, two types are formatted:
  237. 1. ``None`` will be displayed as an empty string
  238. 2. ``list`` will be joined using ', '
  239. Functions the same way as column_type_formatters.
  240. """
  241. column_labels = ObsoleteAttr('column_labels', 'rename_columns', None)
  242. """
  243. Dictionary where key is column name and value is string to display.
  244. For example::
  245. class MyModelView(BaseModelView):
  246. column_labels = dict(name='Name', last_name='Last Name')
  247. """
  248. column_descriptions = None
  249. """
  250. Dictionary where key is column name and
  251. value is description for `list view` column or add/edit form field.
  252. For example::
  253. class MyModelView(BaseModelView):
  254. column_descriptions = dict(
  255. full_name='First and Last name'
  256. )
  257. """
  258. column_sortable_list = ObsoleteAttr('column_sortable_list',
  259. 'sortable_columns',
  260. None)
  261. """
  262. Collection of the sortable columns for the list view.
  263. If set to `None`, will get them from the model.
  264. For example::
  265. class MyModelView(BaseModelView):
  266. column_sortable_list = ('name', 'last_name')
  267. If you want to explicitly specify field/column to be used while
  268. sorting, you can use a tuple::
  269. class MyModelView(BaseModelView):
  270. column_sortable_list = ('name', ('user', 'user.username'))
  271. When using SQLAlchemy models, model attributes can be used instead
  272. of strings::
  273. class MyModelView(BaseModelView):
  274. column_sortable_list = ('name', ('user', User.username))
  275. """
  276. column_default_sort = None
  277. """
  278. Default sort column if no sorting is applied.
  279. Example::
  280. class MyModelView(BaseModelView):
  281. column_default_sort = 'user'
  282. You can use tuple to control ascending descending order. In following example, items
  283. will be sorted in descending order::
  284. class MyModelView(BaseModelView):
  285. column_default_sort = ('user', True)
  286. """
  287. column_searchable_list = ObsoleteAttr('column_searchable_list',
  288. 'searchable_columns',
  289. None)
  290. """
  291. A collection of the searchable columns. It is assumed that only
  292. text-only fields are searchable, but it is up to the model
  293. implementation to decide.
  294. Example::
  295. class MyModelView(BaseModelView):
  296. column_searchable_list = ('name', 'email')
  297. """
  298. column_editable_list = None
  299. """
  300. Collection of the columns which can be edited from the list view.
  301. For example::
  302. class MyModelView(BaseModelView):
  303. column_editable_list = ('name', 'last_name')
  304. """
  305. column_choices = None
  306. """
  307. Map choices to columns in list view
  308. Example::
  309. class MyModelView(BaseModelView):
  310. column_choices = {
  311. 'my_column': [
  312. ('db_value', 'display_value'),
  313. ]
  314. }
  315. """
  316. column_filters = None
  317. """
  318. Collection of the column filters.
  319. Can contain either field names or instances of :class:`~flask_admin.model.filters.BaseFilter` classes.
  320. Example::
  321. class MyModelView(BaseModelView):
  322. column_filters = ('user', 'email')
  323. """
  324. named_filter_urls = False
  325. """
  326. Set to True to use human-readable names for filters in URL parameters.
  327. False by default so as to be robust across translations.
  328. Changing this parameter will break any existing URLs that have filters.
  329. """
  330. column_display_pk = ObsoleteAttr('column_display_pk',
  331. 'list_display_pk',
  332. False)
  333. """
  334. Controls if the primary key should be displayed in the list view.
  335. """
  336. column_display_actions = True
  337. """
  338. Controls the display of the row actions (edit, delete, details, etc.)
  339. column in the list view.
  340. Useful for preventing a blank column from displaying if your view does
  341. not use any build-in or custom row actions.
  342. This column is not hidden automatically due to backwards compatibility.
  343. Note: This only affects display and does not control whether the row
  344. actions endpoints are accessible.
  345. """
  346. column_extra_row_actions = None
  347. """
  348. List of row actions (instances of :class:`~flask_admin.model.template.BaseListRowAction`).
  349. Flask-Admin will generate standard per-row actions (edit, delete, etc)
  350. and will append custom actions from this list right after them.
  351. For example::
  352. from flask_admin.model.template import EndpointLinkRowAction, LinkRowAction
  353. class MyModelView(BaseModelView):
  354. column_extra_row_actions = [
  355. LinkRowAction('glyphicon glyphicon-off', 'http://direct.link/?id={row_id}'),
  356. EndpointLinkRowAction('glyphicon glyphicon-test', 'my_view.index_view')
  357. ]
  358. """
  359. simple_list_pager = False
  360. """
  361. Enable or disable simple list pager.
  362. If enabled, model interface would not run count query and will only show prev/next pager buttons.
  363. """
  364. form = None
  365. """
  366. Form class. Override if you want to use custom form for your model.
  367. Will completely disable form scaffolding functionality.
  368. For example::
  369. class MyForm(Form):
  370. name = StringField('Name')
  371. class MyModelView(BaseModelView):
  372. form = MyForm
  373. """
  374. form_base_class = BaseForm
  375. """
  376. Base form class. Will be used by form scaffolding function when creating model form.
  377. Useful if you want to have custom constructor or override some fields.
  378. Example::
  379. class MyBaseForm(Form):
  380. def do_something(self):
  381. pass
  382. class MyModelView(BaseModelView):
  383. form_base_class = MyBaseForm
  384. """
  385. form_args = None
  386. """
  387. Dictionary of form field arguments. Refer to WTForms documentation for
  388. list of possible options.
  389. Example::
  390. from wtforms.validators import DataRequired
  391. class MyModelView(BaseModelView):
  392. form_args = dict(
  393. name=dict(label='First Name', validators=[DataRequired()])
  394. )
  395. """
  396. form_columns = None
  397. """
  398. Collection of the model field names for the form. If set to `None` will
  399. get them from the model.
  400. Example::
  401. class MyModelView(BaseModelView):
  402. form_columns = ('name', 'email')
  403. (Added in 1.4.0) SQLAlchemy model attributes can be used instead of
  404. strings::
  405. class MyModelView(BaseModelView):
  406. form_columns = ('name', User.last_name)
  407. SQLA Note: Model attributes must be on the same model as your ModelView
  408. or you will need to use `inline_models`.
  409. """
  410. form_excluded_columns = ObsoleteAttr('form_excluded_columns',
  411. 'excluded_form_columns',
  412. None)
  413. """
  414. Collection of excluded form field names.
  415. For example::
  416. class MyModelView(BaseModelView):
  417. form_excluded_columns = ('last_name', 'email')
  418. """
  419. form_overrides = None
  420. """
  421. Dictionary of form column overrides.
  422. Example::
  423. class MyModelView(BaseModelView):
  424. form_overrides = dict(name=wtf.FileField)
  425. """
  426. form_widget_args = None
  427. """
  428. Dictionary of form widget rendering arguments.
  429. Use this to customize how widget is rendered without using custom template.
  430. Example::
  431. class MyModelView(BaseModelView):
  432. form_widget_args = {
  433. 'description': {
  434. 'rows': 10,
  435. 'style': 'color: black'
  436. },
  437. 'other_field': {
  438. 'disabled': True
  439. }
  440. }
  441. Changing the format of a DateTimeField will require changes to both form_widget_args and form_args.
  442. Example::
  443. form_args = dict(
  444. start=dict(format='%Y-%m-%d %I:%M %p') # changes how the input is parsed by strptime (12 hour time)
  445. )
  446. form_widget_args = dict(
  447. start={
  448. 'data-date-format': u'yyyy-mm-dd HH:ii P',
  449. 'data-show-meridian': 'True'
  450. } # changes how the DateTimeField displays the time
  451. )
  452. """
  453. form_extra_fields = None
  454. """
  455. Dictionary of additional fields.
  456. Example::
  457. class MyModelView(BaseModelView):
  458. form_extra_fields = {
  459. 'password': PasswordField('Password')
  460. }
  461. You can control order of form fields using ``form_columns`` property. For example::
  462. class MyModelView(BaseModelView):
  463. form_columns = ('name', 'email', 'password', 'secret')
  464. form_extra_fields = {
  465. 'password': PasswordField('Password')
  466. }
  467. In this case, password field will be put between email and secret fields that are autogenerated.
  468. """
  469. form_ajax_refs = None
  470. """
  471. Use AJAX for foreign key model loading.
  472. Should contain dictionary, where key is field name and value is either a dictionary which
  473. configures AJAX lookups or backend-specific `AjaxModelLoader` class instance.
  474. For example, it can look like::
  475. class MyModelView(BaseModelView):
  476. form_ajax_refs = {
  477. 'user': {
  478. 'fields': ('first_name', 'last_name', 'email'),
  479. 'page_size': 10
  480. }
  481. }
  482. Or with SQLAlchemy backend like this::
  483. class MyModelView(BaseModelView):
  484. form_ajax_refs = {
  485. 'user': QueryAjaxModelLoader('user', db.session, User, fields=['email'], page_size=10)
  486. }
  487. If you need custom loading functionality, you can implement your custom loading behavior
  488. in your `AjaxModelLoader` class.
  489. """
  490. form_rules = None
  491. """
  492. List of rendering rules for model creation form.
  493. This property changed default form rendering behavior and makes possible to rearrange order
  494. of rendered fields, add some text between fields, group them, etc. If not set, will use
  495. default Flask-Admin form rendering logic.
  496. Here's simple example which illustrates how to use::
  497. from flask_admin.form import rules
  498. class MyModelView(ModelView):
  499. form_rules = [
  500. # Define field set with header text and four fields
  501. rules.FieldSet(('first_name', 'last_name', 'email', 'phone'), 'User'),
  502. # ... and it is just shortcut for:
  503. rules.Header('User'),
  504. rules.Field('first_name'),
  505. rules.Field('last_name'),
  506. # ...
  507. # It is possible to create custom rule blocks:
  508. MyBlock('Hello World'),
  509. # It is possible to call macros from current context
  510. rules.Macro('my_macro', foobar='baz')
  511. ]
  512. """
  513. form_edit_rules = None
  514. """
  515. Customized rules for the edit form. Override `form_rules` if present.
  516. """
  517. form_create_rules = None
  518. """
  519. Customized rules for the create form. Override `form_rules` if present.
  520. """
  521. # Actions
  522. action_disallowed_list = ObsoleteAttr('action_disallowed_list',
  523. 'disallowed_actions',
  524. [])
  525. """
  526. Set of disallowed action names. For example, if you want to disable
  527. mass model deletion, do something like this:
  528. class MyModelView(BaseModelView):
  529. action_disallowed_list = ['delete']
  530. """
  531. # Export settings
  532. export_max_rows = 0
  533. """
  534. Maximum number of rows allowed for export.
  535. Unlimited by default. Uses `page_size` if set to `None`.
  536. """
  537. export_types = ['csv']
  538. """
  539. A list of available export filetypes. `csv` only is default, but any
  540. filetypes supported by tablib can be used.
  541. Check tablib for https://github.com/kennethreitz/tablib/blob/master/README.rst
  542. for supported types.
  543. """
  544. # Pagination settings
  545. page_size = 20
  546. """
  547. Default page size for pagination.
  548. """
  549. can_set_page_size = False
  550. """
  551. Allows to select page size via dropdown list
  552. """
  553. def __init__(self, model,
  554. name=None, category=None, endpoint=None, url=None, static_folder=None,
  555. menu_class_name=None, menu_icon_type=None, menu_icon_value=None):
  556. """
  557. Constructor.
  558. :param model:
  559. Model class
  560. :param name:
  561. View name. If not provided, will use the model class name
  562. :param category:
  563. View category
  564. :param endpoint:
  565. Base endpoint. If not provided, will use the model name.
  566. :param url:
  567. Base URL. If not provided, will use endpoint as a URL.
  568. :param menu_class_name:
  569. Optional class name for the menu item.
  570. :param menu_icon_type:
  571. Optional icon. Possible icon types:
  572. - `flask_admin.consts.ICON_TYPE_GLYPH` - Bootstrap glyph icon
  573. - `flask_admin.consts.ICON_TYPE_FONT_AWESOME` - Font Awesome icon
  574. - `flask_admin.consts.ICON_TYPE_IMAGE` - Image relative to Flask static directory
  575. - `flask_admin.consts.ICON_TYPE_IMAGE_URL` - Image with full URL
  576. :param menu_icon_value:
  577. Icon glyph name or URL, depending on `menu_icon_type` setting
  578. """
  579. self.model = model
  580. # If name not provided, it is model name
  581. if name is None:
  582. name = '%s' % self._prettify_class_name(model.__name__)
  583. super(BaseModelView, self).__init__(name, category, endpoint, url, static_folder,
  584. menu_class_name=menu_class_name,
  585. menu_icon_type=menu_icon_type,
  586. menu_icon_value=menu_icon_value)
  587. # Actions
  588. self.init_actions()
  589. # Scaffolding
  590. self._refresh_cache()
  591. # Endpoint
  592. def _get_endpoint(self, endpoint):
  593. if endpoint:
  594. return super(BaseModelView, self)._get_endpoint(endpoint)
  595. return self.model.__name__.lower()
  596. # Caching
  597. def _refresh_forms_cache(self):
  598. # Forms
  599. self._form_ajax_refs = self._process_ajax_references()
  600. if self.form_widget_args is None:
  601. self.form_widget_args = {}
  602. self._create_form_class = self.get_create_form()
  603. self._edit_form_class = self.get_edit_form()
  604. self._delete_form_class = self.get_delete_form()
  605. self._action_form_class = self.get_action_form()
  606. # List View In-Line Editing
  607. if self.column_editable_list:
  608. self._list_form_class = self.get_list_form()
  609. else:
  610. self.column_editable_list = {}
  611. def _refresh_filters_cache(self):
  612. self._filters = self.get_filters()
  613. if self._filters:
  614. self._filter_groups = OrderedDict()
  615. self._filter_args = {}
  616. for i, flt in enumerate(self._filters):
  617. key = as_unicode(flt.name)
  618. if key not in self._filter_groups:
  619. self._filter_groups[key] = FilterGroup(flt.name)
  620. self._filter_groups[key].append({
  621. 'index': i,
  622. 'arg': self.get_filter_arg(i, flt),
  623. 'operation': flt.operation(),
  624. 'options': flt.get_options(self) or None,
  625. 'type': flt.data_type
  626. })
  627. self._filter_args[self.get_filter_arg(i, flt)] = (i, flt)
  628. else:
  629. self._filter_groups = None
  630. self._filter_args = None
  631. def _refresh_form_rules_cache(self):
  632. if self.form_create_rules:
  633. self._form_create_rules = rules.RuleSet(self, self.form_create_rules)
  634. else:
  635. self._form_create_rules = None
  636. if self.form_edit_rules:
  637. self._form_edit_rules = rules.RuleSet(self, self.form_edit_rules)
  638. else:
  639. self._form_edit_rules = None
  640. if self.form_rules:
  641. form_rules = rules.RuleSet(self, self.form_rules)
  642. if not self._form_create_rules:
  643. self._form_create_rules = form_rules
  644. if not self._form_edit_rules:
  645. self._form_edit_rules = form_rules
  646. def _refresh_cache(self):
  647. """
  648. Refresh various cached variables.
  649. """
  650. # List view
  651. self._list_columns = self.get_list_columns()
  652. self._sortable_columns = self.get_sortable_columns()
  653. # Details view
  654. if self.can_view_details:
  655. self._details_columns = self.get_details_columns()
  656. # Export view
  657. self._export_columns = self.get_export_columns()
  658. # Labels
  659. if self.column_labels is None:
  660. self.column_labels = {}
  661. # Forms
  662. self._refresh_forms_cache()
  663. # Search
  664. self._search_supported = self.init_search()
  665. # Choices
  666. if self.column_choices:
  667. self._column_choices_map = dict([
  668. (column, dict(choices))
  669. for column, choices in self.column_choices.items()
  670. ])
  671. else:
  672. self.column_choices = self._column_choices_map = dict()
  673. # Column formatters
  674. if self.column_formatters_export is None:
  675. self.column_formatters_export = self.column_formatters
  676. # Type formatters
  677. if self.column_type_formatters is None:
  678. self.column_type_formatters = dict(typefmt.BASE_FORMATTERS)
  679. if self.column_type_formatters_export is None:
  680. self.column_type_formatters_export = dict(typefmt.EXPORT_FORMATTERS)
  681. if self.column_descriptions is None:
  682. self.column_descriptions = dict()
  683. # Filters
  684. self._refresh_filters_cache()
  685. # Form rendering rules
  686. self._refresh_form_rules_cache()
  687. # Process form rules
  688. self._validate_form_class(self._form_edit_rules, self._edit_form_class)
  689. self._validate_form_class(self._form_create_rules, self._create_form_class)
  690. # Primary key
  691. def get_pk_value(self, model):
  692. """
  693. Return PK value from a model object.
  694. """
  695. raise NotImplementedError()
  696. # List view
  697. def scaffold_list_columns(self):
  698. """
  699. Return list of the model field names. Must be implemented in
  700. the child class.
  701. Expected return format is list of tuples with field name and
  702. display text. For example::
  703. ['name', 'first_name', 'last_name']
  704. """
  705. raise NotImplementedError('Please implement scaffold_list_columns method')
  706. def get_column_name(self, field):
  707. """
  708. Return a human-readable column name.
  709. :param field:
  710. Model field name.
  711. """
  712. if self.column_labels and field in self.column_labels:
  713. return self.column_labels[field]
  714. else:
  715. return self._prettify_name(field)
  716. def get_list_row_actions(self):
  717. """
  718. Return list of row action objects, each is instance of
  719. :class:`~flask_admin.model.template.BaseListRowAction`
  720. """
  721. actions = []
  722. if self.can_view_details:
  723. if self.details_modal:
  724. actions.append(template.ViewPopupRowAction())
  725. else:
  726. actions.append(template.ViewRowAction())
  727. if self.can_edit:
  728. if self.edit_modal:
  729. actions.append(template.EditPopupRowAction())
  730. else:
  731. actions.append(template.EditRowAction())
  732. if self.can_delete:
  733. actions.append(template.DeleteRowAction())
  734. return actions + (self.column_extra_row_actions or [])
  735. def get_column_names(self, only_columns, excluded_columns):
  736. """
  737. Returns a list of tuples with the model field name and formatted
  738. field name.
  739. :param only_columns:
  740. List of columns to include in the results. If not set,
  741. `scaffold_list_columns` will generate the list from the model.
  742. :param excluded_columns:
  743. List of columns to exclude from the results if `only_columns`
  744. is not set.
  745. """
  746. if excluded_columns:
  747. only_columns = [c for c in only_columns if c not in excluded_columns]
  748. return [(c, self.get_column_name(c)) for c in only_columns]
  749. def get_list_columns(self):
  750. """
  751. Uses `get_column_names` to get a list of tuples with the model
  752. field name and formatted name for the columns in `column_list`
  753. and not in `column_exclude_list`. If `column_list` is not set,
  754. the columns from `scaffold_list_columns` will be used.
  755. """
  756. return self.get_column_names(
  757. only_columns=self.column_list or self.scaffold_list_columns(),
  758. excluded_columns=self.column_exclude_list,
  759. )
  760. def get_details_columns(self):
  761. """
  762. Uses `get_column_names` to get a list of tuples with the model
  763. field name and formatted name for the columns in `column_details_list`
  764. and not in `column_details_exclude_list`. If `column_details_list`
  765. is not set, the columns from `scaffold_list_columns` will be used.
  766. """
  767. try:
  768. only_columns = self.column_details_list or self.scaffold_list_columns()
  769. except NotImplementedError:
  770. raise Exception('Please define column_details_list')
  771. return self.get_column_names(
  772. only_columns=only_columns,
  773. excluded_columns=self.column_details_exclude_list,
  774. )
  775. def get_export_columns(self):
  776. """
  777. Uses `get_column_names` to get a list of tuples with the model
  778. field name and formatted name for the columns in `column_export_list`
  779. and not in `column_export_exclude_list`. If `column_export_list` is
  780. not set, it will attempt to use the columns from `column_list`
  781. or finally the columns from `scaffold_list_columns` will be used.
  782. """
  783. only_columns = (self.column_export_list or self.column_list or
  784. self.scaffold_list_columns())
  785. return self.get_column_names(
  786. only_columns=only_columns,
  787. excluded_columns=self.column_export_exclude_list,
  788. )
  789. def scaffold_sortable_columns(self):
  790. """
  791. Returns dictionary of sortable columns. Must be implemented in
  792. the child class.
  793. Expected return format is a dictionary, where keys are field names and
  794. values are property names.
  795. """
  796. raise NotImplementedError('Please implement scaffold_sortable_columns method')
  797. def get_sortable_columns(self):
  798. """
  799. Returns a dictionary of the sortable columns. Key is a model
  800. field name and value is sort column (for example - attribute).
  801. If `column_sortable_list` is set, will use it. Otherwise, will call
  802. `scaffold_sortable_columns` to get them from the model.
  803. """
  804. if self.column_sortable_list is None:
  805. return self.scaffold_sortable_columns() or dict()
  806. else:
  807. result = dict()
  808. for c in self.column_sortable_list:
  809. if isinstance(c, tuple):
  810. result[c[0]] = c[1]
  811. else:
  812. result[c] = c
  813. return result
  814. def init_search(self):
  815. """
  816. Initialize search. If data provider does not support search,
  817. `init_search` will return `False`.
  818. """
  819. return False
  820. # Filter helpers
  821. def scaffold_filters(self, name):
  822. """
  823. Generate filter object for the given name
  824. :param name:
  825. Name of the field
  826. """
  827. return None
  828. def is_valid_filter(self, filter):
  829. """
  830. Verify that the provided filter object is valid.
  831. Override in model backend implementation to verify if
  832. the provided filter type is allowed.
  833. :param filter:
  834. Filter object to verify.
  835. """
  836. return isinstance(filter, filters.BaseFilter)
  837. def handle_filter(self, filter):
  838. """
  839. Postprocess (add joins, etc) for a filter.
  840. :param filter:
  841. Filter object to postprocess
  842. """
  843. return filter
  844. def get_filters(self):
  845. """
  846. Return a list of filter objects.
  847. If your model backend implementation does not support filters,
  848. override this method and return `None`.
  849. """
  850. if self.column_filters:
  851. collection = []
  852. for n in self.column_filters:
  853. if self.is_valid_filter(n):
  854. collection.append(self.handle_filter(n))
  855. else:
  856. flt = self.scaffold_filters(n)
  857. if flt:
  858. collection.extend(flt)
  859. else:
  860. raise Exception('Unsupported filter type %s' % n)
  861. return collection
  862. else:
  863. return None
  864. def get_filter_arg(self, index, flt):
  865. """
  866. Given a filter `flt`, return a unique name for that filter in
  867. this view.
  868. Does not include the `flt[n]_` portion of the filter name.
  869. :param index:
  870. Filter index in _filters array
  871. :param flt:
  872. Filter instance
  873. """
  874. if self.named_filter_urls:
  875. operation = flt.operation()
  876. try:
  877. # get lazy string original value
  878. operation = operation._args[0]
  879. except AttributeError:
  880. pass
  881. name = ('%s %s' % (flt.name, as_unicode(operation))).lower()
  882. name = filter_char_re.sub('', name)
  883. name = filter_compact_re.sub('_', name)
  884. return name
  885. else:
  886. return str(index)
  887. def _get_filter_groups(self):
  888. """
  889. Returns non-lazy version of filter strings
  890. """
  891. if self._filter_groups:
  892. results = OrderedDict()
  893. for group in itervalues(self._filter_groups):
  894. key, items = group.non_lazy()
  895. results[key] = items
  896. return results
  897. return None
  898. # Form helpers
  899. def scaffold_form(self):
  900. """
  901. Create `form.BaseForm` inherited class from the model. Must be
  902. implemented in the child class.
  903. """
  904. raise NotImplementedError('Please implement scaffold_form method')
  905. def scaffold_list_form(self, widget=None, validators=None):
  906. """
  907. Create form for the `index_view` using only the columns from
  908. `self.column_editable_list`.
  909. :param widget:
  910. WTForms widget class. Defaults to `XEditableWidget`.
  911. :param validators:
  912. `form_args` dict with only validators
  913. {'name': {'validators': [DataRequired()]}}
  914. Must be implemented in the child class.
  915. """
  916. raise NotImplementedError('Please implement scaffold_list_form method')
  917. def get_form(self):
  918. """
  919. Get form class.
  920. If ``self.form`` is set, will return it and will call
  921. ``self.scaffold_form`` otherwise.
  922. Override to implement customized behavior.
  923. """
  924. if self.form is not None:
  925. return self.form
  926. return self.scaffold_form()
  927. def get_list_form(self):
  928. """
  929. Get form class for the editable list view.
  930. Uses only validators from `form_args` to build the form class.
  931. Allows overriding the editable list view field/widget. For example::
  932. from flask_admin.model.widgets import XEditableWidget
  933. class CustomWidget(XEditableWidget):
  934. def get_kwargs(self, subfield, kwargs):
  935. if subfield.type == 'TextAreaField':
  936. kwargs['data-type'] = 'textarea'
  937. kwargs['data-rows'] = '20'
  938. # elif: kwargs for other fields
  939. return kwargs
  940. class MyModelView(BaseModelView):
  941. def get_list_form(self):
  942. return self.scaffold_list_form(widget=CustomWidget)
  943. """
  944. if self.form_args:
  945. # get only validators, other form_args can break FieldList wrapper
  946. validators = dict(
  947. (key, {'validators': value["validators"]})
  948. for key, value in iteritems(self.form_args)
  949. if value.get("validators")
  950. )
  951. else:
  952. validators = None
  953. return self.scaffold_list_form(validators=validators)
  954. def get_create_form(self):
  955. """
  956. Create form class for model creation view.
  957. Override to implement customized behavior.
  958. """
  959. return self.get_form()
  960. def get_edit_form(self):
  961. """
  962. Create form class for model editing view.
  963. Override to implement customized behavior.
  964. """
  965. return self.get_form()
  966. def get_delete_form(self):
  967. """
  968. Create form class for model delete view.
  969. Override to implement customized behavior.
  970. """
  971. class DeleteForm(self.form_base_class):
  972. id = HiddenField(validators=[InputRequired()])
  973. url = HiddenField()
  974. return DeleteForm
  975. def get_action_form(self):
  976. """
  977. Create form class for a model action.
  978. Override to implement customized behavior.
  979. """
  980. class ActionForm(self.form_base_class):
  981. action = HiddenField()
  982. url = HiddenField()
  983. # rowid is retrieved using getlist, for backward compatibility
  984. return ActionForm
  985. def create_form(self, obj=None):
  986. """
  987. Instantiate model creation form and return it.
  988. Override to implement custom behavior.
  989. """
  990. return self._create_form_class(get_form_data(), obj=obj)
  991. def edit_form(self, obj=None):
  992. """
  993. Instantiate model editing form and return it.
  994. Override to implement custom behavior.
  995. """
  996. return self._edit_form_class(get_form_data(), obj=obj)
  997. def delete_form(self):
  998. """
  999. Instantiate model delete form and return it.
  1000. Override to implement custom behavior.
  1001. The delete form originally used a GET request, so delete_form
  1002. accepts both GET and POST request for backwards compatibility.
  1003. """
  1004. if request.form:
  1005. return self._delete_form_class(request.form)
  1006. elif request.args:
  1007. # allow request.args for backward compatibility
  1008. return self._delete_form_class(request.args)
  1009. else:
  1010. return self._delete_form_class()
  1011. def list_form(self, obj=None):
  1012. """
  1013. Instantiate model editing form for list view and return it.
  1014. Override to implement custom behavior.
  1015. """
  1016. return self._list_form_class(get_form_data(), obj=obj)
  1017. def action_form(self, obj=None):
  1018. """
  1019. Instantiate model action form and return it.
  1020. Override to implement custom behavior.
  1021. """
  1022. return self._action_form_class(get_form_data(), obj=obj)
  1023. def validate_form(self, form):
  1024. """
  1025. Validate the form on submit.
  1026. :param form:
  1027. Form to validate
  1028. """
  1029. return validate_form_on_submit(form)
  1030. def get_save_return_url(self, model, is_created=False):
  1031. """
  1032. Return url where user is redirected after successful form save.
  1033. :param model:
  1034. Saved object
  1035. :param is_created:
  1036. Whether new object was created or existing one was updated
  1037. For example, redirect use to object details view after form save::
  1038. class MyModelView(ModelView):
  1039. can_view_details = True
  1040. def get_save_return_url(self, model, is_created):
  1041. return self.get_url('.details_view', id=model.id)
  1042. """
  1043. return get_redirect_target() or self.get_url('.index_view')
  1044. def _get_ruleset_missing_fields(self, ruleset, form):
  1045. missing_fields = []
  1046. if ruleset:
  1047. visible_fields = ruleset.visible_fields
  1048. for field in form:
  1049. if field.name not in visible_fields:
  1050. missing_fields.append(field.name)
  1051. return missing_fields
  1052. def _show_missing_fields_warning(self, text):
  1053. warnings.warn(text)
  1054. def _validate_form_class(self, ruleset, form_class, remove_missing=True):
  1055. form_fields = []
  1056. for name, obj in iteritems(form_class.__dict__):
  1057. if isinstance(obj, UnboundField):
  1058. form_fields.append(name)
  1059. missing_fields = []
  1060. if ruleset:
  1061. visible_fields = ruleset.visible_fields
  1062. for field_name in form_fields:
  1063. if field_name not in visible_fields:
  1064. missing_fields.append(field_name)
  1065. if missing_fields:
  1066. self._show_missing_fields_warning('Fields missing from ruleset: %s' % (','.join(missing_fields)))
  1067. if remove_missing:
  1068. self._remove_fields_from_form_class(missing_fields, form_class)
  1069. def _validate_form_instance(self, ruleset, form, remove_missing=True):
  1070. missing_fields = self._get_ruleset_missing_fields(ruleset=ruleset, form=form)
  1071. if missing_fields:
  1072. self._show_missing_fields_warning('Fields missing from ruleset: %s' % (','.join(missing_fields)))
  1073. if remove_missing:
  1074. self._remove_fields_from_form_instance(missing_fields, form)
  1075. def _remove_fields_from_form_instance(self, field_names, form):
  1076. for field_name in field_names:
  1077. form.__delitem__(field_name)
  1078. def _remove_fields_from_form_class(self, field_names, form_class):
  1079. for field_name in field_names:
  1080. delattr(form_class, field_name)
  1081. # Helpers
  1082. def is_sortable(self, name):
  1083. """
  1084. Verify if column is sortable.
  1085. Not case-sensitive.
  1086. :param name:
  1087. Column name.
  1088. """
  1089. return name.lower() in (x.lower() for x in self._sortable_columns)
  1090. def is_editable(self, name):
  1091. """
  1092. Verify if column is editable.
  1093. :param name:
  1094. Column name.
  1095. """
  1096. return name in self.column_editable_list
  1097. def _get_column_by_idx(self, idx):
  1098. """
  1099. Return column index by
  1100. """
  1101. if idx is None or idx < 0 or idx >= len(self._list_columns):
  1102. return None
  1103. return self._list_columns[idx]
  1104. def _get_default_order(self):
  1105. """
  1106. Return default sort order
  1107. """
  1108. if self.column_default_sort:
  1109. if isinstance(self.column_default_sort, tuple):
  1110. return self.column_default_sort
  1111. else:
  1112. return self.column_default_sort, False
  1113. return None
  1114. # Database-related API
  1115. def get_list(self, page, sort_field, sort_desc, search, filters,
  1116. page_size=None):
  1117. """
  1118. Return a paginated and sorted list of models from the data source.
  1119. Must be implemented in the child class.
  1120. :param page:
  1121. Page number, 0 based. Can be set to None if it is first page.
  1122. :param sort_field:
  1123. Sort column name or None.
  1124. :param sort_desc:
  1125. If set to True, sorting is in descending order.
  1126. :param search:
  1127. Search query
  1128. :param filters:
  1129. List of filter tuples. First value in a tuple is a search
  1130. index, second value is a search value.
  1131. :param page_size:
  1132. Number of results. Defaults to ModelView's page_size. Can be
  1133. overriden to change the page_size limit. Removing the page_size
  1134. limit requires setting page_size to 0 or False.
  1135. """
  1136. raise NotImplementedError('Please implement get_list method')
  1137. def get_one(self, id):
  1138. """
  1139. Return one model by its id.
  1140. Must be implemented in the child class.
  1141. :param id:
  1142. Model id
  1143. """
  1144. raise NotImplementedError('Please implement get_one method')
  1145. # Exception handler
  1146. def handle_view_exception(self, exc):
  1147. if isinstance(exc, ValidationError):
  1148. flash(as_unicode(exc), 'error')
  1149. return True
  1150. if current_app.config.get('ADMIN_RAISE_ON_VIEW_EXCEPTION'):
  1151. raise
  1152. if self._debug:
  1153. raise
  1154. return False
  1155. # Model event handlers
  1156. def on_model_change(self, form, model, is_created):
  1157. """
  1158. Perform some actions before a model is created or updated.
  1159. Called from create_model and update_model in the same transaction
  1160. (if it has any meaning for a store backend).
  1161. By default does nothing.
  1162. :param form:
  1163. Form used to create/update model
  1164. :param model:
  1165. Model that will be created/updated
  1166. :param is_created:
  1167. Will be set to True if model was created and to False if edited
  1168. """
  1169. pass
  1170. def _on_model_change(self, form, model, is_created):
  1171. """
  1172. Compatibility helper.
  1173. """
  1174. try:
  1175. self.on_model_change(form, model, is_created)
  1176. except TypeError:
  1177. msg = ('%s.on_model_change() now accepts third ' +
  1178. 'parameter is_created. Please update your code') % self.model
  1179. warnings.warn(msg)
  1180. self.on_model_change(form, model)
  1181. def after_model_change(self, form, model, is_created):
  1182. """
  1183. Perform some actions after a model was created or updated and
  1184. committed to the database.
  1185. Called from create_model after successful database commit.
  1186. By default does nothing.
  1187. :param form:
  1188. Form used to create/update model
  1189. :param model:
  1190. Model that was created/updated
  1191. :param is_created:
  1192. True if model was created, False if model was updated
  1193. """
  1194. pass
  1195. def on_model_delete(self, model):
  1196. """
  1197. Perform some actions before a model is deleted.
  1198. Called from delete_model in the same transaction
  1199. (if it has any meaning for a store backend).
  1200. By default do nothing.
  1201. """
  1202. pass
  1203. def after_model_delete(self, model):
  1204. """
  1205. Perform some actions after a model was deleted and
  1206. committed to the database.
  1207. Called from delete_model after successful database commit
  1208. (if it has any meaning for a store backend).
  1209. By default does nothing.
  1210. :param model:
  1211. Model that was deleted
  1212. """
  1213. pass
  1214. def on_form_prefill(self, form, id):
  1215. """
  1216. Perform additional actions to pre-fill the edit form.
  1217. Called from edit_view, if the current action is rendering
  1218. the form rather than receiving client side input, after
  1219. default pre-filling has been performed.
  1220. By default does nothing.
  1221. You only need to override this if you have added custom
  1222. fields that depend on the database contents in a way that
  1223. Flask-admin can't figure out by itself. Fields that were
  1224. added by name of a normal column or relationship should
  1225. work out of the box.
  1226. :param form:
  1227. Form instance
  1228. :param id:
  1229. id of the object that is going to be edited
  1230. """
  1231. pass
  1232. def create_model(self, form):
  1233. """
  1234. Create model from the form.
  1235. Returns the model instance if operation succeeded.
  1236. Must be implemented in the child class.
  1237. :param form:
  1238. Form instance
  1239. """
  1240. raise NotImplementedError()
  1241. def update_model(self, form, model):
  1242. """
  1243. Update model from the form.
  1244. Returns `True` if operation succeeded.
  1245. Must be implemented in the child class.
  1246. :param form:
  1247. Form instance
  1248. :param model:
  1249. Model instance
  1250. """
  1251. raise NotImplementedError()
  1252. def delete_model(self, model):
  1253. """
  1254. Delete model.
  1255. Returns `True` if operation succeeded.
  1256. Must be implemented in the child class.
  1257. :param model:
  1258. Model instance
  1259. """
  1260. raise NotImplementedError()
  1261. # Various helpers
  1262. def _prettify_name(self, name):
  1263. """
  1264. Prettify pythonic variable name.
  1265. For example, 'hello_world' will be converted to 'Hello World'
  1266. :param name:
  1267. Name to prettify
  1268. """
  1269. return prettify_name(name)
  1270. def get_empty_list_message(self):
  1271. return gettext('There are no items in the table.')
  1272. # URL generation helpers
  1273. def _get_list_filter_args(self):
  1274. if self._filters:
  1275. filters = []
  1276. for n in request.args:
  1277. if not n.startswith('flt'):
  1278. continue
  1279. if '_' not in n:
  1280. continue
  1281. pos, key = n[3:].split('_', 1)
  1282. if key in self._filter_args:
  1283. idx, flt = self._filter_args[key]
  1284. value = request.args[n]
  1285. if flt.validate(value):
  1286. filters.append((pos, (idx, as_unicode(flt.name), value)))
  1287. else:
  1288. flash(gettext('Invalid Filter Value: %(value)s', value=value), 'error')
  1289. # Sort filters
  1290. return [v[1] for v in sorted(filters, key=lambda n: n[0])]
  1291. return None
  1292. def _get_list_extra_args(self):
  1293. """
  1294. Return arguments from query string.
  1295. """
  1296. return ViewArgs(page=request.args.get('page', 0, type=int),
  1297. page_size=request.args.get('page_size', 0, type=int),
  1298. sort=request.args.get('sort', None, type=int),
  1299. sort_desc=request.args.get('desc', None, type=int),
  1300. search=request.args.get('search', None),
  1301. filters=self._get_list_filter_args())
  1302. def _get_filters(self, filters):
  1303. """
  1304. Get active filters as dictionary of URL arguments and values
  1305. :param filters:
  1306. List of filters from ViewArgs object
  1307. """
  1308. kwargs = {}
  1309. if filters:
  1310. for i, pair in enumerate(filters):
  1311. idx, flt_name, value = pair
  1312. key = 'flt%d_%s' % (i, self.get_filter_arg(idx, self._filters[idx]))
  1313. kwargs[key] = value
  1314. return kwargs
  1315. # URL generation helpers
  1316. def _get_list_url(self, view_args):
  1317. """
  1318. Generate page URL with current page, sort column and
  1319. other parameters.
  1320. :param view:
  1321. View name
  1322. :param view_args:
  1323. ViewArgs object with page number, filters, etc.
  1324. """
  1325. page = view_args.page or None
  1326. desc = 1 if view_args.sort_desc else None
  1327. kwargs = dict(page=page, sort=view_args.sort, desc=desc, search=view_args.search)
  1328. kwargs.update(view_args.extra_args)
  1329. if view_args.page_size:
  1330. kwargs['page_size'] = view_args.page_size
  1331. kwargs.update(self._get_filters(view_args.filters))
  1332. return self.get_url('.index_view', **kwargs)
  1333. # Actions
  1334. def is_action_allowed(self, name):
  1335. """
  1336. Override this method to allow or disallow actions based
  1337. on some condition.
  1338. The default implementation only checks if the particular action
  1339. is not in `action_disallowed_list`.
  1340. """
  1341. return name not in self.action_disallowed_list
  1342. def _get_field_value(self, model, name):
  1343. """
  1344. Get unformatted field value from the model
  1345. """
  1346. return rec_getattr(model, name)
  1347. def _get_list_value(self, context, model, name, column_formatters,
  1348. column_type_formatters):
  1349. """
  1350. Returns the value to be displayed.
  1351. :param context:
  1352. :py:class:`jinja2.runtime.Context` if available
  1353. :param model:
  1354. Model instance
  1355. :param name:
  1356. Field name
  1357. :param column_formatters:
  1358. column_formatters to be used.
  1359. :param column_type_formatters:
  1360. column_type_formatters to be used.
  1361. """
  1362. column_fmt = column_formatters.get(name)
  1363. if column_fmt is not None:
  1364. value = column_fmt(self, context, model, name)
  1365. else:
  1366. value = self._get_field_value(model, name)
  1367. choices_map = self._column_choices_map.get(name, {})
  1368. if choices_map:
  1369. return choices_map.get(value) or value
  1370. type_fmt = None
  1371. for typeobj, formatter in column_type_formatters.items():
  1372. if isinstance(value, typeobj):
  1373. type_fmt = formatter
  1374. break
  1375. if type_fmt is not None:
  1376. value = type_fmt(self, value)
  1377. return value
  1378. @contextfunction
  1379. def get_list_value(self, context, model, name):
  1380. """
  1381. Returns the value to be displayed in the list view
  1382. :param context:
  1383. :py:class:`jinja2.runtime.Context`
  1384. :param model:
  1385. Model instance
  1386. :param name:
  1387. Field name
  1388. """
  1389. return self._get_list_value(
  1390. context,
  1391. model,
  1392. name,
  1393. self.column_formatters,
  1394. self.column_type_formatters,
  1395. )
  1396. def get_export_value(self, model, name):
  1397. """
  1398. Returns the value to be displayed in export.
  1399. Allows export to use different (non HTML) formatters.
  1400. :param model:
  1401. Model instance
  1402. :param name:
  1403. Field name
  1404. """
  1405. return self._get_list_value(
  1406. None,
  1407. model,
  1408. name,
  1409. self.column_formatters_export,
  1410. self.column_type_formatters_export,
  1411. )
  1412. def get_export_name(self, export_type='csv'):
  1413. """
  1414. :return: The exported csv file name.
  1415. """
  1416. filename = '%s_%s.%s' % (self.name,
  1417. time.strftime("%Y-%m-%d_%H-%M-%S"),
  1418. export_type)
  1419. return filename
  1420. # AJAX references
  1421. def _process_ajax_references(self):
  1422. """
  1423. Process `form_ajax_refs` and generate model loaders that
  1424. will be used by the `ajax_lookup` view.
  1425. """
  1426. result = {}
  1427. if self.form_ajax_refs:
  1428. for name, options in iteritems(self.form_ajax_refs):
  1429. if isinstance(options, dict):
  1430. result[name] = self._create_ajax_loader(name, options)
  1431. elif isinstance(options, AjaxModelLoader):
  1432. result[name] = options
  1433. else:
  1434. raise ValueError('%s.form_ajax_refs can not handle %s types' % (self, type(options)))
  1435. return result
  1436. def _create_ajax_loader(self, name, options):
  1437. """
  1438. Model backend will override this to implement AJAX model loading.
  1439. """
  1440. raise NotImplementedError()
  1441. # Views
  1442. @expose('/')
  1443. def index_view(self):
  1444. """
  1445. List view
  1446. """
  1447. if self.can_delete:
  1448. delete_form = self.delete_form()
  1449. else:
  1450. delete_form = None
  1451. # Grab parameters from URL
  1452. view_args = self._get_list_extra_args()
  1453. # Map column index to column name
  1454. sort_column = self._get_column_by_idx(view_args.sort)
  1455. if sort_column is not None:
  1456. sort_column = sort_column[0]
  1457. # Get page size
  1458. page_size = view_args.page_size or self.page_size
  1459. # Get count and data
  1460. count, data = self.get_list(view_args.page, sort_column, view_args.sort_desc,
  1461. view_args.search, view_args.filters, page_size=page_size)
  1462. list_forms = {}
  1463. if self.column_editable_list:
  1464. for row in data:
  1465. list_forms[self.get_pk_value(row)] = self.list_form(obj=row)
  1466. # Calculate number of pages
  1467. if count is not None and page_size:
  1468. num_pages = int(ceil(count / float(page_size)))
  1469. elif not page_size:
  1470. num_pages = 0 # hide pager for unlimited page_size
  1471. else:
  1472. num_pages = None # use simple pager
  1473. # Various URL generation helpers
  1474. def pager_url(p):
  1475. # Do not add page number if it is first page
  1476. if p == 0:
  1477. p = None
  1478. return self._get_list_url(view_args.clone(page=p))
  1479. def sort_url(column, invert=False, desc=None):
  1480. if not desc and invert and not view_args.sort_desc:
  1481. desc = 1
  1482. return self._get_list_url(view_args.clone(sort=column, sort_desc=desc))
  1483. def page_size_url(s):
  1484. if not s:
  1485. s = self.page_size
  1486. return self._get_list_url(view_args.clone(page_size=s))
  1487. # Actions
  1488. actions, actions_confirmation = self.get_actions_list()
  1489. if actions:
  1490. action_form = self.action_form()
  1491. else:
  1492. action_form = None
  1493. clear_search_url = self._get_list_url(view_args.clone(page=0,
  1494. sort=view_args.sort,
  1495. sort_desc=view_args.sort_desc,
  1496. search=None,
  1497. filters=None))
  1498. return self.render(
  1499. self.list_template,
  1500. data=data,
  1501. list_forms=list_forms,
  1502. delete_form=delete_form,
  1503. action_form=action_form,
  1504. # List
  1505. list_columns=self._list_columns,
  1506. sortable_columns=self._sortable_columns,
  1507. editable_columns=self.column_editable_list,
  1508. list_row_actions=self.get_list_row_actions(),
  1509. # Pagination
  1510. count=count,
  1511. pager_url=pager_url,
  1512. num_pages=num_pages,
  1513. can_set_page_size=self.can_set_page_size,
  1514. page_size_url=page_size_url,
  1515. page=view_args.page,
  1516. page_size=page_size,
  1517. default_page_size=self.page_size,
  1518. # Sorting
  1519. sort_column=view_args.sort,
  1520. sort_desc=view_args.sort_desc,
  1521. sort_url=sort_url,
  1522. # Search
  1523. search_supported=self._search_supported,
  1524. clear_search_url=clear_search_url,
  1525. search=view_args.search,
  1526. # Filters
  1527. filters=self._filters,
  1528. filter_groups=self._get_filter_groups(),
  1529. active_filters=view_args.filters,
  1530. filter_args=self._get_filters(view_args.filters),
  1531. # Actions
  1532. actions=actions,
  1533. actions_confirmation=actions_confirmation,
  1534. # Misc
  1535. enumerate=enumerate,
  1536. get_pk_value=self.get_pk_value,
  1537. get_value=self.get_list_value,
  1538. return_url=self._get_list_url(view_args),
  1539. )
  1540. @expose('/new/', methods=('GET', 'POST'))
  1541. def create_view(self):
  1542. """
  1543. Create model view
  1544. """
  1545. return_url = get_redirect_target() or self.get_url('.index_view')
  1546. if not self.can_create:
  1547. return redirect(return_url)
  1548. form = self.create_form()
  1549. if not hasattr(form, '_validated_ruleset') or not form._validated_ruleset:
  1550. self._validate_form_instance(ruleset=self._form_create_rules, form=form)
  1551. if self.validate_form(form):
  1552. # in versions 1.1.0 and before, this returns a boolean
  1553. # in later versions, this is the model itself
  1554. model = self.create_model(form)
  1555. if model:
  1556. flash(gettext('Record was successfully created.'), 'success')
  1557. if '_add_another' in request.form:
  1558. return redirect(request.url)
  1559. elif '_continue_editing' in request.form:
  1560. # if we have a valid model, try to go to the edit view
  1561. if model is not True:
  1562. url = self.get_url('.edit_view', id=self.get_pk_value(model), url=return_url)
  1563. else:
  1564. url = return_url
  1565. return redirect(url)
  1566. else:
  1567. # save button
  1568. return redirect(self.get_save_return_url(model, is_created=True))
  1569. form_opts = FormOpts(widget_args=self.form_widget_args,
  1570. form_rules=self._form_create_rules)
  1571. if self.create_modal and request.args.get('modal'):
  1572. template = self.create_modal_template
  1573. else:
  1574. template = self.create_template
  1575. return self.render(template,
  1576. form=form,
  1577. form_opts=form_opts,
  1578. return_url=return_url)
  1579. @expose('/edit/', methods=('GET', 'POST'))
  1580. def edit_view(self):
  1581. """
  1582. Edit model view
  1583. """
  1584. return_url = get_redirect_target() or self.get_url('.index_view')
  1585. if not self.can_edit:
  1586. return redirect(return_url)
  1587. id = get_mdict_item_or_list(request.args, 'id')
  1588. if id is None:
  1589. return redirect(return_url)
  1590. model = self.get_one(id)
  1591. if model is None:
  1592. flash(gettext('Record does not exist.'), 'error')
  1593. return redirect(return_url)
  1594. form = self.edit_form(obj=model)
  1595. if not hasattr(form, '_validated_ruleset') or not form._validated_ruleset:
  1596. self._validate_form_instance(ruleset=self._form_edit_rules, form=form)
  1597. if self.validate_form(form):
  1598. if self.update_model(form, model):
  1599. flash(gettext('Record was successfully saved.'), 'success')
  1600. if '_add_another' in request.form:
  1601. return redirect(self.get_url('.create_view', url=return_url))
  1602. elif '_continue_editing' in request.form:
  1603. return redirect(request.url)
  1604. else:
  1605. # save button
  1606. return redirect(self.get_save_return_url(model, is_created=False))
  1607. if request.method == 'GET' or form.errors:
  1608. self.on_form_prefill(form, id)
  1609. form_opts = FormOpts(widget_args=self.form_widget_args,
  1610. form_rules=self._form_edit_rules)
  1611. if self.edit_modal and request.args.get('modal'):
  1612. template = self.edit_modal_template
  1613. else:
  1614. template = self.edit_template
  1615. return self.render(template,
  1616. model=model,
  1617. form=form,
  1618. form_opts=form_opts,
  1619. return_url=return_url)
  1620. @expose('/details/')
  1621. def details_view(self):
  1622. """
  1623. Details model view
  1624. """
  1625. return_url = get_redirect_target() or self.get_url('.index_view')
  1626. if not self.can_view_details:
  1627. return redirect(return_url)
  1628. id = get_mdict_item_or_list(request.args, 'id')
  1629. if id is None:
  1630. return redirect(return_url)
  1631. model = self.get_one(id)
  1632. if model is None:
  1633. flash(gettext('Record does not exist.'), 'error')
  1634. return redirect(return_url)
  1635. if self.details_modal and request.args.get('modal'):
  1636. template = self.details_modal_template
  1637. else:
  1638. template = self.details_template
  1639. return self.render(template,
  1640. model=model,
  1641. details_columns=self._details_columns,
  1642. get_value=self.get_list_value,
  1643. return_url=return_url)
  1644. @expose('/delete/', methods=('POST',))
  1645. def delete_view(self):
  1646. """
  1647. Delete model view. Only POST method is allowed.
  1648. """
  1649. return_url = get_redirect_target() or self.get_url('.index_view')
  1650. if not self.can_delete:
  1651. return redirect(return_url)
  1652. form = self.delete_form()
  1653. if self.validate_form(form):
  1654. # id is InputRequired()
  1655. id = form.id.data
  1656. model = self.get_one(id)
  1657. if model is None:
  1658. flash(gettext('Record does not exist.'), 'error')
  1659. return redirect(return_url)
  1660. # message is flashed from within delete_model if it fails
  1661. if self.delete_model(model):
  1662. flash(gettext('Record was successfully deleted.'), 'success')
  1663. return redirect(return_url)
  1664. else:
  1665. flash_errors(form, message='Failed to delete record. %(error)s')
  1666. return redirect(return_url)
  1667. @expose('/action/', methods=('POST',))
  1668. def action_view(self):
  1669. """
  1670. Mass-model action view.
  1671. """
  1672. return self.handle_action()
  1673. def _export_data(self):
  1674. # Macros in column_formatters are not supported.
  1675. # Macros will have a function name 'inner'
  1676. # This causes non-macro functions named 'inner' not work.
  1677. for col, func in iteritems(self.column_formatters_export):
  1678. # skip checking columns not being exported
  1679. if col not in [col for col, _ in self._export_columns]:
  1680. continue
  1681. if func.__name__ == 'inner':
  1682. raise NotImplementedError(
  1683. 'Macros are not implemented in export. Exclude column in'
  1684. ' column_formatters_export, column_export_list, or '
  1685. ' column_export_exclude_list. Column: %s' % (col,)
  1686. )
  1687. # Grab parameters from URL
  1688. view_args = self._get_list_extra_args()
  1689. # Map column index to column name
  1690. sort_column = self._get_column_by_idx(view_args.sort)
  1691. if sort_column is not None:
  1692. sort_column = sort_column[0]
  1693. # Get count and data
  1694. count, data = self.get_list(0, sort_column, view_args.sort_desc,
  1695. view_args.search, view_args.filters,
  1696. page_size=self.export_max_rows)
  1697. return count, data
  1698. @expose('/export/<export_type>/')
  1699. def export(self, export_type):
  1700. return_url = get_redirect_target() or self.get_url('.index_view')
  1701. if not self.can_export or (export_type not in self.export_types):
  1702. flash(gettext('Permission denied.'), 'error')
  1703. return redirect(return_url)
  1704. if export_type == 'csv':
  1705. return self._export_csv(return_url)
  1706. else:
  1707. return self._export_tablib(export_type, return_url)
  1708. def _export_csv(self, return_url):
  1709. """
  1710. Export a CSV of records as a stream.
  1711. """
  1712. count, data = self._export_data()
  1713. # https://docs.djangoproject.com/en/1.8/howto/outputting-csv/
  1714. class Echo(object):
  1715. """
  1716. An object that implements just the write method of the file-like
  1717. interface.
  1718. """
  1719. def write(self, value):
  1720. """
  1721. Write the value by returning it, instead of storing
  1722. in a buffer.
  1723. """
  1724. return value
  1725. writer = csv.writer(Echo())
  1726. def generate():
  1727. # Append the column titles at the beginning
  1728. titles = [csv_encode(c[1]) for c in self._export_columns]
  1729. yield writer.writerow(titles)
  1730. for row in data:
  1731. vals = [csv_encode(self.get_export_value(row, c[0]))
  1732. for c in self._export_columns]
  1733. yield writer.writerow(vals)
  1734. filename = self.get_export_name(export_type='csv')
  1735. disposition = 'attachment;filename=%s' % (secure_filename(filename),)
  1736. return Response(
  1737. stream_with_context(generate()),
  1738. headers={'Content-Disposition': disposition},
  1739. mimetype='text/csv'
  1740. )
  1741. def _export_tablib(self, export_type, return_url):
  1742. """
  1743. Exports a variety of formats using the tablib library.
  1744. """
  1745. if tablib is None:
  1746. flash(gettext('Tablib dependency not installed.'), 'error')
  1747. return redirect(return_url)
  1748. filename = self.get_export_name(export_type)
  1749. disposition = 'attachment;filename=%s' % (secure_filename(filename),)
  1750. mimetype, encoding = mimetypes.guess_type(filename)
  1751. if not mimetype:
  1752. mimetype = 'application/octet-stream'
  1753. if encoding:
  1754. mimetype = '%s; charset=%s' % (mimetype, encoding)
  1755. ds = tablib.Dataset(headers=[c[1] for c in self._export_columns])
  1756. count, data = self._export_data()
  1757. for row in data:
  1758. vals = [self.get_export_value(row, c[0]) for c in self._export_columns]
  1759. ds.append(vals)
  1760. try:
  1761. try:
  1762. response_data = ds.export(format=export_type)
  1763. except AttributeError:
  1764. response_data = getattr(ds, export_type)
  1765. except (AttributeError, tablib.UnsupportedFormat):
  1766. flash(gettext('Export type "%(type)s not supported.',
  1767. type=export_type), 'error')
  1768. return redirect(return_url)
  1769. return Response(
  1770. response_data,
  1771. headers={'Content-Disposition': disposition},
  1772. mimetype=mimetype,
  1773. )
  1774. @expose('/ajax/lookup/')
  1775. def ajax_lookup(self):
  1776. name = request.args.get('name')
  1777. query = request.args.get('query')
  1778. offset = request.args.get('offset', type=int)
  1779. limit = request.args.get('limit', 10, type=int)
  1780. loader = self._form_ajax_refs.get(name)
  1781. if not loader:
  1782. abort(404)
  1783. data = [loader.format(m) for m in loader.get_list(query, offset, limit)]
  1784. return Response(json.dumps(data), mimetype='application/json')
  1785. @expose('/ajax/update/', methods=('POST',))
  1786. def ajax_update(self):
  1787. """
  1788. Edits a single column of a record in list view.
  1789. """
  1790. if not self.column_editable_list:
  1791. abort(404)
  1792. form = self.list_form()
  1793. # prevent validation issues due to submitting a single field
  1794. # delete all fields except the submitted fields and csrf token
  1795. for field in list(form):
  1796. if (field.name in request.form) or (field.name == 'csrf_token'):
  1797. pass
  1798. else:
  1799. form.__delitem__(field.name)
  1800. if self.validate_form(form):
  1801. pk = form.list_form_pk.data
  1802. record = self.get_one(pk)
  1803. if record is None:
  1804. return gettext('Record does not exist.'), 500
  1805. if self.update_model(form, record):
  1806. # Success
  1807. return gettext('Record was successfully saved.')
  1808. else:
  1809. # Error: No records changed, or problem saving to database.
  1810. msgs = ", ".join([msg for msg in get_flashed_messages()])
  1811. return gettext('Failed to update record. %(error)s',
  1812. error=msgs), 500
  1813. else:
  1814. for field in form:
  1815. for error in field.errors:
  1816. # return validation error to x-editable
  1817. if isinstance(error, list):
  1818. return gettext('Failed to update record. %(error)s',
  1819. error=", ".join(error)), 500
  1820. else:
  1821. return gettext('Failed to update record. %(error)s',
  1822. error=error), 500