view.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511
  1. import logging
  2. from flask import flash
  3. from flask_admin._compat import string_types, iteritems
  4. from flask_admin.babel import gettext, ngettext, lazy_gettext
  5. from flask_admin.model import BaseModelView
  6. from flask_admin.model.form import create_editable_list_form
  7. from peewee import PrimaryKeyField, ForeignKeyField, Field, CharField, TextField
  8. from flask_admin.actions import action
  9. from flask_admin.contrib.peewee import filters
  10. from .form import get_form, CustomModelConverter, InlineModelConverter, save_inline
  11. from .tools import get_primary_key, parse_like_term
  12. from .ajax import create_ajax_loader
  13. # Set up logger
  14. log = logging.getLogger("flask-admin.peewee")
  15. class ModelView(BaseModelView):
  16. column_filters = None
  17. """
  18. Collection of the column filters.
  19. Can contain either field names or instances of
  20. :class:`flask_admin.contrib.peewee.filters.BasePeeweeFilter` classes.
  21. Filters will be grouped by name when displayed in the drop-down.
  22. For example::
  23. class MyModelView(BaseModelView):
  24. column_filters = ('user', 'email')
  25. or::
  26. from flask_admin.contrib.peewee.filters import BooleanEqualFilter
  27. class MyModelView(BaseModelView):
  28. column_filters = (BooleanEqualFilter(column=User.name, name='Name'),)
  29. or::
  30. from flask_admin.contrib.peewee.filters import BasePeeweeFilter
  31. class FilterLastNameBrown(BasePeeweeFilter):
  32. def apply(self, query, value):
  33. if value == '1':
  34. return query.filter(self.column == "Brown")
  35. else:
  36. return query.filter(self.column != "Brown")
  37. def operation(self):
  38. return 'is Brown'
  39. class MyModelView(BaseModelView):
  40. column_filters = [
  41. FilterLastNameBrown(
  42. column=User.last_name, name='Last Name',
  43. options=(('1', 'Yes'), ('0', 'No'))
  44. )
  45. ]
  46. """
  47. model_form_converter = CustomModelConverter
  48. """
  49. Model form conversion class. Use this to implement custom field conversion logic.
  50. For example::
  51. class MyModelConverter(AdminModelConverter):
  52. pass
  53. class MyAdminView(ModelView):
  54. model_form_converter = MyModelConverter
  55. """
  56. inline_model_form_converter = InlineModelConverter
  57. """
  58. Inline model conversion class. If you need some kind of post-processing for inline
  59. forms, you can customize behavior by doing something like this::
  60. class MyInlineModelConverter(AdminModelConverter):
  61. def post_process(self, form_class, info):
  62. form_class.value = TextField('value')
  63. return form_class
  64. class MyAdminView(ModelView):
  65. inline_model_form_converter = MyInlineModelConverter
  66. """
  67. filter_converter = filters.FilterConverter()
  68. """
  69. Field to filter converter.
  70. Override this attribute to use non-default converter.
  71. """
  72. fast_mass_delete = False
  73. """
  74. If set to `False` and user deletes more than one model using actions,
  75. all models will be read from the database and then deleted one by one
  76. giving Peewee chance to manually cleanup any dependencies (many-to-many
  77. relationships, etc).
  78. If set to True, will run DELETE statement which is somewhat faster, but
  79. might leave corrupted data if you forget to configure DELETE CASCADE
  80. for your model.
  81. """
  82. inline_models = None
  83. """
  84. Inline related-model editing for models with parent to child relation.
  85. Accept enumerable with one of the values:
  86. 1. Child model class::
  87. class MyModelView(ModelView):
  88. inline_models = (Post,)
  89. 2. Child model class and additional options::
  90. class MyModelView(ModelView):
  91. inline_models = [(Post, dict(form_columns=['title']))]
  92. 3. Django-like ``InlineFormAdmin`` class instance::
  93. from flask_admin.model.form import InlineFormAdmin
  94. class MyInlineModelForm(InlineFormAdmin):
  95. form_columns = ('title', 'date')
  96. class MyModelView(ModelView):
  97. inline_models = (MyInlineModelForm(MyInlineModel),)
  98. You can customize generated field name by:
  99. 1. Using `form_name` property as option:
  100. class MyModelView(ModelView):
  101. inline_models = ((Post, dict(form_label='Hello')))
  102. 2. Using field's related_name:
  103. class Model1(Base):
  104. # ...
  105. pass
  106. class Model2(Base):
  107. # ...
  108. model1 = ForeignKeyField(related_name="model_twos")
  109. class MyModel1View(Base):
  110. inline_models = (Model2,)
  111. column_labels = {'model_ones': 'Hello'}
  112. """
  113. def __init__(self, model, name=None,
  114. category=None, endpoint=None, url=None, static_folder=None,
  115. menu_class_name=None, menu_icon_type=None, menu_icon_value=None):
  116. self._search_fields = []
  117. super(ModelView, self).__init__(model, name, category, endpoint, url, static_folder,
  118. menu_class_name=menu_class_name,
  119. menu_icon_type=menu_icon_type,
  120. menu_icon_value=menu_icon_value)
  121. self._primary_key = self.scaffold_pk()
  122. def _get_model_fields(self, model=None):
  123. if model is None:
  124. model = self.model
  125. return iteritems(model._meta.fields)
  126. def scaffold_pk(self):
  127. return get_primary_key(self.model)
  128. def get_pk_value(self, model):
  129. return getattr(model, self._primary_key)
  130. def scaffold_list_columns(self):
  131. columns = []
  132. for n, f in self._get_model_fields():
  133. # Verify type
  134. field_class = type(f)
  135. if field_class == ForeignKeyField:
  136. columns.append(n)
  137. elif self.column_display_pk or field_class != PrimaryKeyField:
  138. columns.append(n)
  139. return columns
  140. def scaffold_sortable_columns(self):
  141. columns = dict()
  142. for n, f in self._get_model_fields():
  143. if self.column_display_pk or type(f) != PrimaryKeyField:
  144. columns[n] = f
  145. return columns
  146. def init_search(self):
  147. if self.column_searchable_list:
  148. for p in self.column_searchable_list:
  149. if isinstance(p, string_types):
  150. p = getattr(self.model, p)
  151. field_type = type(p)
  152. # Check type
  153. if (field_type != CharField and field_type != TextField):
  154. raise Exception('Can only search on text columns. ' +
  155. 'Failed to setup search for "%s"' % p)
  156. self._search_fields.append(p)
  157. return bool(self._search_fields)
  158. def scaffold_filters(self, name):
  159. if isinstance(name, string_types):
  160. attr = getattr(self.model, name, None)
  161. else:
  162. attr = name
  163. if attr is None:
  164. raise Exception('Failed to find field for filter: %s' % name)
  165. # Check if field is in different model
  166. if attr.model_class != self.model:
  167. visible_name = '%s / %s' % (self.get_column_name(attr.model_class.__name__),
  168. self.get_column_name(attr.name))
  169. else:
  170. if not isinstance(name, string_types):
  171. visible_name = self.get_column_name(attr.name)
  172. else:
  173. visible_name = self.get_column_name(name)
  174. type_name = type(attr).__name__
  175. flt = self.filter_converter.convert(type_name,
  176. attr,
  177. visible_name)
  178. return flt
  179. def is_valid_filter(self, filter):
  180. return isinstance(filter, filters.BasePeeweeFilter)
  181. def scaffold_form(self):
  182. form_class = get_form(self.model, self.model_form_converter(self),
  183. base_class=self.form_base_class,
  184. only=self.form_columns,
  185. exclude=self.form_excluded_columns,
  186. field_args=self.form_args,
  187. # Allow child to specify pk, so inline_models
  188. # can be ModelViews. But don't auto-generate
  189. # pk field if form_columns is empty -- allow
  190. # default behaviour in that case.
  191. allow_pk=bool(self.form_columns),
  192. extra_fields=self.form_extra_fields)
  193. if self.inline_models:
  194. form_class = self.scaffold_inline_form_models(form_class)
  195. return form_class
  196. def scaffold_list_form(self, widget=None, validators=None):
  197. """
  198. Create form for the `index_view` using only the columns from
  199. `self.column_editable_list`.
  200. :param widget:
  201. WTForms widget class. Defaults to `XEditableWidget`.
  202. :param validators:
  203. `form_args` dict with only validators
  204. {'name': {'validators': [required()]}}
  205. """
  206. form_class = get_form(self.model, self.model_form_converter(self),
  207. base_class=self.form_base_class,
  208. only=self.column_editable_list,
  209. field_args=validators)
  210. return create_editable_list_form(self.form_base_class, form_class,
  211. widget)
  212. def scaffold_inline_form_models(self, form_class):
  213. converter = self.model_form_converter(self)
  214. inline_converter = self.inline_model_form_converter(self)
  215. for m in self.inline_models:
  216. form_class = inline_converter.contribute(converter,
  217. self.model,
  218. form_class,
  219. m)
  220. return form_class
  221. # AJAX foreignkey support
  222. def _create_ajax_loader(self, name, options):
  223. return create_ajax_loader(self.model, name, name, options)
  224. def _handle_join(self, query, field, joins):
  225. if field.model_class != self.model:
  226. model_name = field.model_class.__name__
  227. if model_name not in joins:
  228. query = query.join(field.model_class)
  229. joins.add(model_name)
  230. return query
  231. def _order_by(self, query, joins, sort_field, sort_desc):
  232. if isinstance(sort_field, string_types):
  233. field = getattr(self.model, sort_field)
  234. query = query.order_by(field.desc() if sort_desc else field.asc())
  235. elif isinstance(sort_field, Field):
  236. if sort_field.model_class != self.model:
  237. query = self._handle_join(query, sort_field, joins)
  238. query = query.order_by(sort_field.desc() if sort_desc else sort_field.asc())
  239. return query, joins
  240. def get_query(self):
  241. return self.model.select()
  242. def get_list(self, page, sort_column, sort_desc, search, filters,
  243. execute=True, page_size=None):
  244. """
  245. Return records from the database.
  246. :param page:
  247. Page number
  248. :param sort_column:
  249. Sort column name
  250. :param sort_desc:
  251. Descending or ascending sort
  252. :param search:
  253. Search query
  254. :param filters:
  255. List of filter tuples
  256. :param execute:
  257. Execute query immediately? Default is `True`
  258. :param page_size:
  259. Number of results. Defaults to ModelView's page_size. Can be
  260. overriden to change the page_size limit. Removing the page_size
  261. limit requires setting page_size to 0 or False.
  262. """
  263. query = self.get_query()
  264. joins = set()
  265. # Search
  266. if self._search_supported and search:
  267. values = search.split(' ')
  268. for value in values:
  269. if not value:
  270. continue
  271. term = parse_like_term(value)
  272. stmt = None
  273. for field in self._search_fields:
  274. query = self._handle_join(query, field, joins)
  275. q = field ** term
  276. if stmt is None:
  277. stmt = q
  278. else:
  279. stmt |= q
  280. query = query.where(stmt)
  281. # Filters
  282. if self._filters:
  283. for flt, flt_name, value in filters:
  284. f = self._filters[flt]
  285. query = self._handle_join(query, f.column, joins)
  286. query = f.apply(query, f.clean(value))
  287. # Get count
  288. count = query.count() if not self.simple_list_pager else None
  289. # Apply sorting
  290. if sort_column is not None:
  291. sort_field = self._sortable_columns[sort_column]
  292. query, joins = self._order_by(query, joins, sort_field, sort_desc)
  293. else:
  294. order = self._get_default_order()
  295. if order:
  296. query, joins = self._order_by(query, joins, order[0], order[1])
  297. # Pagination
  298. if page_size is None:
  299. page_size = self.page_size
  300. if page_size:
  301. query = query.limit(page_size)
  302. if page and page_size:
  303. query = query.offset(page * page_size)
  304. if execute:
  305. query = list(query.execute())
  306. return count, query
  307. def get_one(self, id):
  308. return self.model.get(**{self._primary_key: id})
  309. def create_model(self, form):
  310. try:
  311. model = self.model()
  312. form.populate_obj(model)
  313. self._on_model_change(form, model, True)
  314. model.save()
  315. # For peewee have to save inline forms after model was saved
  316. save_inline(form, model)
  317. except Exception as ex:
  318. if not self.handle_view_exception(ex):
  319. flash(gettext('Failed to create record. %(error)s', error=str(ex)), 'error')
  320. log.exception('Failed to create record.')
  321. return False
  322. else:
  323. self.after_model_change(form, model, True)
  324. return model
  325. def update_model(self, form, model):
  326. try:
  327. form.populate_obj(model)
  328. self._on_model_change(form, model, False)
  329. model.save()
  330. # For peewee have to save inline forms after model was saved
  331. save_inline(form, model)
  332. except Exception as ex:
  333. if not self.handle_view_exception(ex):
  334. flash(gettext('Failed to update record. %(error)s', error=str(ex)), 'error')
  335. log.exception('Failed to update record.')
  336. return False
  337. else:
  338. self.after_model_change(form, model, False)
  339. return True
  340. def delete_model(self, model):
  341. try:
  342. self.on_model_delete(model)
  343. model.delete_instance(recursive=True)
  344. except Exception as ex:
  345. if not self.handle_view_exception(ex):
  346. flash(gettext('Failed to delete record. %(error)s', error=str(ex)), 'error')
  347. log.exception('Failed to delete record.')
  348. return False
  349. else:
  350. self.after_model_delete(model)
  351. return True
  352. # Default model actions
  353. def is_action_allowed(self, name):
  354. # Check delete action permission
  355. if name == 'delete' and not self.can_delete:
  356. return False
  357. return super(ModelView, self).is_action_allowed(name)
  358. @action('delete',
  359. lazy_gettext('Delete'),
  360. lazy_gettext('Are you sure you want to delete selected records?'))
  361. def action_delete(self, ids):
  362. try:
  363. model_pk = getattr(self.model, self._primary_key)
  364. if self.fast_mass_delete:
  365. count = self.model.delete().where(model_pk << ids).execute()
  366. else:
  367. count = 0
  368. query = self.model.select().filter(model_pk << ids)
  369. for m in query:
  370. self.on_model_delete(m)
  371. m.delete_instance(recursive=True)
  372. count += 1
  373. flash(ngettext('Record was successfully deleted.',
  374. '%(count)s records were successfully deleted.',
  375. count,
  376. count=count), 'success')
  377. except Exception as ex:
  378. if not self.handle_view_exception(ex):
  379. flash(gettext('Failed to delete records. %(error)s', error=str(ex)), 'error')