Commit fa757fc6 authored by Serge S. Koval's avatar Serge S. Koval

MongoEngine GridFS support

parent 9dc0af0f
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
*.swo *.swo
*.pyc *.pyc
*.*~ *.*~
*.rdb
*.egg-info *.egg-info
pyenv pyenv
#*# #*#
......
...@@ -59,6 +59,11 @@ class Post(db.Document): ...@@ -59,6 +59,11 @@ class Post(db.Document):
lols = db.ListField(db.StringField(max_length=20)) lols = db.ListField(db.StringField(max_length=20))
class File(db.Document):
name = db.StringField(max_length=20)
data = db.FileField()
# Customized admin views # Customized admin views
class UserView(ModelView): class UserView(ModelView):
column_filters = ['name'] column_filters = ['name']
...@@ -85,6 +90,7 @@ if __name__ == '__main__': ...@@ -85,6 +90,7 @@ if __name__ == '__main__':
admin.add_view(TodoView(Todo)) admin.add_view(TodoView(Todo))
admin.add_view(ModelView(Tag)) admin.add_view(ModelView(Tag))
admin.add_view(ModelView(Post)) admin.add_view(ModelView(Post))
admin.add_view(ModelView(File))
# Start app # Start app
app.run(debug=True) app.run(debug=True)
from wtforms.fields import FormField from flask import request
from wtforms import fields
from . import widgets
class ModelFormField(FormField):
class ModelFormField(fields.FormField):
""" """
Customized ModelFormField for MongoEngine EmbeddedDocuments. Customized ModelFormField for MongoEngine EmbeddedDocuments.
""" """
...@@ -17,3 +20,24 @@ class ModelFormField(FormField): ...@@ -17,3 +20,24 @@ class ModelFormField(FormField):
setattr(obj, name, candidate) setattr(obj, name, candidate)
self.form.populate_obj(candidate) self.form.populate_obj(candidate)
class MongoFileField(fields.FileField):
widget = widgets.MongoFileInput()
def populate_obj(self, obj, name):
field = getattr(obj, name, None)
if field is not None:
data = request.files.get(self.name)
print data.filename
if data:
if not field.grid_id:
field.put(data.stream,
filename=data.filename,
content_type=data.content_type)
else:
field.replace(data.stream,
filename=data.filename,
content_type=data.content_type)
from operator import itemgetter
from mongoengine import ReferenceField from mongoengine import ReferenceField
from mongoengine.base import BaseDocument, DocumentMetaclass from mongoengine.base import BaseDocument, DocumentMetaclass
...@@ -12,7 +10,7 @@ from flask.ext.admin.model.fields import InlineFieldList ...@@ -12,7 +10,7 @@ from flask.ext.admin.model.fields import InlineFieldList
from flask.ext.admin.model.widgets import InlineFormWidget from flask.ext.admin.model.widgets import InlineFormWidget
from flask.ext.admin._compat import iteritems from flask.ext.admin._compat import iteritems
from .fields import ModelFormField from .fields import ModelFormField, MongoFileField
class CustomModelConverter(orm.ModelConverter): class CustomModelConverter(orm.ModelConverter):
...@@ -124,6 +122,10 @@ class CustomModelConverter(orm.ModelConverter): ...@@ -124,6 +122,10 @@ class CustomModelConverter(orm.ModelConverter):
kwargs['widget'] = form.Select2Widget() kwargs['widget'] = form.Select2Widget()
return orm.ModelConverter.conv_Reference(self, model, field, kwargs) return orm.ModelConverter.conv_Reference(self, model, field, kwargs)
@orm.converts('FileField')
def conv_File(self, model, field, kwargs):
return MongoFileField(**kwargs)
def get_form(model, converter, def get_form(model, converter,
base_class=form.BaseForm, base_class=form.BaseForm,
......
from flask import url_for
from jinja2 import Markup, escape
from mongoengine.base import BaseList from mongoengine.base import BaseList
from mongoengine.fields import GridFSProxy
from flask.ext.admin.model.typefmt import BASE_FORMATTERS, list_formatter from flask.ext.admin.model.typefmt import BASE_FORMATTERS, list_formatter
def gridfs_formatter(view, model, name, value):
return Markup(
'<a href="%s" target="_blank"><i class="icon-file"></i>%s</a> %dk (%s)' % (
url_for('.api_file_view', id=model.id, name=name),
escape(value.filename),
value.length // 1024,
escape(value.content_type))
)
DEFAULT_FORMATTERS = BASE_FORMATTERS.copy() DEFAULT_FORMATTERS = BASE_FORMATTERS.copy()
DEFAULT_FORMATTERS.update({ DEFAULT_FORMATTERS.update({
BaseList: list_formatter BaseList: list_formatter,
GridFSProxy: gridfs_formatter
}) })
import logging import logging
from flask import flash from flask import request, flash, abort, Response
from flask.ext.admin import expose
from flask.ext.admin.babel import gettext, ngettext, lazy_gettext from flask.ext.admin.babel import gettext, ngettext, lazy_gettext
from flask.ext.admin.model import BaseModelView from flask.ext.admin.model import BaseModelView
from flask.ext.admin._compat import iteritems, string_types from flask.ext.admin._compat import iteritems, string_types
import mongoengine import mongoengine
from mongoengine.fields import GridFSProxy
from bson.objectid import ObjectId from bson.objectid import ObjectId
from flask.ext.admin.actions import action from flask.ext.admin.actions import action
...@@ -30,7 +32,7 @@ SORTABLE_FIELDS = set(( ...@@ -30,7 +32,7 @@ SORTABLE_FIELDS = set((
mongoengine.EmailField, mongoengine.EmailField,
mongoengine.UUIDField, mongoengine.UUIDField,
mongoengine.URLField mongoengine.URLField
)) ))
class ModelView(BaseModelView): class ModelView(BaseModelView):
...@@ -402,6 +404,32 @@ class ModelView(BaseModelView): ...@@ -402,6 +404,32 @@ class ModelView(BaseModelView):
logging.exception('Failed to delete model') logging.exception('Failed to delete model')
return False return False
# FileField access API
@expose('/api/file/')
def api_file_view(self):
pk = request.args.get('id')
name = request.args.get('name')
if not pk or not name:
abort(404)
model = self.get_one(pk)
if model is None:
abort(404)
attr = getattr(model, name, None)
if attr is None:
abort(404)
if type(attr) != GridFSProxy:
abort(404)
return Response(attr.read(),
content_type=attr.content_type,
headers={
'Content-Length': attr.length
})
# Default model actions # Default model actions
def is_action_allowed(self, name): def is_action_allowed(self, name):
# Check delete action permission # Check delete action permission
......
from wtforms.widgets import HTMLString, html_params
from jinja2 import escape
class MongoFileInput(object):
"""
Renders a file input chooser field.
"""
template = '<div><i class="icon-file"></i>%(name)s %(length)dk (%(content_type)s)</div>'
def __call__(self, field, **kwargs):
kwargs.setdefault('id', field.id)
placeholder = ''
if field.data:
placeholder = self.template % dict(
name=escape(field.data.filename),
content_type=escape(field.data.content_type),
length=field.data.length // 1024)
return HTMLString('%s<input %s>' % (placeholder,
html_params(name=field.name,
type='file',
**kwargs)))
...@@ -930,17 +930,7 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -930,17 +930,7 @@ class BaseModelView(BaseView, ActionsMixin):
""" """
column_fmt = self.column_formatters.get(name) column_fmt = self.column_formatters.get(name)
if column_fmt is not None: if column_fmt is not None:
try: return column_fmt(self, context, model, name)
return column_fmt(self, context, model, name)
except TypeError:
warnings.warn(
u'Column formatter prototype was changed to accept view as first input parameter.\n'
u'Please update %s %s formatter to accept 4 parameters.' % (self.name, name),
stacklevel=2
)
self.column_formatters[name] = lambda _, c, m, n: column_fmt(c, m, n)
return column_fmt(context, model, name)
value = self._get_field_value(model, name) value = self._get_field_value(model, name)
...@@ -951,14 +941,14 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -951,14 +941,14 @@ class BaseModelView(BaseView, ActionsMixin):
type_fmt = self.column_type_formatters.get(type(value)) type_fmt = self.column_type_formatters.get(type(value))
if type_fmt is not None: if type_fmt is not None:
try: try:
value = type_fmt(self, value) value = type_fmt(self, model, name, value)
except TypeError: except TypeError:
warnings.warn('Type formatter prototype was changed to accept view as first input parameter.\n' + warnings.warn('Type formatter prototype was changed to accept view, model, name and value as input parameters.\n' +
'Please update %s %s formatter to accept 2 parameters.' % (self.name, type(value)), 'Please update %s %s formatter to accept 4 parameters.' % (self.name, type(value)),
stacklevel=2) stacklevel=2)
self.column_type_formatters[type(value)] = lambda _, value: type_fmt(value) self.column_type_formatters[type(value)] = lambda view, _model, _name, value: type_fmt(view, value)
value = type_fmt(value) value = type_fmt(self, value)
return value return value
......
...@@ -2,7 +2,7 @@ from jinja2 import Markup ...@@ -2,7 +2,7 @@ from jinja2 import Markup
from flask.ext.admin._compat import text_type from flask.ext.admin._compat import text_type
def null_formatter(view, value): def null_formatter(view, model, name, value):
""" """
Return `NULL` as the string for `None` value Return `NULL` as the string for `None` value
...@@ -12,7 +12,7 @@ def null_formatter(view, value): ...@@ -12,7 +12,7 @@ def null_formatter(view, value):
return Markup('<i>NULL</i>') return Markup('<i>NULL</i>')
def empty_formatter(view, value): def empty_formatter(view, model, name, value):
""" """
Return empty string for `None` value Return empty string for `None` value
...@@ -22,7 +22,7 @@ def empty_formatter(view, value): ...@@ -22,7 +22,7 @@ def empty_formatter(view, value):
return '' return ''
def bool_formatter(view, value): def bool_formatter(view, model, name, value):
""" """
Return check icon if value is `True` or empty string otherwise. Return check icon if value is `True` or empty string otherwise.
...@@ -32,7 +32,7 @@ def bool_formatter(view, value): ...@@ -32,7 +32,7 @@ def bool_formatter(view, value):
return Markup('<i class="icon-ok"></i>' if value else '') return Markup('<i class="icon-ok"></i>' if value else '')
def list_formatter(view, values): def list_formatter(view, model, name, values):
""" """
Return string with comma separated values Return string with comma separated values
......
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