Commit 461db9a0 authored by Serge S. Koval's avatar Serge S. Koval

Basic MongoEngine backend

parent 66005b73
......@@ -3,6 +3,8 @@
- Localization
- Create documentation
- Model Admin
- Simplify scaffold_list_columns - remove excluded_list_columns check, update documentation
- Reduce number of parameters passed to list view
- Filters
- Use table to draw filters so column names will line up?
......@@ -17,8 +19,6 @@
- File size restriction
- Unit tests
- Form generation tests
- Inline form generation tests
- Documentation
- Add all new stuff
- Fixed stylesheet
import datetime
from flask import Flask
from flask.ext import admin
from flask.ext.mongoengine import MongoEngine
from flask.ext.admin.contrib import mongoengine
# Create application
app = Flask(__name__)
# Create dummy secrey key so we can use sessions
app.config['SECRET_KEY'] = '123456790'
app.config['MONGODB_SETTINGS'] = {'DB': 'testing'}
# Create models
db = MongoEngine()
db.init_app(app)
class Todo(db.Document):
title = db.StringField(max_length=60)
text = db.StringField()
done = db.BooleanField(default=False)
pub_date = db.DateTimeField(default=datetime.datetime.now)
# Required for administrative interface
def __unicode__(self):
return self.title
# Flask views
@app.route('/')
def index():
return '<a href="/admin/">Click me to get to Admin!</a>'
if __name__ == '__main__':
# Create admin
admin = admin.Admin(app, 'Simple Models')
# Add views
admin.add_view(mongoengine.ModelView(Todo))
# Start app
app.debug = True
app.run('0.0.0.0', 8000)
try:
import flask.ext.mongoengine
except ImportError:
raise Exception('Please install flask-mongoengine in order to use mongoengine backend')
from .view import ModelView
from flask.ext.admin.babel import gettext
from flask.ext.admin.model import filters
class BaseMongoEngineFilter(filters.BaseFilter):
"""
Base MongoEngine filter.
"""
def __init__(self, column, name, options=None, data_type=None):
"""
Constructor.
:param column:
Model field
:param name:
Display name
:param options:
Fixed set of options
:param data_type:
Client data type
"""
super(BaseMongoEngineFilter, self).__init__(name, options, data_type)
self.column = column
# Common filters
class FilterEqual(BaseMongoEngineFilter):
def apply(self, query, value):
flt = {'%s' % self.column.name: value}
return query.filter(**flt)
def operation(self):
return gettext('equals')
class FilterNotEqual(BaseMongoEngineFilter):
def apply(self, query, value):
flt = {'%s__ne' % self.column.name: value}
return query.filter(**flt)
def operation(self):
return gettext('not equal')
class FilterLike(BaseMongoEngineFilter):
def apply(self, query, value):
term, data = parse_like_term(value)
flt = {'%s__%s' % (self.column.name, term): data}
return query.filter(**flt)
def operation(self):
return gettext('contains')
class FilterNotLike(BaseMongoEngineFilter):
def apply(self, query, value):
term, data = parse_like_term(value)
flt = {'%s__not__%s' % (self.column.name, term): data}
return query.filter(**flt)
def operation(self):
return gettext('not contains')
class FilterGreater(BaseMongoEngineFilter):
def apply(self, query, value):
flt = {'%s__gt' % self.column.name: value}
return query.filter(**flt)
def operation(self):
return gettext('greater than')
class FilterSmaller(BaseMongoEngineFilter):
def apply(self, query, value):
flt = {'%s__lt' % self.column.name: value}
return query.filter(**flt)
def operation(self):
return gettext('smaller than')
# Customized type filters
class BooleanEqualFilter(FilterEqual, filters.BaseBooleanFilter):
pass
class BooleanNotEqualFilter(FilterNotEqual, filters.BaseBooleanFilter):
pass
# Base peewee filter field converter
class FilterConverter(filters.BaseFilterConverter):
strings = (FilterEqual, FilterNotEqual, FilterLike, FilterNotLike)
numeric = (FilterEqual, FilterNotEqual, FilterGreater, FilterSmaller)
def convert(self, type_name, column, name):
#print type_name, column, name
if type_name in self.converters:
return self.converters[type_name](column, name)
return None
@filters.convert('StringField')
def conv_string(self, column, name):
return [f(column, name) for f in self.strings]
@filters.convert('BooleanField')
def conv_bool(self, column, name):
return [BooleanEqualFilter(column, name),
BooleanNotEqualFilter(column, name)]
@filters.convert('IntField', 'DecimalField', 'FloatField')
def conv_int(self, column, name):
return [f(column, name) for f in self.numeric]
@filters.convert('DateField')
def conv_date(self, column, name):
return [f(column, name, data_type='datepicker') for f in self.numeric]
@filters.convert('DateTimeField')
def conv_datetime(self, column, name):
return [f(column, name, data_type='datetimepicker')
for f in self.numeric]
from flask.ext.mongoengine.wtf import orm
from flask.ext.admin.form import BaseForm
class CustomModelConverter(orm.ModelConverter):
pass
def model_form(model, base_class=BaseForm, only=None, exclude=None,
field_args=None, converter=None):
return orm.model_form(model, base_class=base_class, only=only,
exclude=exclude, field_args=field_args,
converter=converter)
import logging
from flask import flash
from flask.ext.admin.babel import gettext, ngettext, lazy_gettext
from flask.ext.admin.model import BaseModelView
import mongoengine
from bson.objectid import ObjectId
from flask.ext.admin.actions import action
from flask.ext.admin.form import BaseForm
from .filters import FilterConverter, BaseMongoEngineFilter
from .form import model_form, CustomModelConverter
SORTABLE_FIELDS = set((
mongoengine.StringField,
mongoengine.IntField,
mongoengine.FloatField,
mongoengine.BooleanField,
mongoengine.DateTimeField,
mongoengine.ObjectIdField,
mongoengine.DecimalField,
mongoengine.ReferenceField,
mongoengine.EmailField,
mongoengine.UUIDField
))
class ModelView(BaseModelView):
column_filters = None
"""
Collection of the column filters.
Can contain either field names or instances of
:class:`flask.ext.admin.contrib.mongoengine.filters.BaseFilter`
classes.
For example::
class MyModelView(BaseModelView):
column_filters = ('user', 'email')
or::
class MyModelView(BaseModelView):
column_filters = (BooleanEqualFilter(User.name, 'Name'))
"""
model_form_converter = CustomModelConverter
"""
Model form conversion class. Use this to implement custom
field conversion logic.
For example::
class MyModelConverter(AdminModelConverter):
pass
class MyAdminView(ModelView):
model_form_converter = MyModelConverter
"""
filter_converter = FilterConverter()
"""
Field to filter converter.
Override this attribute to use non-default converter.
"""
fast_mass_delete = False
"""
If set to `False` and user deletes more than one model using actions,
all models will be read from the database and then deleted one by one
giving SQLAlchemy chance to manually cleanup any dependencies
(many-to-many relationships, etc).
If set to True, will run DELETE statement which is somewhat faster, but
might leave corrupted data if you forget to configure DELETE CASCADE
for your model.
"""
def __init__(self, model, name=None,
category=None, endpoint=None, url=None):
self._search_fields = []
super(ModelView, self).__init__(model, name, category, endpoint, url)
self._primary_key = self.scaffold_pk()
def _get_model_fields(self, model=None):
if model is None:
model = self.model
return sorted(model._fields.iteritems(), key=lambda n: n[1].creation_counter)
def scaffold_pk(self):
# MongoEngine models have predefined 'id' as a key
return 'id'
def get_pk_value(self, model):
return model.pk
def scaffold_list_columns(self):
columns = []
for n, f in self._get_model_fields():
#import pdb; pdb.set_trace()
# Filter by name
if (self.excluded_list_columns and
n in self.excluded_list_columns):
continue
# Verify type
field_class = type(f)
if self.list_display_pk or field_class != mongoengine.ObjectIdField:
columns.append(n)
return columns
def scaffold_sortable_columns(self):
columns = {}
for n, f in self._get_model_fields():
if type(f) in SORTABLE_FIELDS:
if self.list_display_pk or type(f) != mongoengine.ObjectIdField:
columns[n] = f
return columns
def init_search(self):
return bool(self._search_fields)
def scaffold_filters(self, name):
if isinstance(name, basestring):
attr = self.model._fields.get(name)
else:
attr = name
if attr is None:
raise Exception('Failed to find field for filter: %s' % name)
# Find name
visible_name = None
if not isinstance(name, basestring):
visible_name = self.get_column_name(attr.name)
if not visible_name:
visible_name = self.get_column_name(name)
# Convert filter
type_name = type(attr).__name__
flt = self.filter_converter.convert(type_name,
attr,
visible_name)
return flt
def is_valid_filter(self, filter):
return isinstance(filter, BaseMongoEngineFilter)
def scaffold_form(self):
# TODO: Fix base_class
form_class = model_form(self.model,
base_class=BaseForm,
only=self.form_columns,
exclude=self.excluded_form_columns,
field_args=self.form_args,
converter=self.model_form_converter())
return form_class
def get_list(self, page, sort_column, sort_desc, search, filters,
execute=True):
query = self.model.objects
# TODO: Filters
# Get count
count = query.count()
# Sorting
if sort_column:
query = query.order_by('%s%s' % ('-' if sort_desc else '', sort_column))
# Pagination
if page is not None:
query = query.skip(page * self.page_size)
query = query.limit(self.page_size)
if execute:
query = query.all()
return count, query
def get_one(self, id):
return self.model.objects.with_id(id)
def create_model(self, form):
try:
model = self.model(**form.data)
self.on_model_change(form, model)
model.save()
return True
except Exception, ex:
flash(gettext('Failed to create model. %(error)s', error=str(ex)),
'error')
logging.exception('Failed to create model')
return False
def update_model(self, form, model):
try:
for f in form:
name, field = f.name, f
if hasattr(model, name) and getattr(model, name) != field.data:
setattr(model, name, field.data)
self.on_model_change(form, model)
model.save()
return True
except Exception, ex:
flash(gettext('Failed to update model. %(error)s', error=str(ex)),
'error')
logging.exception('Failed to update model')
return False
def delete_model(self, model):
try:
self.on_model_delete(model)
model.delete()
return True
except Exception, ex:
flash(gettext('Failed to delete model. %(error)s', error=str(ex)),
'error')
logging.exception('Failed to delete model')
return False
# 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 models?'))
def action_delete(self, ids):
try:
count = 0
all_ids = [ObjectId(pk) for pk in ids]
for obj in self.model.objects.in_bulk(all_ids).values():
obj.delete()
count += 1
flash(ngettext('Model was successfully deleted.',
'%(count)s models were successfully deleted.',
count,
count=count))
except Exception, ex:
flash(gettext('Failed to delete models. %(error)s', error=str(ex)),
'error')
......@@ -6,7 +6,7 @@ from .tools import parse_like_term
class BasePeeweeFilter(filters.BaseFilter):
"""
Base SQLAlchemy filter.
Base Peewee filter.
"""
def __init__(self, column, name, options=None, data_type=None):
"""
......
......@@ -20,7 +20,8 @@ class ModelView(BaseModelView):
"""
Collection of the column filters.
Can contain either field names or instances of :class:`flask.ext.admin.contrib.sqlamodel.filters.BaseFilter` classes.
Can contain either field names or instances of
:class:`flask.ext.admin.contrib.peeweemodel.filters.BaseFilter` classes.
For example::
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment