Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
F
flask-admin
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
JIRA
JIRA
Merge Requests
0
Merge Requests
0
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Commits
Issue Boards
Open sidebar
Python-Dev
flask-admin
Commits
4ec6473a
Commit
4ec6473a
authored
Feb 27, 2014
by
bryhoyt
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #8 from mrjoes/master
Merge from central repo into bryhoyt's fork
parents
831a42c3
42d35970
Changes
18
Hide whitespace changes
Inline
Side-by-side
Showing
18 changed files
with
270 additions
and
190 deletions
+270
-190
MANIFEST.in
MANIFEST.in
+1
-0
model_guidelines.rst
doc/model_guidelines.rst
+140
-144
auth.py
examples/auth/auth.py
+2
-2
simple.py
examples/sqla/simple.py
+6
-6
_compat.py
flask_admin/_compat.py
+2
-2
fileadmin.py
flask_admin/contrib/fileadmin.py
+31
-23
form.py
flask_admin/contrib/sqla/form.py
+2
-2
view.py
flask_admin/contrib/sqla/view.py
+4
-1
upload.py
flask_admin/form/upload.py
+5
-2
helpers.py
flask_admin/helpers.py
+17
-0
base.py
flask_admin/model/base.py
+7
-7
list.html
flask_admin/templates/admin/file/list.html
+1
-1
list.html
flask_admin/templates/admin/model/list.html
+2
-0
copyleft.gif
flask_admin/tests/data/copyleft.gif
+0
-0
copyleft.jpeg
flask_admin/tests/data/copyleft.jpeg
+0
-0
test_basic.py
flask_admin/tests/sqlamodel/test_basic.py
+23
-0
test_form_upload.py
flask_admin/tests/test_form_upload.py
+23
-0
admin.po
flask_admin/translations/ru/LC_MESSAGES/admin.po
+4
-0
No files found.
MANIFEST.in
View file @
4ec6473a
...
@@ -4,3 +4,4 @@ recursive-include flask_admin/static *
...
@@ -4,3 +4,4 @@ recursive-include flask_admin/static *
recursive-include flask_admin/templates *
recursive-include flask_admin/templates *
recursive-include flask_admin/translations *
recursive-include flask_admin/translations *
recursive-include flask_admin/tests *
recursive-include flask_admin/tests *
recursive-exclude flask_admin *.pyc
doc/model_guidelines.rst
View file @
4ec6473a
Adding new model backend
Adding
a
new model backend
========================
========================
==
If you want to implement new database backend to use with model views, follow steps found in this guideline.
Flask-Admin makes a few assumptions about the database models that it works with. If you want to implement your own
database backend, and still have Flask-Admin's model views work as expected, then you should take note of the following:
There are few assumptions about models:
1. Each model must have one field which acts as a `primary key` to uniquely identify instances of that model.
However, there are no restriction on the data type or the field name of the `primary key` field.
2. Models must make their data accessible as python properties.
1. Model has "primary key" - value which uniquely identifies
If that is the case, then you can implement your own database backend by extending the `BaseModelView` class,
one model in a data store. There's no restriction on the
and implementing the set of scaffolding methods listed below.
data type or field name.
2. Model has readable python properties
3. It is possible to get list of models (optionally - sorted,
filtered, etc) from data store
4. It is possible to get one model by its primary key
Extending BaseModelView
-------------------------
Steps to add new model backend:
Start off by defining a new class, which derives from from :class:`~flask.ext.admin.model.BaseModelView`::
1. Create new class and derive it from :class:`~flask.ext.admin.model.BaseModelView`::
class MyDbModel(BaseModelView):
class MyDbModel(BaseModelView):
pass
pass
By default, all model views accept model class and it
This class inherits BaseModelView's `__init__` method, which accepts a model class as first argument. The model
will be stored as ``self.model``.
class is stored as the attribute ``self.model`` so that other methods may access it.
2. Implement following scaffolding methods:
- :meth:`~flask.ext.admin.model.BaseModelView.get_pk_value`
This method will return primary key value from
the model. For example, in SQLAlchemy backend,
it gets primary key from the model using :meth:`~flask.ext.admin.contrib.sqla.ModelView.scaffold_pk`, caches it
and returns actual value from the model when requested.
For example::
class MyDbModel(BaseModelView):
def get_pk_value(self, model):
return self.model.id
- :meth:`~flask.ext.admin.model.BaseModelView.scaffold_list_columns`
Returns list of columns to be displayed in a list view.
For example::
class MyDbModel(BaseModelView):
def scaffold_list_columns(self):
columns = []
for p in dir(self.model):
attr = getattr(self.model)
if isinstance(attr, MyDbColumn):
columns.append(p)
return columns
Now, implement the following scaffolding methods for the new class:
- :meth:`~flask.ext.admin.model.BaseModelView.scaffold_sortable_columns
`
1. :meth:`~flask.ext.admin.model.BaseModelView.get_pk_value
`
Returns dictionary of sortable columns. Key in a dictionary is field name. Value - implementation
This method returns a primary key value from
specific, value that will be used by you backend implementation to do actual sort operation.
the model instance. In the SQLAlchemy backend, it gets the primary key from the model
using :meth:`~flask.ext.admin.contrib.sqla.ModelView.scaffold_pk`, caches it
and then returns the value from the model whenever requested.
For example, in SQLAlchemy backend it is possible to
For example::
sort by foreign key. If there's a field `user` and
it is foreign key for a `Users` table which has a name
field, key will be `user` and value will be `Users.name`.
If your backend does not support sorting, return
`None` or empty dictionary.
- :meth:`~flask.ext.admin.model.BaseModelView.init_search`
Initialize search functionality. If your backend supports
full-text search, do initializations and return `True`.
If your backend does not support full-text search, return
`False`.
For example, SQLAlchemy backend reads value of the `self.searchable_columns` and verifies if all fields are of
text type, if they're local to the current model (if not,
it will add a join, etc) and caches this information for
future use.
- :meth:`~flask.ext.admin.model.BaseModelView.is_valid_filter`
Verify if provided object is a valid filter.
Each model backend should have its own set of
filter implementations. It is not possible to use
filters from SQLAlchemy models in non-SQLAlchemy backend.
This also means that different backends might have
different set of available filters.
Filter is a class derived from :class:`~flask.ext.admin.model.filters.BaseFilter` which implements at least two methods:
1. :meth:`~flask.ext.admin.model.filters.BaseFilter.apply`
2. :meth:`~flask.ext.admin.model.filters.BaseFilter.operation`
`apply` method accepts two parameters: `query` object and a value from the client. Here you will add
filtering logic for this filter type.
Lets take SQLAlchemy model backend as an example.
All SQLAlchemy filters derive from :class:`~flask.ext.admin.contrib.sqla.filters.BaseSQLAFilter` class.
Each filter implements one simple filter SQL operation
(like, not like, greater, etc) and accepts column as
input parameter.
Whenever model view wants to apply a filter to a query
object, it will call `apply` method in a filter class
with a query and value. Filter will then apply
real filter operation.
For example::
class MyBaseFilter(BaseFilter
):
class MyDbModel(BaseModelView
):
def __init__(self, column, name, options=None, data_type=None
):
def get_pk_value(self, model
):
super(MyBaseFilter, self).__init__(name, options, data_type)
return self.model.id
self.column = column
2. :meth:`~flask.ext.admin.model.BaseModelView.scaffold_list_columns`
class MyEqualFilter(MyBaseFilter):
Returns a list of columns to be displayed in a list view. For example::
def apply(self, query, value):
return query.filter(self.column == value)
def operation(self):
class MyDbModel(BaseModelView):
return gettext('equals')
def scaffold_list_columns(self):
columns = []
# You can validate values. If value is not valid,
for p in dir(self.model):
# return `False`, so filter will be ignored.
attr = getattr(self.model)
def validate(self, value
):
if isinstance(attr, MyDbColumn
):
return True
columns.append(p)
# You can "clean" values before they will be
return columns
# passed to the your data access layer
def clean(self, value):
return value
- :meth:`~flask.ext.admin.model.BaseModelView.scaffold_filter
s`
3. :meth:`~flask.ext.admin.model.BaseModelView.scaffold_sortable_column
s`
Return list of filter objects for one model field.
Returns a dictionary of sortable columns. The keys in the dictionary should correspond to the model's
field names. The values should be those variables that will be used for sorting.
This method will be called once for each entry in the
For example, in the SQLAlchemy backend it is possible to sort by a foreign key field. So, if there is a
`self.column_filters` setting.
field named `user`, which is a foreign key for the `Users` table, and the `Users` table also has a name
field, then the key will be `user` and value will be `Users.name`.
If your backend does not know how to generate filters
If your backend does not support sorting, return
for the provided field, it should return `None`
.
`None` or an empty dictionary
.
For example::
4. :meth:`~flask.ext.admin.model.BaseModelView.init_search`
class MyDbModel(BaseModelView):
Initialize search functionality. If your backend supports
def scaffold_filters(self, name):
full-text search, do initializations and return `True`.
attr = getattr(self.model, name)
If your backend does not support full-text search, return
`False`.
if isinstance(attr, MyDbTextField):
For example, SQLAlchemy backend reads value of the `self.searchable_columns` and verifies if all fields are of
return [MyEqualFilter(name, name)]
text type, if they're local to the current model (if not,
it will add a join, etc) and caches this information for
future use.
-
:meth:`~flask.ext.admin.model.BaseModelView.scaffold_form`
5.
:meth:`~flask.ext.admin.model.BaseModelView.scaffold_form`
Generate `WTForms` form class from the model.
Generate `WTForms` form class from the model.
...
@@ -169,9 +90,9 @@ Steps to add new model backend:
...
@@ -169,9 +90,9 @@ Steps to add new model backend:
# Do something
# Do something
return MyForm
return MyForm
-
:meth:`~flask.ext.admin.model.BaseModelView.get_list`
6.
:meth:`~flask.ext.admin.model.BaseModelView.get_list`
This method should return list of models with paging,
This method should return list of model
instance
s with paging,
sorting, etc applied.
sorting, etc applied.
For SQLAlchemy backend it looks like:
For SQLAlchemy backend it looks like:
...
@@ -199,22 +120,97 @@ Steps to add new model backend:
...
@@ -199,22 +120,97 @@ Steps to add new model backend:
6. Return count, list as a tuple
6. Return count, list as a tuple
- :meth:`~flask.ext.admin.model.BaseModelView.get_one`
7. :meth:`~flask.ext.admin.model.BaseModelView.get_one`
Return a model instance by its primary key.
8. :meth:`~flask.ext.admin.model.BaseModelView.create_model`
Create a new instance of the model from the `Form` object.
9. :meth:`~flask.ext.admin.model.BaseModelView.update_model`
Update the model instance with data from the form.
Return one model by its primary key.
10. :meth:`~flask.ext.admin.model.BaseModelView.delete_model`
- :meth:`~flask.ext.admin.model.BaseModelView.create_model`
Delete the specified model instance from the data store.
Create new model from the `Form` object.
11. :meth:`~flask.ext.admin.model.BaseModelView.is_valid_filter`
- :meth:`~flask.ext.admin.model.BaseModelView.update_model`
Verify whether the given object is a valid filter.
Update provided model with the data from the form.
12. :meth:`~flask.ext.admin.model.BaseModelView.scaffold_filters`
Return a list of filter objects for one model field.
This method will be called once for each entry in the
`self.column_filters` setting.
If your backend does not know how to generate filters
for the provided field, it should return `None`.
For example::
class MyDbModel(BaseModelView):
def scaffold_filters(self, name):
attr = getattr(self.model, name)
- :meth:`~flask.ext.admin.model.BaseModelView.delete_model`
if isinstance(attr, MyDbTextField):
return [MyEqualFilter(name, name)]
Implementing filters
--------------------
Each model backend should have its own set of filter implementations. It is not possible to use the
filters from SQLAlchemy models in a non-SQLAlchemy backend.
This also means that different backends might have different set of available filters.
The filter is a class derived from :class:`~flask.ext.admin.model.filters.BaseFilter` which implements at least two methods:
1. :meth:`~flask.ext.admin.model.filters.BaseFilter.apply`
2. :meth:`~flask.ext.admin.model.filters.BaseFilter.operation`
`apply` method accepts two parameters: `query` object and a value from the client. Here you can add
filtering logic for the filter type.
Lets take SQLAlchemy model backend as an example:
All SQLAlchemy filters derive from :class:`~flask.ext.admin.contrib.sqla.filters.BaseSQLAFilter` class.
Each filter implements one simple filter SQL operation (like, not like, greater, etc) and accepts a column as
input parameter.
Whenever model view wants to apply a filter to a query
object, it will call `apply` method in a filter class
with a query and value. Filter will then apply
real filter operation.
For example::
class MyBaseFilter(BaseFilter):
def __init__(self, column, name, options=None, data_type=None):
super(MyBaseFilter, self).__init__(name, options, data_type)
self.column = column
class MyEqualFilter(MyBaseFilter):
def apply(self, query, value):
return query.filter(self.column == value)
def operation(self):
return gettext('equals')
# You can validate values. If value is not valid,
# return `False`, so filter will be ignored.
def validate(self, value):
return True
# You can "clean" values before they will be
# passed to the your data access layer
def clean(self, value):
return value
Delete provided model from the data store.
Feel free ask questions if you have problem adding new model backend.
Feel free ask questions if you have problems adding a new model backend.
Also, it is good idea to take a look on SQLAlchemy model backend to
Also, if you get stuck, try taking a look at the SQLAlchemy model backend and use it as a reference.
see how it works in different circumstances.
\ No newline at end of file
examples/auth/auth.py
View file @
4ec6473a
...
@@ -197,8 +197,8 @@ def build_sample_db():
...
@@ -197,8 +197,8 @@ def build_sample_db():
if
__name__
==
'__main__'
:
if
__name__
==
'__main__'
:
# Build a sample db on the fly, if one does not exist yet.
# Build a sample db on the fly, if one does not exist yet.
app_dir
=
o
p
.
realpath
(
os
.
path
.
dirname
(
__file__
))
app_dir
=
o
s
.
path
.
realpath
(
os
.
path
.
dirname
(
__file__
))
database_path
=
o
p
.
join
(
app_dir
,
app
.
config
[
'DATABASE_FILE'
])
database_path
=
o
s
.
path
.
join
(
app_dir
,
app
.
config
[
'DATABASE_FILE'
])
if
not
os
.
path
.
exists
(
database_path
):
if
not
os
.
path
.
exists
(
database_path
):
build_sample_db
()
build_sample_db
()
...
...
examples/sqla/simple.py
View file @
4ec6473a
...
@@ -31,8 +31,8 @@ class User(db.Model):
...
@@ -31,8 +31,8 @@ class User(db.Model):
username
=
db
.
Column
(
db
.
String
(
80
),
unique
=
True
)
username
=
db
.
Column
(
db
.
String
(
80
),
unique
=
True
)
email
=
db
.
Column
(
db
.
String
(
120
),
unique
=
True
)
email
=
db
.
Column
(
db
.
String
(
120
),
unique
=
True
)
# Required for administrative interface
# Required for administrative interface
. For python 3 please use __str__ instead.
def
__
str
__
(
self
):
def
__
unicode
__
(
self
):
return
self
.
username
return
self
.
username
...
@@ -54,7 +54,7 @@ class Post(db.Model):
...
@@ -54,7 +54,7 @@ class Post(db.Model):
tags
=
db
.
relationship
(
'Tag'
,
secondary
=
post_tags_table
)
tags
=
db
.
relationship
(
'Tag'
,
secondary
=
post_tags_table
)
def
__
str
__
(
self
):
def
__
unicode
__
(
self
):
return
self
.
title
return
self
.
title
...
@@ -62,7 +62,7 @@ class Tag(db.Model):
...
@@ -62,7 +62,7 @@ class Tag(db.Model):
id
=
db
.
Column
(
db
.
Integer
,
primary_key
=
True
)
id
=
db
.
Column
(
db
.
Integer
,
primary_key
=
True
)
name
=
db
.
Column
(
db
.
Unicode
(
64
))
name
=
db
.
Column
(
db
.
Unicode
(
64
))
def
__
str
__
(
self
):
def
__
unicode
__
(
self
):
return
self
.
name
return
self
.
name
...
@@ -75,7 +75,7 @@ class UserInfo(db.Model):
...
@@ -75,7 +75,7 @@ class UserInfo(db.Model):
user_id
=
db
.
Column
(
db
.
Integer
(),
db
.
ForeignKey
(
User
.
id
))
user_id
=
db
.
Column
(
db
.
Integer
(),
db
.
ForeignKey
(
User
.
id
))
user
=
db
.
relationship
(
User
,
backref
=
'info'
)
user
=
db
.
relationship
(
User
,
backref
=
'info'
)
def
__
str
__
(
self
):
def
__
unicode
__
(
self
):
return
'
%
s -
%
s'
%
(
self
.
key
,
self
.
value
)
return
'
%
s -
%
s'
%
(
self
.
key
,
self
.
value
)
...
@@ -85,7 +85,7 @@ class Tree(db.Model):
...
@@ -85,7 +85,7 @@ class Tree(db.Model):
parent_id
=
db
.
Column
(
db
.
Integer
,
db
.
ForeignKey
(
'tree.id'
))
parent_id
=
db
.
Column
(
db
.
Integer
,
db
.
ForeignKey
(
'tree.id'
))
parent
=
db
.
relationship
(
'Tree'
,
remote_side
=
[
id
],
backref
=
'children'
)
parent
=
db
.
relationship
(
'Tree'
,
remote_side
=
[
id
],
backref
=
'children'
)
def
__
str
__
(
self
):
def
__
unicode
__
(
self
):
return
self
.
name
return
self
.
name
...
...
flask_admin/_compat.py
View file @
4ec6473a
...
@@ -32,7 +32,7 @@ if not PY2:
...
@@ -32,7 +32,7 @@ if not PY2:
# Various tools
# Various tools
from
functools
import
reduce
from
functools
import
reduce
from
urllib.parse
import
urljoin
from
urllib.parse
import
urljoin
,
urlparse
else
:
else
:
text_type
=
unicode
text_type
=
unicode
string_types
=
(
str
,
unicode
)
string_types
=
(
str
,
unicode
)
...
@@ -50,7 +50,7 @@ else:
...
@@ -50,7 +50,7 @@ else:
# Helpers
# Helpers
reduce
=
__builtins__
[
'reduce'
]
if
isinstance
(
__builtins__
,
dict
)
else
__builtins__
.
reduce
reduce
=
__builtins__
[
'reduce'
]
if
isinstance
(
__builtins__
,
dict
)
else
__builtins__
.
reduce
from
urlparse
import
urljoin
from
urlparse
import
urljoin
,
urlparse
def
with_metaclass
(
meta
,
*
bases
):
def
with_metaclass
(
meta
,
*
bases
):
...
...
flask_admin/contrib/fileadmin.py
View file @
4ec6473a
...
@@ -155,6 +155,11 @@ class FileAdmin(BaseView, ActionsMixin):
...
@@ -155,6 +155,11 @@ class FileAdmin(BaseView, ActionsMixin):
Edit template
Edit template
"""
"""
upload_form
=
UploadForm
"""
Upload form class
"""
def
__init__
(
self
,
base_path
,
base_url
=
None
,
def
__init__
(
self
,
base_path
,
base_url
=
None
,
name
=
None
,
category
=
None
,
endpoint
=
None
,
url
=
None
,
name
=
None
,
category
=
None
,
endpoint
=
None
,
url
=
None
,
verify_path
=
True
):
verify_path
=
True
):
...
@@ -285,7 +290,7 @@ class FileAdmin(BaseView, ActionsMixin):
...
@@ -285,7 +290,7 @@ class FileAdmin(BaseView, ActionsMixin):
"""
"""
file_data
.
save
(
path
)
file_data
.
save
(
path
)
def
_get_dir_url
(
self
,
endpoint
,
path
,
**
kwargs
):
def
_get_dir_url
(
self
,
endpoint
,
path
=
None
,
**
kwargs
):
"""
"""
Return prettified URL
Return prettified URL
...
@@ -410,6 +415,17 @@ class FileAdmin(BaseView, ActionsMixin):
...
@@ -410,6 +415,17 @@ class FileAdmin(BaseView, ActionsMixin):
"""
"""
pass
pass
def
_save_form_files
(
self
,
directory
,
path
,
form
):
filename
=
op
.
join
(
directory
,
secure_filename
(
form
.
upload
.
data
.
filename
))
if
op
.
exists
(
filename
):
flash
(
gettext
(
'File "
%(name)
s" already exists.'
,
name
=
filename
),
'error'
)
else
:
self
.
save_file
(
filename
,
form
.
upload
.
data
)
self
.
on_file_upload
(
directory
,
path
,
filename
)
@
expose
(
'/'
)
@
expose
(
'/'
)
@
expose
(
'/b/<path:path>'
)
@
expose
(
'/b/<path:path>'
)
def
index
(
self
,
path
=
None
):
def
index
(
self
,
path
=
None
):
...
@@ -423,7 +439,7 @@ class FileAdmin(BaseView, ActionsMixin):
...
@@ -423,7 +439,7 @@ class FileAdmin(BaseView, ActionsMixin):
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
(
gettext
(
'Permission denied.'
)
))
flash
(
gettext
(
'Permission denied.'
))
return
redirect
(
self
.
_get_dir_url
(
'.index'
))
return
redirect
(
self
.
_get_dir_url
(
'.index'
))
# Get directory listing
# Get directory listing
...
@@ -486,24 +502,16 @@ class FileAdmin(BaseView, ActionsMixin):
...
@@ -486,24 +502,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
(
gettext
(
'Permission denied.'
)
))
flash
(
gettext
(
'Permission denied.'
))
return
redirect
(
self
.
_get_dir_url
(
'.index'
))
return
redirect
(
self
.
_get_dir_url
(
'.index'
))
form
=
UploadF
orm
(
self
)
form
=
self
.
upload_f
orm
(
self
)
if
helpers
.
validate_form_on_submit
(
form
):
if
helpers
.
validate_form_on_submit
(
form
):
filename
=
op
.
join
(
directory
,
try
:
secure_filename
(
form
.
upload
.
data
.
filename
))
self
.
_save_form_files
(
directory
,
path
,
form
)
return
redirect
(
self
.
_get_dir_url
(
'.index'
,
path
))
if
op
.
exists
(
filename
):
except
Exception
as
ex
:
flash
(
gettext
(
'File "
%(name)
s" already exists.'
,
name
=
filename
),
flash
(
gettext
(
'Failed to save file:
%(error)
s'
,
error
=
ex
))
'error'
)
else
:
try
:
self
.
save_file
(
filename
,
form
.
upload
.
data
)
self
.
on_file_upload
(
directory
,
path
,
filename
)
return
redirect
(
self
.
_get_dir_url
(
'.index'
,
path
))
except
Exception
as
ex
:
flash
(
gettext
(
'Failed to save file:
%(error)
s'
,
error
=
ex
))
return
self
.
render
(
self
.
upload_template
,
form
=
form
)
return
self
.
render
(
self
.
upload_template
,
form
=
form
)
...
@@ -547,7 +555,7 @@ class FileAdmin(BaseView, ActionsMixin):
...
@@ -547,7 +555,7 @@ 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
(
gettext
(
'Permission denied.'
)
))
flash
(
gettext
(
'Permission denied.'
))
return
redirect
(
self
.
_get_dir_url
(
'.index'
))
return
redirect
(
self
.
_get_dir_url
(
'.index'
))
form
=
NameForm
(
helpers
.
get_form_data
())
form
=
NameForm
(
helpers
.
get_form_data
())
...
@@ -558,7 +566,7 @@ class FileAdmin(BaseView, ActionsMixin):
...
@@ -558,7 +566,7 @@ class FileAdmin(BaseView, ActionsMixin):
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'
,
ex
),
'error'
)
flash
(
gettext
(
'Failed to create directory:
%(error)
s'
,
e
rror
=
e
x
),
'error'
)
return
self
.
render
(
self
.
mkdir_template
,
return
self
.
render
(
self
.
mkdir_template
,
form
=
form
,
form
=
form
,
...
@@ -584,7 +592,7 @@ class FileAdmin(BaseView, ActionsMixin):
...
@@ -584,7 +592,7 @@ class FileAdmin(BaseView, ActionsMixin):
return
redirect
(
return_url
)
return
redirect
(
return_url
)
if
not
self
.
is_accessible_path
(
path
):
if
not
self
.
is_accessible_path
(
path
):
flash
(
gettext
(
gettext
(
'Permission denied.'
)
))
flash
(
gettext
(
'Permission denied.'
))
return
redirect
(
self
.
_get_dir_url
(
'.index'
))
return
redirect
(
self
.
_get_dir_url
(
'.index'
))
if
op
.
isdir
(
full_path
):
if
op
.
isdir
(
full_path
):
...
@@ -627,7 +635,7 @@ class FileAdmin(BaseView, ActionsMixin):
...
@@ -627,7 +635,7 @@ class FileAdmin(BaseView, ActionsMixin):
return
redirect
(
return_url
)
return
redirect
(
return_url
)
if
not
self
.
is_accessible_path
(
path
):
if
not
self
.
is_accessible_path
(
path
):
flash
(
gettext
(
gettext
(
'Permission denied.'
)
))
flash
(
gettext
(
'Permission denied.'
))
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
):
...
@@ -672,8 +680,8 @@ class FileAdmin(BaseView, ActionsMixin):
...
@@ -672,8 +680,8 @@ 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
):
if
not
self
.
is_accessible_path
(
path
)
or
not
self
.
is_file_editable
(
path
)
:
flash
(
gettext
(
gettext
(
'Permission denied.'
)
))
flash
(
gettext
(
'Permission denied.'
))
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
))
...
...
flask_admin/contrib/sqla/form.py
View file @
4ec6473a
...
@@ -106,9 +106,9 @@ class AdminModelConverter(ModelConverterBase):
...
@@ -106,9 +106,9 @@ class AdminModelConverter(ModelConverterBase):
kwargs
[
'label'
]
=
self
.
_get_label
(
prop
.
key
,
kwargs
)
kwargs
[
'label'
]
=
self
.
_get_label
(
prop
.
key
,
kwargs
)
kwargs
[
'description'
]
=
self
.
_get_description
(
prop
.
key
,
kwargs
)
kwargs
[
'description'
]
=
self
.
_get_description
(
prop
.
key
,
kwargs
)
if
column
.
nullable
:
if
column
.
nullable
or
prop
.
direction
.
name
!=
'MANYTOONE'
:
kwargs
[
'validators'
]
.
append
(
validators
.
Optional
())
kwargs
[
'validators'
]
.
append
(
validators
.
Optional
())
el
if
prop
.
direction
.
name
!=
'MANYTOMANY'
:
el
se
:
kwargs
[
'validators'
]
.
append
(
validators
.
InputRequired
())
kwargs
[
'validators'
]
.
append
(
validators
.
InputRequired
())
# Contribute model-related parameters
# Contribute model-related parameters
...
...
flask_admin/contrib/sqla/view.py
View file @
4ec6473a
...
@@ -428,7 +428,10 @@ class ModelView(BaseModelView):
...
@@ -428,7 +428,10 @@ class ModelView(BaseModelView):
:returns:
:returns:
``True`` for ``String``, ``Unicode``, ``Text``, ``UnicodeText``
``True`` for ``String``, ``Unicode``, ``Text``, ``UnicodeText``
"""
"""
return
name
in
(
'String'
,
'Unicode'
,
'Text'
,
'UnicodeText'
)
if
name
:
name
=
name
.
lower
()
return
name
in
(
'string'
,
'unicode'
,
'text'
,
'unicodetext'
)
def
scaffold_filters
(
self
,
name
):
def
scaffold_filters
(
self
,
name
):
"""
"""
...
...
flask_admin/form/upload.py
View file @
4ec6473a
...
@@ -177,7 +177,8 @@ class FileUploadField(fields.TextField):
...
@@ -177,7 +177,8 @@ class FileUploadField(fields.TextField):
return
True
return
True
return
(
'.'
in
filename
and
return
(
'.'
in
filename
and
filename
.
rsplit
(
'.'
,
1
)[
1
]
in
self
.
allowed_extensions
)
filename
.
rsplit
(
'.'
,
1
)[
1
]
.
lower
()
in
map
(
str
.
lower
,
self
.
allowed_extensions
))
def
pre_validate
(
self
,
form
):
def
pre_validate
(
self
,
form
):
if
(
self
.
data
and
if
(
self
.
data
and
...
@@ -208,6 +209,8 @@ class FileUploadField(fields.TextField):
...
@@ -208,6 +209,8 @@ class FileUploadField(fields.TextField):
filename
=
self
.
generate_name
(
obj
,
self
.
data
)
filename
=
self
.
generate_name
(
obj
,
self
.
data
)
filename
=
self
.
_save_file
(
self
.
data
,
filename
)
filename
=
self
.
_save_file
(
self
.
data
,
filename
)
# update filename of FileStorage to our validated name
self
.
data
.
filename
=
filename
setattr
(
obj
,
name
,
filename
)
setattr
(
obj
,
name
,
filename
)
...
@@ -329,7 +332,7 @@ class ImageUploadField(FileUploadField):
...
@@ -329,7 +332,7 @@ class ImageUploadField(FileUploadField):
"""
"""
# Check if PIL is installed
# Check if PIL is installed
if
Image
is
None
:
if
Image
is
None
:
raise
Exception
(
'PIL library was not found'
)
raise
ImportError
(
'PIL library was not found'
)
self
.
max_size
=
max_size
self
.
max_size
=
max_size
self
.
thumbnail_fn
=
thumbgen
or
thumbgen_filename
self
.
thumbnail_fn
=
thumbgen
or
thumbgen_filename
...
...
flask_admin/helpers.py
View file @
4ec6473a
...
@@ -3,6 +3,9 @@ from jinja2 import contextfunction
...
@@ -3,6 +3,9 @@ from jinja2 import contextfunction
from
flask
import
g
,
request
from
flask
import
g
,
request
from
wtforms.validators
import
DataRequired
,
InputRequired
from
wtforms.validators
import
DataRequired
,
InputRequired
from
flask.ext.admin._compat
import
urljoin
,
urlparse
from
._compat
import
string_types
from
._compat
import
string_types
...
@@ -96,3 +99,17 @@ def prettify_class_name(name):
...
@@ -96,3 +99,17 @@ def prettify_class_name(name):
String to split
String to split
"""
"""
return
sub
(
r'(?<=.)([A-Z])'
,
r' \1'
,
name
)
return
sub
(
r'(?<=.)([A-Z])'
,
r' \1'
,
name
)
def
is_safe_url
(
target
):
ref_url
=
urlparse
(
request
.
host_url
)
test_url
=
urlparse
(
urljoin
(
request
.
host_url
,
target
))
return
(
test_url
.
scheme
in
(
'http'
,
'https'
)
and
ref_url
.
netloc
==
test_url
.
netloc
)
def
get_redirect_target
(
param_name
=
'url'
):
target
=
request
.
values
.
get
(
param_name
)
if
target
and
is_safe_url
(
target
):
return
target
flask_admin/model/base.py
View file @
4ec6473a
...
@@ -11,7 +11,7 @@ from flask.ext.admin.base import BaseView, expose
...
@@ -11,7 +11,7 @@ from flask.ext.admin.base import BaseView, expose
from
flask.ext.admin.form
import
BaseForm
,
FormOpts
,
rules
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
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
,
as_unicode
from
flask.ext.admin._compat
import
iteritems
,
as_unicode
...
@@ -1121,9 +1121,9 @@ class BaseModelView(BaseView, ActionsMixin):
...
@@ -1121,9 +1121,9 @@ class BaseModelView(BaseView, ActionsMixin):
"""
"""
column_fmt
=
self
.
column_formatters
.
get
(
name
)
column_fmt
=
self
.
column_formatters
.
get
(
name
)
if
column_fmt
is
not
None
:
if
column_fmt
is
not
None
:
return
column_fmt
(
self
,
context
,
model
,
name
)
value
=
column_fmt
(
self
,
context
,
model
,
name
)
else
:
value
=
self
.
_get_field_value
(
model
,
name
)
value
=
self
.
_get_field_value
(
model
,
name
)
choices_map
=
self
.
_column_choices_map
.
get
(
name
,
{})
choices_map
=
self
.
_column_choices_map
.
get
(
name
,
{})
if
choices_map
:
if
choices_map
:
...
@@ -1250,7 +1250,7 @@ class BaseModelView(BaseView, ActionsMixin):
...
@@ -1250,7 +1250,7 @@ class BaseModelView(BaseView, ActionsMixin):
"""
"""
Create model view
Create model view
"""
"""
return_url
=
request
.
args
.
get
(
'url'
)
or
url_for
(
'.index_view'
)
return_url
=
get_redirect_target
(
)
or
url_for
(
'.index_view'
)
if
not
self
.
can_create
:
if
not
self
.
can_create
:
return
redirect
(
return_url
)
return
redirect
(
return_url
)
...
@@ -1278,7 +1278,7 @@ class BaseModelView(BaseView, ActionsMixin):
...
@@ -1278,7 +1278,7 @@ class BaseModelView(BaseView, ActionsMixin):
"""
"""
Edit model view
Edit model view
"""
"""
return_url
=
request
.
args
.
get
(
'url'
)
or
url_for
(
'.index_view'
)
return_url
=
get_redirect_target
(
)
or
url_for
(
'.index_view'
)
if
not
self
.
can_edit
:
if
not
self
.
can_edit
:
return
redirect
(
return_url
)
return
redirect
(
return_url
)
...
@@ -1316,7 +1316,7 @@ class BaseModelView(BaseView, ActionsMixin):
...
@@ -1316,7 +1316,7 @@ class BaseModelView(BaseView, ActionsMixin):
"""
"""
Delete model view. Only POST method is allowed.
Delete model view. Only POST method is allowed.
"""
"""
return_url
=
request
.
args
.
get
(
'url'
)
or
url_for
(
'.index_view'
)
return_url
=
get_redirect_target
(
)
or
url_for
(
'.index_view'
)
# TODO: Use post
# TODO: Use post
if
not
self
.
can_delete
:
if
not
self
.
can_delete
:
...
...
flask_admin/templates/admin/file/list.html
View file @
4ec6473a
...
@@ -90,7 +90,7 @@
...
@@ -90,7 +90,7 @@
{% endif %}
{% endif %}
</td>
</td>
<td>
<td>
{{ size }}
{{ size
|filesizeformat
}}
</td>
</td>
{% endif %}
{% endif %}
{% endblock %}
{% endblock %}
...
...
flask_admin/templates/admin/model/list.html
View file @
4ec6473a
...
@@ -99,6 +99,7 @@
...
@@ -99,6 +99,7 @@
<input
type=
"checkbox"
name=
"rowid"
class=
"action-checkbox"
value=
"{{ get_pk_value(row) }}"
title=
"{{ _gettext('Select record') }}"
/>
<input
type=
"checkbox"
name=
"rowid"
class=
"action-checkbox"
value=
"{{ get_pk_value(row) }}"
title=
"{{ _gettext('Select record') }}"
/>
</td>
</td>
{% endif %}
{% endif %}
{% block list_row_actions_column scoped %}
<td>
<td>
{% block list_row_actions scoped %}
{% block list_row_actions scoped %}
{%- if admin_view.can_edit -%}
{%- if admin_view.can_edit -%}
...
@@ -118,6 +119,7 @@
...
@@ -118,6 +119,7 @@
{%- endif -%}
{%- endif -%}
{% endblock %}
{% endblock %}
</td>
</td>
{% endblock %}
{% for c, name in list_columns %}
{% for c, name in list_columns %}
<td>
{{ get_value(row, c) }}
</td>
<td>
{{ get_value(row, c) }}
</td>
{% endfor %}
{% endfor %}
...
...
flask_admin/tests/data/copyleft.gif
0 → 100644
View file @
4ec6473a
1.54 KB
flask_admin/tests/data/copyleft.jpeg
0 → 100644
View file @
4ec6473a
12.4 KB
flask_admin/tests/sqlamodel/test_basic.py
View file @
4ec6473a
...
@@ -833,3 +833,26 @@ def test_ajax_fk_multi():
...
@@ -833,3 +833,26 @@ def test_ajax_fk_multi():
ok_
(
mdl
is
not
None
)
ok_
(
mdl
is
not
None
)
ok_
(
mdl
.
model1
is
not
None
)
ok_
(
mdl
.
model1
is
not
None
)
eq_
(
len
(
mdl
.
model1
),
1
)
eq_
(
len
(
mdl
.
model1
),
1
)
def
test_safe_redirect
():
app
,
db
,
admin
=
setup
()
Model1
,
_
=
create_models
(
db
)
db
.
create_all
()
view
=
CustomModelView
(
Model1
,
db
.
session
)
admin
.
add_view
(
view
)
client
=
app
.
test_client
()
rv
=
client
.
post
(
'/admin/model1view/new/?url=http://localhost/admin/model2view/'
,
data
=
dict
(
test1
=
'test1large'
,
test2
=
'test2'
))
eq_
(
rv
.
status_code
,
302
)
eq_
(
rv
.
location
,
'http://localhost/admin/model2view/'
)
rv
=
client
.
post
(
'/admin/model1view/new/?url=http://google.com/evil/'
,
data
=
dict
(
test1
=
'test1large'
,
test2
=
'test2'
))
eq_
(
rv
.
status_code
,
302
)
eq_
(
rv
.
location
,
'http://localhost/admin/model1view/'
)
flask_admin/tests/test_form_upload.py
View file @
4ec6473a
...
@@ -95,6 +95,10 @@ def test_image_upload_field():
...
@@ -95,6 +95,10 @@ def test_image_upload_field():
safe_delete
(
path
,
'test2.png'
)
safe_delete
(
path
,
'test2.png'
)
safe_delete
(
path
,
'test2_thumb.jpg'
)
safe_delete
(
path
,
'test2_thumb.jpg'
)
safe_delete
(
path
,
'test1.jpg'
)
safe_delete
(
path
,
'test1.jpg'
)
safe_delete
(
path
,
'test1.jpeg'
)
safe_delete
(
path
,
'test1.gif'
)
safe_delete
(
path
,
'test1.png'
)
safe_delete
(
path
,
'test1.tiff'
)
class
TestForm
(
form
.
BaseForm
):
class
TestForm
(
form
.
BaseForm
):
upload
=
form
.
ImageUploadField
(
'Upload'
,
upload
=
form
.
ImageUploadField
(
'Upload'
,
...
@@ -204,6 +208,25 @@ def test_image_upload_field():
...
@@ -204,6 +208,25 @@ def test_image_upload_field():
ok_
(
op
.
exists
(
op
.
join
(
path
,
'test1.jpg'
)))
ok_
(
op
.
exists
(
op
.
join
(
path
,
'test1.jpg'
)))
# check allowed extensions
for
extension
in
(
'gif'
,
'jpg'
,
'jpeg'
,
'png'
,
'tiff'
):
filename
=
'copyleft.'
+
extension
filepath
=
op
.
join
(
op
.
dirname
(
__file__
),
'data'
,
filename
)
with
open
(
filepath
,
'rb'
)
as
fp
:
with
app
.
test_request_context
(
method
=
'POST'
,
data
=
{
'upload'
:
(
fp
,
filename
)}):
my_form
=
TestNoResizeForm
(
helpers
.
get_form_data
())
ok_
(
my_form
.
validate
())
my_form
.
populate_obj
(
dummy
)
eq_
(
dummy
.
upload
,
my_form
.
upload
.
data
.
filename
)
# check case-sensitivity for extensions
filename
=
op
.
join
(
op
.
dirname
(
__file__
),
'data'
,
'copyleft.jpg'
)
with
open
(
filename
,
'rb'
)
as
fp
:
with
app
.
test_request_context
(
method
=
'POST'
,
data
=
{
'upload'
:
(
fp
,
'copyleft.JPG'
)}):
my_form
=
TestNoResizeForm
(
helpers
.
get_form_data
())
ok_
(
my_form
.
validate
())
def
test_relative_path
():
def
test_relative_path
():
app
=
Flask
(
__name__
)
app
=
Flask
(
__name__
)
...
...
flask_admin/translations/ru/LC_MESSAGES/admin.po
View file @
4ec6473a
...
@@ -254,6 +254,10 @@ msgstr "Создать"
...
@@ -254,6 +254,10 @@ msgstr "Создать"
msgid "Save and Add"
msgid "Save and Add"
msgstr "Сохранить и Добавить"
msgstr "Сохранить и Добавить"
#: ../flask_admin/templates/admin/model/edit.html:6
msgid "Save and Continue"
msgstr "Сохранить и Продолжить"
#: ../flask_admin/templates/admin/model/inline_form_list.html:24
#: ../flask_admin/templates/admin/model/inline_form_list.html:24
msgid "Delete?"
msgid "Delete?"
msgstr "Удалить?"
msgstr "Удалить?"
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment