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 ""
msgstr ""
"Project-Id-Version: Flask-Admin VERSION\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"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
......@@ -18,153 +18,159 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 1.3\n"
#: ../flask_admin/base.py:419
#: ../flask_admin/base.py:426
msgid "Home"
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:34
msgid "Invalid directory name"
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:42
#: ../flask_admin/contrib/fileadmin.py:220
msgid "File to upload"
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:51
#: ../flask_admin/contrib/fileadmin.py:228
msgid "File required."
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:56
#: ../flask_admin/contrib/fileadmin.py:233
msgid "Invalid file type."
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:60
#: ../flask_admin/contrib/fileadmin.py:244
msgid "Content"
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
msgid "File \"%(name)s\" already exists."
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:446
#: ../flask_admin/contrib/fileadmin.py:512
#: ../flask_admin/contrib/fileadmin.py:565
#: ../flask_admin/contrib/fileadmin.py:602
#: ../flask_admin/contrib/fileadmin.py:645
#: ../flask_admin/contrib/fileadmin.py:693
#: ../flask_admin/contrib/fileadmin.py:568
#: ../flask_admin/contrib/fileadmin.py:635
#: ../flask_admin/contrib/fileadmin.py:688
#: ../flask_admin/contrib/fileadmin.py:729
#: ../flask_admin/contrib/fileadmin.py:775
#: ../flask_admin/contrib/fileadmin.py:824
msgid "Permission denied."
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:508
#: ../flask_admin/contrib/fileadmin.py:631
msgid "File uploading is disabled."
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:521
#: ../flask_admin/contrib/fileadmin.py:644
#, python-format
msgid "Failed to save file: %(error)s"
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:561
#: ../flask_admin/contrib/fileadmin.py:684
msgid "Directory creation is disabled."
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:576
#: ../flask_admin/contrib/fileadmin.py:699
#, python-format
msgid "Failed to create directory: %(error)s"
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:598
#: ../flask_admin/contrib/fileadmin.py:725
msgid "Deletion is disabled."
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:607
#: ../flask_admin/contrib/fileadmin.py:734
msgid "Directory deletion is disabled."
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:613
#: ../flask_admin/contrib/fileadmin.py:740
#, python-format
msgid "Directory \"%(path)s\" was successfully deleted."
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:615
#: ../flask_admin/contrib/fileadmin.py:742
#, python-format
msgid "Failed to delete directory: %(error)s"
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:620
#: ../flask_admin/contrib/fileadmin.py:759
#: ../flask_admin/contrib/fileadmin.py:747
#: ../flask_admin/contrib/fileadmin.py:892
#, python-format
msgid "File \"%(name)s\" was successfully deleted."
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:622
#: ../flask_admin/contrib/fileadmin.py:761
#: ../flask_admin/contrib/fileadmin.py:749
#: ../flask_admin/contrib/fileadmin.py:894
#, python-format
msgid "Failed to delete file: %(name)s"
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:641
#: ../flask_admin/contrib/fileadmin.py:771
msgid "Renaming is disabled."
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:649
#: ../flask_admin/contrib/fileadmin.py:779
msgid "Path does not exist."
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:661
#: ../flask_admin/contrib/fileadmin.py:790
#, python-format
msgid "Successfully renamed \"%(src)s\" to \"%(dst)s\""
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:664
#: ../flask_admin/contrib/fileadmin.py:793
#, python-format
msgid "Failed to rename: %(error)s"
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:709
#: ../flask_admin/contrib/fileadmin.py:840
#, python-format
msgid "Error saving changes to %(name)s."
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:713
#: ../flask_admin/contrib/fileadmin.py:844
#, python-format
msgid "Changes to %(name)s saved successfully."
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:720
#: ../flask_admin/contrib/fileadmin.py:853
#, python-format
msgid "Error reading %(name)s."
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:723
#: ../flask_admin/contrib/fileadmin.py:732
#: ../flask_admin/contrib/fileadmin.py:856
#: ../flask_admin/contrib/fileadmin.py:865
#, python-format
msgid "Unexpected error while reading from %(name)s"
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:729
#: ../flask_admin/contrib/fileadmin.py:862
#, python-format
msgid "Cannot edit %(name)s."
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:746
#: ../flask_admin/contrib/mongoengine/view.py:612
#: ../flask_admin/contrib/peewee/view.py:418
#: ../flask_admin/contrib/pymongo/view.py:343
#: ../flask_admin/contrib/sqla/view.py:945
#: ../flask_admin/contrib/fileadmin.py:879
#: ../flask_admin/contrib/mongoengine/view.py:625
#: ../flask_admin/contrib/peewee/view.py:429
#: ../flask_admin/contrib/pymongo/view.py:348
#: ../flask_admin/contrib/sqla/view.py:974
msgid "Delete"
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:747
#: ../flask_admin/contrib/fileadmin.py:880
msgid "Are you sure you want to delete these files?"
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:750
#: ../flask_admin/contrib/fileadmin.py:883
msgid "File deletion is disabled."
msgstr ""
#: ../flask_admin/contrib/fileadmin.py:763
#: ../flask_admin/contrib/fileadmin.py:896
msgid "Edit"
msgstr ""
......@@ -246,57 +252,57 @@ msgstr ""
msgid "not between"
msgstr ""
#: ../flask_admin/contrib/mongoengine/view.py:506
#: ../flask_admin/contrib/mongoengine/view.py:519
#, python-format
msgid "Failed to get model. %(error)s"
msgstr ""
#: ../flask_admin/contrib/mongoengine/view.py:525
#: ../flask_admin/contrib/peewee/view.py:369
#: ../flask_admin/contrib/pymongo/view.py:278
#: ../flask_admin/contrib/sqla/view.py:877
#: ../flask_admin/contrib/mongoengine/view.py:538
#: ../flask_admin/contrib/peewee/view.py:380
#: ../flask_admin/contrib/pymongo/view.py:283
#: ../flask_admin/contrib/sqla/view.py:906
#, python-format
msgid "Failed to create record. %(error)s"
msgstr ""
#: ../flask_admin/contrib/mongoengine/view.py:551
#: ../flask_admin/contrib/peewee/view.py:388
#: ../flask_admin/contrib/pymongo/view.py:303
#: ../flask_admin/contrib/sqla/view.py:903 ../flask_admin/model/base.py:1563
#: ../flask_admin/model/base.py:1572
#: ../flask_admin/contrib/mongoengine/view.py:564
#: ../flask_admin/contrib/peewee/view.py:399
#: ../flask_admin/contrib/pymongo/view.py:308
#: ../flask_admin/contrib/sqla/view.py:932 ../flask_admin/model/base.py:1613
#: ../flask_admin/model/base.py:1622
#, python-format
msgid "Failed to update record. %(error)s"
msgstr ""
#: ../flask_admin/contrib/mongoengine/view.py:575
#: ../flask_admin/contrib/peewee/view.py:404
#: ../flask_admin/contrib/pymongo/view.py:329
#: ../flask_admin/contrib/sqla/view.py:929 ../flask_admin/model/base.py:1513
#: ../flask_admin/contrib/mongoengine/view.py:588
#: ../flask_admin/contrib/peewee/view.py:415
#: ../flask_admin/contrib/pymongo/view.py:334
#: ../flask_admin/contrib/sqla/view.py:958
#, python-format
msgid "Failed to delete record. %(error)s"
msgstr ""
#: ../flask_admin/contrib/mongoengine/view.py:613
#: ../flask_admin/contrib/peewee/view.py:419
#: ../flask_admin/contrib/pymongo/view.py:344
#: ../flask_admin/contrib/sqla/view.py:946
#: ../flask_admin/contrib/mongoengine/view.py:626
#: ../flask_admin/contrib/peewee/view.py:430
#: ../flask_admin/contrib/pymongo/view.py:349
#: ../flask_admin/contrib/sqla/view.py:975
msgid "Are you sure you want to delete selected records?"
msgstr ""
#: ../flask_admin/contrib/mongoengine/view.py:622
#: ../flask_admin/contrib/peewee/view.py:435
#: ../flask_admin/contrib/pymongo/view.py:354
#: ../flask_admin/contrib/sqla/view.py:962 ../flask_admin/model/base.py:1506
#: ../flask_admin/contrib/mongoengine/view.py:635
#: ../flask_admin/contrib/peewee/view.py:446
#: ../flask_admin/contrib/pymongo/view.py:359
#: ../flask_admin/contrib/sqla/view.py:991 ../flask_admin/model/base.py:1561
#, python-format
msgid "Record was successfully deleted."
msgid_plural "%(count)s records were successfully deleted."
msgstr[0] ""
msgstr[1] ""
#: ../flask_admin/contrib/mongoengine/view.py:628
#: ../flask_admin/contrib/peewee/view.py:441
#: ../flask_admin/contrib/pymongo/view.py:359
#: ../flask_admin/contrib/sqla/view.py:970
#: ../flask_admin/contrib/mongoengine/view.py:641
#: ../flask_admin/contrib/peewee/view.py:452
#: ../flask_admin/contrib/pymongo/view.py:364
#: ../flask_admin/contrib/sqla/view.py:999
#, python-format
msgid "Failed to delete records. %(error)s"
msgstr ""
......@@ -319,7 +325,7 @@ msgid_plural "At least %d items are required"
msgstr[0] ""
msgstr[1] ""
#: ../flask_admin/contrib/sqla/view.py:856
#: ../flask_admin/contrib/sqla/view.py:885
#, python-format
msgid "Integrity error. %(message)s"
msgstr ""
......@@ -336,20 +342,20 @@ msgstr ""
msgid "Invalid file extension"
msgstr ""
#: ../flask_admin/model/base.py:1173
#: ../flask_admin/model/base.py:1227
msgid "There are no items in the table."
msgstr ""
#: ../flask_admin/model/base.py:1197
#: ../flask_admin/model/base.py:1251
#, python-format
msgid "Invalid Filter Value: %(value)s"
msgstr ""
#: ../flask_admin/model/base.py:1430
#: ../flask_admin/model/base.py:1484
msgid "Record was successfully created."
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."
msgstr ""
......@@ -392,40 +398,35 @@ msgstr ""
msgid "Root"
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/bootstrap3/admin/file/list.html:36
msgid "Size"
msgstr ""
#: ../flask_admin/templates/bootstrap2/admin/file/list.html:62
#: ../flask_admin/templates/bootstrap3/admin/file/list.html:62
#: ../flask_admin/templates/bootstrap2/admin/file/list.html:63
#: ../flask_admin/templates/bootstrap3/admin/file/list.html:63
#, python-format
msgid "Are you sure you want to delete \\'%(name)s\\' recursively?"
msgstr ""
#: ../flask_admin/templates/bootstrap2/admin/file/list.html:70
#: ../flask_admin/templates/bootstrap3/admin/file/list.html:70
#: ../flask_admin/templates/bootstrap2/admin/file/list.html:72
#: ../flask_admin/templates/bootstrap3/admin/file/list.html:72
#, python-format
msgid "Are you sure you want to delete \\'%(name)s\\'?"
msgstr ""
#: ../flask_admin/templates/bootstrap2/admin/file/list.html:105
#: ../flask_admin/templates/bootstrap3/admin/file/list.html:105
#: ../flask_admin/templates/bootstrap2/admin/file/list.html:107
#: ../flask_admin/templates/bootstrap3/admin/file/list.html:107
msgid "Upload File"
msgstr ""
#: ../flask_admin/templates/bootstrap2/admin/file/list.html:110
#: ../flask_admin/templates/bootstrap3/admin/file/list.html:110
#: ../flask_admin/templates/bootstrap2/admin/file/list.html:112
#: ../flask_admin/templates/bootstrap3/admin/file/list.html:112
msgid "Create Directory"
msgstr ""
#: ../flask_admin/templates/bootstrap2/admin/file/list.html:127
#: ../flask_admin/templates/bootstrap3/admin/file/list.html:127
#: ../flask_admin/templates/bootstrap2/admin/file/list.html:129
#: ../flask_admin/templates/bootstrap3/admin/file/list.html:129
msgid "Please select at least one file."
msgstr ""
......@@ -519,17 +520,17 @@ msgstr ""
msgid "Edit record"
msgstr ""
#: ../flask_admin/templates/bootstrap2/admin/model/list.html:116
#: ../flask_admin/templates/bootstrap3/admin/model/list.html:116
#: ../flask_admin/templates/bootstrap2/admin/model/list.html:114
#: ../flask_admin/templates/bootstrap3/admin/model/list.html:114
msgid "Are you sure you want to delete this record?"
msgstr ""
#: ../flask_admin/templates/bootstrap2/admin/model/list.html:116
#: ../flask_admin/templates/bootstrap2/admin/model/list.html:114
msgid "Delete record"
msgstr ""
#: ../flask_admin/templates/bootstrap2/admin/model/list.html:161
#: ../flask_admin/templates/bootstrap3/admin/model/list.html:160
#: ../flask_admin/templates/bootstrap2/admin/model/list.html:159
#: ../flask_admin/templates/bootstrap3/admin/model/list.html:158
msgid "Please select at least one record."
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
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):
"""
Simple file-management interface.
......@@ -74,11 +32,15 @@ class FileAdmin(BaseView, ActionsMixin):
Sample usage::
import os.path as op
from flask.ext.admin import Admin
from flask.ext.admin.contrib.fileadmin import FileAdmin
admin = Admin()
path = op.join(op.dirname(__file__), 'static')
admin.add_view(FileAdmin(path, '/static/', name='Static Files'))
admin.setup_app(app)
"""
can_upload = True
......@@ -156,9 +118,22 @@ class FileAdmin(BaseView, ActionsMixin):
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,
......@@ -231,6 +206,139 @@ class FileAdmin(BaseView, ActionsMixin):
"""
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):
"""
Verify if file can be uploaded.
......@@ -291,6 +399,15 @@ class FileAdmin(BaseView, ActionsMixin):
"""
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):
"""
Return prettified URL
......@@ -439,11 +556,16 @@ class FileAdmin(BaseView, ActionsMixin):
:param path:
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
base_path, directory, path = self._normalize_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'))
# Get directory listing
......@@ -490,7 +612,8 @@ class FileAdmin(BaseView, ActionsMixin):
get_file_url=self._get_file_url,
items=items,
actions=actions,
actions_confirmation=actions_confirmation)
actions_confirmation=actions_confirmation,
delete_form=delete_form)
@expose('/upload/', methods=('GET', 'POST'))
@expose('/upload/<path:path>', methods=('GET', 'POST'))
......@@ -509,16 +632,16 @@ class FileAdmin(BaseView, ActionsMixin):
return redirect(self._get_dir_url('.index', path))
if not self.is_accessible_path(path):
flash(gettext('Permission denied.'))
flash(gettext('Permission denied.'), 'error')
return redirect(self._get_dir_url('.index'))
form = self.upload_form(self)
if helpers.validate_form_on_submit(form):
form = self.upload_form()
if self.validate_form(form):
try:
self._save_form_files(directory, path, form)
return redirect(self._get_dir_url('.index', path))
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)
......@@ -562,18 +685,20 @@ class FileAdmin(BaseView, ActionsMixin):
return redirect(dir_url)
if not self.is_accessible_path(path):
flash(gettext('Permission denied.'))
flash(gettext('Permission denied.'), 'error')
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:
os.mkdir(op.join(directory, form.name.data))
self.on_mkdir(directory, form.name.data)
return redirect(dir_url)
except Exception as ex:
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,
form=form,
......@@ -584,42 +709,46 @@ class FileAdmin(BaseView, ActionsMixin):
"""
Delete view method
"""
path = request.form.get('path')
if not path:
return redirect(self.get_url('.index'))
form = self.delete_form()
# Get path and verify if it is valid
base_path, full_path, path = self._normalize_path(path)
path = form.path.data
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:
flash(gettext('Deletion is disabled.'))
return redirect(return_url)
if not self.can_delete:
flash(gettext('Deletion is disabled.'), 'error')
return redirect(return_url)
if not self.is_accessible_path(path):
flash(gettext('Permission denied.'))
return redirect(self._get_dir_url('.index'))
if not self.is_accessible_path(path):
flash(gettext('Permission denied.'), 'error')
return redirect(self._get_dir_url('.index'))
if op.isdir(full_path):
if not self.can_delete_dirs:
flash(gettext('Directory deletion is disabled.'))
return redirect(return_url)
if op.isdir(full_path):
if not self.can_delete_dirs:
flash(gettext('Directory deletion is disabled.'), 'error')
return redirect(return_url)
try:
shutil.rmtree(full_path)
self.on_directory_delete(full_path, path)
flash(gettext('Directory "%(path)s" was successfully deleted.', path=path))
except Exception as ex:
flash(gettext('Failed to delete directory: %(error)s', error=ex), 'error')
try:
shutil.rmtree(full_path)
self.on_directory_delete(full_path, path)
flash(gettext('Directory "%(path)s" was successfully deleted.', path=path))
except Exception as ex:
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:
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')
helpers.flash_errors(form, message='Failed to delete file. %(error)s')
return redirect(return_url)
......@@ -628,29 +757,29 @@ class FileAdmin(BaseView, ActionsMixin):
"""
Rename view method
"""
path = request.args.get('path')
if not path:
return redirect(self.get_url('.index'))
form = self.name_form()
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:
flash(gettext('Renaming is disabled.'))
flash(gettext('Renaming is disabled.'), 'error')
return redirect(return_url)
if not self.is_accessible_path(path):
flash(gettext('Permission denied.'))
flash(gettext('Permission denied.'), 'error')
return redirect(self._get_dir_url('.index'))
if not op.exists(full_path):
flash(gettext('Path does not exist.'))
flash(gettext('Path does not exist.'), 'error')
return redirect(return_url)
form = NameForm(helpers.get_form_data(), name=op.basename(path))
if helpers.validate_form_on_submit(form):
if self.validate_form(form):
try:
dir_base = op.dirname(full_path)
filename = secure_filename(form.name.data)
......@@ -664,6 +793,8 @@ class FileAdmin(BaseView, ActionsMixin):
flash(gettext('Failed to rename: %(error)s', error=ex), 'error')
return redirect(return_url)
else:
helpers.flash_errors(form, message='Failed to rename: %(error)s')
return self.render(self.rename_template,
form=form,
......@@ -690,16 +821,16 @@ class FileAdmin(BaseView, ActionsMixin):
base_path, full_path, path = self._normalize_path(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'))
dir_url = self._get_dir_url('.index', os.path.dirname(path))
next_url = next_url or dir_url
form = EditForm(helpers.get_form_data())
form = self.edit_form()
error = False
if helpers.validate_form_on_submit(form):
if self.validate_form(form):
form.process(request.form, content='')
if form.validate():
try:
......@@ -713,8 +844,10 @@ class FileAdmin(BaseView, ActionsMixin):
flash(gettext("Changes to %(name)s saved successfully.", name=path))
return redirect(next_url)
else:
helpers.flash_errors(form, message='Failed to edit file. %(error)s')
try:
with open(full_path, 'r') as f:
with open(full_path, 'rb') as f:
content = f.read()
except IOError:
flash(gettext("Error reading %(name)s.", name=path), 'error')
......
......@@ -80,8 +80,7 @@ class ModelView(BaseModelView):
'searchable_columns',
None)
"""
Collection of the searchable columns. Only text-based columns
are searchable (`String`, `Unicode`, `Text`, `UnicodeText`).
Collection of the searchable columns.
Example::
......@@ -491,10 +490,6 @@ class ModelView(BaseModelView):
for column in self._get_columns_for_field(attr):
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)
# Store joins, avoid duplicates
......
from re import sub
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 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
......@@ -93,7 +93,12 @@ def is_field_error(errors):
return True
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
def resolve_ctx(context):
......
......@@ -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.actions import ActionsMixin
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._backwards import ObsoleteAttr
from flask.ext.admin._compat import iteritems, OrderedDict, as_unicode
......@@ -972,6 +972,9 @@ class BaseModelView(BaseView, ActionsMixin):
Instantiate model delete form and return it.
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:
return self._delete_form_class(request.form)
......@@ -1558,12 +1561,7 @@ class BaseModelView(BaseView, ActionsMixin):
flash(gettext('Record was successfully deleted.'))
return redirect(return_url)
else:
# flash validation errors
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')
flash_errors(form, message='Failed to delete record. %(error)s')
return redirect(return_url)
......
......@@ -58,7 +58,8 @@
{% if is_dir %}
{% if name != '..' and admin_view.can_delete_dirs %}
<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) }}')">
<i class="icon-remove"></i>
</button>
......@@ -66,7 +67,8 @@
{% endif %}
{% else %}
<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) }}')">
<i class="icon-remove"></i>
</button>
......
{% macro menu_icon(item) -%}
{% set icon_type = item.get_icon_type() %}
{% if icon_type %}
{%- if icon_type %}
{% set icon_value = item.get_icon_value() %}
{% if icon_type == 'glyph' %}
<i class="{{ icon_value }}"></i>
......@@ -13,45 +13,43 @@
{%- endmacro %}
{% macro menu() %}
{% for item in admin_view.admin.menu() %}
{% if item.is_category() %}
{%- for item in admin_view.admin.menu() %}
{%- if item.is_category() -%}
{% set children = item.get_children() %}
{% if children %}
{%- if children %}
{% 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 %}">
{% else %}
{% else -%}
<li class="dropdown{% if class_name %} {{class_name}}{% endif %}">
{% endif %}
<a class="dropdown-toggle" data-toggle="dropdown" href="javascript:void(0)">
{{ menu_icon(item) }}{{ item.name }}<b class="caret"></b>
</a>
{%- endif %}
<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">
{% for child in children %}
{% set class_name = child.get_class_name() %}
{% if child.is_active(admin_view) %}
<li class="active{% if class_name %} {{class_name}}{% endif %}">
{% else %}
<li{% if class_name %} class="{{class_name}}"{% endif %}>
{% endif %}
<a href="{{ child.get_url() }}">{{ menu_icon(child) }}{{ child.name }}</a>
</li>
{% endfor %}
{%- for child in children -%}
{% set class_name = child.get_class_name() %}
{%- if child.is_active(admin_view) %}
<li class="active{% if class_name %} {{class_name}}{% endif %}">
{% else %}
<li{% if class_name %} class="{{class_name}}"{% endif %}>
{%- endif %}
<a href="{{ child.get_url() }}">{{ menu_icon(child) }}{{ child.name }}</a>
</li>
{%- endfor %}
</ul>
</li>
{% endif %}
{% else %}
{% if item.is_accessible() and item.is_visible() %}
{%- else %}
{%- if item.is_accessible() and item.is_visible() -%}
{% 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 %}">
{% else %}
{%- else %}
<li{% if class_name %} class="{{class_name}}"{% endif %}>
{% endif %}
{%- endif %}
<a href="{{ item.get_url() }}">{{ menu_icon(item) }}{{ item.name }}</a>
</li>
{% endif %}
{% endif %}
{%- endif -%}
{% endif -%}
{% endfor %}
{% endmacro %}
......
{% macro render_inline_fields(field, template, render, check=None) %}
<div class="inline-field">
<div class="inline-field" id="{{ field.id }}">
{# existing inline form fields #}
<div class="inline-field-list">
{% for subfield in field %}
......
......@@ -58,7 +58,8 @@
{% if is_dir %}
{% if name != '..' and admin_view.can_delete_dirs %}
<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) }}')">
<i class="glyphicon glyphicon-remove"></i>
</button>
......@@ -66,7 +67,8 @@
{% endif %}
{% else %}
<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) }}')">
<i class="glyphicon glyphicon-trash"></i>
</button>
......
{% macro menu_icon(item) -%}
{% set icon_type = item.get_icon_type() %}
{% if icon_type %}
{%- if icon_type %}
{% set icon_value = item.get_icon_value() %}
{% if icon_type == 'glyph' %}
<i class="glyphicon {{ icon_value }}"></i>
......@@ -13,42 +13,43 @@
{%- endmacro %}
{% macro menu() %}
{% for item in admin_view.admin.menu() %}
{% if item.is_category() %}
{%- for item in admin_view.admin.menu() %}
{%- if item.is_category() -%}
{% set children = item.get_children() %}
{% if children %}
{% if item.is_active(admin_view) %}
{%- if children %}
{% set class_name = item.get_class_name() %}
{%- if item.is_active(admin_view) %}
<li class="active dropdown{% if class_name %} {{class_name}}{% endif %}">
{% else %}
{% else -%}
<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>
<ul class="dropdown-menu">
{% for child in children %}
{% set class_name = child.get_class_name() %}
{% if child.is_active(admin_view) %}
<li class="active"{% if class_name %} {{class_name}}{% endif %}>
{% else %}
<li{% if class_name %} class="{{class_name}}"{% endif %}>
{% endif %}
<a href="{{ child.get_url() }}">{{ menu_icon(child) }}{{ child.name }}</a>
</li>
{% endfor %}
{%- for child in children -%}
{% set class_name = child.get_class_name() %}
{%- if child.is_active(admin_view) %}
<li class="active{% if class_name %} {{class_name}}{% endif %}">
{% else %}
<li{% if class_name %} class="{{class_name}}"{% endif %}>
{%- endif %}
<a href="{{ child.get_url() }}">{{ menu_icon(child) }}{{ child.name }}</a>
</li>
{%- endfor %}
</ul>
</li>
{% endif %}
{% else %}
{% if item.is_accessible() and item.is_visible() %}
{%- else %}
{%- if item.is_accessible() and item.is_visible() -%}
{% set class_name = item.get_class_name() %}
{% if item.is_active(admin_view) %}
<li class="active"{% if class_name %} {{class_name}}{% endif %}>
{% else %}
{%- if item.is_active(admin_view) %}
<li class="active{% if class_name %} {{class_name}}{% endif %}">
{%- else %}
<li{% if class_name %} class="{{class_name}}"{% endif %}>
{% endif %}
{%- endif %}
<a href="{{ item.get_url() }}">{{ menu_icon(item) }}{{ item.name }}</a>
</li>
{% endif %}
{% endif %}
{%- endif -%}
{% endif -%}
{% endfor %}
{% endmacro %}
......
{% macro render_inline_fields(field, template, render, check=None) %}
<div class="inline-field">
<div class="inline-field" id="{{ field.id }}">
{# existing inline form fields #}
<div class="inline-field-list">
{% for subfield in field %}
......
from nose.tools import eq_, ok_
import os.path as op
from nose.tools import eq_, ok_
from flask.ext.admin.contrib import fileadmin
from . import setup
try:
from StringIO import StringIO
except ImportError:
from io import StringIO
def create_view():
app, admin = setup()
class MyFileAdmin(fileadmin.FileAdmin):
editable_extensions = ('txt',)
path = op.join(op.dirname(__file__), 'files')
view = fileadmin.FileAdmin(path, '/files/', name='Files')
view = MyFileAdmin(path, '/files/', name='Files')
admin.add_view(view)
return app, admin, view
......@@ -21,8 +30,104 @@ def test_file_admin():
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)
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():
Model1, Model2 = create_models(db)
view = CustomModelView(Model1, db.session,
column_searchable_list=['test1', 'test2'])
view = CustomModelView(Model2, db.session,
column_searchable_list=['string_field', 'int_field'])
admin.add_view(view)
eq_(view._search_supported, True)
eq_(len(view._search_fields), 2)
ok_(isinstance(view._search_fields[0], db.Column))
ok_(isinstance(view._search_fields[1], db.Column))
eq_(view._search_fields[0].name, 'test1')
eq_(view._search_fields[1].name, 'test2')
eq_(view._search_fields[0].name, 'string_field')
eq_(view._search_fields[1].name, 'int_field')
db.session.add(Model1('model1'))
db.session.add(Model1('model2'))
db.session.add(Model2('model1-test', 5000))
db.session.add(Model2('model2-test', 9000))
db.session.commit()
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')
ok_('model1' in data)
ok_('model2' not in data)
ok_('model1-test' not in data)
ok_('model2-test' in data)
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