Commit f29d3893 authored by PJ Janse van Rensburg's avatar PJ Janse van Rensburg

Merge branch 'prs-to-merge'

parents d1191c27 dc12800a
#!/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/
...@@ -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
******************* *******************
......
...@@ -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
...@@ -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:
......
...@@ -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', ]
......
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 %}
...@@ -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.
...@@ -627,7 +651,8 @@ class Admin(object): ...@@ -627,7 +651,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.
...@@ -638,10 +663,14 @@ class Admin(object): ...@@ -638,10 +663,14 @@ class Admin(object):
self._init_extension() self._init_extension()
# Register Index view
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))
def _init_extension(self): def _init_extension(self):
if not hasattr(self.app, 'extensions'): if not hasattr(self.app, 'extensions'):
self.app.extensions = dict() self.app.extensions = dict()
...@@ -670,4 +699,4 @@ class Admin(object): ...@@ -670,4 +699,4 @@ class Admin(object):
""" """
Return menu links. Return menu links.
""" """
return self._menu_links return self._menu_links
\ No newline at end of file
from sqlalchemy import or_ from sqlalchemy import or_, and_
from flask_admin._compat import as_unicode, string_types from flask_admin._compat import as_unicode, string_types
from flask_admin.model.ajax import AjaxModelLoader, DEFAULT_PAGE_SIZE from flask_admin.model.ajax import AjaxModelLoader, DEFAULT_PAGE_SIZE
...@@ -13,6 +13,8 @@ class QueryAjaxModelLoader(AjaxModelLoader): ...@@ -13,6 +13,8 @@ class QueryAjaxModelLoader(AjaxModelLoader):
:param fields: :param fields:
Fields to run query against Fields to run query against
:param filters:
Additional filters to apply to the loader
""" """
super(QueryAjaxModelLoader, self).__init__(name, options) super(QueryAjaxModelLoader, self).__init__(name, options)
...@@ -20,6 +22,7 @@ class QueryAjaxModelLoader(AjaxModelLoader): ...@@ -20,6 +22,7 @@ class QueryAjaxModelLoader(AjaxModelLoader):
self.model = model self.model = model
self.fields = options.get('fields') self.fields = options.get('fields')
self.order_by = options.get('order_by') self.order_by = options.get('order_by')
self.filters = options.get('filters')
if not self.fields: if not self.fields:
raise ValueError('AJAX loading requires `fields` to be specified for %s.%s' % (model, self.name)) raise ValueError('AJAX loading requires `fields` to be specified for %s.%s' % (model, self.name))
...@@ -65,6 +68,10 @@ class QueryAjaxModelLoader(AjaxModelLoader): ...@@ -65,6 +68,10 @@ class QueryAjaxModelLoader(AjaxModelLoader):
filters = (field.ilike(u'%%%s%%' % term) for field in self._cached_fields) filters = (field.ilike(u'%%%s%%' % term) for field in self._cached_fields)
query = query.filter(or_(*filters)) query = query.filter(or_(*filters))
if self.filters:
filters = ["%s.%s" % (self.model.__name__.lower(), value) for value in self.filters]
query = query.filter(and_(*filters))
if self.order_by: if self.order_by:
query = query.order_by(self.order_by) query = query.order_by(self.order_by)
......
...@@ -105,16 +105,16 @@ class AdminModelConverter(ModelConverterBase): ...@@ -105,16 +105,16 @@ class AdminModelConverter(ModelConverterBase):
# determine optional/required, or respect existing # determine optional/required, or respect existing
requirement_options = (validators.Optional, validators.InputRequired) requirement_options = (validators.Optional, validators.InputRequired)
if not any(isinstance(v, requirement_options) for v in kwargs['validators']): requirement_validator_specified = any(isinstance(v, requirement_options) for v in kwargs['validators'])
if property_is_association_proxy or column.nullable or prop.direction.name != 'MANYTOONE': if property_is_association_proxy or column.nullable or prop.direction.name != 'MANYTOONE':
kwargs['allow_blank'] = True
if not requirement_validator_specified:
kwargs['validators'].append(validators.Optional()) kwargs['validators'].append(validators.Optional())
else: else:
kwargs['allow_blank'] = False
if not requirement_validator_specified:
kwargs['validators'].append(validators.InputRequired()) kwargs['validators'].append(validators.InputRequired())
# Contribute model-related parameters
if 'allow_blank' not in kwargs:
kwargs['allow_blank'] = column.nullable
# Override field type if necessary # Override field type if necessary
override = self._get_field_override(prop.key) override = self._get_field_override(prop.key)
if override: if override:
......
...@@ -628,13 +628,26 @@ class ModelView(BaseModelView): ...@@ -628,13 +628,26 @@ class ModelView(BaseModelView):
column = columns[0] column = columns[0]
# If filter related to relation column (represented by
# relation_name.target_column) we collect here relation name
joined_column_name = None
if '.' in name:
joined_column_name = name.split('.')[0]
# Join not needed for hybrid properties # Join not needed for hybrid properties
if (not is_hybrid_property and tools.need_join(self.model, column.table) and if (not is_hybrid_property and tools.need_join(self.model, column.table) and
name not in self.column_labels): name not in self.column_labels):
visible_name = '%s / %s' % ( if joined_column_name:
self.get_column_name(column.table.name), visible_name = '%s / %s / %s' % (
self.get_column_name(column.name) joined_column_name,
) self.get_column_name(column.table.name),
self.get_column_name(column.name)
)
else:
visible_name = '%s / %s' % (
self.get_column_name(column.table.name),
self.get_column_name(column.name)
)
else: else:
if not isinstance(name, string_types): if not isinstance(name, string_types):
visible_name = self.get_column_name(name.property.key) visible_name = self.get_column_name(name.property.key)
...@@ -657,10 +670,19 @@ class ModelView(BaseModelView): ...@@ -657,10 +670,19 @@ class ModelView(BaseModelView):
options=self.column_choices.get(name), options=self.column_choices.get(name),
) )
key_name = column
# In case of filter related to relation column filter key
# must be named with relation name (to prevent following same
# target column to replace previous)
if joined_column_name:
key_name = "{0}.{1}".format(joined_column_name, column)
for f in flt:
f.key_name = key_name
if joins: if joins:
self._filter_joins[column] = joins self._filter_joins[key_name] = joins
elif not is_hybrid_property and tools.need_join(self.model, column.table): elif not is_hybrid_property and tools.need_join(self.model, column.table):
self._filter_joins[column] = [column.table] self._filter_joins[key_name] = [column.table]
return flt return flt
...@@ -900,7 +922,9 @@ class ModelView(BaseModelView): ...@@ -900,7 +922,9 @@ class ModelView(BaseModelView):
# Figure out joins # Figure out joins
if isinstance(flt, sqla_filters.BaseSQLAFilter): if isinstance(flt, sqla_filters.BaseSQLAFilter):
path = self._filter_joins.get(flt.column, []) # If no key_name is specified, use filter column as filter key
filter_key = flt.key_name or flt.column
path = self._filter_joins.get(filter_key, [])
query, joins, alias = self._apply_path_joins(query, joins, path, inner_join=False) query, joins, alias = self._apply_path_joins(query, joins, path, inner_join=False)
......
...@@ -42,8 +42,9 @@ class ViewArgs(object): ...@@ -42,8 +42,9 @@ class ViewArgs(object):
""" """
List view arguments. List view arguments.
""" """
def __init__(self, page=None, sort=None, sort_desc=None, search=None, filters=None, extra_args=None): def __init__(self, page=None, page_size=None, sort=None, sort_desc=None, search=None, filters=None, extra_args=None):
self.page = page self.page = page
self.page_size = page_size
self.sort = sort self.sort = sort
self.sort_desc = bool(sort_desc) self.sort_desc = bool(sort_desc)
self.search = search self.search = search
...@@ -61,6 +62,7 @@ class ViewArgs(object): ...@@ -61,6 +62,7 @@ class ViewArgs(object):
flt = None flt = None
kwargs.setdefault('page', self.page) kwargs.setdefault('page', self.page)
kwargs.setdefault('page_size', self.page_size)
kwargs.setdefault('sort', self.sort) kwargs.setdefault('sort', self.sort)
kwargs.setdefault('sort_desc', self.sort_desc) kwargs.setdefault('sort_desc', self.sort_desc)
kwargs.setdefault('search', self.search) kwargs.setdefault('search', self.search)
...@@ -719,12 +721,17 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -719,12 +721,17 @@ class BaseModelView(BaseView, ActionsMixin):
for supported types. for supported types.
""" """
# Various settings # Pagination settings
page_size = 20 page_size = 20
""" """
Default page size for pagination. Default page size for pagination.
""" """
can_set_page_size = False
"""
Allows to select page size via dropdown list
"""
def __init__(self, model, def __init__(self, model,
name=None, category=None, endpoint=None, url=None, static_folder=None, name=None, category=None, endpoint=None, url=None, static_folder=None,
menu_class_name=None, menu_icon_type=None, menu_icon_value=None): menu_class_name=None, menu_icon_type=None, menu_icon_value=None):
...@@ -1646,6 +1653,7 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -1646,6 +1653,7 @@ class BaseModelView(BaseView, ActionsMixin):
Return arguments from query string. Return arguments from query string.
""" """
return ViewArgs(page=request.args.get('page', 0, type=int), return ViewArgs(page=request.args.get('page', 0, type=int),
page_size=request.args.get('page_size', 0, type=int),
sort=request.args.get('sort', None, type=int), sort=request.args.get('sort', None, type=int),
sort_desc=request.args.get('desc', None, type=int), sort_desc=request.args.get('desc', None, type=int),
search=request.args.get('search', None), search=request.args.get('search', None),
...@@ -1668,6 +1676,9 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -1668,6 +1676,9 @@ class BaseModelView(BaseView, ActionsMixin):
kwargs = dict(page=page, sort=view_args.sort, desc=desc, search=view_args.search) kwargs = dict(page=page, sort=view_args.sort, desc=desc, search=view_args.search)
kwargs.update(view_args.extra_args) kwargs.update(view_args.extra_args)
if view_args.page_size:
kwargs['page_size'] = view_args.page_size
if view_args.filters: if view_args.filters:
for i, pair in enumerate(view_args.filters): for i, pair in enumerate(view_args.filters):
idx, flt_name, value = pair idx, flt_name, value = pair
...@@ -1821,9 +1832,12 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -1821,9 +1832,12 @@ class BaseModelView(BaseView, ActionsMixin):
if sort_column is not None: if sort_column is not None:
sort_column = sort_column[0] sort_column = sort_column[0]
# Get page size
page_size = view_args.page_size or self.page_size
# Get count and data # Get count and data
count, data = self.get_list(view_args.page, sort_column, view_args.sort_desc, count, data = self.get_list(view_args.page, sort_column, view_args.sort_desc,
view_args.search, view_args.filters) view_args.search, view_args.filters, page_size=page_size)
list_forms = {} list_forms = {}
if self.column_editable_list: if self.column_editable_list:
...@@ -1831,9 +1845,9 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -1831,9 +1845,9 @@ class BaseModelView(BaseView, ActionsMixin):
list_forms[self.get_pk_value(row)] = self.list_form(obj=row) list_forms[self.get_pk_value(row)] = self.list_form(obj=row)
# Calculate number of pages # Calculate number of pages
if count is not None and self.page_size: if count is not None and page_size:
num_pages = int(ceil(count / float(self.page_size))) num_pages = int(ceil(count / float(page_size)))
elif not self.page_size: elif not page_size:
num_pages = 0 # hide pager for unlimited page_size num_pages = 0 # hide pager for unlimited page_size
else: else:
num_pages = None # use simple pager num_pages = None # use simple pager
...@@ -1852,6 +1866,12 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -1852,6 +1866,12 @@ class BaseModelView(BaseView, ActionsMixin):
return self._get_list_url(view_args.clone(sort=column, sort_desc=desc)) return self._get_list_url(view_args.clone(sort=column, sort_desc=desc))
def page_size_url(s):
if not s:
s = self.page_size
return self._get_list_url(view_args.clone(page_size=s))
# Actions # Actions
actions, actions_confirmation = self.get_actions_list() actions, actions_confirmation = self.get_actions_list()
...@@ -1877,8 +1897,10 @@ class BaseModelView(BaseView, ActionsMixin): ...@@ -1877,8 +1897,10 @@ class BaseModelView(BaseView, ActionsMixin):
count=count, count=count,
pager_url=pager_url, pager_url=pager_url,
num_pages=num_pages, num_pages=num_pages,
can_set_page_size=self.can_set_page_size,
page_size_url=page_size_url,
page=view_args.page, page=view_args.page,
page_size=self.page_size, page_size=page_size,
# Sorting # Sorting
sort_column=view_args.sort, sort_column=view_args.sort,
......
...@@ -8,7 +8,7 @@ class BaseFilter(object): ...@@ -8,7 +8,7 @@ class BaseFilter(object):
""" """
Base filter class. Base filter class.
""" """
def __init__(self, name, options=None, data_type=None): def __init__(self, name, options=None, data_type=None, key_name=None):
""" """
Constructor. Constructor.
...@@ -18,10 +18,13 @@ class BaseFilter(object): ...@@ -18,10 +18,13 @@ class BaseFilter(object):
List of fixed options. If provided, will use drop down instead of textbox. List of fixed options. If provided, will use drop down instead of textbox.
:param data_type: :param data_type:
Client-side widget type to use. Client-side widget type to use.
:param key_name:
Optional name who represent this filter.
""" """
self.name = name self.name = name
self.options = options self.options = options
self.data_type = data_type self.data_type = data_type
self.key_name = key_name
def get_options(self, view): def get_options(self, view):
""" """
......
...@@ -89,3 +89,53 @@ table.filters tr td { ...@@ -89,3 +89,53 @@ table.filters tr td {
.admin-form .controls { .admin-form .controls {
margin-left: 110px; margin-left: 110px;
} }
@media only screen and (max-width: 800px) {
/* Force table to not be like tables anymore */
#no-more-tables table,
#no-more-tables thead,
#no-more-tables tbody,
#no-more-tables th,
#no-more-tables td,
#no-more-tables tr {
display: block;
}
/* Hide table headers (but not display: none;, for accessibility) */
#no-more-tables thead tr {
position: absolute;
top: -9999px;
left: -9999px;
}
#no-more-tables tr { border: 1px solid #ccc; }
#no-more-tables td {
/* Behave like a "row" */
border: none;
border-bottom: 1px solid #eee;
position: relative;
padding-left: 50%;
white-space: normal;
text-align:left;
}
#no-more-tables td:before {
/* Now like a table header */
position: absolute;
/* Top/left values mimic padding */
top: 6px;
left: 6px;
width: 45%;
padding-right: 10px;
white-space: nowrap;
text-align:left;
font-weight: bold;
}
/*
Label the data
*/
#no-more-tables td:before { content: attr(data-title); }
}
...@@ -103,3 +103,8 @@ body.modal-open { ...@@ -103,3 +103,8 @@ body.modal-open {
margin-top: 20px; margin-top: 20px;
margin-bottom: 10px; margin-bottom: 10px;
} }
.table-responsive
{
overflow-x: auto;
}
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -12,9 +12,14 @@ ...@@ -12,9 +12,14 @@
<meta name="author" content=""> <meta name="author" content="">
{% endblock %} {% endblock %}
{% block head_css %} {% block head_css %}
<link href="{{ admin_static.url(filename='bootstrap/bootstrap2/css/bootstrap.css', v='2.3.2') }}" rel="stylesheet"> <link href="{{ admin_static.url(filename='bootstrap/bootstrap2/swatch/{swatch}/bootstrap.min.css'.format(swatch=config.get('FLASK_ADMIN_SWATCH', 'default')), v='2.3.2') }}" rel="stylesheet">
<link href="{{ admin_static.url(filename='bootstrap/bootstrap2/css/bootstrap-responsive.css', v='2.3.2') }}" rel="stylesheet"> <link href="{{ admin_static.url(filename='bootstrap/bootstrap2/css/bootstrap-responsive.css', v='2.3.2') }}" rel="stylesheet">
<link href="{{ admin_static.url(filename='admin/css/bootstrap2/admin.css', v='1.1.1') }}" rel="stylesheet"> <link href="{{ admin_static.url(filename='admin/css/bootstrap2/admin.css', v='1.1.1') }}" rel="stylesheet">
{% if admin_view.extra_css %}
{% for css_url in admin_view.extra_css %}
<link href="{{ css_url }}" rel="stylesheet">
{% endfor %}
{% endif %}
<style> <style>
body { body {
padding-top: 4px; padding-top: 4px;
...@@ -65,6 +70,11 @@ ...@@ -65,6 +70,11 @@
<script src="{{ admin_static.url(filename='bootstrap/bootstrap2/js/bootstrap.min.js', v='2.3.2') }}" type="text/javascript"></script> <script src="{{ admin_static.url(filename='bootstrap/bootstrap2/js/bootstrap.min.js', v='2.3.2') }}" type="text/javascript"></script>
<script src="{{ admin_static.url(filename='vendor/moment.min.js', v='2.9.0') }}" type="text/javascript"></script> <script src="{{ admin_static.url(filename='vendor/moment.min.js', v='2.9.0') }}" type="text/javascript"></script>
<script src="{{ admin_static.url(filename='vendor/select2/select2.min.js', v='3.5.2') }}" type="text/javascript"></script> <script src="{{ admin_static.url(filename='vendor/select2/select2.min.js', v='3.5.2') }}" type="text/javascript"></script>
{% if admin_view.extra_js %}
{% for js_url in admin_view.extra_js %}
<script src="{{ js_url }}" type="text/javascript"></script>
{% endfor %}
{% endif %}
{% endblock %} {% endblock %}
{% block tail %} {% block tail %}
......
...@@ -22,6 +22,7 @@ ...@@ -22,6 +22,7 @@
{% endblock %} {% endblock %}
{% block file_list_table %} {% block file_list_table %}
<div id="no-more-tables">
<table class="table table-striped table-bordered model-list"> <table class="table table-striped table-bordered model-list">
<thead> <thead>
<tr> <tr>
...@@ -102,13 +103,13 @@ ...@@ -102,13 +103,13 @@
{% endblock %} {% endblock %}
</td> </td>
{% if is_dir %} {% if is_dir %}
<td colspan="2"> <td colspan="2" data-title="{{ _gettext('Name') }}">
<a href="{{ get_dir_url('.index_view', path)|safe }}"> <a href="{{ get_dir_url('.index_view', path)|safe }}">
<i class="fa fa-folder-o icon-folder-close"></i> <span>{{ name }}</span> <i class="fa fa-folder-o icon-folder-close"></i> <span>{{ name }}</span>
</a> </a>
</td> </td>
{% else %} {% else %}
<td> <td data-title="{{ _gettext('Name') }}">
{% if admin_view.can_download %} {% if admin_view.can_download %}
{%- if admin_view.edit_modal and admin_view.is_file_editable(path) -%} {%- if admin_view.edit_modal and admin_view.is_file_editable(path) -%}
{{ lib.add_modal_button(url=get_file_url(path, modal=True)|safe, {{ lib.add_modal_button(url=get_file_url(path, modal=True)|safe,
...@@ -121,7 +122,7 @@ ...@@ -121,7 +122,7 @@
{% endif %} {% endif %}
</td> </td>
{% if admin_view.is_column_visible('size') %} {% if admin_view.is_column_visible('size') %}
<td> <td data-title="{{ _gettext('Size') }}">
{{ size|filesizeformat }} {{ size|filesizeformat }}
</td> </td>
{% endif %} {% endif %}
...@@ -135,6 +136,7 @@ ...@@ -135,6 +136,7 @@
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
</div>
{% endblock %} {% endblock %}
{% block toolbar %} {% block toolbar %}
<div class="btn-toolbar"> <div class="btn-toolbar">
......
...@@ -68,3 +68,14 @@ ...@@ -68,3 +68,14 @@
{% endif %} {% endif %}
</form> </form>
{% endmacro %} {% endmacro %}
{% macro page_size_form(generator, btn_class='dropdown-toggle') %}
<a class="{{ btn_class }}" data-toggle="dropdown" href="javascript:void(0)">
{{ page_size }} {{ _gettext('items') }}<b class="caret"></b>
</a>
<ul class="dropdown-menu">
<li><a href="{{ generator(20) }}">20 {{ _gettext('items') }}</a></li>
<li><a href="{{ generator(50) }}">50 {{ _gettext('items') }}</a></li>
<li><a href="{{ generator(100) }}">100 {{ _gettext('items') }}</a></li>
</ul>
{% endmacro %}
...@@ -39,6 +39,12 @@ ...@@ -39,6 +39,12 @@
</li> </li>
{% endif %} {% endif %}
{% if can_set_page_size %}
<li class="dropdown">
{{ model_layout.page_size_form(page_size_url) }}
</li>
{% endif %}
{% if actions %} {% if actions %}
<li class="dropdown"> <li class="dropdown">
{{ actionlib.dropdown(actions) }} {{ actionlib.dropdown(actions) }}
...@@ -61,8 +67,9 @@ ...@@ -61,8 +67,9 @@
{% endif %} {% endif %}
{% block model_list_table %} {% block model_list_table %}
<table class="table table-striped table-bordered table-hover model-list"> <div id="no-more-tables">
<thead> <table class="table table-striped table-bordered table-hover model-list cf">
<thead class="cf">
<tr> <tr>
{% block list_header scoped %} {% block list_header scoped %}
{% if actions %} {% if actions %}
...@@ -127,7 +134,7 @@ ...@@ -127,7 +134,7 @@
{% endblock %} {% endblock %}
{% for c, name in list_columns %} {% for c, name in list_columns %}
<td class="col-{{c}}"> <td class="col-{{c}}" data-title="{{ name }}">
{% if admin_view.is_editable(c) %} {% if admin_view.is_editable(c) %}
{% set form = list_forms[get_pk_value(row)] %} {% set form = list_forms[get_pk_value(row)] %}
{% if form.csrf_token %} {% if form.csrf_token %}
...@@ -154,6 +161,7 @@ ...@@ -154,6 +161,7 @@
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
</div>
{% block list_pager %} {% block list_pager %}
{% if num_pages is not none %} {% if num_pages is not none %}
{{ lib.pager(page, num_pages, pager_url) }} {{ lib.pager(page, num_pages, pager_url) }}
......
...@@ -12,9 +12,16 @@ ...@@ -12,9 +12,16 @@
<meta name="author" content=""> <meta name="author" content="">
{% endblock %} {% endblock %}
{% block head_css %} {% block head_css %}
<link href="{{ admin_static.url(filename='bootstrap/bootstrap3/css/bootstrap.min.css', v='3.3.5') }}" rel="stylesheet"> <link href="{{ admin_static.url(filename='bootstrap/bootstrap3/swatch/{swatch}/bootstrap.min.css'.format(swatch=config.get('FLASK_ADMIN_SWATCH', 'default')), v='3.3.5') }}" rel="stylesheet">
{%if config.get('FLASK_ADMIN_SWATCH', 'default') == 'default' %}
<link href="{{ admin_static.url(filename='bootstrap/bootstrap3/css/bootstrap-theme.min.css', v='3.3.5') }}" rel="stylesheet"> <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/admin.css', v='1.1.1') }}" rel="stylesheet">
{% if admin_view.extra_css %}
{% for css_url in admin_view.extra_css %}
<link href="{{ css_url }}" rel="stylesheet">
{% endfor %}
{% endif %}
<style> <style>
body { body {
padding-top: 4px; padding-top: 4px;
...@@ -76,6 +83,11 @@ ...@@ -76,6 +83,11 @@
<script src="{{ admin_static.url(filename='bootstrap/bootstrap3/js/bootstrap.min.js', v='3.3.5') }}" type="text/javascript"></script> <script src="{{ admin_static.url(filename='bootstrap/bootstrap3/js/bootstrap.min.js', v='3.3.5') }}" type="text/javascript"></script>
<script src="{{ admin_static.url(filename='vendor/moment.min.js', v='2.9.0') }}" type="text/javascript"></script> <script src="{{ admin_static.url(filename='vendor/moment.min.js', v='2.9.0') }}" type="text/javascript"></script>
<script src="{{ admin_static.url(filename='vendor/select2/select2.min.js', v='3.5.2') }}" type="text/javascript"></script> <script src="{{ admin_static.url(filename='vendor/select2/select2.min.js', v='3.5.2') }}" type="text/javascript"></script>
{% if admin_view.extra_js %}
{% for js_url in admin_view.extra_js %}
<script src="{{ js_url }}" type="text/javascript"></script>
{% endfor %}
{% endif %}
{% endblock %} {% endblock %}
{% block tail %} {% block tail %}
......
...@@ -22,6 +22,7 @@ ...@@ -22,6 +22,7 @@
{% endblock %} {% endblock %}
{% block file_list_table %} {% block file_list_table %}
<div class="table-responsive">
<table class="table table-striped table-bordered model-list"> <table class="table table-striped table-bordered model-list">
<thead> <thead>
<tr> <tr>
...@@ -135,6 +136,7 @@ ...@@ -135,6 +136,7 @@
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
</div>
{% endblock %} {% endblock %}
{% block toolbar %} {% block toolbar %}
<div class="btn-toolbar"> <div class="btn-toolbar">
......
...@@ -66,3 +66,14 @@ ...@@ -66,3 +66,14 @@
{% endif %} {% endif %}
</form> </form>
{% endmacro %} {% endmacro %}
{% macro page_size_form(generator, btn_class='dropdown-toggle') %}
<a class="{{ btn_class }}" data-toggle="dropdown" href="javascript:void(0)">
{{ page_size }} {{ _gettext('items') }}<b class="caret"></b>
</a>
<ul class="dropdown-menu">
<li><a href="{{ generator(20) }}">20 {{ _gettext('items') }}</a></li>
<li><a href="{{ generator(50) }}">50 {{ _gettext('items') }}</a></li>
<li><a href="{{ generator(100) }}">100 {{ _gettext('items') }}</a></li>
</ul>
{% endmacro %}
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