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

Merge branch 'master' of github.com:mrjoes/flask-admin

parents d4480885 1b36345c
...@@ -9,7 +9,7 @@ msgid "" ...@@ -9,7 +9,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Flask-Admin VERSION\n" "Project-Id-Version: Flask-Admin VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2015-01-16 12:12-0600\n" "POT-Creation-Date: 2015-02-28 21:53-0600\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
...@@ -18,153 +18,159 @@ msgstr "" ...@@ -18,153 +18,159 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 1.3\n" "Generated-By: Babel 1.3\n"
#: ../flask_admin/base.py:419 #: ../flask_admin/base.py:426
msgid "Home" msgid "Home"
msgstr "" msgstr ""
#: ../flask_admin/contrib/fileadmin.py:34 #: ../flask_admin/contrib/fileadmin.py:220
msgid "Invalid directory name"
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:42
msgid "File to upload" msgid "File to upload"
msgstr "" msgstr ""
#: ../flask_admin/contrib/fileadmin.py:51 #: ../flask_admin/contrib/fileadmin.py:228
msgid "File required." msgid "File required."
msgstr "" msgstr ""
#: ../flask_admin/contrib/fileadmin.py:56 #: ../flask_admin/contrib/fileadmin.py:233
msgid "Invalid file type." msgid "Invalid file type."
msgstr "" msgstr ""
#: ../flask_admin/contrib/fileadmin.py:60 #: ../flask_admin/contrib/fileadmin.py:244
msgid "Content" msgid "Content"
msgstr "" msgstr ""
#: ../flask_admin/contrib/fileadmin.py:427 #: ../flask_admin/contrib/fileadmin.py:258
msgid "Invalid name"
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:266
#: ../flask_admin/templates/bootstrap2/admin/file/list.html:35
#: ../flask_admin/templates/bootstrap3/admin/file/list.html:35
msgid "Name"
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:544
#, python-format #, python-format
msgid "File \"%(name)s\" already exists." msgid "File \"%(name)s\" already exists."
msgstr "" msgstr ""
#: ../flask_admin/contrib/fileadmin.py:446 #: ../flask_admin/contrib/fileadmin.py:568
#: ../flask_admin/contrib/fileadmin.py:512 #: ../flask_admin/contrib/fileadmin.py:635
#: ../flask_admin/contrib/fileadmin.py:565 #: ../flask_admin/contrib/fileadmin.py:688
#: ../flask_admin/contrib/fileadmin.py:602 #: ../flask_admin/contrib/fileadmin.py:729
#: ../flask_admin/contrib/fileadmin.py:645 #: ../flask_admin/contrib/fileadmin.py:775
#: ../flask_admin/contrib/fileadmin.py:693 #: ../flask_admin/contrib/fileadmin.py:824
msgid "Permission denied." msgid "Permission denied."
msgstr "" msgstr ""
#: ../flask_admin/contrib/fileadmin.py:508 #: ../flask_admin/contrib/fileadmin.py:631
msgid "File uploading is disabled." msgid "File uploading is disabled."
msgstr "" msgstr ""
#: ../flask_admin/contrib/fileadmin.py:521 #: ../flask_admin/contrib/fileadmin.py:644
#, python-format #, python-format
msgid "Failed to save file: %(error)s" msgid "Failed to save file: %(error)s"
msgstr "" msgstr ""
#: ../flask_admin/contrib/fileadmin.py:561 #: ../flask_admin/contrib/fileadmin.py:684
msgid "Directory creation is disabled." msgid "Directory creation is disabled."
msgstr "" msgstr ""
#: ../flask_admin/contrib/fileadmin.py:576 #: ../flask_admin/contrib/fileadmin.py:699
#, python-format #, python-format
msgid "Failed to create directory: %(error)s" msgid "Failed to create directory: %(error)s"
msgstr "" msgstr ""
#: ../flask_admin/contrib/fileadmin.py:598 #: ../flask_admin/contrib/fileadmin.py:725
msgid "Deletion is disabled." msgid "Deletion is disabled."
msgstr "" msgstr ""
#: ../flask_admin/contrib/fileadmin.py:607 #: ../flask_admin/contrib/fileadmin.py:734
msgid "Directory deletion is disabled." msgid "Directory deletion is disabled."
msgstr "" msgstr ""
#: ../flask_admin/contrib/fileadmin.py:613 #: ../flask_admin/contrib/fileadmin.py:740
#, python-format #, python-format
msgid "Directory \"%(path)s\" was successfully deleted." msgid "Directory \"%(path)s\" was successfully deleted."
msgstr "" msgstr ""
#: ../flask_admin/contrib/fileadmin.py:615 #: ../flask_admin/contrib/fileadmin.py:742
#, python-format #, python-format
msgid "Failed to delete directory: %(error)s" msgid "Failed to delete directory: %(error)s"
msgstr "" msgstr ""
#: ../flask_admin/contrib/fileadmin.py:620 #: ../flask_admin/contrib/fileadmin.py:747
#: ../flask_admin/contrib/fileadmin.py:759 #: ../flask_admin/contrib/fileadmin.py:892
#, python-format #, python-format
msgid "File \"%(name)s\" was successfully deleted." msgid "File \"%(name)s\" was successfully deleted."
msgstr "" msgstr ""
#: ../flask_admin/contrib/fileadmin.py:622 #: ../flask_admin/contrib/fileadmin.py:749
#: ../flask_admin/contrib/fileadmin.py:761 #: ../flask_admin/contrib/fileadmin.py:894
#, python-format #, python-format
msgid "Failed to delete file: %(name)s" msgid "Failed to delete file: %(name)s"
msgstr "" msgstr ""
#: ../flask_admin/contrib/fileadmin.py:641 #: ../flask_admin/contrib/fileadmin.py:771
msgid "Renaming is disabled." msgid "Renaming is disabled."
msgstr "" msgstr ""
#: ../flask_admin/contrib/fileadmin.py:649 #: ../flask_admin/contrib/fileadmin.py:779
msgid "Path does not exist." msgid "Path does not exist."
msgstr "" msgstr ""
#: ../flask_admin/contrib/fileadmin.py:661 #: ../flask_admin/contrib/fileadmin.py:790
#, python-format #, python-format
msgid "Successfully renamed \"%(src)s\" to \"%(dst)s\"" msgid "Successfully renamed \"%(src)s\" to \"%(dst)s\""
msgstr "" msgstr ""
#: ../flask_admin/contrib/fileadmin.py:664 #: ../flask_admin/contrib/fileadmin.py:793
#, python-format #, python-format
msgid "Failed to rename: %(error)s" msgid "Failed to rename: %(error)s"
msgstr "" msgstr ""
#: ../flask_admin/contrib/fileadmin.py:709 #: ../flask_admin/contrib/fileadmin.py:840
#, python-format #, python-format
msgid "Error saving changes to %(name)s." msgid "Error saving changes to %(name)s."
msgstr "" msgstr ""
#: ../flask_admin/contrib/fileadmin.py:713 #: ../flask_admin/contrib/fileadmin.py:844
#, python-format #, python-format
msgid "Changes to %(name)s saved successfully." msgid "Changes to %(name)s saved successfully."
msgstr "" msgstr ""
#: ../flask_admin/contrib/fileadmin.py:720 #: ../flask_admin/contrib/fileadmin.py:853
#, python-format #, python-format
msgid "Error reading %(name)s." msgid "Error reading %(name)s."
msgstr "" msgstr ""
#: ../flask_admin/contrib/fileadmin.py:723 #: ../flask_admin/contrib/fileadmin.py:856
#: ../flask_admin/contrib/fileadmin.py:732 #: ../flask_admin/contrib/fileadmin.py:865
#, python-format #, python-format
msgid "Unexpected error while reading from %(name)s" msgid "Unexpected error while reading from %(name)s"
msgstr "" msgstr ""
#: ../flask_admin/contrib/fileadmin.py:729 #: ../flask_admin/contrib/fileadmin.py:862
#, python-format #, python-format
msgid "Cannot edit %(name)s." msgid "Cannot edit %(name)s."
msgstr "" msgstr ""
#: ../flask_admin/contrib/fileadmin.py:746 #: ../flask_admin/contrib/fileadmin.py:879
#: ../flask_admin/contrib/mongoengine/view.py:612 #: ../flask_admin/contrib/mongoengine/view.py:625
#: ../flask_admin/contrib/peewee/view.py:418 #: ../flask_admin/contrib/peewee/view.py:429
#: ../flask_admin/contrib/pymongo/view.py:343 #: ../flask_admin/contrib/pymongo/view.py:348
#: ../flask_admin/contrib/sqla/view.py:945 #: ../flask_admin/contrib/sqla/view.py:974
msgid "Delete" msgid "Delete"
msgstr "" msgstr ""
#: ../flask_admin/contrib/fileadmin.py:747 #: ../flask_admin/contrib/fileadmin.py:880
msgid "Are you sure you want to delete these files?" msgid "Are you sure you want to delete these files?"
msgstr "" msgstr ""
#: ../flask_admin/contrib/fileadmin.py:750 #: ../flask_admin/contrib/fileadmin.py:883
msgid "File deletion is disabled." msgid "File deletion is disabled."
msgstr "" msgstr ""
#: ../flask_admin/contrib/fileadmin.py:763 #: ../flask_admin/contrib/fileadmin.py:896
msgid "Edit" msgid "Edit"
msgstr "" msgstr ""
...@@ -246,57 +252,57 @@ msgstr "" ...@@ -246,57 +252,57 @@ msgstr ""
msgid "not between" msgid "not between"
msgstr "" msgstr ""
#: ../flask_admin/contrib/mongoengine/view.py:506 #: ../flask_admin/contrib/mongoengine/view.py:519
#, python-format #, python-format
msgid "Failed to get model. %(error)s" msgid "Failed to get model. %(error)s"
msgstr "" msgstr ""
#: ../flask_admin/contrib/mongoengine/view.py:525 #: ../flask_admin/contrib/mongoengine/view.py:538
#: ../flask_admin/contrib/peewee/view.py:369 #: ../flask_admin/contrib/peewee/view.py:380
#: ../flask_admin/contrib/pymongo/view.py:278 #: ../flask_admin/contrib/pymongo/view.py:283
#: ../flask_admin/contrib/sqla/view.py:877 #: ../flask_admin/contrib/sqla/view.py:906
#, python-format #, python-format
msgid "Failed to create record. %(error)s" msgid "Failed to create record. %(error)s"
msgstr "" msgstr ""
#: ../flask_admin/contrib/mongoengine/view.py:551 #: ../flask_admin/contrib/mongoengine/view.py:564
#: ../flask_admin/contrib/peewee/view.py:388 #: ../flask_admin/contrib/peewee/view.py:399
#: ../flask_admin/contrib/pymongo/view.py:303 #: ../flask_admin/contrib/pymongo/view.py:308
#: ../flask_admin/contrib/sqla/view.py:903 ../flask_admin/model/base.py:1563 #: ../flask_admin/contrib/sqla/view.py:932 ../flask_admin/model/base.py:1613
#: ../flask_admin/model/base.py:1572 #: ../flask_admin/model/base.py:1622
#, python-format #, python-format
msgid "Failed to update record. %(error)s" msgid "Failed to update record. %(error)s"
msgstr "" msgstr ""
#: ../flask_admin/contrib/mongoengine/view.py:575 #: ../flask_admin/contrib/mongoengine/view.py:588
#: ../flask_admin/contrib/peewee/view.py:404 #: ../flask_admin/contrib/peewee/view.py:415
#: ../flask_admin/contrib/pymongo/view.py:329 #: ../flask_admin/contrib/pymongo/view.py:334
#: ../flask_admin/contrib/sqla/view.py:929 ../flask_admin/model/base.py:1513 #: ../flask_admin/contrib/sqla/view.py:958
#, python-format #, python-format
msgid "Failed to delete record. %(error)s" msgid "Failed to delete record. %(error)s"
msgstr "" msgstr ""
#: ../flask_admin/contrib/mongoengine/view.py:613 #: ../flask_admin/contrib/mongoengine/view.py:626
#: ../flask_admin/contrib/peewee/view.py:419 #: ../flask_admin/contrib/peewee/view.py:430
#: ../flask_admin/contrib/pymongo/view.py:344 #: ../flask_admin/contrib/pymongo/view.py:349
#: ../flask_admin/contrib/sqla/view.py:946 #: ../flask_admin/contrib/sqla/view.py:975
msgid "Are you sure you want to delete selected records?" msgid "Are you sure you want to delete selected records?"
msgstr "" msgstr ""
#: ../flask_admin/contrib/mongoengine/view.py:622 #: ../flask_admin/contrib/mongoengine/view.py:635
#: ../flask_admin/contrib/peewee/view.py:435 #: ../flask_admin/contrib/peewee/view.py:446
#: ../flask_admin/contrib/pymongo/view.py:354 #: ../flask_admin/contrib/pymongo/view.py:359
#: ../flask_admin/contrib/sqla/view.py:962 ../flask_admin/model/base.py:1506 #: ../flask_admin/contrib/sqla/view.py:991 ../flask_admin/model/base.py:1561
#, python-format #, python-format
msgid "Record was successfully deleted." msgid "Record was successfully deleted."
msgid_plural "%(count)s records were successfully deleted." msgid_plural "%(count)s records were successfully deleted."
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
#: ../flask_admin/contrib/mongoengine/view.py:628 #: ../flask_admin/contrib/mongoengine/view.py:641
#: ../flask_admin/contrib/peewee/view.py:441 #: ../flask_admin/contrib/peewee/view.py:452
#: ../flask_admin/contrib/pymongo/view.py:359 #: ../flask_admin/contrib/pymongo/view.py:364
#: ../flask_admin/contrib/sqla/view.py:970 #: ../flask_admin/contrib/sqla/view.py:999
#, python-format #, python-format
msgid "Failed to delete records. %(error)s" msgid "Failed to delete records. %(error)s"
msgstr "" msgstr ""
...@@ -319,7 +325,7 @@ msgid_plural "At least %d items are required" ...@@ -319,7 +325,7 @@ msgid_plural "At least %d items are required"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
#: ../flask_admin/contrib/sqla/view.py:856 #: ../flask_admin/contrib/sqla/view.py:885
#, python-format #, python-format
msgid "Integrity error. %(message)s" msgid "Integrity error. %(message)s"
msgstr "" msgstr ""
...@@ -336,20 +342,20 @@ msgstr "" ...@@ -336,20 +342,20 @@ msgstr ""
msgid "Invalid file extension" msgid "Invalid file extension"
msgstr "" msgstr ""
#: ../flask_admin/model/base.py:1173 #: ../flask_admin/model/base.py:1227
msgid "There are no items in the table." msgid "There are no items in the table."
msgstr "" msgstr ""
#: ../flask_admin/model/base.py:1197 #: ../flask_admin/model/base.py:1251
#, python-format #, python-format
msgid "Invalid Filter Value: %(value)s" msgid "Invalid Filter Value: %(value)s"
msgstr "" msgstr ""
#: ../flask_admin/model/base.py:1430 #: ../flask_admin/model/base.py:1484
msgid "Record was successfully created." msgid "Record was successfully created."
msgstr "" msgstr ""
#: ../flask_admin/model/base.py:1467 ../flask_admin/model/base.py:1568 #: ../flask_admin/model/base.py:1521 ../flask_admin/model/base.py:1618
msgid "Record was successfully saved." msgid "Record was successfully saved."
msgstr "" msgstr ""
...@@ -392,40 +398,35 @@ msgstr "" ...@@ -392,40 +398,35 @@ msgstr ""
msgid "Root" msgid "Root"
msgstr "" msgstr ""
#: ../flask_admin/templates/bootstrap2/admin/file/list.html:35
#: ../flask_admin/templates/bootstrap3/admin/file/list.html:35
msgid "Name"
msgstr ""
#: ../flask_admin/templates/bootstrap2/admin/file/list.html:36 #: ../flask_admin/templates/bootstrap2/admin/file/list.html:36
#: ../flask_admin/templates/bootstrap3/admin/file/list.html:36 #: ../flask_admin/templates/bootstrap3/admin/file/list.html:36
msgid "Size" msgid "Size"
msgstr "" msgstr ""
#: ../flask_admin/templates/bootstrap2/admin/file/list.html:62 #: ../flask_admin/templates/bootstrap2/admin/file/list.html:63
#: ../flask_admin/templates/bootstrap3/admin/file/list.html:62 #: ../flask_admin/templates/bootstrap3/admin/file/list.html:63
#, python-format #, python-format
msgid "Are you sure you want to delete \\'%(name)s\\' recursively?" msgid "Are you sure you want to delete \\'%(name)s\\' recursively?"
msgstr "" msgstr ""
#: ../flask_admin/templates/bootstrap2/admin/file/list.html:70 #: ../flask_admin/templates/bootstrap2/admin/file/list.html:72
#: ../flask_admin/templates/bootstrap3/admin/file/list.html:70 #: ../flask_admin/templates/bootstrap3/admin/file/list.html:72
#, python-format #, python-format
msgid "Are you sure you want to delete \\'%(name)s\\'?" msgid "Are you sure you want to delete \\'%(name)s\\'?"
msgstr "" msgstr ""
#: ../flask_admin/templates/bootstrap2/admin/file/list.html:105 #: ../flask_admin/templates/bootstrap2/admin/file/list.html:107
#: ../flask_admin/templates/bootstrap3/admin/file/list.html:105 #: ../flask_admin/templates/bootstrap3/admin/file/list.html:107
msgid "Upload File" msgid "Upload File"
msgstr "" msgstr ""
#: ../flask_admin/templates/bootstrap2/admin/file/list.html:110 #: ../flask_admin/templates/bootstrap2/admin/file/list.html:112
#: ../flask_admin/templates/bootstrap3/admin/file/list.html:110 #: ../flask_admin/templates/bootstrap3/admin/file/list.html:112
msgid "Create Directory" msgid "Create Directory"
msgstr "" msgstr ""
#: ../flask_admin/templates/bootstrap2/admin/file/list.html:127 #: ../flask_admin/templates/bootstrap2/admin/file/list.html:129
#: ../flask_admin/templates/bootstrap3/admin/file/list.html:127 #: ../flask_admin/templates/bootstrap3/admin/file/list.html:129
msgid "Please select at least one file." msgid "Please select at least one file."
msgstr "" msgstr ""
...@@ -519,17 +520,17 @@ msgstr "" ...@@ -519,17 +520,17 @@ msgstr ""
msgid "Edit record" msgid "Edit record"
msgstr "" msgstr ""
#: ../flask_admin/templates/bootstrap2/admin/model/list.html:116 #: ../flask_admin/templates/bootstrap2/admin/model/list.html:114
#: ../flask_admin/templates/bootstrap3/admin/model/list.html:116 #: ../flask_admin/templates/bootstrap3/admin/model/list.html:114
msgid "Are you sure you want to delete this record?" msgid "Are you sure you want to delete this record?"
msgstr "" msgstr ""
#: ../flask_admin/templates/bootstrap2/admin/model/list.html:116 #: ../flask_admin/templates/bootstrap2/admin/model/list.html:114
msgid "Delete record" msgid "Delete record"
msgstr "" msgstr ""
#: ../flask_admin/templates/bootstrap2/admin/model/list.html:161 #: ../flask_admin/templates/bootstrap2/admin/model/list.html:159
#: ../flask_admin/templates/bootstrap3/admin/model/list.html:160 #: ../flask_admin/templates/bootstrap3/admin/model/list.html:158
msgid "Please select at least one record." msgid "Please select at least one record."
msgstr "" msgstr ""
try:
import wtforms_appengine
except ImportError:
raise Exception('Please install wtforms_appengine in order to use appengine backend')
from .view import ModelView
import logging
from flask.ext.admin.model import BaseModelView
from wtforms_appengine import db as wt_db
from wtforms_appengine import ndb as wt_ndb
from google.appengine.ext import db
from google.appengine.ext import ndb
class NdbModelView(BaseModelView):
"""
AppEngine NDB model scaffolding.
"""
def get_pk_value(self, model):
return model.key.urlsafe()
def scaffold_list_columns(self):
return sorted([k for (k, v) in self.model.__dict__.iteritems() if isinstance(v, ndb.Property)])
def scaffold_sortable_columns(self):
return [k for (k, v) in self.model.__dict__.iteritems() if isinstance(v, ndb.Property) and v._indexed]
def init_search(self):
return None
def is_valid_filter(self):
pass
def scaffold_filters(self):
#TODO: implement
pass
def scaffold_form(self):
return wt_ndb.model_form(self.model())
def get_list(self, page, sort_field, sort_desc, search, filters):
#TODO: implement filters (don't think search can work here)
q = self.model.query()
if sort_field:
order_field = getattr(self.model, sort_field)
if sort_desc:
order_field = -order_field
q = q.order(order_field)
results = q.fetch(self.page_size, offset=page*self.page_size)
return q.count(), results
def get_one(self, urlsafe_key):
return ndb.Key(urlsafe=urlsafe_key).get()
def create_model(self, form):
try:
model = self.model()
form.populate_obj(model)
model.put()
return True
except Exception as ex:
if not self.handle_view_exception(ex):
#flash(gettext('Failed to create record. %(error)s',
# error=ex), 'error')
logging.exception('Failed to create record.')
return False
def update_model(self, form, model):
try:
form.populate_obj(model)
model.put()
return True
except Exception as ex:
if not self.handle_view_exception(ex):
#flash(gettext('Failed to update record. %(error)s',
# error=ex), 'error')
logging.exception('Failed to update record.')
return False
def delete_model(self, model):
try:
model.key.delete()
return True
except Exception as ex:
if not self.handle_view_exception(ex):
#flash(gettext('Failed to delete record. %(error)s',
# error=ex),
# 'error')
logging.exception('Failed to delete record.')
return False
class DbModelView(BaseModelView):
"""
AppEngine DB model scaffolding.
"""
def get_pk_value(self, model):
return str(model.key())
def scaffold_list_columns(self):
return sorted([k for (k, v) in self.model.__dict__.iteritems() if isinstance(v, db.Property)])
def scaffold_sortable_columns(self):
return [k for (k, v) in self.model.__dict__.iteritems() if isinstance(v, db.Property) and v._indexed]
def init_search(self):
return None
def is_valid_filter(self):
pass
def scaffold_filters(self):
#TODO: implement
pass
def scaffold_form(self):
return wt_db.model_form(self.model())
def get_list(self, page, sort_field, sort_desc, search, filters):
#TODO: implement filters (don't think search can work here)
q = self.model.all()
if sort_field:
if sort_desc:
sort_field = "-" + sort_field
q.order(sort_field)
results = q.fetch(self.page_size, offset=page*self.page_size)
return q.count(), results
def get_one(self, encoded_key):
return db.get(db.Key(encoded=encoded_key))
def create_model(self, form):
try:
model = self.model()
form.populate_obj(model)
model.put()
return True
except Exception as ex:
if not self.handle_view_exception(ex):
#flash(gettext('Failed to create record. %(error)s',
# error=ex), 'error')
logging.exception('Failed to create record.')
return False
def update_model(self, form, model):
try:
form.populate_obj(model)
model.put()
return True
except Exception as ex:
if not self.handle_view_exception(ex):
#flash(gettext('Failed to update record. %(error)s',
# error=ex), 'error')
logging.exception('Failed to update record.')
return False
def delete_model(self, model):
try:
model.delete()
return True
except Exception as ex:
if not self.handle_view_exception(ex):
#flash(gettext('Failed to delete record. %(error)s',
# error=ex),
# 'error')
logging.exception('Failed to delete record.')
return False
def ModelView(model):
if issubclass(model, ndb.Model):
return NdbModelView(model)
elif issubclass(model, db.Model):
return DbModelView(model)
else:
raise ValueError("Unsupported model: %s" % model)
...@@ -19,48 +19,6 @@ from flask.ext.admin.actions import action, ActionsMixin ...@@ -19,48 +19,6 @@ from flask.ext.admin.actions import action, ActionsMixin
from flask.ext.admin.babel import gettext, lazy_gettext from flask.ext.admin.babel import gettext, lazy_gettext
class NameForm(form.BaseForm):
"""
Form with a filename input field.
Validates if provided name is valid for *nix and Windows systems.
"""
name = fields.StringField()
regexp = re.compile(r'^(?!^(PRN|AUX|CLOCK\$|NUL|CON|COM\d|LPT\d|\..*)(\..+)?$)[^\x00-\x1f\\?*:\";|/]+$')
def validate_name(self, field):
if not self.regexp.match(field.data):
raise validators.ValidationError(gettext('Invalid directory name'))
class UploadForm(form.BaseForm):
"""
File upload form. Works with FileAdmin instance to check if it is allowed
to upload file with given extension.
"""
upload = fields.FileField(lazy_gettext('File to upload'))
def __init__(self, admin):
self.admin = admin
super(UploadForm, self).__init__(helpers.get_form_data())
def validate_upload(self, field):
if not self.upload.data:
raise validators.ValidationError(gettext('File required.'))
filename = self.upload.data.filename
if not self.admin.is_file_allowed(filename):
raise validators.ValidationError(gettext('Invalid file type.'))
class EditForm(form.BaseForm):
content = fields.TextAreaField(lazy_gettext('Content'),
(validators.required(),))
class FileAdmin(BaseView, ActionsMixin): class FileAdmin(BaseView, ActionsMixin):
""" """
Simple file-management interface. Simple file-management interface.
...@@ -74,11 +32,15 @@ class FileAdmin(BaseView, ActionsMixin): ...@@ -74,11 +32,15 @@ class FileAdmin(BaseView, ActionsMixin):
Sample usage:: Sample usage::
import os.path as op
from flask.ext.admin import Admin
from flask.ext.admin.contrib.fileadmin import FileAdmin
admin = Admin() admin = Admin()
path = op.join(op.dirname(__file__), 'static') path = op.join(op.dirname(__file__), 'static')
admin.add_view(FileAdmin(path, '/static/', name='Static Files')) admin.add_view(FileAdmin(path, '/static/', name='Static Files'))
admin.setup_app(app)
""" """
can_upload = True can_upload = True
...@@ -156,9 +118,22 @@ class FileAdmin(BaseView, ActionsMixin): ...@@ -156,9 +118,22 @@ class FileAdmin(BaseView, ActionsMixin):
Edit template Edit template
""" """
upload_form = UploadForm form_base_class = form.BaseForm
""" """
Upload form class Base form class. Will be used to create the upload, rename, edit, and delete form.
Allows enabling CSRF validation and useful if you want to have custom
contructor or override some fields.
Example::
class MyBaseForm(Form):
def do_something(self):
pass
class MyAdmin(FileAdmin):
form_base_class = MyBaseForm
""" """
def __init__(self, base_path, base_url=None, def __init__(self, base_path, base_url=None,
...@@ -231,6 +206,139 @@ class FileAdmin(BaseView, ActionsMixin): ...@@ -231,6 +206,139 @@ class FileAdmin(BaseView, ActionsMixin):
""" """
return self.base_url return self.base_url
def get_upload_form(self):
"""
Upload form class for file upload view.
Override to implement customized behavior.
"""
class UploadForm(self.form_base_class):
"""
File upload form. Works with FileAdmin instance to check if it
is allowed to upload file with given extension.
"""
upload = fields.FileField(lazy_gettext('File to upload'))
def __init__(self, *args, **kwargs):
super(UploadForm, self).__init__(*args, **kwargs)
self.admin = kwargs['admin']
def validate_upload(self, field):
if not self.upload.data:
raise validators.ValidationError(gettext('File required.'))
filename = self.upload.data.filename
if not self.admin.is_file_allowed(filename):
raise validators.ValidationError(gettext('Invalid file type.'))
return UploadForm
def get_edit_form(self):
"""
Create form class for file editing view.
Override to implement customized behavior.
"""
class EditForm(self.form_base_class):
content = fields.TextAreaField(lazy_gettext('Content'),
(validators.required(),))
return EditForm
def get_name_form(self):
"""
Create form class for renaming and mkdir views.
Override to implement customized behavior.
"""
def validate_name(self, field):
regexp = re.compile(r'^(?!^(PRN|AUX|CLOCK\$|NUL|CON|COM\d|LPT\d|\..*)(\..+)?$)[^\x00-\x1f\\?*:\";|/]+$')
if not regexp.match(field.data):
raise validators.ValidationError(gettext('Invalid name'))
class NameForm(self.form_base_class):
"""
Form with a filename input field.
Validates if provided name is valid for *nix and Windows systems.
"""
name = fields.StringField(lazy_gettext('Name'),
validators=[validators.Required(),
validate_name])
path = fields.HiddenField()
return NameForm
def get_delete_form(self):
"""
Create form class for model delete view.
Override to implement customized behavior.
"""
class DeleteForm(self.form_base_class):
path = fields.HiddenField(validators=[validators.Required()])
return DeleteForm
def upload_form(self):
"""
Instantiate file upload form and return it.
Override to implement custom behavior.
"""
upload_form_class = self.get_upload_form()
if request.form:
# Workaround for allowing both CSRF token + FileField to be submitted
# https://bitbucket.org/danjac/flask-wtf/issue/12/fieldlist-filefield-does-not-follow
formdata = request.form.copy() # as request.form is immutable
formdata.update(request.files)
# admin=self allows the form to use self.is_file_allowed
return upload_form_class(formdata, admin=self)
elif request.files:
return upload_form_class(request.files, admin=self)
else:
return upload_form_class(admin=self)
def name_form(self):
"""
Instantiate form used in rename and mkdir then return it.
Override to implement custom behavior.
"""
name_form_class = self.get_name_form()
if request.form:
return name_form_class(request.form)
elif request.args:
return name_form_class(request.args)
else:
return name_form_class()
def edit_form(self):
"""
Instantiate file editing form and return it.
Override to implement custom behavior.
"""
edit_form_class = self.get_edit_form()
if request.form:
return edit_form_class(request.form)
else:
return edit_form_class()
def delete_form(self):
"""
Instantiate file delete form and return it.
Override to implement custom behavior.
"""
delete_form_class = self.get_delete_form()
if request.form:
return delete_form_class(request.form)
else:
return delete_form_class()
def is_file_allowed(self, filename): def is_file_allowed(self, filename):
""" """
Verify if file can be uploaded. Verify if file can be uploaded.
...@@ -291,6 +399,15 @@ class FileAdmin(BaseView, ActionsMixin): ...@@ -291,6 +399,15 @@ class FileAdmin(BaseView, ActionsMixin):
""" """
file_data.save(path) file_data.save(path)
def validate_form(self, form):
"""
Validate the form on submit.
:param form:
Form to validate
"""
return helpers.validate_form_on_submit(form)
def _get_dir_url(self, endpoint, path=None, **kwargs): def _get_dir_url(self, endpoint, path=None, **kwargs):
""" """
Return prettified URL Return prettified URL
...@@ -439,11 +556,16 @@ class FileAdmin(BaseView, ActionsMixin): ...@@ -439,11 +556,16 @@ class FileAdmin(BaseView, ActionsMixin):
:param path: :param path:
Optional directory path. If not provided, will use the base directory Optional directory path. If not provided, will use the base directory
""" """
if self.can_delete:
delete_form = self.delete_form()
else:
delete_form = None
# Get path and verify if it is valid # Get path and verify if it is valid
base_path, directory, path = self._normalize_path(path) base_path, directory, path = self._normalize_path(path)
if not self.is_accessible_path(path): if not self.is_accessible_path(path):
flash(gettext('Permission denied.')) flash(gettext('Permission denied.'), 'error')
return redirect(self._get_dir_url('.index')) return redirect(self._get_dir_url('.index'))
# Get directory listing # Get directory listing
...@@ -490,7 +612,8 @@ class FileAdmin(BaseView, ActionsMixin): ...@@ -490,7 +612,8 @@ class FileAdmin(BaseView, ActionsMixin):
get_file_url=self._get_file_url, get_file_url=self._get_file_url,
items=items, items=items,
actions=actions, actions=actions,
actions_confirmation=actions_confirmation) actions_confirmation=actions_confirmation,
delete_form=delete_form)
@expose('/upload/', methods=('GET', 'POST')) @expose('/upload/', methods=('GET', 'POST'))
@expose('/upload/<path:path>', methods=('GET', 'POST')) @expose('/upload/<path:path>', methods=('GET', 'POST'))
...@@ -509,16 +632,16 @@ class FileAdmin(BaseView, ActionsMixin): ...@@ -509,16 +632,16 @@ class FileAdmin(BaseView, ActionsMixin):
return redirect(self._get_dir_url('.index', path)) return redirect(self._get_dir_url('.index', path))
if not self.is_accessible_path(path): if not self.is_accessible_path(path):
flash(gettext('Permission denied.')) flash(gettext('Permission denied.'), 'error')
return redirect(self._get_dir_url('.index')) return redirect(self._get_dir_url('.index'))
form = self.upload_form(self) form = self.upload_form()
if helpers.validate_form_on_submit(form): if self.validate_form(form):
try: try:
self._save_form_files(directory, path, form) self._save_form_files(directory, path, form)
return redirect(self._get_dir_url('.index', path)) return redirect(self._get_dir_url('.index', path))
except Exception as ex: except Exception as ex:
flash(gettext('Failed to save file: %(error)s', error=ex)) flash(gettext('Failed to save file: %(error)s', error=ex), 'error')
return self.render(self.upload_template, form=form) return self.render(self.upload_template, form=form)
...@@ -562,18 +685,20 @@ class FileAdmin(BaseView, ActionsMixin): ...@@ -562,18 +685,20 @@ class FileAdmin(BaseView, ActionsMixin):
return redirect(dir_url) return redirect(dir_url)
if not self.is_accessible_path(path): if not self.is_accessible_path(path):
flash(gettext('Permission denied.')) flash(gettext('Permission denied.'), 'error')
return redirect(self._get_dir_url('.index')) return redirect(self._get_dir_url('.index'))
form = NameForm(helpers.get_form_data()) form = self.name_form()
if helpers.validate_form_on_submit(form): if self.validate_form(form):
try: try:
os.mkdir(op.join(directory, form.name.data)) os.mkdir(op.join(directory, form.name.data))
self.on_mkdir(directory, form.name.data) self.on_mkdir(directory, form.name.data)
return redirect(dir_url) return redirect(dir_url)
except Exception as ex: except Exception as ex:
flash(gettext('Failed to create directory: %(error)s', error=ex), 'error') flash(gettext('Failed to create directory: %(error)s', error=ex), 'error')
else:
helpers.flash_errors(form, message='Failed to create directory: %(error)s')
return self.render(self.mkdir_template, return self.render(self.mkdir_template,
form=form, form=form,
...@@ -584,42 +709,46 @@ class FileAdmin(BaseView, ActionsMixin): ...@@ -584,42 +709,46 @@ class FileAdmin(BaseView, ActionsMixin):
""" """
Delete view method Delete view method
""" """
path = request.form.get('path') form = self.delete_form()
if not path:
return redirect(self.get_url('.index'))
# Get path and verify if it is valid path = form.path.data
base_path, full_path, path = self._normalize_path(path) if path:
return_url = self._get_dir_url('.index', op.dirname(path))
else:
return_url = self.get_url('.index')
return_url = self._get_dir_url('.index', op.dirname(path)) if self.validate_form(form):
# Get path and verify if it is valid
base_path, full_path, path = self._normalize_path(path)
if not self.can_delete: if not self.can_delete:
flash(gettext('Deletion is disabled.')) flash(gettext('Deletion is disabled.'), 'error')
return redirect(return_url) return redirect(return_url)
if not self.is_accessible_path(path): if not self.is_accessible_path(path):
flash(gettext('Permission denied.')) flash(gettext('Permission denied.'), 'error')
return redirect(self._get_dir_url('.index')) return redirect(self._get_dir_url('.index'))
if op.isdir(full_path): if op.isdir(full_path):
if not self.can_delete_dirs: if not self.can_delete_dirs:
flash(gettext('Directory deletion is disabled.')) flash(gettext('Directory deletion is disabled.'), 'error')
return redirect(return_url) return redirect(return_url)
try: try:
shutil.rmtree(full_path) shutil.rmtree(full_path)
self.on_directory_delete(full_path, path) self.on_directory_delete(full_path, path)
flash(gettext('Directory "%(path)s" was successfully deleted.', path=path)) flash(gettext('Directory "%(path)s" was successfully deleted.', path=path))
except Exception as ex: except Exception as ex:
flash(gettext('Failed to delete directory: %(error)s', error=ex), 'error') flash(gettext('Failed to delete directory: %(error)s', error=ex), 'error')
else:
try:
os.remove(full_path)
self.on_file_delete(full_path, path)
flash(gettext('File "%(name)s" was successfully deleted.', name=path))
except Exception as ex:
flash(gettext('Failed to delete file: %(name)s', name=ex), 'error')
else: else:
try: helpers.flash_errors(form, message='Failed to delete file. %(error)s')
os.remove(full_path)
self.on_file_delete(full_path, path)
flash(gettext('File "%(name)s" was successfully deleted.', name=path))
except Exception as ex:
flash(gettext('Failed to delete file: %(name)s', name=ex), 'error')
return redirect(return_url) return redirect(return_url)
...@@ -628,29 +757,29 @@ class FileAdmin(BaseView, ActionsMixin): ...@@ -628,29 +757,29 @@ class FileAdmin(BaseView, ActionsMixin):
""" """
Rename view method Rename view method
""" """
path = request.args.get('path') form = self.name_form()
if not path:
return redirect(self.get_url('.index'))
base_path, full_path, path = self._normalize_path(path) path = form.path.data
if path:
base_path, full_path, path = self._normalize_path(path)
return_url = self._get_dir_url('.index', op.dirname(path)) return_url = self._get_dir_url('.index', op.dirname(path))
else:
return redirect(self.get_url('.index'))
if not self.can_rename: if not self.can_rename:
flash(gettext('Renaming is disabled.')) flash(gettext('Renaming is disabled.'), 'error')
return redirect(return_url) return redirect(return_url)
if not self.is_accessible_path(path): if not self.is_accessible_path(path):
flash(gettext('Permission denied.')) flash(gettext('Permission denied.'), 'error')
return redirect(self._get_dir_url('.index')) return redirect(self._get_dir_url('.index'))
if not op.exists(full_path): if not op.exists(full_path):
flash(gettext('Path does not exist.')) flash(gettext('Path does not exist.'), 'error')
return redirect(return_url) return redirect(return_url)
form = NameForm(helpers.get_form_data(), name=op.basename(path)) if self.validate_form(form):
if helpers.validate_form_on_submit(form):
try: try:
dir_base = op.dirname(full_path) dir_base = op.dirname(full_path)
filename = secure_filename(form.name.data) filename = secure_filename(form.name.data)
...@@ -664,6 +793,8 @@ class FileAdmin(BaseView, ActionsMixin): ...@@ -664,6 +793,8 @@ class FileAdmin(BaseView, ActionsMixin):
flash(gettext('Failed to rename: %(error)s', error=ex), 'error') flash(gettext('Failed to rename: %(error)s', error=ex), 'error')
return redirect(return_url) return redirect(return_url)
else:
helpers.flash_errors(form, message='Failed to rename: %(error)s')
return self.render(self.rename_template, return self.render(self.rename_template,
form=form, form=form,
...@@ -690,16 +821,16 @@ class FileAdmin(BaseView, ActionsMixin): ...@@ -690,16 +821,16 @@ class FileAdmin(BaseView, ActionsMixin):
base_path, full_path, path = self._normalize_path(path) base_path, full_path, path = self._normalize_path(path)
if not self.is_accessible_path(path) or not self.is_file_editable(path): if not self.is_accessible_path(path) or not self.is_file_editable(path):
flash(gettext('Permission denied.')) flash(gettext('Permission denied.'), 'error')
return redirect(self._get_dir_url('.index')) return redirect(self._get_dir_url('.index'))
dir_url = self._get_dir_url('.index', os.path.dirname(path)) dir_url = self._get_dir_url('.index', os.path.dirname(path))
next_url = next_url or dir_url next_url = next_url or dir_url
form = EditForm(helpers.get_form_data()) form = self.edit_form()
error = False error = False
if helpers.validate_form_on_submit(form): if self.validate_form(form):
form.process(request.form, content='') form.process(request.form, content='')
if form.validate(): if form.validate():
try: try:
...@@ -713,8 +844,10 @@ class FileAdmin(BaseView, ActionsMixin): ...@@ -713,8 +844,10 @@ class FileAdmin(BaseView, ActionsMixin):
flash(gettext("Changes to %(name)s saved successfully.", name=path)) flash(gettext("Changes to %(name)s saved successfully.", name=path))
return redirect(next_url) return redirect(next_url)
else: else:
helpers.flash_errors(form, message='Failed to edit file. %(error)s')
try: try:
with open(full_path, 'r') as f: with open(full_path, 'rb') as f:
content = f.read() content = f.read()
except IOError: except IOError:
flash(gettext("Error reading %(name)s.", name=path), 'error') flash(gettext("Error reading %(name)s.", name=path), 'error')
......
...@@ -80,8 +80,7 @@ class ModelView(BaseModelView): ...@@ -80,8 +80,7 @@ class ModelView(BaseModelView):
'searchable_columns', 'searchable_columns',
None) None)
""" """
Collection of the searchable columns. Only text-based columns Collection of the searchable columns.
are searchable (`String`, `Unicode`, `Text`, `UnicodeText`).
Example:: Example::
...@@ -491,10 +490,6 @@ class ModelView(BaseModelView): ...@@ -491,10 +490,6 @@ class ModelView(BaseModelView):
for column in self._get_columns_for_field(attr): for column in self._get_columns_for_field(attr):
column_type = type(column.type).__name__ column_type = type(column.type).__name__
if not self.is_text_column_type(column_type):
raise Exception('Can only search on text columns. ' +
'Failed to setup search for "%s"' % p)
self._search_fields.append(column) self._search_fields.append(column)
# Store joins, avoid duplicates # Store joins, avoid duplicates
......
from re import sub from re import sub
from jinja2 import contextfunction from jinja2 import contextfunction
from flask import g, request, url_for from flask import g, request, url_for, flash
from wtforms.validators import DataRequired, InputRequired from wtforms.validators import DataRequired, InputRequired
from flask.ext.admin._compat import urljoin, urlparse from flask.ext.admin._compat import urljoin, urlparse, iteritems
from flask.ext.admin.babel import gettext
from ._compat import string_types from ._compat import string_types
...@@ -93,7 +93,12 @@ def is_field_error(errors): ...@@ -93,7 +93,12 @@ def is_field_error(errors):
return True return True
return False return False
def flash_errors(form, message):
for field_name, errors in iteritems(form.errors):
errors = form[field_name].label.text + u": " + u", ".join(errors)
flash(gettext(message, error=str(errors)), 'error')
@contextfunction @contextfunction
def resolve_ctx(context): def resolve_ctx(context):
......
...@@ -14,7 +14,7 @@ from flask.ext.admin.form import BaseForm, FormOpts, rules ...@@ -14,7 +14,7 @@ from flask.ext.admin.form import BaseForm, FormOpts, rules
from flask.ext.admin.model import filters, typefmt from flask.ext.admin.model import filters, typefmt
from flask.ext.admin.actions import ActionsMixin from flask.ext.admin.actions import ActionsMixin
from flask.ext.admin.helpers import (get_form_data, validate_form_on_submit, from flask.ext.admin.helpers import (get_form_data, validate_form_on_submit,
get_redirect_target) get_redirect_target, flash_errors)
from flask.ext.admin.tools import rec_getattr from flask.ext.admin.tools import rec_getattr
from flask.ext.admin._backwards import ObsoleteAttr from flask.ext.admin._backwards import ObsoleteAttr
from flask.ext.admin._compat import iteritems, OrderedDict, as_unicode from flask.ext.admin._compat import iteritems, OrderedDict, as_unicode
...@@ -972,6 +972,9 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -972,6 +972,9 @@ class BaseModelView(BaseView, ActionsMixin):
Instantiate model delete form and return it. Instantiate model delete form and return it.
Override to implement custom behavior. Override to implement custom behavior.
The delete form originally used a GET request, so delete_form
accepts both GET and POST request for backwards compatibility.
""" """
if request.form: if request.form:
return self._delete_form_class(request.form) return self._delete_form_class(request.form)
...@@ -1558,12 +1561,7 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -1558,12 +1561,7 @@ class BaseModelView(BaseView, ActionsMixin):
flash(gettext('Record was successfully deleted.')) flash(gettext('Record was successfully deleted.'))
return redirect(return_url) return redirect(return_url)
else: else:
# flash validation errors flash_errors(form, message='Failed to delete record. %(error)s')
for field_name, errors in iteritems(form.errors):
errors = field_name + u": " + u", ".join(errors)
flash(gettext('Failed to delete record. %(error)s',
error=str(errors)),
'error')
return redirect(return_url) return redirect(return_url)
......
...@@ -58,7 +58,8 @@ ...@@ -58,7 +58,8 @@
{% if is_dir %} {% if is_dir %}
{% if name != '..' and admin_view.can_delete_dirs %} {% if name != '..' and admin_view.can_delete_dirs %}
<form class="icon" method="POST" action="{{ get_url('.delete') }}"> <form class="icon" method="POST" action="{{ get_url('.delete') }}">
<input type="hidden" name="path" value="{{ path }}"></input> {{ delete_form.path(value=path) }}
{{ delete_form.csrf_token }}
<button onclick="return confirm('{{ _gettext('Are you sure you want to delete \\\'%(name)s\\\' recursively?', name=name) }}')"> <button onclick="return confirm('{{ _gettext('Are you sure you want to delete \\\'%(name)s\\\' recursively?', name=name) }}')">
<i class="icon-remove"></i> <i class="icon-remove"></i>
</button> </button>
...@@ -66,7 +67,8 @@ ...@@ -66,7 +67,8 @@
{% endif %} {% endif %}
{% else %} {% else %}
<form class="icon" method="POST" action="{{ get_url('.delete') }}"> <form class="icon" method="POST" action="{{ get_url('.delete') }}">
<input type="hidden" name="path" value="{{ path }}"></input> {{ delete_form.path(value=path) }}
{{ delete_form.csrf_token }}
<button onclick="return confirm('{{ _gettext('Are you sure you want to delete \\\'%(name)s\\\'?', name=name) }}')"> <button onclick="return confirm('{{ _gettext('Are you sure you want to delete \\\'%(name)s\\\'?', name=name) }}')">
<i class="icon-remove"></i> <i class="icon-remove"></i>
</button> </button>
......
{% macro menu_icon(item) -%} {% macro menu_icon(item) -%}
{% set icon_type = item.get_icon_type() %} {% set icon_type = item.get_icon_type() %}
{% if icon_type %} {%- if icon_type %}
{% set icon_value = item.get_icon_value() %} {% set icon_value = item.get_icon_value() %}
{% if icon_type == 'glyph' %} {% if icon_type == 'glyph' %}
<i class="{{ icon_value }}"></i> <i class="{{ icon_value }}"></i>
...@@ -13,45 +13,43 @@ ...@@ -13,45 +13,43 @@
{%- endmacro %} {%- endmacro %}
{% macro menu() %} {% macro menu() %}
{% for item in admin_view.admin.menu() %} {%- for item in admin_view.admin.menu() %}
{% if item.is_category() %} {%- if item.is_category() -%}
{% set children = item.get_children() %} {% set children = item.get_children() %}
{% if children %} {%- if children %}
{% set class_name = item.get_class_name() %} {% set class_name = item.get_class_name() %}
{% if item.is_active(admin_view) %} {%- if item.is_active(admin_view) %}
<li class="active dropdown{% if class_name %} {{class_name}}{% endif %}"> <li class="active dropdown{% if class_name %} {{class_name}}{% endif %}">
{% else %} {% else -%}
<li class="dropdown{% if class_name %} {{class_name}}{% endif %}"> <li class="dropdown{% if class_name %} {{class_name}}{% endif %}">
{% endif %} {%- endif %}
<a class="dropdown-toggle" data-toggle="dropdown" href="javascript:void(0)"> <a class="dropdown-toggle" data-toggle="dropdown" href="javascript:void(0)">{{ menu_icon(item) }}{{ item.name }}<b class="caret"></b></a>
{{ menu_icon(item) }}{{ item.name }}<b class="caret"></b>
</a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
{% for child in children %} {%- for child in children -%}
{% set class_name = child.get_class_name() %} {% set class_name = child.get_class_name() %}
{% if child.is_active(admin_view) %} {%- if child.is_active(admin_view) %}
<li class="active{% if class_name %} {{class_name}}{% endif %}"> <li class="active{% if class_name %} {{class_name}}{% endif %}">
{% else %} {% else %}
<li{% if class_name %} class="{{class_name}}"{% endif %}> <li{% if class_name %} class="{{class_name}}"{% endif %}>
{% endif %} {%- endif %}
<a href="{{ child.get_url() }}">{{ menu_icon(child) }}{{ child.name }}</a> <a href="{{ child.get_url() }}">{{ menu_icon(child) }}{{ child.name }}</a>
</li> </li>
{% endfor %} {%- endfor %}
</ul> </ul>
</li> </li>
{% endif %} {% endif %}
{% else %} {%- else %}
{% if item.is_accessible() and item.is_visible() %} {%- if item.is_accessible() and item.is_visible() -%}
{% set class_name = item.get_class_name() %} {% set class_name = item.get_class_name() %}
{% if item.is_active(admin_view) %} {%- if item.is_active(admin_view) %}
<li class="active{% if class_name %} {{class_name}}{% endif %}"> <li class="active{% if class_name %} {{class_name}}{% endif %}">
{% else %} {%- else %}
<li{% if class_name %} class="{{class_name}}"{% endif %}> <li{% if class_name %} class="{{class_name}}"{% endif %}>
{% endif %} {%- endif %}
<a href="{{ item.get_url() }}">{{ menu_icon(item) }}{{ item.name }}</a> <a href="{{ item.get_url() }}">{{ menu_icon(item) }}{{ item.name }}</a>
</li> </li>
{% endif %} {%- endif -%}
{% endif %} {% endif -%}
{% endfor %} {% endfor %}
{% endmacro %} {% endmacro %}
......
{% macro render_inline_fields(field, template, render, check=None) %} {% macro render_inline_fields(field, template, render, check=None) %}
<div class="inline-field"> <div class="inline-field" id="{{ field.id }}">
{# existing inline form fields #} {# existing inline form fields #}
<div class="inline-field-list"> <div class="inline-field-list">
{% for subfield in field %} {% for subfield in field %}
......
...@@ -58,7 +58,8 @@ ...@@ -58,7 +58,8 @@
{% if is_dir %} {% if is_dir %}
{% if name != '..' and admin_view.can_delete_dirs %} {% if name != '..' and admin_view.can_delete_dirs %}
<form class="icon" method="POST" action="{{ get_url('.delete') }}"> <form class="icon" method="POST" action="{{ get_url('.delete') }}">
<input type="hidden" name="path" value="{{ path }}"></input> {{ delete_form.path(value=path) }}
{{ delete_form.csrf_token }}
<button onclick="return confirm('{{ _gettext('Are you sure you want to delete \\\'%(name)s\\\' recursively?', name=name) }}')"> <button onclick="return confirm('{{ _gettext('Are you sure you want to delete \\\'%(name)s\\\' recursively?', name=name) }}')">
<i class="glyphicon glyphicon-remove"></i> <i class="glyphicon glyphicon-remove"></i>
</button> </button>
...@@ -66,7 +67,8 @@ ...@@ -66,7 +67,8 @@
{% endif %} {% endif %}
{% else %} {% else %}
<form class="icon" method="POST" action="{{ get_url('.delete') }}"> <form class="icon" method="POST" action="{{ get_url('.delete') }}">
<input type="hidden" name="path" value="{{ path }}"></input> {{ delete_form.path(value=path) }}
{{ delete_form.csrf_token }}
<button onclick="return confirm('{{ _gettext('Are you sure you want to delete \\\'%(name)s\\\'?', name=name) }}')"> <button onclick="return confirm('{{ _gettext('Are you sure you want to delete \\\'%(name)s\\\'?', name=name) }}')">
<i class="glyphicon glyphicon-trash"></i> <i class="glyphicon glyphicon-trash"></i>
</button> </button>
......
{% macro menu_icon(item) -%} {% macro menu_icon(item) -%}
{% set icon_type = item.get_icon_type() %} {% set icon_type = item.get_icon_type() %}
{% if icon_type %} {%- if icon_type %}
{% set icon_value = item.get_icon_value() %} {% set icon_value = item.get_icon_value() %}
{% if icon_type == 'glyph' %} {% if icon_type == 'glyph' %}
<i class="glyphicon {{ icon_value }}"></i> <i class="glyphicon {{ icon_value }}"></i>
...@@ -13,42 +13,43 @@ ...@@ -13,42 +13,43 @@
{%- endmacro %} {%- endmacro %}
{% macro menu() %} {% macro menu() %}
{% for item in admin_view.admin.menu() %} {%- for item in admin_view.admin.menu() %}
{% if item.is_category() %} {%- if item.is_category() -%}
{% set children = item.get_children() %} {% set children = item.get_children() %}
{% if children %} {%- if children %}
{% if item.is_active(admin_view) %} {% set class_name = item.get_class_name() %}
{%- if item.is_active(admin_view) %}
<li class="active dropdown{% if class_name %} {{class_name}}{% endif %}"> <li class="active dropdown{% if class_name %} {{class_name}}{% endif %}">
{% else %} {% else -%}
<li class="dropdown{% if class_name %} {{class_name}}{% endif %}"> <li class="dropdown{% if class_name %} {{class_name}}{% endif %}">
{% endif %} {%- endif %}
<a class="dropdown-toggle" data-toggle="dropdown" href="javascript:void(0)">{{ menu_icon(item) }}{{ item.name }}<b class="caret"></b></a> <a class="dropdown-toggle" data-toggle="dropdown" href="javascript:void(0)">{{ menu_icon(item) }}{{ item.name }}<b class="caret"></b></a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
{% for child in children %} {%- for child in children -%}
{% set class_name = child.get_class_name() %} {% set class_name = child.get_class_name() %}
{% if child.is_active(admin_view) %} {%- if child.is_active(admin_view) %}
<li class="active"{% if class_name %} {{class_name}}{% endif %}> <li class="active{% if class_name %} {{class_name}}{% endif %}">
{% else %} {% else %}
<li{% if class_name %} class="{{class_name}}"{% endif %}> <li{% if class_name %} class="{{class_name}}"{% endif %}>
{% endif %} {%- endif %}
<a href="{{ child.get_url() }}">{{ menu_icon(child) }}{{ child.name }}</a> <a href="{{ child.get_url() }}">{{ menu_icon(child) }}{{ child.name }}</a>
</li> </li>
{% endfor %} {%- endfor %}
</ul> </ul>
</li> </li>
{% endif %} {% endif %}
{% else %} {%- else %}
{% if item.is_accessible() and item.is_visible() %} {%- if item.is_accessible() and item.is_visible() -%}
{% set class_name = item.get_class_name() %} {% set class_name = item.get_class_name() %}
{% if item.is_active(admin_view) %} {%- if item.is_active(admin_view) %}
<li class="active"{% if class_name %} {{class_name}}{% endif %}> <li class="active{% if class_name %} {{class_name}}{% endif %}">
{% else %} {%- else %}
<li{% if class_name %} class="{{class_name}}"{% endif %}> <li{% if class_name %} class="{{class_name}}"{% endif %}>
{% endif %} {%- endif %}
<a href="{{ item.get_url() }}">{{ menu_icon(item) }}{{ item.name }}</a> <a href="{{ item.get_url() }}">{{ menu_icon(item) }}{{ item.name }}</a>
</li> </li>
{% endif %} {%- endif -%}
{% endif %} {% endif -%}
{% endfor %} {% endfor %}
{% endmacro %} {% endmacro %}
......
{% macro render_inline_fields(field, template, render, check=None) %} {% macro render_inline_fields(field, template, render, check=None) %}
<div class="inline-field"> <div class="inline-field" id="{{ field.id }}">
{# existing inline form fields #} {# existing inline form fields #}
<div class="inline-field-list"> <div class="inline-field-list">
{% for subfield in field %} {% for subfield in field %}
......
from nose.tools import eq_, ok_
import os.path as op import os.path as op
from nose.tools import eq_, ok_
from flask.ext.admin.contrib import fileadmin from flask.ext.admin.contrib import fileadmin
from . import setup from . import setup
try:
from StringIO import StringIO
except ImportError:
from io import StringIO
def create_view(): def create_view():
app, admin = setup() app, admin = setup()
class MyFileAdmin(fileadmin.FileAdmin):
editable_extensions = ('txt',)
path = op.join(op.dirname(__file__), 'files') path = op.join(op.dirname(__file__), 'files')
view = fileadmin.FileAdmin(path, '/files/', name='Files') view = MyFileAdmin(path, '/files/', name='Files')
admin.add_view(view) admin.add_view(view)
return app, admin, view return app, admin, view
...@@ -21,8 +30,104 @@ def test_file_admin(): ...@@ -21,8 +30,104 @@ def test_file_admin():
client = app.test_client() client = app.test_client()
rv = client.get('/admin/fileadmin/') # index
rv = client.get('/admin/myfileadmin/')
eq_(rv.status_code, 200)
ok_('path=dummy.txt' in rv.data.decode('utf-8'))
# edit
rv = client.get('/admin/myfileadmin/edit/?path=dummy.txt')
eq_(rv.status_code, 200) eq_(rv.status_code, 200)
ok_('dummy.txt' in rv.data.decode('utf-8')) ok_('dummy.txt' in rv.data.decode('utf-8'))
# TODO: Check actions, etc rv = client.post('/admin/myfileadmin/edit/?path=dummy.txt', data=dict(
content='new_string'
))
eq_(rv.status_code, 302)
rv = client.get('/admin/myfileadmin/edit/?path=dummy.txt')
eq_(rv.status_code, 200)
ok_('dummy.txt' in rv.data.decode('utf-8'))
ok_('new_string' in rv.data.decode('utf-8'))
# rename
rv = client.get('/admin/myfileadmin/rename/?path=dummy.txt')
eq_(rv.status_code, 200)
ok_('dummy.txt' in rv.data.decode('utf-8'))
rv = client.post('/admin/myfileadmin/rename/?path=dummy.txt', data=dict(
name='dummy_renamed.txt',
path='dummy.txt'
))
eq_(rv.status_code, 302)
rv = client.get('/admin/myfileadmin/')
eq_(rv.status_code, 200)
ok_('path=dummy_renamed.txt' in rv.data.decode('utf-8'))
ok_('path=dummy.txt' not in rv.data.decode('utf-8'))
# upload
rv = client.get('/admin/myfileadmin/upload/')
eq_(rv.status_code, 200)
rv = client.post('/admin/myfileadmin/upload/', data=dict(
upload=(StringIO(""), 'dummy.txt'),
))
eq_(rv.status_code, 302)
rv = client.get('/admin/myfileadmin/')
eq_(rv.status_code, 200)
ok_('path=dummy.txt' in rv.data.decode('utf-8'))
ok_('path=dummy_renamed.txt' in rv.data.decode('utf-8'))
# delete
rv = client.post('/admin/myfileadmin/delete/', data=dict(
path='dummy_renamed.txt'
))
eq_(rv.status_code, 302)
rv = client.get('/admin/myfileadmin/')
eq_(rv.status_code, 200)
ok_('path=dummy_renamed.txt' not in rv.data.decode('utf-8'))
ok_('path=dummy.txt' in rv.data.decode('utf-8'))
# mkdir
rv = client.get('/admin/myfileadmin/mkdir/')
eq_(rv.status_code, 200)
rv = client.post('/admin/myfileadmin/mkdir/', data=dict(
name='dummy_dir'
))
eq_(rv.status_code, 302)
rv = client.get('/admin/myfileadmin/')
eq_(rv.status_code, 200)
ok_('path=dummy.txt' in rv.data.decode('utf-8'))
ok_('path=dummy_dir' in rv.data.decode('utf-8'))
# rename - directory
rv = client.get('/admin/myfileadmin/rename/?path=dummy_dir')
eq_(rv.status_code, 200)
ok_('dummy_dir' in rv.data.decode('utf-8'))
rv = client.post('/admin/myfileadmin/rename/?path=dummy_dir', data=dict(
name='dummy_renamed_dir',
path='dummy_dir'
))
eq_(rv.status_code, 302)
rv = client.get('/admin/myfileadmin/')
eq_(rv.status_code, 200)
ok_('path=dummy_renamed_dir' in rv.data.decode('utf-8'))
ok_('path=dummy_dir' not in rv.data.decode('utf-8'))
# delete - directory
rv = client.post('/admin/myfileadmin/delete/', data=dict(
path='dummy_renamed_dir'
))
eq_(rv.status_code, 302)
rv = client.get('/admin/myfileadmin/')
eq_(rv.status_code, 200)
ok_('path=dummy_renamed_dir' not in rv.data.decode('utf-8'))
ok_('path=dummy.txt' in rv.data.decode('utf-8'))
...@@ -253,27 +253,32 @@ def test_column_searchable_list(): ...@@ -253,27 +253,32 @@ def test_column_searchable_list():
Model1, Model2 = create_models(db) Model1, Model2 = create_models(db)
view = CustomModelView(Model1, db.session, view = CustomModelView(Model2, db.session,
column_searchable_list=['test1', 'test2']) column_searchable_list=['string_field', 'int_field'])
admin.add_view(view) admin.add_view(view)
eq_(view._search_supported, True) eq_(view._search_supported, True)
eq_(len(view._search_fields), 2) eq_(len(view._search_fields), 2)
ok_(isinstance(view._search_fields[0], db.Column)) ok_(isinstance(view._search_fields[0], db.Column))
ok_(isinstance(view._search_fields[1], db.Column)) ok_(isinstance(view._search_fields[1], db.Column))
eq_(view._search_fields[0].name, 'test1') eq_(view._search_fields[0].name, 'string_field')
eq_(view._search_fields[1].name, 'test2') eq_(view._search_fields[1].name, 'int_field')
db.session.add(Model1('model1')) db.session.add(Model2('model1-test', 5000))
db.session.add(Model1('model2')) db.session.add(Model2('model2-test', 9000))
db.session.commit() db.session.commit()
client = app.test_client() client = app.test_client()
rv = client.get('/admin/model1/?search=model1') rv = client.get('/admin/model2/?search=model1')
data = rv.data.decode('utf-8')
ok_('model1-test' in data)
ok_('model2-test' not in data)
rv = client.get('/admin/model2/?search=9000')
data = rv.data.decode('utf-8') data = rv.data.decode('utf-8')
ok_('model1' in data) ok_('model1-test' not in data)
ok_('model2' not in data) ok_('model2-test' in data)
def test_complex_searchable_list(): def test_complex_searchable_list():
......
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