402 lines
11 KiB
Python
402 lines
11 KiB
Python
import logging
|
|
|
|
import pymongo
|
|
from bson import ObjectId
|
|
from bson.errors import InvalidId
|
|
|
|
from flask import flash
|
|
|
|
from flask_admin._compat import string_types
|
|
from flask_admin.babel import gettext, ngettext, lazy_gettext
|
|
from flask_admin.model import BaseModelView
|
|
from flask_admin.actions import action
|
|
from flask_admin.helpers import get_form_data
|
|
|
|
from .filters import BasePyMongoFilter
|
|
from .tools import parse_like_term
|
|
|
|
# Set up logger
|
|
log = logging.getLogger("flask-admin.pymongo")
|
|
|
|
|
|
class ModelView(BaseModelView):
|
|
"""
|
|
MongoEngine model scaffolding.
|
|
"""
|
|
|
|
column_filters = None
|
|
"""
|
|
Collection of the column filters.
|
|
|
|
Should contain instances of
|
|
:class:`flask_admin.contrib.pymongo.filters.BasePyMongoFilter` classes.
|
|
|
|
Filters will be grouped by name when displayed in the drop-down.
|
|
|
|
For example::
|
|
|
|
from flask_admin.contrib.pymongo.filters import BooleanEqualFilter
|
|
|
|
class MyModelView(BaseModelView):
|
|
column_filters = (BooleanEqualFilter(column=User.name, name='Name'),)
|
|
|
|
or::
|
|
|
|
from flask_admin.contrib.pymongo.filters import BasePyMongoFilter
|
|
|
|
class FilterLastNameBrown(BasePyMongoFilter):
|
|
def apply(self, query, value):
|
|
if value == '1':
|
|
return query.filter(self.column == "Brown")
|
|
else:
|
|
return query.filter(self.column != "Brown")
|
|
|
|
def operation(self):
|
|
return 'is Brown'
|
|
|
|
class MyModelView(BaseModelView):
|
|
column_filters = [
|
|
FilterLastNameBrown(
|
|
column=User.last_name, name='Last Name',
|
|
options=(('1', 'Yes'), ('0', 'No'))
|
|
)
|
|
]
|
|
"""
|
|
|
|
def __init__(self, coll,
|
|
name=None, category=None, endpoint=None, url=None,
|
|
menu_class_name=None, menu_icon_type=None, menu_icon_value=None):
|
|
"""
|
|
Constructor
|
|
|
|
:param coll:
|
|
MongoDB collection object
|
|
:param name:
|
|
Display name
|
|
:param category:
|
|
Display category
|
|
:param endpoint:
|
|
Endpoint
|
|
:param url:
|
|
Custom URL
|
|
:param menu_class_name:
|
|
Optional class name for the menu item.
|
|
:param menu_icon_type:
|
|
Optional icon. Possible icon types:
|
|
|
|
- `flask_admin.consts.ICON_TYPE_GLYPH` - Bootstrap glyph icon
|
|
- `flask_admin.consts.ICON_TYPE_FONT_AWESOME` - Font Awesome icon
|
|
- `flask_admin.consts.ICON_TYPE_IMAGE` - Image relative to Flask static directory
|
|
- `flask_admin.consts.ICON_TYPE_IMAGE_URL` - Image with full URL
|
|
:param menu_icon_value:
|
|
Icon glyph name or URL, depending on `menu_icon_type` setting
|
|
"""
|
|
self._search_fields = []
|
|
|
|
if name is None:
|
|
name = self._prettify_name(coll.name)
|
|
|
|
if endpoint is None:
|
|
endpoint = ('%sview' % coll.name).lower()
|
|
|
|
super(ModelView, self).__init__(None, name, category, endpoint, url,
|
|
menu_class_name=menu_class_name,
|
|
menu_icon_type=menu_icon_type,
|
|
menu_icon_value=menu_icon_value)
|
|
|
|
self.coll = coll
|
|
|
|
def scaffold_pk(self):
|
|
return '_id'
|
|
|
|
def get_pk_value(self, model):
|
|
"""
|
|
Return primary key value from the model instance
|
|
|
|
:param model:
|
|
Model instance
|
|
"""
|
|
return model.get('_id')
|
|
|
|
def scaffold_list_columns(self):
|
|
"""
|
|
Scaffold list columns
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
def scaffold_sortable_columns(self):
|
|
"""
|
|
Return sortable columns dictionary (name, field)
|
|
"""
|
|
return []
|
|
|
|
def init_search(self):
|
|
"""
|
|
Init search
|
|
"""
|
|
if self.column_searchable_list:
|
|
for p in self.column_searchable_list:
|
|
if not isinstance(p, string_types):
|
|
raise ValueError('Expected string')
|
|
|
|
# TODO: Validation?
|
|
|
|
self._search_fields.append(p)
|
|
|
|
return bool(self._search_fields)
|
|
|
|
def scaffold_filters(self, attr):
|
|
"""
|
|
Return filter object(s) for the field
|
|
|
|
:param name:
|
|
Either field name or field instance
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
def is_valid_filter(self, filter):
|
|
"""
|
|
Validate if it is valid MongoEngine filter
|
|
|
|
:param filter:
|
|
Filter object
|
|
"""
|
|
return isinstance(filter, BasePyMongoFilter)
|
|
|
|
def scaffold_form(self):
|
|
raise NotImplementedError()
|
|
|
|
def _get_field_value(self, model, name):
|
|
"""
|
|
Get unformatted field value from the model
|
|
"""
|
|
return model.get(name)
|
|
|
|
def _search(self, query, search_term):
|
|
values = search_term.split(' ')
|
|
|
|
queries = []
|
|
|
|
# Construct inner querie
|
|
for value in values:
|
|
if not value:
|
|
continue
|
|
|
|
regex = parse_like_term(value)
|
|
|
|
stmt = []
|
|
for field in self._search_fields:
|
|
stmt.append({field: {'$regex': regex}})
|
|
|
|
if stmt:
|
|
if len(stmt) == 1:
|
|
queries.append(stmt[0])
|
|
else:
|
|
queries.append({'$or': stmt})
|
|
|
|
# Construct final query
|
|
if queries:
|
|
if len(queries) == 1:
|
|
final = queries[0]
|
|
else:
|
|
final = {'$and': queries}
|
|
|
|
if query:
|
|
query = {'$and': [query, final]}
|
|
else:
|
|
query = final
|
|
|
|
return query
|
|
|
|
def get_list(self, page, sort_column, sort_desc, search, filters,
|
|
execute=True, page_size=None):
|
|
"""
|
|
Get list of objects from MongoEngine
|
|
|
|
:param page:
|
|
Page number
|
|
:param sort_column:
|
|
Sort column
|
|
:param sort_desc:
|
|
Sort descending
|
|
:param search:
|
|
Search criteria
|
|
:param filters:
|
|
List of applied fiters
|
|
:param execute:
|
|
Run query immediately or not
|
|
:param page_size:
|
|
Number of results. Defaults to ModelView's page_size. Can be
|
|
overriden to change the page_size limit. Removing the page_size
|
|
limit requires setting page_size to 0 or False.
|
|
"""
|
|
query = {}
|
|
|
|
# Filters
|
|
if self._filters:
|
|
data = []
|
|
|
|
for flt, flt_name, value in filters:
|
|
f = self._filters[flt]
|
|
data = f.apply(data, f.clean(value))
|
|
|
|
if data:
|
|
if len(data) == 1:
|
|
query = data[0]
|
|
else:
|
|
query['$and'] = data
|
|
|
|
# Search
|
|
if self._search_supported and search:
|
|
query = self._search(query, search)
|
|
|
|
# Get count
|
|
count = self.coll.count_documents(query) if not self.simple_list_pager else None
|
|
|
|
# Sorting
|
|
sort_by = None
|
|
|
|
if sort_column:
|
|
sort_by = [(sort_column, pymongo.DESCENDING if sort_desc else pymongo.ASCENDING)]
|
|
else:
|
|
order = self._get_default_order()
|
|
|
|
if order:
|
|
sort_by = [(col, pymongo.DESCENDING if desc else pymongo.ASCENDING)
|
|
for (col, desc) in order]
|
|
|
|
# Pagination
|
|
if page_size is None:
|
|
page_size = self.page_size
|
|
|
|
skip = 0
|
|
|
|
if page and page_size:
|
|
skip = page * page_size
|
|
|
|
results = self.coll.find(query, sort=sort_by, skip=skip, limit=page_size)
|
|
|
|
if execute:
|
|
results = list(results)
|
|
|
|
return count, results
|
|
|
|
def _get_valid_id(self, id):
|
|
try:
|
|
return ObjectId(id)
|
|
except InvalidId:
|
|
return id
|
|
|
|
def get_one(self, id):
|
|
"""
|
|
Return single model instance by ID
|
|
|
|
:param id:
|
|
Model ID
|
|
"""
|
|
return self.coll.find_one({'_id': self._get_valid_id(id)})
|
|
|
|
def edit_form(self, obj):
|
|
"""
|
|
Create edit form from the MongoDB document
|
|
"""
|
|
return self._edit_form_class(get_form_data(), **obj)
|
|
|
|
def create_model(self, form):
|
|
"""
|
|
Create model helper
|
|
|
|
:param form:
|
|
Form instance
|
|
"""
|
|
try:
|
|
model = form.data
|
|
self._on_model_change(form, model, True)
|
|
self.coll.insert_one(model)
|
|
except Exception as ex:
|
|
flash(gettext('Failed to create record. %(error)s', error=str(ex)),
|
|
'error')
|
|
log.exception('Failed to create record.')
|
|
return False
|
|
else:
|
|
self.after_model_change(form, model, True)
|
|
|
|
return model
|
|
|
|
def update_model(self, form, model):
|
|
"""
|
|
Update model helper
|
|
|
|
:param form:
|
|
Form instance
|
|
:param model:
|
|
Model instance to update
|
|
"""
|
|
try:
|
|
model.update(form.data)
|
|
self._on_model_change(form, model, False)
|
|
|
|
pk = self.get_pk_value(model)
|
|
self.coll.replace_one({'_id': pk}, model)
|
|
except Exception as ex:
|
|
flash(gettext('Failed to update record. %(error)s', error=str(ex)),
|
|
'error')
|
|
log.exception('Failed to update record.')
|
|
return False
|
|
else:
|
|
self.after_model_change(form, model, False)
|
|
|
|
return True
|
|
|
|
def delete_model(self, model):
|
|
"""
|
|
Delete model helper
|
|
|
|
:param model:
|
|
Model instance
|
|
"""
|
|
try:
|
|
pk = self.get_pk_value(model)
|
|
|
|
if not pk:
|
|
raise ValueError('Document does not have _id')
|
|
|
|
self.on_model_delete(model)
|
|
self.coll.delete_one({'_id': pk})
|
|
except Exception as ex:
|
|
flash(gettext('Failed to delete record. %(error)s', error=str(ex)),
|
|
'error')
|
|
log.exception('Failed to delete record.')
|
|
return False
|
|
else:
|
|
self.after_model_delete(model)
|
|
|
|
return True
|
|
|
|
# Default model actions
|
|
def is_action_allowed(self, name):
|
|
# Check delete action permission
|
|
if name == 'delete' and not self.can_delete:
|
|
return False
|
|
|
|
return super(ModelView, self).is_action_allowed(name)
|
|
|
|
@action('delete',
|
|
lazy_gettext('Delete'),
|
|
lazy_gettext('Are you sure you want to delete selected records?'))
|
|
def action_delete(self, ids):
|
|
try:
|
|
count = 0
|
|
|
|
# TODO: Optimize me
|
|
for pk in ids:
|
|
if self.delete_model(self.get_one(pk)):
|
|
count += 1
|
|
|
|
flash(ngettext('Record was successfully deleted.',
|
|
'%(count)s records were successfully deleted.',
|
|
count,
|
|
count=count), 'success')
|
|
except Exception as ex:
|
|
flash(gettext('Failed to delete records. %(error)s', error=str(ex)), 'error')
|