Commit 0efc3d08 authored by Brian Peterson's avatar Brian Peterson

Merge remote-tracking branch 'origin' into sub-categories

parents 26d460dc 58e9ea7a
...@@ -26,3 +26,4 @@ examples/appengine/lib ...@@ -26,3 +26,4 @@ examples/appengine/lib
env env
*.egg *.egg
.eggs .eggs
.tox/
sudo: false sudo: false
language: python language: python
python: matrix:
- "2.6" include:
- "2.7" - python: 2.6
- "3.3" env: TOX_ENV=py26-WTForms1
- "3.4" - python: 2.6
env: TOX_ENV=py26-WTForms2
- python: 2.7
env: TOX_ENV=py27-WTForms1
- python: 2.7
env: TOX_ENV=py27-WTForms2
- python: 2.7
env: TOX_ENV=flake8
- python: 2.7
env: TOX_ENV=docs-html
- python: 3.3
env: TOX_ENV=py33-WTForms1
- python: 3.3
env: TOX_ENV=py33-WTForms2
- python: 3.4
env: TOX_ENV=py34-WTForms1
- python: 3.4
env: TOX_ENV=py34-WTForms2
- python: 3.5
env: TOX_ENV=py35-WTForms1
- python: 3.5
env: TOX_ENV=py35-WTForms2
- python: 3.6
env: TOX_ENV=py36-WTForms1
- python: 3.6
env: TOX_ENV=py36-WTForms2
env: addons:
- WTFORMS_VERSION=1 postgresql: "9.4"
- WTFORMS_VERSION=2
services: services:
- postgresql - postgresql
...@@ -20,8 +44,9 @@ before_script: ...@@ -20,8 +44,9 @@ before_script:
- psql -U postgres -c 'CREATE EXTENSION hstore;' flask_admin_test - psql -U postgres -c 'CREATE EXTENSION hstore;' flask_admin_test
install: install:
- pip install "wtforms<$WTFORMS_VERSION.99" - pip install tox
- pip install -r requirements-dev.txt
script: tox -e $TOX_ENV
script: nosetests flask_admin/tests after_success:
- coveralls
...@@ -4,10 +4,10 @@ Flask-Admin ...@@ -4,10 +4,10 @@ Flask-Admin
The project was recently moved into its own organization. Please update your The project was recently moved into its own organization. Please update your
references to *git@github.com:flask-admin/flask-admin.git*. references to *git@github.com:flask-admin/flask-admin.git*.
.. image:: https://d322cqt584bo4o.cloudfront.net/flask-admin/localized.png .. image:: https://d322cqt584bo4o.cloudfront.net/flask-admin/localized.svg
:target: https://crowdin.com/project/flask-admin :target: https://crowdin.com/project/flask-admin
.. image:: https://travis-ci.org/flask-admin/flask-admin.png?branch=master .. image:: https://travis-ci.org/flask-admin/flask-admin.svg?branch=master
:target: https://travis-ci.org/flask-admin/flask-admin :target: https://travis-ci.org/flask-admin/flask-admin
Introduction Introduction
...@@ -49,15 +49,14 @@ To run the examples on your local environment, one at a time, do something like: ...@@ -49,15 +49,14 @@ To run the examples on your local environment, one at a time, do something like:
Documentation Documentation
------------- -------------
Flask-Admin is extensively documented, you can find all of the documentation at `http://flask-admin.readthedocs.org/en/latest/ <http://flask-admin.readthedocs.org/en/latest/>`_. Flask-Admin is extensively documented, you can find all of the documentation at `https://flask-admin.readthedocs.io/en/latest/ <https://flask-admin.readthedocs.io/en/latest/>`_.
The docs are auto-generated from the *.rst* files in the */doc* folder. So if you come across any errors, or The docs are auto-generated from the *.rst* files in the */doc* folder. So if you come across any errors, or
if you think of anything else that should be included, then please make the changes and submit them as a *pull-request*. if you think of anything else that should be included, then please make the changes and submit them as a *pull-request*.
To build the docs in your local environment, from the project directory:: To build the docs in your local environment, from the project directory::
pip install -r requirements-dev.txt tox -e docs-html
sudo make html
And if you want to preview any *.rst* snippets that you may want to contribute, go to `http://rst.ninjs.org/ <http://rst.ninjs.org/>`_. And if you want to preview any *.rst* snippets that you may want to contribute, go to `http://rst.ninjs.org/ <http://rst.ninjs.org/>`_.
...@@ -75,7 +74,7 @@ Or alternatively, you can download the repository and install manually by doing: ...@@ -75,7 +74,7 @@ Or alternatively, you can download the repository and install manually by doing:
Tests Tests
----- -----
Test are run with *nose*. If you are not familiar with this package you can get some more info from `their website <http://nose.readthedocs.org/>`_. Test are run with *nose*. If you are not familiar with this package you can get some more info from `their website <https://nose.readthedocs.io/>`_.
To run the tests, from the project directory, simply:: To run the tests, from the project directory, simply::
...@@ -95,6 +94,8 @@ For all the tests to pass successfully, you'll need Postgres & MongoDB to be run ...@@ -95,6 +94,8 @@ For all the tests to pass successfully, you'll need Postgres & MongoDB to be run
CREATE DATABASE flask_admin_test; CREATE DATABASE flask_admin_test;
CREATE EXTENSION postgis; CREATE EXTENSION postgis;
You can also run the tests on multiple environments using *tox*.
3rd Party Stuff 3rd Party Stuff
--------------- ---------------
......
This diff is collapsed.
#!/bin/sh #!/bin/sh
pybabel extract -F babel.ini -k _gettext -k _ngettext -k lazy_gettext -o admin.pot --project Flask-Admin ../flask_admin pybabel extract -F babel.ini -k _gettext -k _ngettext -k lazy_gettext -o admin.pot --project Flask-Admin ../flask_admin
pybabel compile -f -D admin -d ../flask_admin/translations/ pybabel compile -f -D admin -d ../flask_admin/translations/
# docs
cd ..
make gettext
cp build/locale/*.pot babel/
sphinx-intl update -p build/locale/ -d flask_admin/translations/
File mode changed from 100644 to 100755
File mode changed from 100644 to 100755
...@@ -48,7 +48,7 @@ Extending BaseModelView ...@@ -48,7 +48,7 @@ Extending BaseModelView
columns = [] columns = []
for p in dir(self.model): for p in dir(self.model):
attr = getattr(self.model) attr = getattr(self.model, p)
if isinstance(attr, MyDbColumn): if isinstance(attr, MyDbColumn):
columns.append(p) columns.append(p)
......
...@@ -148,22 +148,11 @@ classes as follows:: ...@@ -148,22 +148,11 @@ classes as follows::
widget = CKTextAreaWidget() widget = CKTextAreaWidget()
class MessageAdmin(ModelView): class MessageAdmin(ModelView):
extra_js = ['//cdn.ckeditor.com/4.6.0/standard/ckeditor.js']
form_overrides = { form_overrides = {
'body': CKTextAreaField 'body': CKTextAreaField
} }
create_template = 'ckeditor.html'
edit_template = 'ckeditor.html'
For this to work, you would also need to create a template that extends the default
functionality by including the necessary CKEditor javascript on the `create` and
`edit` pages. Save this in `templates/ckeditor.html`::
{% extends 'admin/model/edit.html' %}
{% block tail %}
{{ super() }}
<script src="//cdn.ckeditor.com/4.5.1/standard/ckeditor.js"></script>
{% endblock %}
File & Image Fields File & Image Fields
******************* *******************
...@@ -189,7 +178,7 @@ Managing Geographical Models ...@@ -189,7 +178,7 @@ Managing Geographical Models
If you want to store spatial information in a GIS database, Flask-Admin has If you want to store spatial information in a GIS database, Flask-Admin has
you covered. The GeoAlchemy backend extends the SQLAlchemy backend (just as you covered. The GeoAlchemy backend extends the SQLAlchemy backend (just as
`GeoAlchemy <http://geoalchemy-2.readthedocs.org/>`_ extends SQLAlchemy) to give you a pretty and functional map-based `GeoAlchemy <https://geoalchemy-2.readthedocs.io/>`_ extends SQLAlchemy) to give you a pretty and functional map-based
editor for your admin pages. editor for your admin pages.
Some notable features include: Some notable features include:
...@@ -200,7 +189,7 @@ Some notable features include: ...@@ -200,7 +189,7 @@ Some notable features include:
interactively using `Leaflet.Draw <https://github.com/Leaflet/Leaflet.draw>`_. interactively using `Leaflet.Draw <https://github.com/Leaflet/Leaflet.draw>`_.
- Graceful fallback: `GeoJSON <http://geojson.org/>`_ data can be edited in a ``<textarea>``, if the - Graceful fallback: `GeoJSON <http://geojson.org/>`_ data can be edited in a ``<textarea>``, if the
user has turned off Javascript. user has turned off Javascript.
- Works with a `Geometry <http://geoalchemy-2.readthedocs.org/en/latest/types.html#geoalchemy2.types.Geometry>`_ SQL field that is integrated with `Shapely <http://toblerity.org/shapely/>`_ objects. - Works with a `Geometry <https://geoalchemy-2.readthedocs.io/en/latest/types.html#geoalchemy2.types.Geometry>`_ SQL field that is integrated with `Shapely <http://toblerity.org/shapely/>`_ objects.
To get started, define some fields on your model using GeoAlchemy's *Geometry* To get started, define some fields on your model using GeoAlchemy's *Geometry*
field. Next, add model views to your interface using the ModelView class field. Next, add model views to your interface using the ModelView class
...@@ -387,7 +376,7 @@ Features: ...@@ -387,7 +376,7 @@ Features:
- GridFS support for file and image uploads - GridFS support for file and image uploads
In order to use MongoEngine integration, install the In order to use MongoEngine integration, install the
`Flask-MongoEngine <https://flask-mongoengine.readthedocs.org>`_ package. `Flask-MongoEngine <https://flask-mongoengine.readthedocs.io>`_ package.
Flask-Admin uses form scaffolding from it. Flask-Admin uses form scaffolding from it.
Known issues: Known issues:
...@@ -407,7 +396,7 @@ Features: ...@@ -407,7 +396,7 @@ Features:
- Inline editing of related models; - Inline editing of related models;
In order to use peewee integration, you need to install two additional Python In order to use peewee integration, you need to install two additional Python
packages: `peewee <https://peewee.readthedocs.org/>`_ and `wtf-peewee <https://github.com/coleifer/wtf-peewee/>`_. packages: `peewee <http://docs.peewee-orm.com/>`_ and `wtf-peewee <https://github.com/coleifer/wtf-peewee/>`_.
Known issues: Known issues:
...@@ -428,8 +417,8 @@ The bare minimum you have to provide for Flask-Admin to work with PyMongo: ...@@ -428,8 +417,8 @@ The bare minimum you have to provide for Flask-Admin to work with PyMongo:
This is minimal PyMongo view:: This is minimal PyMongo view::
class UserForm(Form): class UserForm(Form):
name = TextField('Name') name = StringField('Name')
email = TextField('Email') email = StringField('Email')
class UserView(ModelView): class UserView(ModelView):
column_list = ('name', 'email') column_list = ('name', 'email')
...@@ -519,7 +508,7 @@ do with it, so it won't generate a form field. In this case, you would need to m ...@@ -519,7 +508,7 @@ do with it, so it won't generate a form field. In this case, you would need to m
class MyView(ModelView): class MyView(ModelView):
def scaffold_form(self): def scaffold_form(self):
form_class = super(UserView, self).scaffold_form() form_class = super(UserView, self).scaffold_form()
form_class.extra = TextField('Extra') form_class.extra = StringField('Extra')
return form_class return form_class
Customizing Batch Actions Customizing Batch Actions
......
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
form_widget_args, form_extra_fields, form_widget_args, form_extra_fields,
form_ajax_refs, form_create_rules, form_ajax_refs, form_create_rules,
form_edit_rules, form_edit_rules,
page_size page_size, can_set_page_size
.. autoattribute:: can_create .. autoattribute:: can_create
.. autoattribute:: can_edit .. autoattribute:: can_edit
...@@ -58,3 +58,4 @@ ...@@ -58,3 +58,4 @@
.. autoattribute:: action_disallowed_list .. autoattribute:: action_disallowed_list
.. autoattribute:: page_size .. autoattribute:: page_size
.. autoattribute:: can_set_page_size
Changelog Changelog
========= =========
1.5.0
-----
* Fixed CSRF generation logic for multi-process deployments
* Added WTForms >= 3.0 support
* Flask-Admin would not recursively save inline models, allowing arbitrary nesting
* Added configuration properties that allow injection of additional CSS and JS dependencies into templates without overriding them
* SQLAlchemy backend
- Updated hybrid property detection using new SQLAlchemy APIs
- Added support for association proxies
- Added support for remote hybrid properties filters
- Added support for ARRAY column type
* Localization-related fixes
* MongoEngine backend is now properly formats model labels
* Improved Google App Engine support:
- Added TextProperty, KeyProperty and SelectField support
- Added support for form_args, excluded_columns, page_size and after_model_update
* Fixed URL generation with localized named filters
* FileAdmin has Bootstrap 2 support now
* Geoalchemy fixes
- Use Google Places (by default) for place search
* Updated translations
* Bug fixes
1.4.2
-----
* Small bug fix release. Fixes regression that prevented usage of "virtual" columns with a custom formatter.
1.4.1
-----
* Official Python 3.5 support
* Customizable row actions
* Tablib support (exporting to XLS, XLSX, CSV, etc)
* Updated external dependencies (jQuery, x-editable, etc)
* Added settings that allows exceptions to be raised on view errors
* Bug fixes
1.4.0 1.4.0
----- -----
...@@ -26,25 +64,3 @@ Changelog ...@@ -26,25 +64,3 @@ Changelog
* Updated documentation and examples * Updated documentation and examples
* Updated translations * Updated translations
* Bug fixes * Bug fixes
1.2.0
-----
* Codebase was migrated to Flask-Admin GitHub organization
* Automatically inject Flask-WTF CSRF token to internal Flask-Admin forms
* MapBox v4 support for GeoAlchemy
* Updated translations with help of CrowdIn
* Show warning if field was ignored in form rendering rules
* Simple AppEngine backend
* Optional support for Font Awesome in templates and menus
* Bug fixes
1.1.0
-----
Mostly bug fix release. Highlights:
* Inline model editing on the list page
* FileAdmin refactoring and fixes
* FileUploadField and ImageUploadField will work with Required() validator
* Bug fixes
...@@ -57,6 +57,7 @@ release = version ...@@ -57,6 +57,7 @@ release = version
# The language for content autogenerated by Sphinx. Refer to documentation # The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages. # for a list of supported languages.
#language = None #language = None
locale_dirs = ['../flask_admin/translations/']
# There are two options for replacing |today|: either, you set today to some # There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used: # non-false value, then it is used:
......
...@@ -52,7 +52,7 @@ Straight out of the box, this gives you a set of fully featured *CRUD* views for ...@@ -52,7 +52,7 @@ Straight out of the box, this gives you a set of fully featured *CRUD* views for
* An optional, read-only `details` view. * An optional, read-only `details` view.
There are many options available for customizing the display and functionality of these built-in views. There are many options available for customizing the display and functionality of these built-in views.
For more details on that, see :ref:`customising-builtin-views`. For more details on the other For more details on that, see :ref:`customizing-builtin-views`. For more details on the other
ORM backends that are available, see :ref:`database-backends`. ORM backends that are available, see :ref:`database-backends`.
Adding Content to the Index Page Adding Content to the Index Page
...@@ -85,7 +85,7 @@ with your database models, and it doesn't require you to write any new view logi ...@@ -85,7 +85,7 @@ with your database models, and it doesn't require you to write any new view logi
template code. So it's great for when you're deploying something that's still template code. So it's great for when you're deploying something that's still
under development, before you want the whole world to see it. under development, before you want the whole world to see it.
Have a look at `Flask-BasicAuth <http://flask-basicauth.readthedocs.org/>`_ to see just how Have a look at `Flask-BasicAuth <https://flask-basicauth.readthedocs.io/>`_ to see just how
easy it is to put your whole application behind HTTP Basic Auth. easy it is to put your whole application behind HTTP Basic Auth.
Unfortunately, there is no easy way of applying HTTP Basic Auth just to your admin Unfortunately, there is no easy way of applying HTTP Basic Auth just to your admin
...@@ -96,13 +96,13 @@ Rolling Your Own ...@@ -96,13 +96,13 @@ Rolling Your Own
For a more flexible solution, Flask-Admin lets you define access control rules For a more flexible solution, Flask-Admin lets you define access control rules
on each of your admin view classes by simply overriding the `is_accessible` method. on each of your admin view classes by simply overriding the `is_accessible` method.
How you implement the logic is up to you, but if you were to use a low-level library like How you implement the logic is up to you, but if you were to use a low-level library like
`Flask-Login <https://flask-login.readthedocs.org/>`_, then restricting access `Flask-Login <https://flask-login.readthedocs.io/>`_, then restricting access
could be as simple as:: could be as simple as::
class MicroBlogModelView(sqla.ModelView): class MicroBlogModelView(sqla.ModelView):
def is_accessible(self): def is_accessible(self):
return login.current_user.is_authenticated() return login.current_user.is_authenticated
def inaccessible_callback(self, name, **kwargs): def inaccessible_callback(self, name, **kwargs):
# redirect to login page if user doesn't have access # redirect to login page if user doesn't have access
...@@ -149,7 +149,7 @@ https://github.com/flask-admin/Flask-Admin/tree/master/examples/auth. ...@@ -149,7 +149,7 @@ https://github.com/flask-admin/Flask-Admin/tree/master/examples/auth.
The example only uses the built-in `register` and `login` views, but you could follow the same The example only uses the built-in `register` and `login` views, but you could follow the same
approach for including the other views, like `forgot_password`, `send_confirmation`, etc. approach for including the other views, like `forgot_password`, `send_confirmation`, etc.
.. _customising-builtin-views: .. _customizing-builtin-views:
Customizing Built-in Views Customizing Built-in Views
========================= =========================
...@@ -267,6 +267,13 @@ When your forms contain foreign keys, have those **related models loaded via aja ...@@ -267,6 +267,13 @@ When your forms contain foreign keys, have those **related models loaded via aja
} }
} }
To filter the results that are loaded via ajax, you can use::
form_ajax_refs = {
'active_user': QueryAjaxModelLoader('user', db.session, User,
filters=["is_active=True", "id>1000"])
}
To **manage related models inline**:: To **manage related models inline**::
inline_models = ['post', ] inline_models = ['post', ]
......
...@@ -51,7 +51,7 @@ class User(db.Model): ...@@ -51,7 +51,7 @@ class User(db.Model):
# Define login and registration forms (for flask-login) # Define login and registration forms (for flask-login)
class LoginForm(form.Form): class LoginForm(form.Form):
login = fields.TextField(validators=[validators.required()]) login = fields.StringField(validators=[validators.required()])
password = fields.PasswordField(validators=[validators.required()]) password = fields.PasswordField(validators=[validators.required()])
def validate_login(self, field): def validate_login(self, field):
...@@ -71,8 +71,8 @@ class LoginForm(form.Form): ...@@ -71,8 +71,8 @@ class LoginForm(form.Form):
class RegistrationForm(form.Form): class RegistrationForm(form.Form):
login = fields.TextField(validators=[validators.required()]) login = fields.StringField(validators=[validators.required()])
email = fields.TextField() email = fields.StringField()
password = fields.PasswordField(validators=[validators.required()]) password = fields.PasswordField(validators=[validators.required()])
def validate_login(self, field): def validate_login(self, field):
......
...@@ -6,7 +6,6 @@ from wtforms import form, fields, validators ...@@ -6,7 +6,6 @@ from wtforms import form, fields, validators
import flask_admin as admin import flask_admin as admin
import flask_login as login import flask_login as login
from flask_admin.contrib.mongoengine import ModelView from flask_admin.contrib.mongoengine import ModelView
from flask_admin import helpers
# Create application # Create application
app = Flask(__name__) app = Flask(__name__)
...@@ -47,7 +46,7 @@ class User(db.Document): ...@@ -47,7 +46,7 @@ class User(db.Document):
# Define login and registration forms (for flask-login) # Define login and registration forms (for flask-login)
class LoginForm(form.Form): class LoginForm(form.Form):
login = fields.TextField(validators=[validators.required()]) login = fields.StringField(validators=[validators.required()])
password = fields.PasswordField(validators=[validators.required()]) password = fields.PasswordField(validators=[validators.required()])
def validate_login(self, field): def validate_login(self, field):
...@@ -64,8 +63,8 @@ class LoginForm(form.Form): ...@@ -64,8 +63,8 @@ class LoginForm(form.Form):
class RegistrationForm(form.Form): class RegistrationForm(form.Form):
login = fields.TextField(validators=[validators.required()]) login = fields.StringField(validators=[validators.required()])
email = fields.TextField() email = fields.StringField()
password = fields.PasswordField(validators=[validators.required()]) password = fields.PasswordField(validators=[validators.required()])
def validate_login(self, field): def validate_login(self, field):
......
...@@ -101,6 +101,7 @@ def security_context_processor(): ...@@ -101,6 +101,7 @@ def security_context_processor():
admin_base_template=admin.base_template, admin_base_template=admin.base_template,
admin_view=admin.index_view, admin_view=admin.index_view,
h=admin_helpers, h=admin_helpers,
get_url=url_for
) )
......
...@@ -22,4 +22,5 @@ SECURITY_POST_REGISTER_VIEW = "/admin/" ...@@ -22,4 +22,5 @@ SECURITY_POST_REGISTER_VIEW = "/admin/"
# Flask-Security features # Flask-Security features
SECURITY_REGISTERABLE = True SECURITY_REGISTERABLE = True
SECURITY_SEND_REGISTER_EMAIL = False SECURITY_SEND_REGISTER_EMAIL = False
\ No newline at end of file SQLALCHEMY_TRACK_MODIFICATIONS = False
...@@ -23,14 +23,14 @@ db = conn.test ...@@ -23,14 +23,14 @@ db = conn.test
# User admin # User admin
class InnerForm(form.Form): class InnerForm(form.Form):
name = fields.TextField('Name') name = fields.StringField('Name')
test = fields.TextField('Test') test = fields.StringField('Test')
class UserForm(form.Form): class UserForm(form.Form):
name = fields.TextField('Name') name = fields.StringField('Name')
email = fields.TextField('Email') email = fields.StringField('Email')
password = fields.TextField('Password') password = fields.StringField('Password')
# Inner form # Inner form
inner = InlineFormField(InnerForm) inner = InlineFormField(InnerForm)
...@@ -48,9 +48,9 @@ class UserView(ModelView): ...@@ -48,9 +48,9 @@ class UserView(ModelView):
# Tweet view # Tweet view
class TweetForm(form.Form): class TweetForm(form.Form):
name = fields.TextField('Name') name = fields.StringField('Name')
user_id = fields.SelectField('User', widget=Select2Widget()) user_id = fields.SelectField('User', widget=Select2Widget())
text = fields.TextField('Text') text = fields.StringField('Text')
testie = fields.BooleanField('Test') testie = fields.BooleanField('Test')
......
Flask Flask
Flask-Admin Flask-Admin
pymongo==2.4.1 pymongo==2.4.1
bson
Example of how to use (and filter on) an association proxy with the SQLAlchemy backend.
For information about association proxies and how to use them, please visit:
http://docs.sqlalchemy.org/en/latest/orm/extensions/associationproxy.html
To run this example:
1. Clone the repository::
git clone https://github.com/flask-admin/flask-admin.git
cd flask-admin
2. Create and activate a virtual environment::
virtualenv env
source env/bin/activate
3. Install requirements::
pip install -r 'examples/sqla-association_proxy/requirements.txt'
4. Run the application::
python examples/sqla-association_proxy/app.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.orm import relationship, backref
import flask_admin as admin
from flask_admin.contrib import sqla
# Create application
app = Flask(__name__)
# Create dummy secrey key so we can use sessions
app.config['SECRET_KEY'] = '123456790'
# Create in-memory database
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://'
app.config['SQLALCHEMY_ECHO'] = True
db = SQLAlchemy(app)
# Flask views
@app.route('/')
def index():
return '<a href="/admin/">Click me to get to Admin!</a>'
class User(db.Model):
__tablename__ = 'user'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64))
# Association proxy of "user_keywords" collection to "keyword" attribute - a list of keywords objects.
keywords = association_proxy('user_keywords', 'keyword')
# Association proxy to association proxy - a list of keywords strings.
keywords_values = association_proxy('user_keywords', 'keyword_value')
def __init__(self, name=None):
self.name = name
class UserKeyword(db.Model):
__tablename__ = 'user_keyword'
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True)
keyword_id = db.Column(db.Integer, db.ForeignKey('keyword.id'), primary_key=True)
special_key = db.Column(db.String(50))
# bidirectional attribute/collection of "user"/"user_keywords"
user = relationship(User, backref=backref("user_keywords", cascade="all, delete-orphan"))
# reference to the "Keyword" object
keyword = relationship("Keyword")
# Reference to the "keyword" column inside the "Keyword" object.
keyword_value = association_proxy('keyword', 'keyword')
def __init__(self, keyword=None, user=None, special_key=None):
self.user = user
self.keyword = keyword
self.special_key = special_key
class Keyword(db.Model):
__tablename__ = 'keyword'
id = db.Column(db.Integer, primary_key=True)
keyword = db.Column('keyword', db.String(64))
def __init__(self, keyword=None):
self.keyword = keyword
def __repr__(self):
return 'Keyword(%s)' % repr(self.keyword)
class UserAdmin(sqla.ModelView):
""" Flask-admin can not automatically find a association_proxy yet. You will
need to manually define the column in list_view/filters/sorting/etc.
Moreover, support for association proxies to association proxies
(e.g.: keywords_values) is currently limited to column_list only."""
column_list = ('id', 'name', 'keywords', 'keywords_values')
column_sortable_list = ('id', 'name')
column_filters = ('id', 'name', 'keywords')
form_columns = ('name', 'keywords')
class KeywordAdmin(sqla.ModelView):
column_list = ('id', 'keyword')
# Create admin
admin = admin.Admin(app, name='Example: SQLAlchemy Association Proxy', template_mode='bootstrap3')
admin.add_view(UserAdmin(User, db.session))
admin.add_view(KeywordAdmin(Keyword, db.session))
if __name__ == '__main__':
# Create DB
db.create_all()
# Add sample data
user = User('log')
for kw in (Keyword('new_from_blammo'), Keyword('its_big')):
user.keywords.append(kw)
db.session.add(user)
db.session.commit()
# Start app
app.run(debug=True)
Flask
Flask-Admin
Flask-SQLAlchemy
SQLA backend example showing how to filter select dropdown options in forms.
To run this example:
1. Clone the repository::
git clone https://github.com/flask-admin/flask-admin.git
cd flask-admin
2. Create and activate a virtual environment::
virtualenv env
source env/bin/activate
3. Install requirements::
pip install -r 'examples/sqla-filter-selectable/requirements.txt'
4. Run the application::
python examples/sqla-filter-selectable/app.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
import flask_admin as admin
from flask_admin.contrib import sqla
# Create application
app = Flask(__name__)
# Create dummy secrey key so we can use sessions
app.config['SECRET_KEY'] = '123456790'
# Create in-memory database
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///sample_db_3.sqlite'
app.config['SQLALCHEMY_ECHO'] = True
db = SQLAlchemy(app)
# Flask views
@app.route('/')
def index():
return '<a href="/admin/">Click me to get to Admin!</a>'
class Person(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50))
pets = db.relationship('Pet', backref='person')
def __unicode__(self):
return self.name
class Pet(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50))
person_id = db.Column(db.Integer, db.ForeignKey('person.id'))
available = db.Column(db.Boolean)
def __unicode__(self):
return self.name
class PersonAdmin(sqla.ModelView):
""" Override ModelView to filter options available in forms. """
def create_form(self):
return self._use_filtered_parent(
super(PersonAdmin, self).create_form()
)
def edit_form(self, obj):
return self._use_filtered_parent(
super(PersonAdmin, self).edit_form(obj)
)
def _use_filtered_parent(self, form):
form.pets.query_factory = self._get_parent_list
return form
def _get_parent_list(self):
# only show available pets in the form
return Pet.query.filter_by(available=True).all()
def __unicode__(self):
return self.name
# Create admin
admin = admin.Admin(app, name='Example: SQLAlchemy - Filtered Form Selectable',
template_mode='bootstrap3')
admin.add_view(PersonAdmin(Person, db.session))
admin.add_view(sqla.ModelView(Pet, db.session))
if __name__ == '__main__':
# Recreate DB
db.drop_all()
db.create_all()
person = Person(name='Bill')
pet1 = Pet(name='Dog', available=True)
pet2 = Pet(name='Fish', available=True)
pet3 = Pet(name='Ocelot', available=False)
db.session.add_all([person, pet1, pet2, pet3])
db.session.commit()
# Start app
app.run(debug=True)
Flask
Flask-Admin
Flask-SQLAlchemy
...@@ -4,7 +4,6 @@ from sqlalchemy.ext.hybrid import hybrid_property ...@@ -4,7 +4,6 @@ from sqlalchemy.ext.hybrid import hybrid_property
import flask_admin as admin import flask_admin as admin
from flask_admin.contrib import sqla from flask_admin.contrib import sqla
from flask_admin.contrib.sqla.filters import IntGreaterFilter
# Create application # Create application
app = Flask(__name__) app = Flask(__name__)
...@@ -36,14 +35,13 @@ class Screen(db.Model): ...@@ -36,14 +35,13 @@ class Screen(db.Model):
class ScreenAdmin(sqla.ModelView): class ScreenAdmin(sqla.ModelView):
''' Flask-admin can not automatically find a hybrid_property yet. You will """ Flask-admin can not automatically find a hybrid_property yet. You will
need to manually define the column in list_view/filters/sorting/etc.''' need to manually define the column in list_view/filters/sorting/etc."""
list_columns = ['id', 'width', 'height', 'number_of_pixels'] column_list = ['id', 'width', 'height', 'number_of_pixels']
column_sortable_list = ['id', 'width', 'height', 'number_of_pixels'] column_sortable_list = ['id', 'width', 'height', 'number_of_pixels']
# make sure the type of your filter matches your hybrid_property # Flask-admin can automatically detect the relevant filters for hybrid properties.
column_filters = [IntGreaterFilter(Screen.number_of_pixels, column_filters = ('number_of_pixels', )
'Number of Pixels')]
# Create admin # Create admin
......
...@@ -31,8 +31,7 @@ class User(db.Model): ...@@ -31,8 +31,7 @@ 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. For python 3 please use __str__ instead. def __str__(self):
def __unicode__(self):
return self.username return self.username
...@@ -54,7 +53,7 @@ class Post(db.Model): ...@@ -54,7 +53,7 @@ class Post(db.Model):
tags = db.relationship('Tag', secondary=post_tags_table) tags = db.relationship('Tag', secondary=post_tags_table)
def __unicode__(self): def __str__(self):
return self.title return self.title
...@@ -62,7 +61,7 @@ class Tag(db.Model): ...@@ -62,7 +61,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 __unicode__(self): def __str__(self):
return self.name return self.name
...@@ -75,7 +74,7 @@ class UserInfo(db.Model): ...@@ -75,7 +74,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 __unicode__(self): def __str__(self):
return '%s - %s' % (self.key, self.value) return '%s - %s' % (self.key, self.value)
...@@ -85,7 +84,7 @@ class Tree(db.Model): ...@@ -85,7 +84,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 __unicode__(self): def __str__(self):
return self.name return self.name
......
...@@ -28,7 +28,7 @@ class Car(db.Model): ...@@ -28,7 +28,7 @@ class Car(db.Model):
id = db.Column(db.Integer, primary_key=True, autoincrement=True) id = db.Column(db.Integer, primary_key=True, autoincrement=True)
desc = db.Column(db.String(50)) desc = db.Column(db.String(50))
def __unicode__(self): def __str__(self):
return self.desc return self.desc
......
This example shows how you can change bootswatch themes
To run this example:
1. Clone the repository::
git clone https://github.com/flask-admin/flask-admin.git
cd flask-admin
2. Create and activate a virtual environment::
virtualenv env
source env/bin/activate
3. Install requirements::
pip install -r 'examples/forms/requirements.txt'
4. Run the application::
python examples/forms/app.py
The first time you run this example, a sample sqlite database gets populated automatically. To suppress this behaviour,
comment the following lines in app.py:::
if not os.path.exists(database_path):
build_sample_db()
__author__ = 'rochacbruno'
import os
import os.path as op
from flask import Flask, url_for
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.event import listens_for
from jinja2 import Markup
from flask_admin import Admin, form
from flask_admin.form import rules
from flask_admin.contrib import sqla
# Create application
app = Flask(__name__, static_folder='files')
# set flask admin swatch
app.config['FLASK_ADMIN_SWATCH'] = 'cerulean'
# Create dummy secrey key so we can use sessions
app.config['SECRET_KEY'] = '123456790'
# Create in-memory database
app.config['DATABASE_FILE'] = 'sample_db.sqlite'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + app.config['DATABASE_FILE']
app.config['SQLALCHEMY_ECHO'] = True
db = SQLAlchemy(app)
# Create directory for file fields to use
file_path = op.join(op.dirname(__file__), 'files')
try:
os.mkdir(file_path)
except OSError:
pass
# Create models
class File(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.Unicode(64))
path = db.Column(db.Unicode(128))
def __unicode__(self):
return self.name
class Image(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.Unicode(64))
path = db.Column(db.Unicode(128))
def __unicode__(self):
return self.name
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
first_name = db.Column(db.Unicode(64))
last_name = db.Column(db.Unicode(64))
email = db.Column(db.Unicode(128))
phone = db.Column(db.Unicode(32))
city = db.Column(db.Unicode(128))
country = db.Column(db.Unicode(128))
notes = db.Column(db.UnicodeText)
# Delete hooks for models, delete files if models are getting deleted
@listens_for(File, 'after_delete')
def del_file(mapper, connection, target):
if target.path:
try:
os.remove(op.join(file_path, target.path))
except OSError:
# Don't care if was not deleted because it does not exist
pass
@listens_for(Image, 'after_delete')
def del_image(mapper, connection, target):
if target.path:
# Delete image
try:
os.remove(op.join(file_path, target.path))
except OSError:
pass
# Delete thumbnail
try:
os.remove(op.join(file_path,
form.thumbgen_filename(target.path)))
except OSError:
pass
# Administrative views
class FileView(sqla.ModelView):
# Override form field to use Flask-Admin FileUploadField
form_overrides = {
'path': form.FileUploadField
}
# Pass additional parameters to 'path' to FileUploadField constructor
form_args = {
'path': {
'label': 'File',
'base_path': file_path,
'allow_overwrite': False
}
}
class ImageView(sqla.ModelView):
def _list_thumbnail(view, context, model, name):
if not model.path:
return ''
return Markup('<img src="%s">' % url_for('static',
filename=form.thumbgen_filename(model.path)))
column_formatters = {
'path': _list_thumbnail
}
# Alternative way to contribute field is to override it completely.
# In this case, Flask-Admin won't attempt to merge various parameters for the field.
form_extra_fields = {
'path': form.ImageUploadField('Image',
base_path=file_path,
thumbnail_size=(100, 100, True))
}
class UserView(sqla.ModelView):
"""
This class demonstrates the use of 'rules' for controlling the rendering of forms.
"""
form_create_rules = [
# Header and four fields. Email field will go above phone field.
rules.FieldSet(('first_name', 'last_name', 'email', 'phone'), 'Personal'),
# Separate header and few fields
rules.Header('Location'),
rules.Field('city'),
# String is resolved to form field, so there's no need to explicitly use `rules.Field`
'country',
# Show macro from Flask-Admin lib.html (it is included with 'lib' prefix)
rules.Container('rule_demo.wrap', rules.Field('notes'))
]
# Use same rule set for edit page
form_edit_rules = form_create_rules
create_template = 'rule_create.html'
edit_template = 'rule_edit.html'
# Flask views
@app.route('/')
def index():
return '<a href="/admin/">Click me to get to Admin!</a>'
# Create admin
admin = Admin(app, 'Example: Bootswatch', template_mode='bootstrap2')
# Add views
admin.add_view(FileView(File, db.session))
admin.add_view(ImageView(Image, db.session))
admin.add_view(UserView(User, db.session, name='User'))
def build_sample_db():
"""
Populate a small db with some example entries.
"""
import random
import string
db.drop_all()
db.create_all()
first_names = [
'Harry', 'Amelia', 'Oliver', 'Jack', 'Isabella', 'Charlie','Sophie', 'Mia',
'Jacob', 'Thomas', 'Emily', 'Lily', 'Ava', 'Isla', 'Alfie', 'Olivia', 'Jessica',
'Riley', 'William', 'James', 'Geoffrey', 'Lisa', 'Benjamin', 'Stacey', 'Lucy'
]
last_names = [
'Brown', 'Smith', 'Patel', 'Jones', 'Williams', 'Johnson', 'Taylor', 'Thomas',
'Roberts', 'Khan', 'Lewis', 'Jackson', 'Clarke', 'James', 'Phillips', 'Wilson',
'Ali', 'Mason', 'Mitchell', 'Rose', 'Davis', 'Davies', 'Rodriguez', 'Cox', 'Alexander'
]
locations = [
("Shanghai", "China"),
("Istanbul", "Turkey"),
("Karachi", "Pakistan"),
("Mumbai", "India"),
("Moscow", "Russia"),
("Sao Paulo", "Brazil"),
("Beijing", "China"),
("Tianjin", "China"),
("Guangzhou", "China"),
("Delhi", "India"),
("Seoul", "South Korea"),
("Shenzhen", "China"),
("Jakarta", "Indonesia"),
("Tokyo", "Japan"),
("Mexico City", "Mexico"),
("Kinshasa", "Democratic Republic of the Congo"),
("Bangalore", "India"),
("New York City", "United States"),
("London", "United Kingdom"),
("Bangkok", "Thailand"),
("Tehran", "Iran"),
("Dongguan", "China"),
("Lagos", "Nigeria"),
("Lima", "Peru"),
("Ho Chi Minh City", "Vietnam"),
]
for i in range(len(first_names)):
user = User()
user.first_name = first_names[i]
user.last_name = last_names[i]
user.email = user.first_name.lower() + "@example.com"
tmp = ''.join(random.choice(string.digits) for i in range(10))
user.phone = "(" + tmp[0:3] + ") " + tmp[3:6] + " " + tmp[6::]
user.city = locations[i][0]
user.country = locations[i][1]
db.session.add(user)
images = ["Buffalo", "Elephant", "Leopard", "Lion", "Rhino"]
for name in images:
image = Image()
image.name = name
image.path = name.lower() + ".jpg"
db.session.add(image)
for i in [1, 2, 3]:
file = File()
file.name = "Example " + str(i)
file.path = "example_" + str(i) + ".pdf"
db.session.add(file)
db.session.commit()
return
if __name__ == '__main__':
# Build a sample db on the fly, if one does not exist yet.
app_dir = op.realpath(os.path.dirname(__file__))
database_path = op.join(app_dir, app.config['DATABASE_FILE'])
if not os.path.exists(database_path):
build_sample_db()
# Start app
app.run(debug=True, use_reloader=True)
Flask
Flask-Admin
Flask-SQLAlchemy
pillow
\ No newline at end of file
{% extends 'admin/master.html' %}
{% block body %}
{{ super() }}
<div class="container">
<div class="row">
<div class="span12">
<h1>Flask-Admin example</h1>
<p class="lead">
Bootswatch
</p>
<p>
This example shows how you can define a bootstrap swatch theme
</p>
<pre># set flask admin swatch
app.config['FLASK_ADMIN_SWATCH'] = 'cerulean'</pre>
<p>
Available swatches in <a href="http://bootswatch.com/2/">http://bootswatch.com/2/</a>
</p>
<a class="btn btn-primary" href="/"><i class="glyphicon glyphicon-chevron-left"></i> Back</a>
</div>
</div>
</div>
{% endblock body %}
{% extends 'admin/model/create.html' %}
{% import 'rule_demo.html' as rule_demo %}
\ No newline at end of file
{% macro wrap() %}
<div style="border: 1px solid gray; background-color: #f0f0f0; padding-top: 8px; margin-bottom: 8px">
{{ caller() }}
</div>
{% endmacro %}
\ No newline at end of file
{% extends 'admin/model/edit.html' %}
{% import 'rule_demo.html' as rule_demo %}
This example shows how you can change bootswatch themes
To run this example:
1. Clone the repository::
git clone https://github.com/flask-admin/flask-admin.git
cd flask-admin
2. Create and activate a virtual environment::
virtualenv env
source env/bin/activate
3. Install requirements::
pip install -r 'examples/forms/requirements.txt'
4. Run the application::
python examples/forms/app.py
The first time you run this example, a sample sqlite database gets populated automatically. To suppress this behaviour,
comment the following lines in app.py:::
if not os.path.exists(database_path):
build_sample_db()
__author__ = 'rochacbruno'
import os
import os.path as op
from flask import Flask, url_for
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.event import listens_for
from jinja2 import Markup
from flask_admin import Admin, form
from flask_admin.form import rules
from flask_admin.contrib import sqla
# Create application
app = Flask(__name__, static_folder='files')
# set flask admin swatch
app.config['FLASK_ADMIN_SWATCH'] = 'cerulean'
# Create dummy secrey key so we can use sessions
app.config['SECRET_KEY'] = '123456790'
# Create in-memory database
app.config['DATABASE_FILE'] = 'sample_db.sqlite'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + app.config['DATABASE_FILE']
app.config['SQLALCHEMY_ECHO'] = True
db = SQLAlchemy(app)
# Create directory for file fields to use
file_path = op.join(op.dirname(__file__), 'files')
try:
os.mkdir(file_path)
except OSError:
pass
# Create models
class File(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.Unicode(64))
path = db.Column(db.Unicode(128))
def __unicode__(self):
return self.name
class Image(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.Unicode(64))
path = db.Column(db.Unicode(128))
def __unicode__(self):
return self.name
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
first_name = db.Column(db.Unicode(64))
last_name = db.Column(db.Unicode(64))
email = db.Column(db.Unicode(128))
phone = db.Column(db.Unicode(32))
city = db.Column(db.Unicode(128))
country = db.Column(db.Unicode(128))
notes = db.Column(db.UnicodeText)
# Delete hooks for models, delete files if models are getting deleted
@listens_for(File, 'after_delete')
def del_file(mapper, connection, target):
if target.path:
try:
os.remove(op.join(file_path, target.path))
except OSError:
# Don't care if was not deleted because it does not exist
pass
@listens_for(Image, 'after_delete')
def del_image(mapper, connection, target):
if target.path:
# Delete image
try:
os.remove(op.join(file_path, target.path))
except OSError:
pass
# Delete thumbnail
try:
os.remove(op.join(file_path,
form.thumbgen_filename(target.path)))
except OSError:
pass
# Administrative views
class FileView(sqla.ModelView):
# Override form field to use Flask-Admin FileUploadField
form_overrides = {
'path': form.FileUploadField
}
# Pass additional parameters to 'path' to FileUploadField constructor
form_args = {
'path': {
'label': 'File',
'base_path': file_path,
'allow_overwrite': False
}
}
class ImageView(sqla.ModelView):
def _list_thumbnail(view, context, model, name):
if not model.path:
return ''
return Markup('<img src="%s">' % url_for('static',
filename=form.thumbgen_filename(model.path)))
column_formatters = {
'path': _list_thumbnail
}
# Alternative way to contribute field is to override it completely.
# In this case, Flask-Admin won't attempt to merge various parameters for the field.
form_extra_fields = {
'path': form.ImageUploadField('Image',
base_path=file_path,
thumbnail_size=(100, 100, True))
}
class UserView(sqla.ModelView):
"""
This class demonstrates the use of 'rules' for controlling the rendering of forms.
"""
form_create_rules = [
# Header and four fields. Email field will go above phone field.
rules.FieldSet(('first_name', 'last_name', 'email', 'phone'), 'Personal'),
# Separate header and few fields
rules.Header('Location'),
rules.Field('city'),
# String is resolved to form field, so there's no need to explicitly use `rules.Field`
'country',
# Show macro from Flask-Admin lib.html (it is included with 'lib' prefix)
rules.Container('rule_demo.wrap', rules.Field('notes'))
]
# Use same rule set for edit page
form_edit_rules = form_create_rules
create_template = 'rule_create.html'
edit_template = 'rule_edit.html'
# Flask views
@app.route('/')
def index():
return '<a href="/admin/">Click me to get to Admin!</a>'
# Create admin
admin = Admin(app, 'Example: Bootswatch', template_mode='bootstrap3')
# Add views
admin.add_view(FileView(File, db.session))
admin.add_view(ImageView(Image, db.session))
admin.add_view(UserView(User, db.session, name='User'))
def build_sample_db():
"""
Populate a small db with some example entries.
"""
import random
import string
db.drop_all()
db.create_all()
first_names = [
'Harry', 'Amelia', 'Oliver', 'Jack', 'Isabella', 'Charlie','Sophie', 'Mia',
'Jacob', 'Thomas', 'Emily', 'Lily', 'Ava', 'Isla', 'Alfie', 'Olivia', 'Jessica',
'Riley', 'William', 'James', 'Geoffrey', 'Lisa', 'Benjamin', 'Stacey', 'Lucy'
]
last_names = [
'Brown', 'Smith', 'Patel', 'Jones', 'Williams', 'Johnson', 'Taylor', 'Thomas',
'Roberts', 'Khan', 'Lewis', 'Jackson', 'Clarke', 'James', 'Phillips', 'Wilson',
'Ali', 'Mason', 'Mitchell', 'Rose', 'Davis', 'Davies', 'Rodriguez', 'Cox', 'Alexander'
]
locations = [
("Shanghai", "China"),
("Istanbul", "Turkey"),
("Karachi", "Pakistan"),
("Mumbai", "India"),
("Moscow", "Russia"),
("Sao Paulo", "Brazil"),
("Beijing", "China"),
("Tianjin", "China"),
("Guangzhou", "China"),
("Delhi", "India"),
("Seoul", "South Korea"),
("Shenzhen", "China"),
("Jakarta", "Indonesia"),
("Tokyo", "Japan"),
("Mexico City", "Mexico"),
("Kinshasa", "Democratic Republic of the Congo"),
("Bangalore", "India"),
("New York City", "United States"),
("London", "United Kingdom"),
("Bangkok", "Thailand"),
("Tehran", "Iran"),
("Dongguan", "China"),
("Lagos", "Nigeria"),
("Lima", "Peru"),
("Ho Chi Minh City", "Vietnam"),
]
for i in range(len(first_names)):
user = User()
user.first_name = first_names[i]
user.last_name = last_names[i]
user.email = user.first_name.lower() + "@example.com"
tmp = ''.join(random.choice(string.digits) for i in range(10))
user.phone = "(" + tmp[0:3] + ") " + tmp[3:6] + " " + tmp[6::]
user.city = locations[i][0]
user.country = locations[i][1]
db.session.add(user)
images = ["Buffalo", "Elephant", "Leopard", "Lion", "Rhino"]
for name in images:
image = Image()
image.name = name
image.path = name.lower() + ".jpg"
db.session.add(image)
for i in [1, 2, 3]:
file = File()
file.name = "Example " + str(i)
file.path = "example_" + str(i) + ".pdf"
db.session.add(file)
db.session.commit()
return
if __name__ == '__main__':
# Build a sample db on the fly, if one does not exist yet.
app_dir = op.realpath(os.path.dirname(__file__))
database_path = op.join(app_dir, app.config['DATABASE_FILE'])
if not os.path.exists(database_path):
build_sample_db()
# Start app
app.run(debug=True, use_reloader=True)
Flask
Flask-Admin
Flask-SQLAlchemy
pillow
\ No newline at end of file
{% extends 'admin/master.html' %}
{% block body %}
{{ super() }}
<div class="container">
<div class="row">
<div class="col-sm-10 col-sm-offset-1">
<h1>Flask-Admin example</h1>
<p class="lead">
Bootswatch
</p>
<p>
This example shows how you can define a bootstrap swatch theme
</p>
<pre># set flask admin swatch
app.config['FLASK_ADMIN_SWATCH'] = 'cerulean'</pre>
<p>
Available swatches in <a href="http://bootswatch.com">http://bootswatch.com</a>
</p>
<a class="btn btn-primary" href="/"><i class="glyphicon glyphicon-chevron-left"></i> Back</a>
</div>
</div>
</div>
{% endblock body %}
{% extends 'admin/model/create.html' %}
{% import 'rule_demo.html' as rule_demo %}
\ No newline at end of file
{% macro wrap() %}
<div style="border: 1px solid gray; background-color: #f0f0f0; padding-top: 8px; margin-bottom: 8px">
{{ caller() }}
</div>
{% endmacro %}
\ No newline at end of file
{% extends 'admin/model/edit.html' %}
{% import 'rule_demo.html' as rule_demo %}
TinyMongo model backend integration example.
TinyMongo is the Pymongo for TinyDB and it stores data in JSON files.
To run this example:
1. Clone the repository::
git clone https://github.com/flask-admin/flask-admin.git
cd flask-admin
2. Create and activate a virtual environment::
virtualenv env
source env/bin/activate
3. Install requirements::
pip install -r 'examples/tinymongo/requirements.txt'
4. Run the application::
python examples/tinymongo/app.py
"""
Example of Flask-Admin using TinyDB with TinyMongo
refer to README.txt for instructions
Author: Bruno Rocha <@rochacbruno>
Based in PyMongo Example and TinyMongo
"""
import flask_admin as admin
from flask import Flask
from flask_admin.contrib.pymongo import ModelView, filters
from flask_admin.form import Select2Widget
from flask_admin.model.fields import InlineFieldList, InlineFormField
from wtforms import fields, form
from tinymongo import TinyMongoClient
# Create application
app = Flask(__name__)
# Create dummy secrey key so we can use sessions
app.config['SECRET_KEY'] = '123456790'
# Create models in a JSON file localted at
DATAFOLDER = '/tmp/flask_admin_test'
conn = TinyMongoClient(DATAFOLDER)
db = conn.test
# create some users for testing
# for i in range(30):
# db.user.insert({'name': 'Mike %s' % i})
# User admin
class InnerForm(form.Form):
name = fields.StringField('Name')
test = fields.StringField('Test')
class UserForm(form.Form):
foo = fields.StringField('foo')
name = fields.StringField('Name')
email = fields.StringField('Email')
password = fields.StringField('Password')
# Inner form
inner = InlineFormField(InnerForm)
# Form list
form_list = InlineFieldList(InlineFormField(InnerForm))
class UserView(ModelView):
column_list = ('name', 'email', 'password', 'foo')
column_sortable_list = ('name', 'email', 'password')
form = UserForm
page_size = 20
can_set_page_size = True
# Tweet view
class TweetForm(form.Form):
name = fields.StringField('Name')
user_id = fields.SelectField('User', widget=Select2Widget())
text = fields.StringField('Text')
testie = fields.BooleanField('Test')
class TweetView(ModelView):
column_list = ('name', 'user_name', 'text')
column_sortable_list = ('name', 'text')
column_filters = (filters.FilterEqual('name', 'Name'),
filters.FilterNotEqual('name', 'Name'),
filters.FilterLike('name', 'Name'),
filters.FilterNotLike('name', 'Name'),
filters.BooleanEqualFilter('testie', 'Testie'))
# column_searchable_list = ('name', 'text')
form = TweetForm
def get_list(self, *args, **kwargs):
count, data = super(TweetView, self).get_list(*args, **kwargs)
# Contribute user_name to the models
for item in data:
item['user_name'] = db.user.find_one(
{'_id': item['user_id']}
)['name']
return count, data
# Contribute list of user choices to the forms
def _feed_user_choices(self, form):
users = db.user.find(fields=('name',))
form.user_id.choices = [(str(x['_id']), x['name']) for x in users]
return form
def create_form(self):
form = super(TweetView, self).create_form()
return self._feed_user_choices(form)
def edit_form(self, obj):
form = super(TweetView, self).edit_form(obj)
return self._feed_user_choices(form)
# Correct user_id reference before saving
def on_model_change(self, form, model):
user_id = model.get('user_id')
model['user_id'] = user_id
return model
# Flask views
@app.route('/')
def index():
return '<a href="/admin/">Click me to get to Admin!</a>'
if __name__ == '__main__':
# Create admin
admin = admin.Admin(app, name='Example: TinyMongo - TinyDB')
# Add views
admin.add_view(UserView(db.user, 'User'))
admin.add_view(TweetView(db.tweet, 'Tweets'))
# Start app
app.run(debug=True)
Flask
Flask-Admin
pymongo==2.4.1
git+https://github.com/schapman1974/tinymongo.git#egg=tinymongo
...@@ -21,7 +21,7 @@ db = SQLAlchemy(app) ...@@ -21,7 +21,7 @@ db = SQLAlchemy(app)
''' Define a wtforms widget and field. ''' Define a wtforms widget and field.
WTForms documentation on custom widgets: WTForms documentation on custom widgets:
http://wtforms.readthedocs.org/en/latest/widgets.html#custom-widgets https://wtforms.readthedocs.io/en/latest/widgets.html#custom-widgets
''' '''
class CKTextAreaWidget(widgets.TextArea): class CKTextAreaWidget(widgets.TextArea):
def __call__(self, field, **kwargs): def __call__(self, field, **kwargs):
......
__version__ = '1.4.0' __version__ = '1.5.0'
__author__ = 'Flask-Admin team' __author__ = 'Flask-Admin team'
__email__ = 'serge.koval+github@gmail.com' __email__ = 'serge.koval+github@gmail.com'
from .base import expose, expose_plugview, Admin, BaseView, AdminIndexView from .base import expose, expose_plugview, Admin, BaseView, AdminIndexView # noqa: F401
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# flake8: noqa
""" """
flask_admin._compat flask_admin._compat
~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~
...@@ -87,17 +88,4 @@ def with_metaclass(meta, *bases): ...@@ -87,17 +88,4 @@ def with_metaclass(meta, *bases):
try: try:
from collections import OrderedDict from collections import OrderedDict
except ImportError: except ImportError:
# Bare-bones OrderedDict implementation for Python2.6 compatibility from ordereddict import OrderedDict
class OrderedDict(dict):
def __init__(self, *args, **kwargs):
dict.__init__(self, *args, **kwargs)
self.ordered_keys = []
def __setitem__(self, key, value):
self.ordered_keys.append(key)
dict.__setitem__(self, key, value)
def __iter__(self):
return (k for k in self.ordered_keys)
def iteritems(self):
return ((k, self[k]) for k in self.ordered_keys)
def items(self):
return list(self.iteritems())
...@@ -3,7 +3,7 @@ from flask import request, redirect ...@@ -3,7 +3,7 @@ from flask import request, redirect
from flask_admin import tools from flask_admin import tools
from flask_admin._compat import text_type from flask_admin._compat import text_type
from flask_admin.helpers import get_redirect_target from flask_admin.helpers import get_redirect_target, flash_errors
def action(name, text, confirmation=None): def action(name, text, confirmation=None):
...@@ -104,16 +104,22 @@ class ActionsMixin(object): ...@@ -104,16 +104,22 @@ class ActionsMixin(object):
If not provided, will return user to the return url in the form If not provided, will return user to the return url in the form
or the list view. or the list view.
""" """
action = request.form.get('action') form = self.action_form()
ids = request.form.getlist('rowid')
handler = self._actions_data.get(action) if self.validate_form(form):
# using getlist instead of FieldList for backward compatibility
ids = request.form.getlist('rowid')
action = form.action.data
if handler and self.is_action_allowed(action): handler = self._actions_data.get(action)
response = handler[0](ids)
if response is not None: if handler and self.is_action_allowed(action):
return response response = handler[0](ids)
if response is not None:
return response
else:
flash_errors(form, message='Failed to perform action. %(error)s')
if return_view: if return_view:
url = self.get_url('.' + return_view) url = self.get_url('.' + return_view)
......
...@@ -52,10 +52,12 @@ else: ...@@ -52,10 +52,12 @@ else:
class Translations(object): class Translations(object):
''' Fixes WTForms translation support and uses wtforms translations ''' ''' Fixes WTForms translation support and uses wtforms translations '''
def gettext(self, string): def gettext(self, string):
return wtforms_domain.gettext(string) t = wtforms_domain.get_translations()
return t.ugettext(string)
def ngettext(self, singular, plural, n): def ngettext(self, singular, plural, n):
return wtforms_domain.ngettext(singular, plural, n) t = wtforms_domain.get_translations()
return t.ungettext(singular, plural, n)
# lazy imports # lazy imports
......
...@@ -9,7 +9,7 @@ from flask_admin._compat import with_metaclass, as_unicode ...@@ -9,7 +9,7 @@ from flask_admin._compat import with_metaclass, as_unicode
from flask_admin import helpers as h from flask_admin import helpers as h
# For compatibility reasons import MenuLink # For compatibility reasons import MenuLink
from flask_admin.menu import MenuCategory, MenuView, MenuLink, SubMenuCategory from flask_admin.menu import MenuCategory, MenuView, MenuLink, SubMenuCategory # noqa: F401
def expose(url='/', methods=('GET',)): def expose(url='/', methods=('GET',)):
...@@ -518,8 +518,8 @@ class Admin(object): ...@@ -518,8 +518,8 @@ class Admin(object):
self.template_mode = template_mode or 'bootstrap2' self.template_mode = template_mode or 'bootstrap2'
self.category_icon_classes = category_icon_classes or dict() self.category_icon_classes = category_icon_classes or dict()
# Add predefined index view # Add index view
self.add_view(self.index_view) self._set_admin_index_view(index_view=index_view, endpoint=endpoint, url=url)
# Register with application # Register with application
if app is not None: if app is not None:
...@@ -541,6 +541,30 @@ class Admin(object): ...@@ -541,6 +541,30 @@ class Admin(object):
self._add_view_to_menu(view) self._add_view_to_menu(view)
def _set_admin_index_view(self, index_view=None,
endpoint=None, url=None):
"""
Add the admin index view.
:param index_view:
Home page view to use. Defaults to `AdminIndexView`.
:param url:
Base URL
:param endpoint:
Base endpoint name for index view. If you use multiple instances of the `Admin` class with
a single Flask application, you have to set a unique endpoint name for each instance.
"""
self.index_view = index_view or AdminIndexView(endpoint=endpoint, url=url)
self.endpoint = endpoint or self.index_view.endpoint
self.url = url or self.index_view.url
# Add predefined index view
# assume index view is always the first element of views.
if len(self._views) > 0:
self._views[0] = self.index_view
else:
self.add_view(self.index_view)
def add_views(self, *args): def add_views(self, *args):
""" """
Add one or more views to the collection. Add one or more views to the collection.
...@@ -648,7 +672,8 @@ class Admin(object): ...@@ -648,7 +672,8 @@ class Admin(object):
def get_category_menu_item(self, name): def get_category_menu_item(self, name):
return self._menu_categories.get(name) return self._menu_categories.get(name)
def init_app(self, app): def init_app(self, app, index_view=None,
endpoint=None, url=None):
""" """
Register all views with the Flask application. Register all views with the Flask application.
...@@ -659,6 +684,14 @@ class Admin(object): ...@@ -659,6 +684,14 @@ class Admin(object):
self._init_extension() self._init_extension()
# Register Index view
if index_view is not None:
self._set_admin_index_view(
index_view=index_view,
endpoint=endpoint,
url=url
)
# Register views # Register views
for view in self._views: for view in self._views:
app.register_blueprint(view.create_blueprint(self)) app.register_blueprint(view.create_blueprint(self))
......
# flake8: noqa
try: try:
import wtforms_appengine import wtforms_appengine
except ImportError: except ImportError:
......
from wtforms.fields import StringField
from google.appengine.ext import ndb
import decimal
class GeoPtPropertyField(StringField):
def process_formdata(self, valuelist):
if valuelist:
try:
lat, lon = valuelist[0].split(',')
self.data = ndb.GeoPt(
decimal.Decimal(lat.strip()),
decimal.Decimal(lon.strip())
)
except (decimal.InvalidOperation, ValueError):
raise ValueError('Not a valid coordinate location')
from wtforms_appengine.ndb import ModelConverter
from .fields import GeoPtPropertyField
from flask_admin.model.form import converts
class AdminModelConverter(ModelConverter):
@converts('GeoPt')
def convert_GeoPtProperty(self, model, prop, kwargs):
"""Returns a form field for a ``ndb.GeoPtProperty``."""
return GeoPtPropertyField(**kwargs)
...@@ -7,6 +7,11 @@ from wtforms_appengine import ndb as wt_ndb ...@@ -7,6 +7,11 @@ from wtforms_appengine import ndb as wt_ndb
from google.appengine.ext import db from google.appengine.ext import db
from google.appengine.ext import ndb from google.appengine.ext import ndb
from flask_wtf import Form
from flask_admin.model.form import create_editable_list_form
from .form import AdminModelConverter
class NdbModelView(BaseModelView): class NdbModelView(BaseModelView):
""" """
AppEngine NDB model scaffolding. AppEngine NDB model scaffolding.
...@@ -28,14 +33,50 @@ class NdbModelView(BaseModelView): ...@@ -28,14 +33,50 @@ class NdbModelView(BaseModelView):
pass pass
def scaffold_filters(self): def scaffold_filters(self):
#TODO: implement # TODO: implement
pass pass
def scaffold_form(self): form_args = None
return wt_ndb.model_form(self.model())
def get_list(self, page, sort_field, sort_desc, search, filters): model_form_converter = AdminModelConverter
#TODO: implement filters (don't think search can work here) """
Model form conversion class. Use this to implement custom field conversion logic.
For example::
class MyModelConverter(AdminModelConverter):
pass
class MyAdminView(ModelView):
model_form_converter = MyModelConverter
"""
def scaffold_form(self):
form_class = wt_ndb.model_form(
self.model(),
base_class=Form,
only=self.form_columns,
exclude=self.form_excluded_columns,
field_args=self.form_args,
converter=self.model_form_converter(),
)
return form_class
def scaffold_list_form(self, widget=None, validators=None):
form_class = wt_ndb.model_form(
self.model(),
base_class=Form,
only=self.column_editable_list,
field_args=self.form_args,
converter=self.model_form_converter(),
)
result = create_editable_list_form(Form, form_class, widget)
return result
def get_list(self, page, sort_field, sort_desc, search, filters,
page_size=None):
# TODO: implement filters (don't think search can work here)
q = self.model.query() q = self.model.query()
...@@ -45,7 +86,11 @@ class NdbModelView(BaseModelView): ...@@ -45,7 +86,11 @@ class NdbModelView(BaseModelView):
order_field = -order_field order_field = -order_field
q = q.order(order_field) q = q.order(order_field)
results = q.fetch(self.page_size, offset=page*self.page_size) if not page_size:
page_size = self.page_size
results = q.fetch(page_size, offset=page * page_size)
return q.count(), results return q.count(), results
def get_one(self, urlsafe_key): def get_one(self, urlsafe_key):
...@@ -56,37 +101,46 @@ class NdbModelView(BaseModelView): ...@@ -56,37 +101,46 @@ class NdbModelView(BaseModelView):
model = self.model() model = self.model()
form.populate_obj(model) form.populate_obj(model)
model.put() model.put()
return model
except Exception as ex: except Exception as ex:
if not self.handle_view_exception(ex): if not self.handle_view_exception(ex):
#flash(gettext('Failed to create record. %(error)s', # flash(gettext('Failed to create record. %(error)s',
# error=ex), 'error') # error=ex), 'error')
logging.exception('Failed to create record.') logging.exception('Failed to create record.')
return False return False
else:
self.after_model_change(form, model, True)
return model
def update_model(self, form, model): def update_model(self, form, model):
try: try:
form.populate_obj(model) form.populate_obj(model)
model.put() model.put()
return True
except Exception as ex: except Exception as ex:
if not self.handle_view_exception(ex): if not self.handle_view_exception(ex):
#flash(gettext('Failed to update record. %(error)s', # flash(gettext('Failed to update record. %(error)s',
# error=ex), 'error') # error=ex), 'error')
logging.exception('Failed to update record.') logging.exception('Failed to update record.')
return False return False
else:
self.after_model_change(form, model, False)
return True
def delete_model(self, model): def delete_model(self, model):
try: try:
model.key.delete() model.key.delete()
return True
except Exception as ex: except Exception as ex:
if not self.handle_view_exception(ex): if not self.handle_view_exception(ex):
#flash(gettext('Failed to delete record. %(error)s', # flash(gettext('Failed to delete record. %(error)s',
# error=ex), # error=ex),
# 'error') # 'error')
logging.exception('Failed to delete record.') logging.exception('Failed to delete record.')
return False return False
else:
self.after_model_delete(model)
return True
class DbModelView(BaseModelView): class DbModelView(BaseModelView):
...@@ -102,7 +156,8 @@ class DbModelView(BaseModelView): ...@@ -102,7 +156,8 @@ class DbModelView(BaseModelView):
def scaffold_sortable_columns(self): def scaffold_sortable_columns(self):
# We use getattr() because ReferenceProperty does not specify a 'indexed' field # We use getattr() because ReferenceProperty does not specify a 'indexed' field
return [k for (k, v) in self.model.__dict__.iteritems() if isinstance(v, db.Property) and getattr(v, 'indexed', None)] return [k for (k, v) in self.model.__dict__.iteritems()
if isinstance(v, db.Property) and getattr(v, 'indexed', None)]
def init_search(self): def init_search(self):
return None return None
...@@ -111,14 +166,14 @@ class DbModelView(BaseModelView): ...@@ -111,14 +166,14 @@ class DbModelView(BaseModelView):
pass pass
def scaffold_filters(self): def scaffold_filters(self):
#TODO: implement # TODO: implement
pass pass
def scaffold_form(self): def scaffold_form(self):
return wt_db.model_form(self.model()) return wt_db.model_form(self.model())
def get_list(self, page, sort_field, sort_desc, search, filters): def get_list(self, page, sort_field, sort_desc, search, filters):
#TODO: implement filters (don't think search can work here) # TODO: implement filters (don't think search can work here)
q = self.model.all() q = self.model.all()
...@@ -127,7 +182,7 @@ class DbModelView(BaseModelView): ...@@ -127,7 +182,7 @@ class DbModelView(BaseModelView):
sort_field = "-" + sort_field sort_field = "-" + sort_field
q.order(sort_field) q.order(sort_field)
results = q.fetch(self.page_size, offset=page*self.page_size) results = q.fetch(self.page_size, offset=page * self.page_size)
return q.count(), results return q.count(), results
def get_one(self, encoded_key): def get_one(self, encoded_key):
...@@ -141,7 +196,7 @@ class DbModelView(BaseModelView): ...@@ -141,7 +196,7 @@ class DbModelView(BaseModelView):
return model return model
except Exception as ex: except Exception as ex:
if not self.handle_view_exception(ex): if not self.handle_view_exception(ex):
#flash(gettext('Failed to create record. %(error)s', # flash(gettext('Failed to create record. %(error)s',
# error=ex), 'error') # error=ex), 'error')
logging.exception('Failed to create record.') logging.exception('Failed to create record.')
return False return False
...@@ -153,23 +208,24 @@ class DbModelView(BaseModelView): ...@@ -153,23 +208,24 @@ class DbModelView(BaseModelView):
return True return True
except Exception as ex: except Exception as ex:
if not self.handle_view_exception(ex): if not self.handle_view_exception(ex):
#flash(gettext('Failed to update record. %(error)s', # flash(gettext('Failed to update record. %(error)s',
# error=ex), 'error') # error=ex), 'error')
logging.exception('Failed to update record.') logging.exception('Failed to update record.')
return False return False
def delete_model(self, model): def delete_model(self, model):
try: try:
model.delete() model.delete()
return True return True
except Exception as ex: except Exception as ex:
if not self.handle_view_exception(ex): if not self.handle_view_exception(ex):
#flash(gettext('Failed to delete record. %(error)s', # flash(gettext('Failed to delete record. %(error)s',
# error=ex), # error=ex),
# 'error') # 'error')
logging.exception('Failed to delete record.') logging.exception('Failed to delete record.')
return False return False
def ModelView(model): def ModelView(model):
if issubclass(model, ndb.Model): if issubclass(model, ndb.Model):
return NdbModelView(model) return NdbModelView(model)
......
This diff is collapsed.
...@@ -55,10 +55,11 @@ class S3Storage(object): ...@@ -55,10 +55,11 @@ class S3Storage(object):
raise ValueError('Could not import boto. You can install boto by ' raise ValueError('Could not import boto. You can install boto by '
'using pip install boto') 'using pip install boto')
connection = s3.connect_to_region(region, connection = s3.connect_to_region(
aws_access_key_id=aws_access_key_id, region,
aws_secret_access_key= aws_access_key_id=aws_access_key_id,
aws_secret_access_key) aws_secret_access_key=aws_secret_access_key,
)
self.bucket = connection.get_bucket(bucket_name) self.bucket = connection.get_bucket(bucket_name)
self.separator = '/' self.separator = '/'
......
# flake8: noqa
try: try:
import geoalchemy2 import geoalchemy2
import shapely import shapely
......
import json
from wtforms.fields import TextAreaField
from shapely.geometry import shape, mapping
from .widgets import LeafletWidget
from sqlalchemy import func
import geoalchemy2 import geoalchemy2
#from types import NoneType from shapely.geometry import shape
#from .. import db how do you get db.session in a Field? from sqlalchemy import func
class JSONField(TextAreaField):
def _value(self):
if self.raw_data:
return self.raw_data[0]
if self.data:
return self.data
return ""
def process_formdata(self, valuelist):
if valuelist:
value = valuelist[0]
if not value:
self.data = None
return
try:
self.data = self.from_json(value)
except ValueError:
self.data = None
raise ValueError(self.gettext('Invalid JSON'))
def to_json(self, obj): from flask_admin.form import JSONField
return json.dumps(obj)
def from_json(self, data): from .widgets import LeafletWidget
return json.loads(data)
class GeoJSONField(JSONField): class GeoJSONField(JSONField):
widget = LeafletWidget() widget = LeafletWidget()
def __init__(self, label=None, validators=None, geometry_type="GEOMETRY", srid='-1', session=None, **kwargs): def __init__(self, label=None, validators=None, geometry_type="GEOMETRY",
srid='-1', session=None, **kwargs):
super(GeoJSONField, self).__init__(label, validators, **kwargs) super(GeoJSONField, self).__init__(label, validators, **kwargs)
self.web_srid = 4326 self.web_srid = 4326
self.srid = srid self.srid = srid
if self.srid is -1: if self.srid is -1:
self.transform_srid = self.web_srid self.transform_srid = self.web_srid
else: else:
self.transform_srid = self.srid self.transform_srid = self.srid
self.geometry_type = geometry_type.upper() self.geometry_type = geometry_type.upper()
self.session = session self.session = session
def _value(self): def _value(self):
if self.raw_data: if self.raw_data:
return self.raw_data[0] return self.raw_data[0]
if type(self.data) is geoalchemy2.elements.WKBElement: if type(self.data) is geoalchemy2.elements.WKBElement:
if self.srid is -1: if self.srid is -1:
self.data = self.session.scalar(func.ST_AsGeoJson(self.data)) return self.session.scalar(func.ST_AsGeoJson(self.data))
else: else:
self.data = self.session.scalar(func.ST_AsGeoJson(func.ST_Transform(self.data, self.web_srid))) return self.session.scalar(
return super(GeoJSONField, self)._value() func.ST_AsGeoJson(
func.ST_Transform(self.data, self.web_srid)
)
)
else:
return ''
def process_formdata(self, valuelist): def process_formdata(self, valuelist):
super(GeoJSONField, self).process_formdata(valuelist) super(GeoJSONField, self).process_formdata(valuelist)
if str(self.data) is '': if str(self.data) is '':
self.data = None self.data = None
if self.data is not None: if self.data is not None:
web_shape = self.session.scalar(func.ST_AsText(func.ST_Transform(func.ST_GeomFromText(shape(self.data).wkt, self.web_srid), self.transform_srid))) web_shape = self.session.scalar(
self.data = 'SRID='+str(self.srid)+';'+str(web_shape) func.ST_AsText(
func.ST_Transform(
func.ST_GeomFromText(
shape(self.data).wkt,
self.web_srid
),
self.transform_srid
)
)
)
self.data = 'SRID=' + str(self.srid) + ';' + str(web_shape)
...@@ -17,7 +17,7 @@ def geom_formatter(view, value): ...@@ -17,7 +17,7 @@ def geom_formatter(view, value):
}) })
if value.srid is -1: if value.srid is -1:
value.srid = 4326 value.srid = 4326
geojson = view.model.query.with_entities(func.ST_AsGeoJSON(value)).scalar() geojson = view.session.query(view.model).with_entities(func.ST_AsGeoJSON(value)).scalar()
return Markup('<textarea %s>%s</textarea>' % (params, geojson)) return Markup('<textarea %s>%s</textarea>' % (params, geojson))
......
...@@ -22,7 +22,7 @@ class LeafletWidget(TextArea): ...@@ -22,7 +22,7 @@ class LeafletWidget(TextArea):
editable. editable.
""" """
def __init__( def __init__(
self, width=300, height=300, center=None, self, width='auto', height=350, center=None,
zoom=None, min_zoom=None, max_zoom=None, max_bounds=None): zoom=None, min_zoom=None, max_zoom=None, max_bounds=None):
self.width = width self.width = width
self.height = height self.height = height
...@@ -38,9 +38,9 @@ class LeafletWidget(TextArea): ...@@ -38,9 +38,9 @@ class LeafletWidget(TextArea):
kwargs.setdefault('data-geometry-type', gtype) kwargs.setdefault('data-geometry-type', gtype)
# set optional values from constructor # set optional values from constructor
if not "data-width" in kwargs: if "data-width" not in kwargs:
kwargs["data-width"] = self.width kwargs["data-width"] = self.width
if not "data-height" in kwargs: if "data-height" not in kwargs:
kwargs["data-height"] = self.height kwargs["data-height"] = self.height
if self.center: if self.center:
kwargs["data-lat"] = lat(self.center) kwargs["data-lat"] = lat(self.center)
......
# flake8: noqa
try: try:
import flask_mongoengine import flask_mongoengine
except ImportError: except ImportError:
......
...@@ -76,7 +76,7 @@ def create_ajax_loader(model, name, field_name, opts): ...@@ -76,7 +76,7 @@ def create_ajax_loader(model, name, field_name, opts):
ftype = type(prop).__name__ ftype = type(prop).__name__
if ftype == 'ListField': if ftype == 'ListField' or ftype == 'SortedListField':
prop = prop.field prop = prop.field
ftype = type(prop).__name__ ftype = type(prop).__name__
...@@ -97,7 +97,7 @@ def process_ajax_references(references, view): ...@@ -97,7 +97,7 @@ def process_ajax_references(references, view):
def handle_field(field, subdoc, base): def handle_field(field, subdoc, base):
ftype = type(field).__name__ ftype = type(field).__name__
if ftype == 'ListField': if ftype == 'ListField' or ftype == 'SortedListField':
child_doc = getattr(subdoc, '_form_subdocuments', {}).get(None) child_doc = getattr(subdoc, '_form_subdocuments', {}).get(None)
if child_doc: if child_doc:
......
...@@ -36,13 +36,14 @@ class ModelFormField(InlineFormField): ...@@ -36,13 +36,14 @@ class ModelFormField(InlineFormField):
def populate_obj(self, obj, name): def populate_obj(self, obj, name):
candidate = getattr(obj, name, None) candidate = getattr(obj, name, None)
if candidate is None: is_created = candidate is None
if is_created:
candidate = self.model() candidate = self.model()
setattr(obj, name, candidate) setattr(obj, name, candidate)
self.form.populate_obj(candidate) self.form.populate_obj(candidate)
self.view.on_model_change(self.form, candidate) self.view._on_model_change(self.form, candidate, is_created)
class MongoFileField(fields.FileField): class MongoFileField(fields.FileField):
......
import datetime
from flask_admin.babel import lazy_gettext from flask_admin.babel import lazy_gettext
from flask_admin.model import filters from flask_admin.model import filters
from .tools import parse_like_term from .tools import parse_like_term
from mongoengine.queryset import Q from mongoengine.queryset import Q
from bson.errors import InvalidId
from bson.objectid import ObjectId
class BaseMongoEngineFilter(filters.BaseFilter): class BaseMongoEngineFilter(filters.BaseFilter):
""" """
...@@ -221,6 +222,31 @@ class DateTimeNotBetweenFilter(DateTimeBetweenFilter): ...@@ -221,6 +222,31 @@ class DateTimeNotBetweenFilter(DateTimeBetweenFilter):
return lazy_gettext('not between') return lazy_gettext('not between')
class ReferenceObjectIdFilter(BaseMongoEngineFilter):
def validate(self, value):
"""
Validate value.
If value is valid, returns `True` and `False` otherwise.
:param value:
Value to validate
"""
try:
self.clean(value)
return True
except InvalidId:
return False
def clean(self, value):
return ObjectId(value.strip())
def apply(self, query, value):
flt = {'%s' % self.column.name: value}
return query.filter(**flt)
def operation(self):
return lazy_gettext('ObjectId equals')
# Base MongoEngine filter field converter # Base MongoEngine filter field converter
class FilterConverter(filters.BaseFilterConverter): class FilterConverter(filters.BaseFilterConverter):
strings = (FilterLike, FilterNotLike, FilterEqual, FilterNotEqual, strings = (FilterLike, FilterNotLike, FilterEqual, FilterNotEqual,
...@@ -236,6 +262,7 @@ class FilterConverter(filters.BaseFilterConverter): ...@@ -236,6 +262,7 @@ class FilterConverter(filters.BaseFilterConverter):
DateTimeGreaterFilter, DateTimeSmallerFilter, DateTimeGreaterFilter, DateTimeSmallerFilter,
DateTimeBetweenFilter, DateTimeNotBetweenFilter, DateTimeBetweenFilter, DateTimeNotBetweenFilter,
FilterEmpty) FilterEmpty)
reference_filters = (ReferenceObjectIdFilter,)
def convert(self, type_name, column, name): def convert(self, type_name, column, name):
filter_name = type_name.lower() filter_name = type_name.lower()
...@@ -264,3 +291,7 @@ class FilterConverter(filters.BaseFilterConverter): ...@@ -264,3 +291,7 @@ class FilterConverter(filters.BaseFilterConverter):
@filters.convert('DateTimeField', 'ComplexDateTimeField') @filters.convert('DateTimeField', 'ComplexDateTimeField')
def conv_datetime(self, column, name): def conv_datetime(self, column, name):
return [f(column, name) for f in self.datetime_filters] return [f(column, name) for f in self.datetime_filters]
@filters.convert('ReferenceField')
def conv_reference(self, column, name):
return [f(column, name) for f in self.reference_filters]
...@@ -7,7 +7,6 @@ from flask_mongoengine.wtf import orm, fields as mongo_fields ...@@ -7,7 +7,6 @@ from flask_mongoengine.wtf import orm, fields as mongo_fields
from flask_admin import form from flask_admin import form
from flask_admin.model.form import FieldPlaceholder from flask_admin.model.form import FieldPlaceholder
from flask_admin.model.fields import InlineFieldList, AjaxSelectField, AjaxSelectMultipleField from flask_admin.model.fields import InlineFieldList, AjaxSelectField, AjaxSelectMultipleField
from flask_admin.model.widgets import InlineFormWidget
from flask_admin._compat import iteritems from flask_admin._compat import iteritems
from .fields import ModelFormField, MongoFileField, MongoImageField from .fields import ModelFormField, MongoFileField, MongoImageField
...@@ -60,7 +59,7 @@ class CustomModelConverter(orm.ModelConverter): ...@@ -60,7 +59,7 @@ class CustomModelConverter(orm.ModelConverter):
return form.recreate_field(field.field) return form.recreate_field(field.field)
kwargs = { kwargs = {
'label': getattr(field, 'verbose_name', field.name), 'label': getattr(field, 'verbose_name', None),
'description': getattr(field, 'help_text', ''), 'description': getattr(field, 'help_text', ''),
'validators': [], 'validators': [],
'filters': [], 'filters': [],
......
from mongoengine import ValidationError from mongoengine import ValidationError
from wtforms.validators import ValidationError as wtfValidationError
from flask_admin._compat import itervalues, as_unicode from flask_admin._compat import itervalues, as_unicode
...@@ -31,6 +32,9 @@ def make_thumb_args(value): ...@@ -31,6 +32,9 @@ def make_thumb_args(value):
def format_error(error): def format_error(error):
if isinstance(error, ValidationError): if isinstance(error, ValidationError):
return as_unicode(error)
if isinstance(error, wtfValidationError):
return '. '.join(itervalues(error.to_dict())) return '. '.join(itervalues(error.to_dict()))
return as_unicode(error) return as_unicode(error)
...@@ -18,6 +18,7 @@ def convert_subdocuments(values): ...@@ -18,6 +18,7 @@ def convert_subdocuments(values):
elif isinstance(p, EmbeddedForm): elif isinstance(p, EmbeddedForm):
result[name] = p result[name] = p
else: else:
raise ValueError('Invalid subdocument type: expecting dict or instance of flask_admin.contrib.mongoengine.EmbeddedForm, got %s' % type(p)) raise ValueError('Invalid subdocument type: expecting dict or '
'instance of flask_admin.contrib.mongoengine.EmbeddedForm, got %s' % type(p))
return result return result
...@@ -2,7 +2,7 @@ def parse_like_term(term): ...@@ -2,7 +2,7 @@ def parse_like_term(term):
""" """
Parse search term into (operation, term) tuple. Recognizes operators Parse search term into (operation, term) tuple. Recognizes operators
in the beginning of the search term. in the beginning of the search term.
* = case insensitive (can precede other operators) * = case insensitive (can precede other operators)
^ = starts with ^ = starts with
= = exact = = exact
...@@ -24,5 +24,5 @@ def parse_like_term(term): ...@@ -24,5 +24,5 @@ def parse_like_term(term):
oper = 'contains' oper = 'contains'
# add case insensitive flag # add case insensitive flag
if case_insensitive: if case_insensitive:
oper = 'i'+oper oper = 'i' + oper
return oper, term return oper, term
...@@ -323,7 +323,7 @@ class ModelView(BaseModelView): ...@@ -323,7 +323,7 @@ class ModelView(BaseModelView):
field_class = type(f) field_class = type(f)
if (field_class == mongoengine.ListField and if (field_class == mongoengine.ListField and
isinstance(f.field, mongoengine.EmbeddedDocumentField)): isinstance(f.field, mongoengine.EmbeddedDocumentField)):
continue continue
if field_class == mongoengine.EmbeddedDocumentField: if field_class == mongoengine.EmbeddedDocumentField:
...@@ -626,7 +626,6 @@ class ModelView(BaseModelView): ...@@ -626,7 +626,6 @@ class ModelView(BaseModelView):
return True return True
# FileField access API # FileField access API
@expose('/api/file/') @expose('/api/file/')
def api_file_view(self): def api_file_view(self):
...@@ -645,9 +644,7 @@ class ModelView(BaseModelView): ...@@ -645,9 +644,7 @@ class ModelView(BaseModelView):
return Response(data.read(), return Response(data.read(),
content_type=data.content_type, content_type=data.content_type,
headers={ headers={'Content-Length': data.length})
'Content-Length': data.length
})
# Default model actions # Default model actions
def is_action_allowed(self, name): def is_action_allowed(self, name):
...@@ -671,7 +668,7 @@ class ModelView(BaseModelView): ...@@ -671,7 +668,7 @@ class ModelView(BaseModelView):
flash(ngettext('Record was successfully deleted.', flash(ngettext('Record was successfully deleted.',
'%(count)s records were successfully deleted.', '%(count)s records were successfully deleted.',
count, count,
count=count)) count=count), 'success')
except Exception as ex: except Exception as ex:
if not self.handle_view_exception(ex): if not self.handle_view_exception(ex):
flash(gettext('Failed to delete records. %(error)s', error=str(ex)), flash(gettext('Failed to delete records. %(error)s', error=str(ex)),
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
File mode changed from 100644 to 100755
File mode changed from 100644 to 100755
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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