view.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  1. import logging
  2. import pymongo
  3. from bson import ObjectId
  4. from bson.errors import InvalidId
  5. from flask import flash
  6. from flask_admin._compat import string_types
  7. from flask_admin.babel import gettext, ngettext, lazy_gettext
  8. from flask_admin.model import BaseModelView
  9. from flask_admin.actions import action
  10. from flask_admin.helpers import get_form_data
  11. from .filters import BasePyMongoFilter
  12. from .tools import parse_like_term
  13. # Set up logger
  14. log = logging.getLogger("flask-admin.pymongo")
  15. class ModelView(BaseModelView):
  16. """
  17. MongoEngine model scaffolding.
  18. """
  19. column_filters = None
  20. """
  21. Collection of the column filters.
  22. Should contain instances of
  23. :class:`flask_admin.contrib.pymongo.filters.BasePyMongoFilter` classes.
  24. Filters will be grouped by name when displayed in the drop-down.
  25. For example::
  26. from flask_admin.contrib.pymongo.filters import BooleanEqualFilter
  27. class MyModelView(BaseModelView):
  28. column_filters = (BooleanEqualFilter(column=User.name, name='Name'),)
  29. or::
  30. from flask_admin.contrib.pymongo.filters import BasePyMongoFilter
  31. class FilterLastNameBrown(BasePyMongoFilter):
  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. def __init__(self, coll,
  48. name=None, category=None, endpoint=None, url=None,
  49. menu_class_name=None, menu_icon_type=None, menu_icon_value=None):
  50. """
  51. Constructor
  52. :param coll:
  53. MongoDB collection object
  54. :param name:
  55. Display name
  56. :param category:
  57. Display category
  58. :param endpoint:
  59. Endpoint
  60. :param url:
  61. Custom URL
  62. :param menu_class_name:
  63. Optional class name for the menu item.
  64. :param menu_icon_type:
  65. Optional icon. Possible icon types:
  66. - `flask_admin.consts.ICON_TYPE_GLYPH` - Bootstrap glyph icon
  67. - `flask_admin.consts.ICON_TYPE_FONT_AWESOME` - Font Awesome icon
  68. - `flask_admin.consts.ICON_TYPE_IMAGE` - Image relative to Flask static directory
  69. - `flask_admin.consts.ICON_TYPE_IMAGE_URL` - Image with full URL
  70. :param menu_icon_value:
  71. Icon glyph name or URL, depending on `menu_icon_type` setting
  72. """
  73. self._search_fields = []
  74. if name is None:
  75. name = self._prettify_name(coll.name)
  76. if endpoint is None:
  77. endpoint = ('%sview' % coll.name).lower()
  78. super(ModelView, self).__init__(None, name, category, endpoint, url,
  79. menu_class_name=menu_class_name,
  80. menu_icon_type=menu_icon_type,
  81. menu_icon_value=menu_icon_value)
  82. self.coll = coll
  83. def scaffold_pk(self):
  84. return '_id'
  85. def get_pk_value(self, model):
  86. """
  87. Return primary key value from the model instance
  88. :param model:
  89. Model instance
  90. """
  91. return model.get('_id')
  92. def scaffold_list_columns(self):
  93. """
  94. Scaffold list columns
  95. """
  96. raise NotImplementedError()
  97. def scaffold_sortable_columns(self):
  98. """
  99. Return sortable columns dictionary (name, field)
  100. """
  101. return []
  102. def init_search(self):
  103. """
  104. Init search
  105. """
  106. if self.column_searchable_list:
  107. for p in self.column_searchable_list:
  108. if not isinstance(p, string_types):
  109. raise ValueError('Expected string')
  110. # TODO: Validation?
  111. self._search_fields.append(p)
  112. return bool(self._search_fields)
  113. def scaffold_filters(self, attr):
  114. """
  115. Return filter object(s) for the field
  116. :param name:
  117. Either field name or field instance
  118. """
  119. raise NotImplementedError()
  120. def is_valid_filter(self, filter):
  121. """
  122. Validate if it is valid MongoEngine filter
  123. :param filter:
  124. Filter object
  125. """
  126. return isinstance(filter, BasePyMongoFilter)
  127. def scaffold_form(self):
  128. raise NotImplementedError()
  129. def _get_field_value(self, model, name):
  130. """
  131. Get unformatted field value from the model
  132. """
  133. return model.get(name)
  134. def _search(self, query, search_term):
  135. values = search_term.split(' ')
  136. queries = []
  137. # Construct inner querie
  138. for value in values:
  139. if not value:
  140. continue
  141. regex = parse_like_term(value)
  142. stmt = []
  143. for field in self._search_fields:
  144. stmt.append({field: {'$regex': regex}})
  145. if stmt:
  146. if len(stmt) == 1:
  147. queries.append(stmt[0])
  148. else:
  149. queries.append({'$or': stmt})
  150. # Construct final query
  151. if queries:
  152. if len(queries) == 1:
  153. final = queries[0]
  154. else:
  155. final = {'$and': queries}
  156. if query:
  157. query = {'$and': [query, final]}
  158. else:
  159. query = final
  160. return query
  161. def get_list(self, page, sort_column, sort_desc, search, filters,
  162. execute=True, page_size=None):
  163. """
  164. Get list of objects from MongoEngine
  165. :param page:
  166. Page number
  167. :param sort_column:
  168. Sort column
  169. :param sort_desc:
  170. Sort descending
  171. :param search:
  172. Search criteria
  173. :param filters:
  174. List of applied fiters
  175. :param execute:
  176. Run query immediately or not
  177. :param page_size:
  178. Number of results. Defaults to ModelView's page_size. Can be
  179. overriden to change the page_size limit. Removing the page_size
  180. limit requires setting page_size to 0 or False.
  181. """
  182. query = {}
  183. # Filters
  184. if self._filters:
  185. data = []
  186. for flt, flt_name, value in filters:
  187. f = self._filters[flt]
  188. data = f.apply(data, value)
  189. if data:
  190. if len(data) == 1:
  191. query = data[0]
  192. else:
  193. query['$and'] = data
  194. # Search
  195. if self._search_supported and search:
  196. query = self._search(query, search)
  197. # Get count
  198. count = self.coll.find(query).count() if not self.simple_list_pager else None
  199. # Sorting
  200. sort_by = None
  201. if sort_column:
  202. sort_by = [(sort_column, pymongo.DESCENDING if sort_desc else pymongo.ASCENDING)]
  203. else:
  204. order = self._get_default_order()
  205. if order:
  206. sort_by = [(order[0], pymongo.DESCENDING if order[1] else pymongo.ASCENDING)]
  207. # Pagination
  208. if page_size is None:
  209. page_size = self.page_size
  210. skip = 0
  211. if page and page_size:
  212. skip = page * page_size
  213. results = self.coll.find(query, sort=sort_by, skip=skip, limit=page_size)
  214. if execute:
  215. results = list(results)
  216. return count, results
  217. def _get_valid_id(self, id):
  218. try:
  219. return ObjectId(id)
  220. except InvalidId:
  221. return id
  222. def get_one(self, id):
  223. """
  224. Return single model instance by ID
  225. :param id:
  226. Model ID
  227. """
  228. return self.coll.find_one({'_id': self._get_valid_id(id)})
  229. def edit_form(self, obj):
  230. """
  231. Create edit form from the MongoDB document
  232. """
  233. return self._edit_form_class(get_form_data(), **obj)
  234. def create_model(self, form):
  235. """
  236. Create model helper
  237. :param form:
  238. Form instance
  239. """
  240. try:
  241. model = form.data
  242. self._on_model_change(form, model, True)
  243. self.coll.insert(model)
  244. except Exception as ex:
  245. flash(gettext('Failed to create record. %(error)s', error=str(ex)),
  246. 'error')
  247. log.exception('Failed to create record.')
  248. return False
  249. else:
  250. self.after_model_change(form, model, True)
  251. return model
  252. def update_model(self, form, model):
  253. """
  254. Update model helper
  255. :param form:
  256. Form instance
  257. :param model:
  258. Model instance to update
  259. """
  260. try:
  261. model.update(form.data)
  262. self._on_model_change(form, model, False)
  263. pk = self.get_pk_value(model)
  264. self.coll.update({'_id': pk}, model)
  265. except Exception as ex:
  266. flash(gettext('Failed to update record. %(error)s', error=str(ex)),
  267. 'error')
  268. log.exception('Failed to update record.')
  269. return False
  270. else:
  271. self.after_model_change(form, model, False)
  272. return True
  273. def delete_model(self, model):
  274. """
  275. Delete model helper
  276. :param model:
  277. Model instance
  278. """
  279. try:
  280. pk = self.get_pk_value(model)
  281. if not pk:
  282. raise ValueError('Document does not have _id')
  283. self.on_model_delete(model)
  284. self.coll.remove({'_id': pk})
  285. except Exception as ex:
  286. flash(gettext('Failed to delete record. %(error)s', error=str(ex)),
  287. 'error')
  288. log.exception('Failed to delete record.')
  289. return False
  290. else:
  291. self.after_model_delete(model)
  292. return True
  293. # Default model actions
  294. def is_action_allowed(self, name):
  295. # Check delete action permission
  296. if name == 'delete' and not self.can_delete:
  297. return False
  298. return super(ModelView, self).is_action_allowed(name)
  299. @action('delete',
  300. lazy_gettext('Delete'),
  301. lazy_gettext('Are you sure you want to delete selected records?'))
  302. def action_delete(self, ids):
  303. try:
  304. count = 0
  305. # TODO: Optimize me
  306. for pk in ids:
  307. if self.delete_model(self.get_one(pk)):
  308. count += 1
  309. flash(ngettext('Record was successfully deleted.',
  310. '%(count)s records were successfully deleted.',
  311. count,
  312. count=count), 'success')
  313. except Exception as ex:
  314. flash(gettext('Failed to delete records. %(error)s', error=str(ex)), 'error')