Commit 53787acd authored by PJ Janse van Rensburg's avatar PJ Janse van Rensburg

Merge branch 'master' into fix-search-placeholder

parents e2f95005 203291f7
......@@ -38,14 +38,28 @@ Flask-Admin is an active project, well-tested and production ready.
Examples
--------
Several usage examples are included in the */examples* folder. Please feel free to add your own examples, or improve
on some of the existing ones, and then submit them via GitHub as a *pull-request*.
Several usage examples are included in the */examples* folder. Please add your own, or improve
on the existing examples, and submit a *pull-request*.
You can see some of these examples in action at `http://examples.flask-admin.org <http://examples.flask-admin.org/>`_.
To run the examples on your local environment, one at a time, do something like::
To run the examples in your local environment::
1. Clone the repository::
git clone https://github.com/flask-admin/flask-admin.git
cd flask-admin
python examples/simple/app.py
2. Create and activate a virtual environment::
virtualenv env -p python3
source env/bin/activate
3. Install requirements::
pip install -r 'examples/sqla/requirements.txt'
4. Run the application::
python examples/sqla/app.py
Documentation
-------------
......@@ -104,7 +118,8 @@ You can also run the tests on multiple environments using *tox*.
3rd Party Stuff
---------------
Flask-Admin is built with the help of `Bootstrap <http://getbootstrap.com/>`_ and `Select2 <https://github.com/ivaynberg/select2>`_.
Flask-Admin is built with the help of `Bootstrap <http://getbootstrap.com/>`_, `Select2 <https://github.com/ivaynberg/select2>`_
and `Bootswatch <http://bootswatch.com/>`_.
If you want to localize your application, install the `Flask-BabelEx <https://pypi.python.org/pypi/Flask-BabelEx>`_ package.
......
......@@ -2,7 +2,6 @@
<ul>
<li><a href="http://flask.pocoo.org/" target="_blank">Flask</a></li>
<li><a href="http://github.com/flask-admin/flask-admin" target="_blank">Flask-Admin @ github</a></li>
<li><a href="http://examples.flask-admin.org/" target="_blank">Flask-Admin Examples</a></li>
</ul>
<a class="github" href="http://github.com/flask-admin/flask-admin" target="_blank"><img style="position: fixed; top: 0; right: 0; border: 0;"
......
......@@ -164,7 +164,7 @@ Image handling also requires you to have `Pillow <https://pypi.python.org/pypi/P
installed if you need to do any processing on the image files.
Have a look at the example at
https://github.com/flask-admin/Flask-Admin/tree/master/examples/forms.
https://github.com/flask-admin/Flask-Admin/tree/master/examples/forms-files-images.
If you are using the MongoEngine backend, Flask-Admin supports GridFS-backed image and file uploads through WTForms fields. Documentation can be found at :mod:`flask_admin.contrib.mongoengine.fields`.
......@@ -544,4 +544,3 @@ While the wrapped function should accept only one parameter - `ids`::
raise
flash(gettext('Failed to approve users. %(error)s', error=str(ex)), 'error')
......@@ -4,12 +4,21 @@ Changelog
next release
-----
* SQLA: Sort on multiple columns with `column_default_sort` and related models in `column_sortable_list`
* Show searchable fields in search input's placeholder text
* Support nested categories in the navbar menu
* SQLAlchemy
* sort on multiple columns with `column_default_sort`
* sort on related models in `column_sortable_list`
* show searchable fields in search input's placeholder text
* fix: inline model forms can now also be used for models with multiple primary keys
* support for using mapped `column_property`
* Upgrade Leaflet and Leaflet.draw plugins, used for geoalchemy integration
* Specify `minimum_input_length` for ajax widget
* SQLAlchemy fix that lets you use inline model forms where models have multiple primary keys
* Peewee: support composite keys
* MongoEngine: when searching/filtering the input is now regarded as case-insensitive by default
* FileAdmin
* handle special characters in filename
* fix a bug with listing directories on Windows
* avoid raising an exception when unknown sort parameter is encountered
1.5.2
-----
......
......@@ -23,9 +23,9 @@ because they let you group together all of the usual
*Create, Read, Update, Delete* (CRUD) view logic into a single, self-contained
class for each of your models.
**What does it look like?** At http://examples.flask-admin.org/ you can see
some examples of Flask-Admin in action, or browse through the `examples/`
directory in the `GitHub repository <https://github.com/flask-admin/flask-admin>`_.
**What does it look like?** Clone the `GitHub repository <https://github.com/flask-admin/flask-admin>`_
and run the provided examples locally to get a feel for Flask-Admin. There are several to choose from
in the `examples` directory.
.. toctree::
:maxdepth: 2
......
......@@ -16,6 +16,9 @@ The first step is to initialize an empty admin interface for your Flask app::
from flask import Flask
from flask_admin import Admin
# set optional bootswatch theme
app.config['FLASK_ADMIN_SWATCH'] = 'cerulean'
app = Flask(__name__)
admin = Admin(app, name='microblog', template_mode='bootstrap3')
......@@ -27,7 +30,8 @@ Here, both the *name* and *template_mode* parameters are optional. Alternatively
you could use the :meth:`~flask_admin.base.Admin.init_app` method.
If you start this application and navigate to `http://localhost:5000/admin/ <http://localhost:5000/admin/>`_,
you should see an empty page with a navigation bar on top.
you should see an empty page with a navigation bar on top. Customize the look by
specifying a Bootswatch theme that suits your needs (see http://bootswatch.com/3/ for available swatches).
Adding Model Views
------------------
......@@ -156,12 +160,9 @@ Customizing Built-in Views
****
The built-in `ModelView` class is great for getting started quickly. But, you'll want
to configure its functionality to suit your particular models. This is done by setting
values for the configuration attributes that are made available in the `ModelView` class.
To specify some global configuration parameters, you can subclass `ModelView` and use that
subclass when adding your models to the interface::
When inheriting from `ModelView`, values can be specified for numerous
configuration parameters. Use these to customize the views to suit your
particular models::
from flask_admin.contrib.sqla import ModelView
......@@ -287,6 +288,28 @@ To **enable csv export** of the model view::
This will add a button to the model view that exports records, truncating at :attr:`~flask_admin.model.BaseModelView.export_max_rows`.
Grouping Views
==============
When adding a view, specify a value for the `category` parameter
to group related views together in the menu::
admin.add_view(UserView(User, db.session, category="Team"))
admin.add_view(ModelView(Role, db.session, category="Team"))
admin.add_view(ModelView(Permission, db.session, category="Team"))
This will create a top-level menu item named 'Team', and a drop-down containing
links to the three views.
To nest related views within these drop-downs, use the `add_sub_category` method::
admin.add_sub_category(name="Links", parent_name="Team")
And to add arbitrary hyperlinks to the menu::
admin.add_link(MenuLink(name='Home Page', url='/', category='Links'))
Adding Your Own Views
=====================
......@@ -440,7 +463,7 @@ list_row_actions Row action cell with edit/remove/etc buttons
empty_list_message Message that will be displayed if there are no models found
======================= ============================================
Have a look at the `layout` example at https://github.com/flask-admin/flask-admin/tree/master/examples/layout
Have a look at the `layout` example at https://github.com/flask-admin/flask-admin/tree/master/examples/custom-layout
to see how you can take full stylistic control over the admin interface.
Environment Variables
......
......@@ -14,11 +14,11 @@ To run this example:
3. Install requirements::
pip install -r 'examples/layout/requirements.txt'
pip install -r 'examples/custom-layout/requirements.txt'
4. Run the application::
python examples/layout/app.py
python examples/custom-layout/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:::
......
......@@ -7,7 +7,7 @@
{% endblock %}
{% block page_body %}
<div class="container">
<div class="container-fluid">
<div class="row">
<div class="col-md-2" role="navigation">
<ul class="nav nav-pills nav-stacked">
......
Simple file management interface example.
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/file/requirements.txt'
4. Run the application::
python examples/file/app.py
import os
import os.path as op
from flask import Flask
import flask_admin as admin
from flask_admin.contrib import fileadmin
# Create flask app
app = Flask(__name__, template_folder='templates', static_folder='files')
# Create dummy secrey key so we can use flash
app.config['SECRET_KEY'] = '123456790'
# Flask views
@app.route('/')
def index():
return '<a href="/admin/">Click me to get to Admin!</a>'
if __name__ == '__main__':
# Create directory
path = op.join(op.dirname(__file__), 'files')
try:
os.mkdir(path)
except OSError:
pass
# Create admin interface
admin = admin.Admin(app, 'Example: Files')
admin.add_view(fileadmin.FileAdmin(path, '/files/', name='Files'))
# Start app
app.run(debug=True)
Example of custom filters for the SQLAlchemy backend.
This example shows how you can::
* define your own custom forms by using form rendering rules
* handle generic static file uploads
* handle image uploads
* turn a TextArea field into a rich WYSIWYG editor using WTForms and CKEditor
* set up a Flask-Admin view as a Redis terminal
To run this example:
......@@ -14,11 +21,11 @@ To run this example:
3. Install requirements::
pip install -r 'examples/sqla-custom-filter/requirements.txt'
pip install -r 'examples/forms-files-images/requirements.txt'
4. Run the application::
python examples/sqla-custom-filter/app.py
python examples/forms-files-images/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:::
......
......@@ -3,18 +3,24 @@ import os.path as op
from flask import Flask, url_for
from flask_sqlalchemy import SQLAlchemy
from redis import Redis
from wtforms import fields, widgets
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
from flask_admin.contrib import sqla, rediscli
# Create application
app = Flask(__name__, static_folder='files')
# set optional bootswatch theme
# see http://bootswatch.com/3/ for available swatches
app.config['FLASK_ADMIN_SWATCH'] = 'cerulean'
# Create dummy secrey key so we can use sessions
app.config['SECRET_KEY'] = '123456790'
......@@ -62,6 +68,15 @@ class User(db.Model):
notes = db.Column(db.UnicodeText)
class Page(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.Unicode(64))
text = db.Column(db.UnicodeText)
def __unicode__(self):
return self.name
# Delete hooks for models, delete files if models are getting deleted
@listens_for(File, 'after_delete')
def del_file(mapper, connection, target):
......@@ -90,7 +105,28 @@ def del_image(mapper, connection, target):
pass
# define a custom wtforms widget and field.
# see https://wtforms.readthedocs.io/en/latest/widgets.html#custom-widgets
class CKTextAreaWidget(widgets.TextArea):
def __call__(self, field, **kwargs):
# add WYSIWYG class to existing classes
existing_classes = kwargs.pop('class', '') or kwargs.pop('class_', '')
kwargs['class'] = '{} {}'.format(existing_classes, "ckeditor")
return super(CKTextAreaWidget, self).__call__(field, **kwargs)
class CKTextAreaField(fields.TextAreaField):
widget = CKTextAreaWidget()
# Administrative views
class PageView(sqla.ModelView):
form_overrides = {
'text': CKTextAreaField
}
create_template = 'create_page.html'
edit_template = 'edit_page.html'
class FileView(sqla.ModelView):
# Override form field to use Flask-Admin FileUploadField
form_overrides = {
......@@ -140,15 +176,15 @@ class UserView(sqla.ModelView):
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)
# Show macro that's included in the templates
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'
create_template = 'create_user.html'
edit_template = 'edit_user.html'
# Flask views
......@@ -162,7 +198,9 @@ admin = Admin(app, 'Example: Forms', 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'))
admin.add_view(UserView(User, db.session))
admin.add_view(PageView(Page, db.session))
admin.add_view(rediscli.RedisCli(Redis()))
def build_sample_db():
......@@ -238,6 +276,10 @@ def build_sample_db():
file.path = "example_" + str(i) + ".pdf"
db.session.add(file)
sample_text = "<h2>This is a test</h2>" + \
"<p>Create HTML content in a text area field with the help of <i>WTForms</i> and <i>CKEditor</i>.</p>"
db.session.add(Page(name="Test Page", text=sample_text))
db.session.commit()
return
......
{% extends 'admin/model/create.html' %}
{% block tail %}
{{ super() }}
<script src="https://cdn.ckeditor.com/ckeditor5/11.1.1/classic/ckeditor.js"></script>
<script>
ClassicEditor
.create(document.querySelector('.ckeditor'))
.catch(error => {
console.error( error );
});
</script>
{% endblock %}
{% extends 'admin/model/create.html' %}
{% import 'rule_demo.html' as rule_demo %}
\ No newline at end of file
{% import 'macros.html' as rule_demo %}
{% extends 'admin/model/create.html' %}
{% block tail %}
{{ super() }}
<script src="https://cdn.ckeditor.com/ckeditor5/11.1.1/classic/ckeditor.js"></script>
<script>
ClassicEditor
.create(document.querySelector('.ckeditor'))
.catch(error => {
console.error( error );
});
</script>
{% endblock %}
{% extends 'admin/model/edit.html' %}
{% import 'rule_demo.html' as rule_demo %}
{% import 'macros.html' as rule_demo %}
This example shows how you can define your own custom forms by using form rendering rules. It also demonstrates general file handling as well as the handling of image files specifically.
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()
Flask
Flask-Admin
Flask-SQLAlchemy
pillow==2.9.0
import os
import os.path as op
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
import flask_admin as admin
from flask_admin.contrib.sqla import ModelView
# 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['DATABASE_FILE'] = 'sample_db.sqlite'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + app.config['DATABASE_FILE']
app.config['SQLALCHEMY_ECHO'] = True
db = SQLAlchemy(app)
# Models
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.Unicode(64))
email = db.Column(db.Unicode(64))
def __unicode__(self):
return self.name
class Page(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.Unicode(64))
content = db.Column(db.UnicodeText)
def __unicode__(self):
return self.name
# Customized admin interface
class CustomView(ModelView):
list_template = 'list.html'
create_template = 'create.html'
edit_template = 'edit.html'
class UserAdmin(CustomView):
column_searchable_list = ('name',)
column_filters = ('name', 'email')
# Flask views
@app.route('/')
def index():
return '<a href="/admin/">Click me to get to Admin!</a>'
# Create admin with custom base template
admin = admin.Admin(app, 'Example: Layout', base_template='layout.html')
# Add views
admin.add_view(UserAdmin(User, db.session))
admin.add_view(CustomView(Page, db.session))
def build_sample_db():
"""
Populate a small db with some example entries.
"""
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'
]
for i in range(len(first_names)):
user = User()
user.name = first_names[i] + " " + last_names[i]
user.email = first_names[i].lower() + "@example.com"
db.session.add(user)
sample_text = [
{
'title': "de Finibus Bonorum et Malorum - Part I",
'content': "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor \
incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud \
exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure \
dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. \
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt \
mollit anim id est laborum."
},
{
'title': "de Finibus Bonorum et Malorum - Part II",
'content': "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque \
laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto \
beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur \
aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi \
nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, \
adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam \
aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam \
corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum \
iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum \
qui dolorem eum fugiat quo voluptas nulla pariatur?"
},
{
'title': "de Finibus Bonorum et Malorum - Part III",
'content': "At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium \
voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati \
cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi, id \
est laborum et dolorum fuga. Et harum quidem rerum facilis est et expedita distinctio. Nam \
libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod \
maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus. \
Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet \
ut et voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur \
a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis \
doloribus asperiores repellat."
}
]
for entry in sample_text:
page = Page()
page.title = entry['title']
page.content = entry['content']
db.session.add(page)
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)
body {
background: #EEE;
}
#content {
background: white;
border: 1px solid #CCC;
padding: 12px;
overflow: scroll;
}
#brand {
float: left;
font-weight: 300;
margin: 0;
}
.search-form {
margin: 0 5px;
}
.search-form form {
margin: 0;
}
.btn-menu {
margin: 4px 5px 0 0;
float: right;
}
.btn-menu a, .btn-menu input {
padding: 7px 16px !important;
border-radius: 0 !important;
}
.btn, textarea, input[type], button, .model-list {
border-radius: 0;
}
.model-list {
border-radius: 0;
}
.nav-pills li > a {
border-radius: 0;
}
.select2-container .select2-choice {
border-radius: 0;
}
\ No newline at end of file
{% extends 'admin/master.html' %}
{% block body %}
{{ super() }}
<div class="row-fluid">
<h1>Flask-Admin example</h1>
<p class="lead">
Customize the layout
</p>
<p>
This example shows how you can customize the look & feel of the admin interface.
</p>
<p>
This is done by overriding some of the built-in templates.
</p>
<a class="btn btn-primary" href="/"><i class="icon-arrow-left icon-white"></i> Back</a>
</div>
{% endblock body %}
\ No newline at end of file
{% import 'admin/layout.html' as layout with context -%}
{% extends 'admin/base.html' %}
{% block head_tail %}
{{ super() }}
<link href="{{ url_for('static', filename='layout.css') }}" rel="stylesheet">
{% endblock %}
{% block page_body %}
<div class="container">
<div class="row">
<div class="span2">
<ul class="nav nav-pills nav-stacked">
{{ layout.menu() }}
{{ layout.menu_links() }}
</ul>
</div>
<div class="span10">
<div id="content">
{% block brand %}
<h2 id="brand">{{ admin_view.name|capitalize }}</h2>
<div class="clearfix"></div>
{% endblock %}
{{ layout.messages() }}
{% block body %}{% endblock %}
</div>
</div>
</div>
</div>
{% endblock %}
{% extends 'admin/model/list.html' %}
{% import 'admin/model/layout.html' as model_layout with context %}
{% block model_menu_bar %}
<h2 id="brand">{{ admin_view.name|capitalize }} list</h2>
{% if admin_view.can_create %}
<div class="btn-menu">
<a href="{{ url_for('.create_view', url=return_url) }}" class="btn btn-primary pull-right">{{ _gettext('Create') }}</a>
</div>
{% endif %}
{% if filter_groups %}
<div class="btn-group btn-menu">
{{ model_layout.filter_options(btn_class='btn dropdown-toggle btn-title') }}
</div>
{% endif %}
{% if actions %}
<div class="btn-group btn-menu">
{{ actionlib.dropdown(actions, btn_class='btn dropdown-toggle btn-title') }}
</div>
{% endif %}
{% if search_supported %}
<div class="search-form btn-menu">
{{ model_layout.search_form(input_class='span2 btn-title') }}
</div>
{% endif %}
<div class="clearfix"></div>
<hr>
{% endblock %}
This example shows how you can customize the look & feel of the admin interface. This is done by overriding some of the built-in templates.
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/layout_bootstrap3/requirements.txt'
4. Run the application::
python examples/layout_bootstrap3/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()
{% extends 'admin/model/create.html' %}
{% block brand %}
<h2 id="brand">Create {{ admin_view.name|capitalize }}</h2>
<div class="clearfix"></div>
<hr>
{% endblock %}
{% block body %}
{% call lib.form_tag(form) %}
{{ lib.render_form_fields(form, form_opts=form_opts) }}
<div class="form-buttons">
{{ lib.render_form_buttons(return_url, extra()) }}
</div>
{% endcall %}
{% endblock %}
{% extends 'admin/model/edit.html' %}
{% block brand %}
<h2 id="brand">Edit {{ admin_view.name|capitalize }}</h2>
<div class="clearfix"></div>
<hr>
{% endblock %}
{% block body %}
{% call lib.form_tag(form) %}
{{ lib.render_form_fields(form, form_opts=form_opts) }}
<div class="form-buttons">
{{ lib.render_form_buttons(return_url) }}
</div>
{% endcall %}
{% endblock %}
This example shows how you can add links to external (non flask-admin) pages to the navbar menu, and how you can hide certain links if a user is not logged-in.
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/menu-external-links/requirements.txt'
4. Run the application::
python examples/menu-external-links/app.py
from flask import Flask, redirect, url_for
from flask_login import current_user, UserMixin, login_user, logout_user, LoginManager
from flask_admin.base import MenuLink, Admin, BaseView, expose
# Create fake user class for authentication
class User(UserMixin):
users_id = 0
def __init__(self, id=None):
if not id:
self.users_id += 1
self.id = self.users_id
else:
self.id = id
# Create menu links classes with reloaded accessible
class AuthenticatedMenuLink(MenuLink):
def is_accessible(self):
return current_user.is_authenticated
class NotAuthenticatedMenuLink(MenuLink):
def is_accessible(self):
return not current_user.is_authenticated
# Create custom admin view for authenticated users
class MyAdminView(BaseView):
@expose('/')
def index(self):
return self.render('authenticated-admin.html')
def is_accessible(self):
return current_user.is_authenticated
# Create flask app
app = Flask(__name__, template_folder='templates')
# Create dummy secrey key so we can use sessions
app.config['SECRET_KEY'] = '123456790'
# Flask views
@app.route('/')
def index():
return '<a href="/admin/">Click me to get to Admin!</a>'
@app.route('/login/')
def login_view():
login_user(User())
return redirect(url_for('admin.index'))
@app.route('/logout/')
def logout_view():
logout_user()
return redirect(url_for('admin.index'))
login_manager = LoginManager()
login_manager.init_app(app)
# Create user loader function
@login_manager.user_loader
def load_user(user_id):
return User(user_id)
if __name__ == '__main__':
# Create admin interface
admin = Admin(name='Example: Menu')
admin.add_view(MyAdminView(name='Authenticated'))
# Add home link by url
admin.add_link(MenuLink(name='Back Home', url='/'))
# Add login link by endpoint
admin.add_link(NotAuthenticatedMenuLink(name='Login',
endpoint='login_view'))
# Add links with categories
admin.add_link(MenuLink(name='Google', category='Links', url='http://www.google.com/'))
admin.add_link(MenuLink(name='Mozilla', category='Links', url='http://mozilla.org/'))
# Add logout link by endpoint
admin.add_link(AuthenticatedMenuLink(name='Logout',
endpoint='logout_view'))
admin.init_app(app)
# Start app
app.run(debug=True)
{% extends 'admin/master.html' %}
{% block body %}
Hello World from Authenticated Admin!
{% endblock %}
......@@ -14,9 +14,8 @@ To run this example:
3. Install requirements::
pip install -r 'examples/multi/requirements.txt'
pip install -r 'examples/multiple-admin-instances/requirements.txt'
4. Run the application::
python examples/multi/app.py
python examples/multiple-admin-instances/app.py
Simple Flask-Admin examples used by the quickstart tutorial.
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/quickstart/requirements.txt'
4. Run the application with any of the following::
python examples/quickstart/app.py
python examples/quickstart/app2.py
python examples/quickstart/app3.py
from flask import Flask
from flask_admin import Admin
app = Flask(__name__)
app.debug = True
admin = Admin(app, name="Example: Quickstart")
if __name__ == '__main__':
# Start app
app.run(debug=True)
from flask import Flask
from flask_admin import Admin, BaseView, expose
class MyView(BaseView):
@expose('/')
def index(self):
return self.render('index.html')
app = Flask(__name__)
app.debug = True
admin = Admin(app, name="Example: Quickstart2")
admin.add_view(MyView(name='Hello'))
if __name__ == '__main__':
# Start app
app.run()
from flask import Flask
from flask_admin import Admin, BaseView, expose
class MyView(BaseView):
@expose('/')
def index(self):
return self.render('index.html')
app = Flask(__name__)
app.debug = True
admin = Admin(app, name="Example: Quickstart3")
admin.add_view(MyView(name='Hello 1', endpoint='test1', category='Test'))
admin.add_view(MyView(name='Hello 2', endpoint='test2', category='Test'))
admin.add_view(MyView(name='Hello 3', endpoint='test3', category='Test'))
if __name__ == '__main__':
# Start app
app.run()
{% extends 'admin/master.html' %}
{% block body %}
Hello World from MyView!
{% endblock %}
This example shows how to set up a Flask-Admin view as a Redis terminal.
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/rediscli/requirements.txt'
4. Run the application::
python examples/rediscli/app.py
You should now be able to access a Redis instance on your machine (if it is running) through the admin interface.
from flask import Flask
from redis import Redis
import flask_admin as admin
from flask_admin.contrib import rediscli
# Create flask app
app = Flask(__name__)
# Flask views
@app.route('/')
def index():
return '<a href="/admin/">Click me to get to Admin!</a>'
if __name__ == '__main__':
# Create admin interface
admin = admin.Admin(app, name="Example: Redis")
admin.add_view(rediscli.RedisCli(Redis()))
# Start app
app.run(debug=True)
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_admin.contrib import sqla
from flask_admin import Admin
# required for creating custom filters
from flask_admin.contrib.sqla.filters import BaseSQLAFilter, FilterEqual
# 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['DATABASE_FILE'] = 'sample_db.sqlite'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + app.config['DATABASE_FILE']
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>'
# Create model
class User(db.Model):
def __init__(self, first_name, last_name, username, email):
self.first_name = first_name
self.last_name = last_name
self.username = username
self.email = email
id = db.Column(db.Integer, primary_key=True)
first_name = db.Column(db.String(100))
last_name = db.Column(db.String(100))
username = db.Column(db.String(80), unique=True)
email = db.Column(db.String(120), unique=True)
# Required for admin interface. For python 3 please use __str__ instead.
def __unicode__(self):
return self.username
# Create custom filter class
class FilterLastNameBrown(BaseSQLAFilter):
def apply(self, query, value, alias=None):
if value == '1':
return query.filter(self.column == "Brown")
else:
return query.filter(self.column != "Brown")
def operation(self):
return 'is Brown'
# Add custom filter and standard FilterEqual to ModelView
class UserAdmin(sqla.ModelView):
# each filter in the list is a filter operation (equals, not equals, etc)
# filters with the same name will appear as operations under the same filter
column_filters = [
FilterEqual(column=User.last_name, name='Last Name'),
FilterLastNameBrown(column=User.last_name, name='Last Name',
options=(('1', 'Yes'), ('0', 'No')))
]
admin = Admin(app, template_mode="bootstrap3")
admin.add_view(UserAdmin(User, db.session))
def build_sample_db():
db.drop_all()
db.create_all()
user_obj1 = User("Paul", "Brown", "pbrown", "paul@gmail.com")
user_obj2 = User("Luke", "Brown", "lbrown", "luke@gmail.com")
user_obj3 = User("Serge", "Koval", "skoval", "serge@gmail.com")
db.session.add_all([user_obj1, user_obj2, user_obj3])
db.session.commit()
if __name__ == '__main__':
build_sample_db()
app.run(port=5000, debug=True)
......@@ -14,10 +14,8 @@ To run this example:
3. Install requirements::
pip install -r 'examples/sqla-inline/requirements.txt'
pip install -r 'examples/sqla-custom-inline-forms/requirements.txt'
4. Run the application::
python examples/sqla-inline/app.py
python examples/sqla-custom-inline-forms/app.py
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)
Example of how to use (and filter on) a hybrid_property with the SQLAlchemy backend.
Hybrid properties allow you to treat calculations (for example: first_name + last_name)
like any other database column.
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-hybrid_property/requirements.txt'
4. Run the application::
python examples/sqla-hybrid_property/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()
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.ext.hybrid import hybrid_property
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_2.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 Screen(db.Model):
__tablename__ = 'screen'
id = db.Column(db.Integer, primary_key=True)
width = db.Column(db.Integer, nullable=False)
height = db.Column(db.Integer, nullable=False)
@hybrid_property
def number_of_pixels(self):
return self.width * self.height
class ScreenAdmin(sqla.ModelView):
""" Flask-admin can not automatically find a hybrid_property yet. You will
need to manually define the column in list_view/filters/sorting/etc."""
column_list = ['id', 'width', 'height', 'number_of_pixels']
column_sortable_list = ['id', 'width', 'height', 'number_of_pixels']
# Flask-admin can automatically detect the relevant filters for hybrid properties.
column_filters = ('number_of_pixels', )
# Create admin
admin = admin.Admin(app, name='Example: SQLAlchemy2', template_mode='bootstrap3')
admin.add_view(ScreenAdmin(Screen, db.session))
if __name__ == '__main__':
# Create DB
db.create_all()
# Start app
app.run(debug=True)
......@@ -16,10 +16,9 @@ To run this example:
pip install -r 'examples/sqla/requirements.txt'
4. Run either of these applications::
4. Run the application::
python examples/sqla/app.py
python examples/sqla/app2.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:::
......
......@@ -2,17 +2,26 @@ import os
import os.path as op
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.ext.hybrid import hybrid_property
from wtforms import validators
import flask_admin as admin
from flask_admin.base import MenuLink
from flask_admin.contrib import sqla
from flask_admin.contrib.sqla import filters
from flask_admin.contrib.sqla.form import InlineModelConverter
from flask_admin.contrib.sqla.fields import InlineModelFormList
from flask_admin.contrib.sqla.filters import BaseSQLAFilter, FilterEqual
# Create application
app = Flask(__name__)
# set optional bootswatch theme
# see http://bootswatch.com/3/ for available swatches
app.config['FLASK_ADMIN_SWATCH'] = 'cerulean'
# Create dummy secrey key so we can use sessions
app.config['SECRET_KEY'] = '123456790'
......@@ -29,11 +38,22 @@ class User(db.Model):
first_name = db.Column(db.String(100))
last_name = db.Column(db.String(100))
email = db.Column(db.String(120), unique=True)
pets = db.relationship('Pet', backref='owner')
def __str__(self):
return "{}, {}".format(self.last_name, self.first_name)
class Pet(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50), nullable=False)
person_id = db.Column(db.Integer, db.ForeignKey('user.id'))
available = db.Column(db.Boolean)
def __str__(self):
return self.name
# Create M2M table
post_tags_table = db.Table('post_tags', db.Model.metadata,
db.Column('post_id', db.Integer, db.ForeignKey('post.id')),
......@@ -87,22 +107,82 @@ class Tree(db.Model):
return "{}".format(self.name)
class Screen(db.Model):
__tablename__ = 'screen'
id = db.Column(db.Integer, primary_key=True)
width = db.Column(db.Integer, nullable=False)
height = db.Column(db.Integer, nullable=False)
@hybrid_property
def number_of_pixels(self):
return self.width * self.height
# Flask views
@app.route('/')
def index():
return '<a href="/admin/">Click me to get to Admin!</a>'
# Custom filter class
class FilterLastNameBrown(BaseSQLAFilter):
def apply(self, query, value, alias=None):
if value == '1':
return query.filter(self.column == "Brown")
else:
return query.filter(self.column != "Brown")
def operation(self):
return 'is Brown'
# Customized User model admin
inline_form_options = {
'form_label': "Info item",
'form_columns': ['id', 'key', 'value'],
'form_args': None,
'form_extra_fields': None,
}
class UserAdmin(sqla.ModelView):
column_display_pk = True
column_list = [
'id',
'last_name',
'first_name',
'email',
'pets',
]
column_default_sort = [('last_name', False), ('first_name', False)] # sort on multiple columns
inline_models = (UserInfo,)
# custom filter: each filter in the list is a filter operation (equals, not equals, etc)
# filters with the same name will appear as operations under the same filter
column_filters = [
FilterEqual(column=User.last_name, name='Last Name'),
FilterLastNameBrown(column=User.last_name, name='Last Name',
options=(('1', 'Yes'), ('0', 'No')))
]
inline_models = [(UserInfo, inline_form_options), ]
# setup create & edit forms so that only 'available' pets can be selected
def create_form(self):
return self._use_filtered_parent(
super(UserAdmin, self).create_form()
)
def edit_form(self, obj):
return self._use_filtered_parent(
super(UserAdmin, 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()
# Customized Post model admin
......@@ -162,6 +242,14 @@ class TreeView(sqla.ModelView):
form_excluded_columns = ['children', ]
class ScreenView(sqla.ModelView):
column_list = ['id', 'width', 'height', 'number_of_pixels'] # not that 'number_of_pixels' is a hybrid property, not a field
column_sortable_list = ['id', 'width', 'height', 'number_of_pixels']
# Flask-admin can automatically detect the relevant filters for hybrid properties.
column_filters = ('number_of_pixels', )
# Create admin
admin = admin.Admin(app, name='Example: SQLAlchemy', template_mode='bootstrap3')
......@@ -169,7 +257,14 @@ admin = admin.Admin(app, name='Example: SQLAlchemy', template_mode='bootstrap3')
admin.add_view(UserAdmin(User, db.session))
admin.add_view(sqla.ModelView(Tag, db.session))
admin.add_view(PostAdmin(db.session))
admin.add_view(TreeView(Tree, db.session))
admin.add_view(sqla.ModelView(Pet, db.session, category="Other"))
admin.add_view(sqla.ModelView(UserInfo, db.session, category="Other"))
admin.add_view(TreeView(Tree, db.session, category="Other"))
admin.add_view(ScreenView(Screen, db.session, category="Other"))
admin.add_sub_category(name="Links", parent_name="Other")
admin.add_link(MenuLink(name='Back Home', url='/', category='Links'))
admin.add_link(MenuLink(name='Google', url='http://www.google.com/', category='Links'))
admin.add_link(MenuLink(name='Mozilla', url='http://mozilla.org/', category='Links'))
def build_sample_db():
......@@ -201,6 +296,7 @@ def build_sample_db():
user.first_name = first_names[i]
user.last_name = last_names[i]
user.email = first_names[i].lower() + "@example.com"
user.info.append(UserInfo(key="foo", value="bar"))
user_list.append(user)
db.session.add(user)
......@@ -276,6 +372,15 @@ def build_sample_db():
leaf.parent = branch
db.session.add(leaf)
db.session.add(Pet(name='Dog', available=True))
db.session.add(Pet(name='Fish', available=True))
db.session.add(Pet(name='Cat', available=True))
db.session.add(Pet(name='Parrot', available=True))
db.session.add(Pet(name='Ocelot', available=False))
db.session.add(Screen(width=500, height=2000))
db.session.add(Screen(width=550, height=1900))
db.session.commit()
return
......
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_2.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 Car(db.Model):
__tablename__ = 'cars'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
desc = db.Column(db.String(50))
def __str__(self):
return self.desc
class Tyre(db.Model):
__tablename__ = 'tyres'
car_id = db.Column(db.Integer, db.ForeignKey('cars.id'), primary_key=True)
tyre_id = db.Column(db.Integer, primary_key=True)
car = db.relationship('Car', backref='tyres')
desc = db.Column(db.String(50))
class CarAdmin(sqla.ModelView):
column_display_pk = True
form_columns = ['id', 'desc']
class TyreAdmin(sqla.ModelView):
column_display_pk = True
form_columns = ['car', 'tyre_id', 'desc']
# Create admin
admin = admin.Admin(app, name='Example: SQLAlchemy2', template_mode='bootstrap3')
admin.add_view(CarAdmin(Car, db.session))
admin.add_view(TyreAdmin(Tyre, db.session))
if __name__ == '__main__':
# Create DB
db.create_all()
# Start app
app.run(debug=True)
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 %}
This example shows how you can turn a TextArea field into a rich WYSIWYG editor using WTForms and CKEditor.
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/wysiwyg/requirements.txt'
4. Run the application::
python examples/wysiwyg/app.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from wtforms import fields, widgets
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.sqlite'
app.config['SQLALCHEMY_ECHO'] = True
db = SQLAlchemy(app)
''' Define a wtforms widget and field.
WTForms documentation on custom widgets:
https://wtforms.readthedocs.io/en/latest/widgets.html#custom-widgets
'''
class CKTextAreaWidget(widgets.TextArea):
def __call__(self, field, **kwargs):
# add WYSIWYG class to existing classes
existing_classes = kwargs.pop('class', '') or kwargs.pop('class_', '')
kwargs['class'] = u'%s %s' % (existing_classes, "ckeditor")
return super(CKTextAreaWidget, self).__call__(field, **kwargs)
class CKTextAreaField(fields.TextAreaField):
widget = CKTextAreaWidget()
# Model
class Page(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.Unicode(64))
text = db.Column(db.UnicodeText)
def __unicode__(self):
return self.name
# Customized admin interface
class PageAdmin(sqla.ModelView):
form_overrides = dict(text=CKTextAreaField)
create_template = 'create.html'
edit_template = 'edit.html'
# 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: WYSIWYG")
# Add views
admin.add_view(PageAdmin(Page, db.session))
# Create DB
db.create_all()
# Start app
app.run(debug=True)
Flask
Flask-Admin
Flask-SQLAlchemy
{% extends 'admin/model/create.html' %}
{% block tail %}
{{ super() }}
<script src="http://cdnjs.cloudflare.com/ajax/libs/ckeditor/4.0.1/ckeditor.js"></script>
{% endblock %}
{% extends 'admin/model/create.html' %}
{% block tail %}
{{ super() }}
<script src="http://cdnjs.cloudflare.com/ajax/libs/ckeditor/4.0.1/ckeditor.js"></script>
{% endblock %}
......@@ -38,7 +38,7 @@ if not PY2:
# Various tools
from functools import reduce
from urllib.parse import urljoin, urlparse
from urllib.parse import urljoin, urlparse, quote
else:
text_type = unicode
string_types = (str, unicode)
......@@ -62,6 +62,7 @@ else:
# Helpers
reduce = __builtins__['reduce'] if isinstance(__builtins__, dict) else __builtins__.reduce
from urlparse import urljoin, urlparse
from urllib import quote
def with_metaclass(meta, *bases):
......
......@@ -9,7 +9,7 @@ from flask_admin._compat import with_metaclass, as_unicode
from flask_admin import helpers as h
# For compatibility reasons import MenuLink
from flask_admin.menu import MenuCategory, MenuView, MenuLink # noqa: F401
from flask_admin.menu import MenuCategory, MenuView, MenuLink, SubMenuCategory # noqa: F401
def expose(url='/', methods=('GET',)):
......@@ -581,6 +581,27 @@ class Admin(object):
for view in args:
self.add_view(view)
def add_sub_category(self, name, parent_name):
"""
Add a category of a given name underneath
the category with parent_name.
:param name:
The name of the new menu category.
:param parent_name:
The name of a parent_name category
"""
name_text = as_unicode(name)
parent_name_text = as_unicode(parent_name)
category = self.get_category_menu_item(name_text)
parent = self.get_category_menu_item(parent_name_text)
if category is None and parent is not None:
category = SubMenuCategory(name)
self._menu_categories[name_text] = category
parent.add_child(category)
def add_link(self, link):
"""
Add link to menu links collection.
......
......@@ -12,7 +12,7 @@ from werkzeug import secure_filename
from wtforms import fields, validators
from flask_admin import form, helpers
from flask_admin._compat import urljoin, as_unicode
from flask_admin._compat import urljoin, as_unicode, quote
from flask_admin.base import BaseView, expose
from flask_admin.actions import action, ActionsMixin
from flask_admin.babel import gettext, lazy_gettext
......@@ -607,6 +607,9 @@ class BaseFileAdmin(BaseView, ActionsMixin):
:param path:
Static file path
"""
if self._on_windows:
path = path.replace('\\', '/')
if self.is_file_editable(path):
route = '.edit'
else:
......@@ -832,8 +835,8 @@ class BaseFileAdmin(BaseView, ActionsMixin):
if self.is_accessible_path(rel_path):
items.append(item)
sort_column = request.args.get('sort', None, type=str)
sort_desc = request.args.get('desc', 0, type=int)
sort_column = request.args.get('sort', None, type=str) or self.default_sort_column
sort_desc = request.args.get('desc', 0, type=int) or self.default_desc
if sort_column is None:
if self.default_sort_column:
......@@ -841,15 +844,20 @@ class BaseFileAdmin(BaseView, ActionsMixin):
if self.default_desc:
sort_desc = self.default_desc
try:
column_index = self.possible_columns.index(sort_column)
except ValueError:
sort_column = self.default_sort_column
if sort_column is None:
# Sort by name
items.sort(key=itemgetter(0))
# Sort by type
items.sort(key=itemgetter(2), reverse=True)
# Sort by modified date
items.sort(key=lambda x: (x[0], x[1], x[2], x[3], datetime.utcfromtimestamp(x[4])), reverse=True)
if not self._on_windows:
# Sort by modified date
items.sort(key=lambda x: (x[0], x[1], x[2], x[3], datetime.utcfromtimestamp(x[4])), reverse=True)
else:
column_index = self.possible_columns.index(sort_column)
items.sort(key=itemgetter(column_index), reverse=sort_desc)
# Generate breadcrumbs
......@@ -944,7 +952,7 @@ class BaseFileAdmin(BaseView, ActionsMixin):
base_url = self.get_base_url()
if base_url:
base_url = urljoin(self.get_url('.index_view'), base_url)
return redirect(urljoin(base_url, path))
return redirect(urljoin(quote(base_url), quote(path)))
return self.storage.send_file(directory)
......
......@@ -184,6 +184,10 @@ class ModelView(BaseModelView):
return get_primary_key(self.model)
def get_pk_value(self, model):
if self.model._meta.composite_key:
return tuple([
model._data[field_name]
for field_name in self.model._meta.primary_key.field_names])
return getattr(model, self._primary_key)
def scaffold_list_columns(self):
......@@ -440,6 +444,8 @@ class ModelView(BaseModelView):
return count, query
def get_one(self, id):
if self.model._meta.composite_key:
return self.model.get(**dict(zip(self.model._meta.primary_key.field_names, id)))
return self.model.get(**{self._primary_key: id})
def create_model(self, form):
......
......@@ -3,6 +3,7 @@ from enum import Enum
from wtforms import fields, validators
from sqlalchemy import Boolean, Column
from sqlalchemy.orm import ColumnProperty
from flask_admin import form
from flask_admin.model.form import (converts, ModelConverterBase,
......@@ -152,7 +153,7 @@ class AdminModelConverter(ModelConverterBase):
return self._convert_relation(name, prop, property_is_association_proxy, kwargs)
elif hasattr(prop, 'columns'): # Ignore pk/fk
# Check if more than one column mapped to the property
if len(prop.columns) > 1:
if len(prop.columns) > 1 and not isinstance(prop, ColumnProperty):
columns = filter_foreign_columns(model.__table__, prop.columns)
if len(columns) == 0:
......
......@@ -509,8 +509,6 @@ class ModelView(BaseModelView):
# column is in same table, use only model attribute name
if getattr(column, 'key', None) is not None:
column_name = column.key
else:
column_name = text_type(c)
# column_name must match column_name used in `get_list_columns`
result[column_name] = column
......@@ -1083,7 +1081,10 @@ class ModelView(BaseModelView):
# Error handler
def handle_view_exception(self, exc):
if isinstance(exc, IntegrityError):
if current_app.config.get('ADMIN_RAISE_ON_VIEW_EXCEPTION'):
if current_app.config.get(
'ADMIN_RAISE_ON_INTEGRITY_ERROR',
current_app.config.get('ADMIN_RAISE_ON_VIEW_EXCEPTION')
):
raise
else:
flash(gettext('Integrity error. %(message)s', message=text_type(exc)), 'error')
......
......@@ -7,7 +7,7 @@ class BaseMenu(object):
"""
def __init__(self, name, class_name=None, icon_type=None, icon_value=None, target=None):
self.name = name
self.class_name = class_name
self.class_name = class_name if class_name is not None else ''
self.icon_type = icon_type
self.icon_value = icon_value
self.target = target
......@@ -141,3 +141,9 @@ class MenuLink(BaseMenu):
def get_url(self):
return self.url or url_for(self.endpoint)
class SubMenuCategory(MenuCategory):
def __init__(self, *args, **kwargs):
super(SubMenuCategory, self).__init__(*args, **kwargs)
self.class_name += ' dropdown-submenu'
......@@ -18,7 +18,7 @@ from wtforms.fields import HiddenField
from wtforms.fields.core import UnboundField
from wtforms.validators import ValidationError, InputRequired
from flask_admin.babel import gettext
from flask_admin.babel import gettext, ngettext
from flask_admin.base import BaseView, expose
from flask_admin.form import BaseForm, FormOpts, rules
......@@ -783,7 +783,7 @@ class BaseModelView(BaseView, ActionsMixin):
:param name:
View name. If not provided, will use the model class name
:param category:
View category
Optional category name, for grouping views in the menu
:param endpoint:
Base endpoint. If not provided, will use the model name.
:param url:
......@@ -1739,7 +1739,12 @@ class BaseModelView(BaseView, ActionsMixin):
sort=request.args.get('sort', None, type=int),
sort_desc=request.args.get('desc', None, type=int),
search=request.args.get('search', None),
filters=self._get_list_filter_args())
filters=self._get_list_filter_args(),
extra_args=dict([
(k, v) for k, v in request.args.items()
if k not in ('page', 'page_size', 'sort', 'desc', 'search', ) and
not k.startswith('flt')
]))
def _get_filters(self, filters):
"""
......@@ -2204,7 +2209,11 @@ class BaseModelView(BaseView, ActionsMixin):
# message is flashed from within delete_model if it fails
if self.delete_model(model):
flash(gettext('Record was successfully deleted.'), 'success')
count = 1
flash(
ngettext('Record was successfully deleted.',
'%(count)s records were successfully deleted.',
count, count=count), 'success')
return redirect(return_url)
else:
flash_errors(form, message='Failed to delete record. %(error)s')
......
.nav li.dropdown ul.dropdown-menu li:hover ul {
display:block;
position:absolute;
left:100%;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
}
.nav li.dropdown ul.dropdown-menu ul {
display: none;
float:right;
position: relative;
top: auto;
margin-top: -30px;
}
.nav li.dropdown a.dropdown-toggle .glyphicon {
margin: 0 4px;
}
......@@ -20,25 +20,33 @@
{%- if item.is_category() -%}
{% set children = item.get_children() %}
{%- if children %}
{% set class_name = item.get_class_name() %}
{% set class_name = item.get_class_name() or '' %}
{%- if item.is_active(admin_view) %}
<li class="active dropdown">
<li class="active dropdown{% if class_name %} {{class_name}}{% endif %}">
{% else -%}
<li class="dropdown">
<li class="dropdown{% if class_name %} {{class_name}}{% endif %}">
{%- endif %}
<a class="dropdown-toggle" data-toggle="dropdown" href="javascript:void(0)">
{% if item.class_name %}<i class="{{ item.class_name }}"></i> {% endif %}{{ item.name }}<b class="caret"></b>
{% if item.class_name %}<i class="{{ item.class_name }}"></i> {% endif %}
{{ menu_icon(item) }}{{ item.name }}
{%- if 'dropdown-submenu' not in class_name -%}<b class="caret"></b>{%- endif -%}
</a>
<ul class="dropdown-menu">
{%- for child in children -%}
{% set class_name = child.get_class_name() %}
{%- if child.is_active(admin_view) %}
<li class="active{% if class_name %} {{class_name}}{% endif %}">
{%- if child.is_category() -%}
{{ menu(menu_root=[child]) }}
{% else %}
<li{% if class_name %} class="{{class_name}}"{% endif %}>
{% set class_name = child.get_class_name() %}
{%- if child.is_active(admin_view) %}
<li class="active{% if class_name %} {{class_name}}{% endif %}">
{% else %}
<li{% if class_name %} class="{{class_name}}"{% endif %}>
{%- endif %}
<a href="{{ child.get_url() }}"{% if child.target %}
target="{{ child.target }}"{% endif %}>
{{ menu_icon(child) }}{{ child.name }}</a>
</li>
{%- endif %}
<a href="{{ child.get_url() }}"{% if child.target %} target="{{ child.target }}"{% endif %}>{{ menu_icon(child) }}{{ child.name }}</a>
</li>
{%- endfor %}
</ul>
</li>
......
......@@ -17,6 +17,7 @@
<link href="{{ admin_static.url(filename='bootstrap/bootstrap3/css/bootstrap-theme.min.css', v='3.3.5') }}" rel="stylesheet">
{%endif%}
<link href="{{ admin_static.url(filename='admin/css/bootstrap3/admin.css', v='1.1.1') }}" rel="stylesheet">
<link href="{{ admin_static.url(filename='admin/css/bootstrap3/submenu.css') }}" rel="stylesheet">
{% if admin_view.extra_css %}
{% for css_url in admin_view.extra_css %}
<link href="{{ css_url }}" rel="stylesheet">
......
......@@ -20,25 +20,37 @@
{%- if item.is_category() -%}
{% set children = item.get_children() %}
{%- if children %}
{% set class_name = item.get_class_name() %}
{% set class_name = item.get_class_name() or '' %}
{%- if item.is_active(admin_view) %}
<li class="active dropdown">
<li class="active dropdown{% if class_name %} {{class_name}}{% endif %}">
{% else -%}
<li class="dropdown">
<li class="dropdown{% if class_name %} {{class_name}}{% endif %}">
{%- endif %}
<a class="dropdown-toggle" data-toggle="dropdown" href="javascript:void(0)">
{% if item.class_name %}<span class="{{ item.class_name }}"></span> {% endif %}{{ item.name }}<b class="caret"></b>
{% if item.class_name %}<span class="{{ item.class_name }}"></span> {% endif %}
{{ menu_icon(item) }}{{ item.name }}
{%- if 'dropdown-submenu' in class_name -%}
<i class="glyphicon glyphicon-chevron-right small"></i>
{%- else -%}
<i class="glyphicon glyphicon-chevron-down small"></i>
{%- endif -%}
</a>
<ul class="dropdown-menu">
{%- for child in children -%}
{% set class_name = child.get_class_name() %}
{%- if child.is_active(admin_view) %}
<li class="active{% if class_name %} {{class_name}}{% endif %}">
{%- if child.is_category() -%}
{{ menu(menu_root=[child]) }}
{% else %}
<li{% if class_name %} class="{{class_name}}"{% endif %}>
{% set class_name = child.get_class_name() %}
{%- if child.is_active(admin_view) %}
<li class="active{% if class_name %} {{class_name}}{% endif %}">
{% else %}
<li{% if class_name %} class="{{class_name}}"{% endif %}>
{%- endif %}
<a href="{{ child.get_url() }}"{% if child.target %}
target="{{ child.target }}"{% endif %}>
{{ menu_icon(child) }}{{ child.name }}</a>
</li>
{%- endif %}
<a href="{{ child.get_url() }}"{% if child.target %} target="{{ child.target }}"{% endif %}>{{ menu_icon(child) }}{{ child.name }}</a>
</li>
{%- endfor %}
</ul>
</li>
......
......@@ -31,7 +31,7 @@
{% elif csrf_token %}
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
{% endif %}
<button onclick="return safeConfirm('{{ _gettext('Are you sure you want to delete this record?') }}');" title="Delete record">
<button onclick="return safeConfirm('{{ _gettext('Are you sure you want to delete this record?') }}');" title="{{ _gettext('Delete record') }}">
<span class="fa fa-trash glyphicon glyphicon-trash"></span>
</button>
</form>
......
import os
import os.path as op
import unittest
......@@ -208,3 +209,37 @@ class LocalFileAdminTests(Base.FileAdminTests):
def fileadmin_args(self):
return (self._test_files_root, '/files'), {}
def test_fileadmin_sort_bogus_url_param(self):
fileadmin_class = self.fileadmin_class()
fileadmin_args, fileadmin_kwargs = self.fileadmin_args()
app, admin = setup()
class MyFileAdmin(fileadmin_class):
editable_extensions = ('txt',)
view_kwargs = dict(fileadmin_kwargs)
view_kwargs.setdefault('name', 'Files')
view = MyFileAdmin(*fileadmin_args, **view_kwargs)
admin.add_view(view)
client = app.test_client()
with open(op.join(self._test_files_root, 'dummy2.txt'), 'w') as fp:
# make sure that 'files/dummy2.txt' exists, is newest and has bigger size
fp.write('test')
rv = client.get('/admin/myfileadmin/?sort=bogus')
eq_(rv.status_code, 200)
ok_(rv.data.decode('utf-8').find('path=dummy2.txt') <
rv.data.decode('utf-8').find('path=dummy.txt'))
rv = client.get('/admin/myfileadmin/?sort=name')
eq_(rv.status_code, 200)
ok_(rv.data.decode('utf-8').find('path=dummy.txt') <
rv.data.decode('utf-8').find('path=dummy2.txt'))
try:
# clean up
os.remove(op.join(self._test_files_root, 'dummy2.txt'))
except (IOError, OSError):
pass
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