├── .gitmodules ├── examples ├── __init__.py ├── django │ ├── __init__.py │ ├── utils.py │ └── simple.py ├── quickstart │ ├── templates │ │ └── index.html │ ├── first.py │ ├── second.py │ └── third.py ├── simple │ ├── templates │ │ ├── myadmin.html │ │ ├── test.html │ │ └── anotheradmin.html │ └── simple.py ├── auth │ ├── templates │ │ ├── index.html │ │ └── form.html │ ├── auth.py │ └── mongoauth.py ├── file │ └── file.py ├── mongoengine │ └── simple.py ├── babel │ └── simple.py └── sqlalchemy │ └── simple.py ├── flask_superadmin ├── tests │ ├── __init__.py │ ├── templates │ │ └── mock.html │ ├── test_django.py │ └── test_base.py ├── model │ ├── backends │ │ ├── __init__.py │ │ ├── django │ │ │ ├── __init__.py │ │ │ ├── view.py │ │ │ ├── fields.py │ │ │ └── orm.py │ │ ├── mongoengine │ │ │ ├── __init__.py │ │ │ └── view.py │ │ └── sqlalchemy │ │ │ ├── __init__.py │ │ │ ├── tools.py │ │ │ ├── filters.py │ │ │ ├── view.py │ │ │ └── orm.py │ └── __init__.py ├── translations │ ├── __init__.py │ ├── admin.pot │ ├── fr │ │ └── LC_MESSAGES │ │ │ └── admin.po │ ├── zh_CN │ │ └── LC_MESSAGES │ │ │ └── admin.po │ └── ru │ │ └── LC_MESSAGES │ │ └── admin.po ├── static │ ├── bootstrap │ │ ├── js │ │ │ └── admin.js │ │ └── img │ │ │ ├── glyphicons-halflings.png │ │ │ └── glyphicons-halflings-white.png │ ├── img │ │ ├── favicon.png │ │ ├── background.png │ │ └── search-input-icon.png │ ├── chosen │ │ └── chosen-sprite.png │ ├── css │ │ ├── file.css │ │ ├── datepicker.css │ │ ├── forms.css │ │ └── admin.css │ └── js │ │ ├── jquery.browser.js │ │ ├── form.js │ │ └── filters.js ├── templates │ └── admin │ │ ├── master.html │ │ ├── index.html │ │ ├── file │ │ ├── rename.html │ │ ├── form.html │ │ └── list.html │ │ ├── model │ │ ├── add.html │ │ ├── edit.html │ │ ├── delete.html │ │ └── list.html │ │ ├── layout.html │ │ └── _macros.html ├── __init__.py ├── contrib │ ├── djangomodel.py │ ├── sqlamodel.py │ ├── mongoenginemodel.py │ └── __init__.py ├── babel.py └── form.py ├── doc ├── _themes │ ├── .gitignore │ ├── flask │ │ ├── theme.conf │ │ ├── relations.html │ │ ├── layout.html │ │ └── static │ │ │ ├── small_flask.css │ │ │ └── flasky.css_t │ ├── flask_small │ │ ├── theme.conf │ │ ├── layout.html │ │ └── static │ │ │ └── flasky.css_t │ ├── README │ ├── LICENSE │ └── flask_theme_support.py ├── images │ └── quickstart │ │ ├── quickstart_1.png │ │ ├── quickstart_2.png │ │ ├── quickstart_3.png │ │ ├── quickstart_4.png │ │ └── quickstart_5.png ├── api │ ├── index.rst │ ├── mod_form.rst │ ├── mod_tools.rst │ ├── mod_base.rst │ ├── mod_contrib_sqlamodel.rst │ ├── mod_contrib_fileadmin.rst │ └── mod_model.rst ├── _templates │ └── sidebarintro.html ├── index.rst └── model_guidelines.rst ├── logo.png ├── babel.cfg ├── babel ├── babel.ini ├── babel.bat ├── babel.sh └── admin.pot ├── screenshots ├── model-edit.png └── model-list.png ├── requirements.txt ├── .travis.yml ├── MANIFEST.in ├── .gitignore ├── AUTHORS ├── setup.cfg ├── Gruntfile.js ├── TODO.txt ├── LICENSE ├── setup.py ├── README.rst └── Makefile /.gitmodules: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/django/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /flask_superadmin/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /flask_superadmin/model/backends/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /flask_superadmin/translations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /flask_superadmin/static/bootstrap/js/admin.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/_themes/.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /flask_superadmin/model/backends/django/__init__.py: -------------------------------------------------------------------------------- 1 | from .view import ModelAdmin 2 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syrusakbary/Flask-SuperAdmin/HEAD/logo.png -------------------------------------------------------------------------------- /flask_superadmin/model/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import ModelAdmin, AdminModelConverter 2 | -------------------------------------------------------------------------------- /flask_superadmin/model/backends/mongoengine/__init__.py: -------------------------------------------------------------------------------- 1 | from .view import ModelAdmin 2 | -------------------------------------------------------------------------------- /flask_superadmin/model/backends/sqlalchemy/__init__.py: -------------------------------------------------------------------------------- 1 | from .view import ModelAdmin 2 | -------------------------------------------------------------------------------- /flask_superadmin/tests/templates/mock.html: -------------------------------------------------------------------------------- 1 | {% if admin_view %}Success!{% else %}Failure{% endif %} -------------------------------------------------------------------------------- /babel.cfg: -------------------------------------------------------------------------------- 1 | [python: **.py] 2 | [jinja2: **/templates/**.html] 3 | extensions=jinja2.ext.autoescape,jinja2.ext.with_ 4 | -------------------------------------------------------------------------------- /babel/babel.ini: -------------------------------------------------------------------------------- 1 | # Python 2 | [python: **.py] 3 | # Jinja2 4 | [jinja2: **/templates/**.html] 5 | encoding = utf-8 6 | -------------------------------------------------------------------------------- /flask_superadmin/templates/admin/master.html: -------------------------------------------------------------------------------- 1 | DEPRECATED TEMPLATE: use admin/layout.html instead of admin/master.html -------------------------------------------------------------------------------- /screenshots/model-edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syrusakbary/Flask-SuperAdmin/HEAD/screenshots/model-edit.png -------------------------------------------------------------------------------- /screenshots/model-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syrusakbary/Flask-SuperAdmin/HEAD/screenshots/model-list.png -------------------------------------------------------------------------------- /flask_superadmin/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import expose, Admin, BaseView, AdminIndexView 2 | from model import ModelAdmin 3 | 4 | -------------------------------------------------------------------------------- /flask_superadmin/templates/admin/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/layout.html' %} 2 | 3 | {% block body %} 4 | {% endblock %} 5 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask>=0.7 2 | Flask-WTF>=0.9 3 | Flask-SQLAlchemy>=0.15 4 | mongoengine 5 | flask-mongoengine>=0.7 6 | Django>=1.3 7 | -------------------------------------------------------------------------------- /doc/images/quickstart/quickstart_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syrusakbary/Flask-SuperAdmin/HEAD/doc/images/quickstart/quickstart_1.png -------------------------------------------------------------------------------- /doc/images/quickstart/quickstart_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syrusakbary/Flask-SuperAdmin/HEAD/doc/images/quickstart/quickstart_2.png -------------------------------------------------------------------------------- /doc/images/quickstart/quickstart_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syrusakbary/Flask-SuperAdmin/HEAD/doc/images/quickstart/quickstart_3.png -------------------------------------------------------------------------------- /doc/images/quickstart/quickstart_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syrusakbary/Flask-SuperAdmin/HEAD/doc/images/quickstart/quickstart_4.png -------------------------------------------------------------------------------- /doc/images/quickstart/quickstart_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syrusakbary/Flask-SuperAdmin/HEAD/doc/images/quickstart/quickstart_5.png -------------------------------------------------------------------------------- /flask_superadmin/static/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syrusakbary/Flask-SuperAdmin/HEAD/flask_superadmin/static/img/favicon.png -------------------------------------------------------------------------------- /flask_superadmin/static/img/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syrusakbary/Flask-SuperAdmin/HEAD/flask_superadmin/static/img/background.png -------------------------------------------------------------------------------- /babel/babel.bat: -------------------------------------------------------------------------------- 1 | pybabel extract -F babel.ini -k _gettext -k _ngettext -k lazy_gettext -o admin.pot --project Flask-SuperAdmin ..\flask_superadmin 2 | -------------------------------------------------------------------------------- /examples/quickstart/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/layout.html' %} 2 | {% block body %} 3 | Hello World from MyView! 4 | {% endblock %} 5 | -------------------------------------------------------------------------------- /examples/simple/templates/myadmin.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/layout.html' %} 2 | {% block body %} 3 | Hello World from MyAdmin! 4 | {% endblock %} 5 | -------------------------------------------------------------------------------- /babel/babel.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | pybabel extract -F babel.ini -k _gettext -k _ngettext -k lazy_gettext -o admin.pot --project Flask-SuperAdmin ../flask_superadmin 3 | -------------------------------------------------------------------------------- /flask_superadmin/static/chosen/chosen-sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syrusakbary/Flask-SuperAdmin/HEAD/flask_superadmin/static/chosen/chosen-sprite.png -------------------------------------------------------------------------------- /flask_superadmin/static/img/search-input-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syrusakbary/Flask-SuperAdmin/HEAD/flask_superadmin/static/img/search-input-icon.png -------------------------------------------------------------------------------- /examples/simple/templates/test.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/layout.html' %} 2 | {% block body %} 3 | This is TEST subitem from the AnotherAdminView! 4 | {% endblock %} 5 | -------------------------------------------------------------------------------- /examples/quickstart/first.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask.ext.superadmin import Admin 3 | 4 | 5 | app = Flask(__name__) 6 | 7 | admin = Admin(app) 8 | app.run() 9 | -------------------------------------------------------------------------------- /flask_superadmin/static/bootstrap/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syrusakbary/Flask-SuperAdmin/HEAD/flask_superadmin/static/bootstrap/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /flask_superadmin/static/css/file.css: -------------------------------------------------------------------------------- 1 | #file-paths { 2 | list-style:none; 3 | margin:0; 4 | margin-bottom:10px; 5 | padding:0; 6 | } 7 | #file-paths li { 8 | display: inline-block; 9 | } -------------------------------------------------------------------------------- /doc/api/index.rst: -------------------------------------------------------------------------------- 1 | API 2 | === 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | mod_base 8 | mod_model 9 | mod_form 10 | mod_tools 11 | 12 | mod_contrib_fileadmin 13 | -------------------------------------------------------------------------------- /doc/api/mod_form.rst: -------------------------------------------------------------------------------- 1 | ``flask.ext.superadmin.form`` 2 | ======================== 3 | 4 | .. automodule:: flask.ext.superadmin.form 5 | 6 | .. autoclass:: BaseForm 7 | :members: 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.6" 4 | - "2.7" 5 | 6 | install: "pip install -r requirements.txt --use-mirrors" 7 | 8 | script: nosetests 9 | 10 | services: mongodb 11 | -------------------------------------------------------------------------------- /flask_superadmin/static/bootstrap/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syrusakbary/Flask-SuperAdmin/HEAD/flask_superadmin/static/bootstrap/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | recursive-include flask_superadmin/static * 4 | recursive-include flask_superadmin/templates * 5 | recursive-include flask_superadmin/translations * 6 | -------------------------------------------------------------------------------- /examples/simple/templates/anotheradmin.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/layout.html' %} 2 | {% block body %} 3 | Hello World from AnotherMyAdmin!
4 | Click me to go to test view 5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /flask_superadmin/contrib/djangomodel.py: -------------------------------------------------------------------------------- 1 | from flask_superadmin.contrib import DeprecatedModelView 2 | 3 | from flask_superadmin.model.backends.django import ModelAdmin 4 | 5 | 6 | class ModelView(DeprecatedModelView, ModelAdmin): 7 | pass 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | *.swp 3 | *.swo 4 | *.pyc 5 | *.*~ 6 | *.egg 7 | *.egg-info 8 | pyenv 9 | node_modules 10 | #*# 11 | build 12 | source/_static* 13 | source/_templates* 14 | make.bat 15 | venv 16 | dist/* 17 | *.sqlite 18 | *.mo 19 | -------------------------------------------------------------------------------- /flask_superadmin/contrib/sqlamodel.py: -------------------------------------------------------------------------------- 1 | from flask_superadmin.contrib import DeprecatedModelView 2 | 3 | from flask_superadmin.model.backends.sqlalchemy import ModelAdmin 4 | 5 | 6 | class ModelView(DeprecatedModelView, ModelAdmin): 7 | pass 8 | -------------------------------------------------------------------------------- /flask_superadmin/contrib/mongoenginemodel.py: -------------------------------------------------------------------------------- 1 | from flask_superadmin.contrib import DeprecatedModelView 2 | 3 | from flask_superadmin.model.backends.mongoengine import ModelAdmin 4 | 5 | 6 | class ModelView(DeprecatedModelView, ModelAdmin): 7 | pass 8 | -------------------------------------------------------------------------------- /doc/_themes/flask/theme.conf: -------------------------------------------------------------------------------- 1 | [theme] 2 | inherit = basic 3 | stylesheet = flasky.css 4 | pygments_style = flask_theme_support.FlaskyStyle 5 | 6 | [options] 7 | index_logo = 8 | index_logo_height = 120px 9 | touch_icon = 10 | github_fork = 'MrJoes/Flask-Admin' 11 | -------------------------------------------------------------------------------- /doc/_themes/flask_small/theme.conf: -------------------------------------------------------------------------------- 1 | [theme] 2 | inherit = basic 3 | stylesheet = flasky.css 4 | nosidebar = true 5 | pygments_style = flask_theme_support.FlaskyStyle 6 | 7 | [options] 8 | index_logo = 9 | index_logo_height = 120px 10 | github_fork = 'MrJoes/Flask-Admin' 11 | 12 | -------------------------------------------------------------------------------- /flask_superadmin/model/backends/sqlalchemy/tools.py: -------------------------------------------------------------------------------- 1 | def parse_like_term(term): 2 | if term.startswith('^'): 3 | stmt = '%s%%' % term[1:] 4 | elif term.startswith('='): 5 | stmt = term[1:] 6 | else: 7 | stmt = '%%%s%%' % term 8 | 9 | return stmt 10 | -------------------------------------------------------------------------------- /doc/api/mod_tools.rst: -------------------------------------------------------------------------------- 1 | ``flask.ext.superadmin.tools`` 2 | ========================= 3 | 4 | .. automodule:: flask.ext.superadmin.tools 5 | 6 | .. autofunction:: import_module 7 | .. autofunction:: import_attribute 8 | .. autofunction:: module_not_found 9 | .. autofunction:: rec_getattr 10 | 11 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Flask-SuperAdmin is written and maintained by Syrus Akbary and 2 | various contributors: 3 | 4 | Lead Developer: 5 | 6 | - Syrus Akbary 7 | 8 | Contributors: 9 | 10 | - Stefan Wójcik 11 | - Axel Haustant 12 | 13 | Patches and suggestions: 14 | 15 | - Andrey Martyanov 16 | -------------------------------------------------------------------------------- /examples/quickstart/second.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask.ext.superadmin import Admin, BaseView, expose 3 | 4 | 5 | class MyView(BaseView): 6 | @expose('/') 7 | def index(self): 8 | return self.render('index.html') 9 | 10 | app = Flask(__name__) 11 | 12 | admin = Admin(app) 13 | admin.add_view(MyView(name='Hello')) 14 | 15 | app.run() 16 | -------------------------------------------------------------------------------- /flask_superadmin/templates/admin/file/rename.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/layout.html' %} 2 | {% import 'admin/_macros.html' as lib with context %} 3 | 4 | {% block body %} 5 |

{{ _gettext('Please provide new name for %(name)s', name=name) }}

6 |
/{{ base_path.split('/')[-1] }}/{% if path %}{{path}}/{% endif %} 7 | {{ lib.render_form(form, dir_url) }} 8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /flask_superadmin/templates/admin/file/form.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/layout.html' %} 2 | {% import 'admin/_macros.html' as lib with context %} 3 | 4 | {% block body %} 5 |

{{_gettext('Files')}}{%if msg %} - {{msg}} {% endif %}

6 |
7 | /{{ base_path.split('/')[-1] }}/{% if path %}{{path}}/{% endif %} 8 |
9 | {{ lib.render_form(form, dir_url) }} 10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /doc/api/mod_base.rst: -------------------------------------------------------------------------------- 1 | ``flask.ext.superadmin.base`` 2 | ======================== 3 | 4 | .. automodule:: flask.ext.superadmin.base 5 | 6 | Base View 7 | --------- 8 | 9 | .. autofunction:: expose 10 | 11 | .. autoclass:: BaseView 12 | :members: 13 | 14 | Default view 15 | ------------ 16 | 17 | .. autoclass:: AdminIndexView 18 | :members: 19 | 20 | Admin 21 | ----- 22 | 23 | .. autoclass:: Admin 24 | :members: -------------------------------------------------------------------------------- /examples/quickstart/third.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask.ext.superadmin import Admin, BaseView, expose 3 | 4 | class MyView(BaseView): 5 | @expose('/') 6 | def index(self): 7 | return self.render('index.html') 8 | 9 | app = Flask(__name__) 10 | 11 | admin = Admin(app) 12 | admin.add_view(MyView(name='Hello 1', endpoint='test1', category='Test')) 13 | admin.add_view(MyView(name='Hello 2', endpoint='test2', category='Test')) 14 | admin.add_view(MyView(name='Hello 3', endpoint='test3', category='Test')) 15 | app.run() 16 | -------------------------------------------------------------------------------- /doc/_templates/sidebarintro.html: -------------------------------------------------------------------------------- 1 |

Useful Links

2 | 7 | 8 | Fork me on GitHub 10 | -------------------------------------------------------------------------------- /examples/auth/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | {% if user and user.is_authenticated() %} 5 | Hello {{ user.login }}! Logout 6 | {% else %} 7 | Welcome anonymous user! 8 | Login Register 9 | {% endif %} 10 |
11 |
12 | Go to admin! 13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/auth/templates/form.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | {{ form.hidden_tag() }} 5 | {% for f in form if f.label.text != 'Csrf' %} 6 |
7 | {{ f.label }} 8 | {{ f }} 9 | {% if f.errors %} 10 |
    11 | {% for e in f.errrors %} 12 |
  • {{ e }}
  • 13 | {% endfor %} 14 |
15 | {% endif %} 16 |
17 | {% endfor %} 18 | 19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [nosetests] 2 | verbosity = 3 3 | detailed-errors=1 4 | debug=nose.loader 5 | where=flask_superadmin/tests 6 | 7 | [extract_messages] 8 | keywords = _ gettext ngettext 9 | mapping_file = babel.cfg 10 | width = 80 11 | output_file = flask_superadmin/translations/admin.pot 12 | 13 | [init_catalog] 14 | input_file = flask_superadmin/translations/admin.pot 15 | output_dir = flask_superadmin/translations/ 16 | domain = admin 17 | 18 | [update_catalog] 19 | input_file = flask_superadmin/translations/admin.pot 20 | output_dir = flask_superadmin/translations/ 21 | domain = admin 22 | 23 | [compile_catalog] 24 | directory = flask_superadmin/translations/ 25 | domain = admin 26 | -------------------------------------------------------------------------------- /doc/_themes/flask/relations.html: -------------------------------------------------------------------------------- 1 |

Related Topics

2 | 20 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | // Grunt config file to help with static JS assets - run the requirejs optimizer, compilation, etc. 2 | // https://github.com/gruntjs/grunt/ 3 | module.exports = function(grunt) { 4 | 5 | grunt.loadNpmTasks('grunt-contrib'); 6 | 7 | grunt.initConfig({ 8 | 9 | jshint: { 10 | all: [ 11 | 'flask_superadmin/static/js/filters.js', 12 | 'flask_superadmin/static/js/form.js' 13 | ], 14 | options: { 15 | scripturl: true, 16 | expr: true, 17 | multistr: true 18 | } 19 | }, 20 | 21 | }); 22 | 23 | grunt.registerTask('lint', 'jshint'); 24 | 25 | }; 26 | -------------------------------------------------------------------------------- /flask_superadmin/contrib/__init__.py: -------------------------------------------------------------------------------- 1 | from flask_superadmin.model import ModelAdmin 2 | 3 | 4 | def print_kwargs(d): 5 | return ', '.join(['%s=...' % k for k in d.keys()]) 6 | 7 | 8 | class DeprecatedModelView(object): 9 | def __init__(self, model, *args, **kwargs): 10 | import warnings 11 | msg = "The %s class is deprecated, use superadmin.model.ModelAdmin instead.\nOr just do admin.register(%s%s) for register your models."% ( 12 | self.__class__.__name__, 13 | model.__name__, 14 | (', %s'%print_kwargs(kwargs) if kwargs else ''), 15 | ) 16 | warnings.warn(msg, FutureWarning, stacklevel=2) 17 | super(DeprecatedModelView, self).__init__(model, *args, **kwargs) 18 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | Flask-SuperAdmin 2 | ================ 3 | 4 | Flask-Superadmin he **best** admin interface framework for `Flask `_. As good as Django admin. 5 | 6 | Batteries included: 7 | 8 | * Admin interface 9 | * **Scaffolding for MongoEngine, Django and SQLAlchemy** 10 | * File administrator (optional) 11 | 12 | Requirements: 13 | 14 | * `Flask`_ 15 | * `WTForms `_ 16 | 17 | 18 | This package is a vitamined fork of `Flask-Admin `_. 19 | 20 | .. toctree:: 21 | :maxdepth: 2 22 | 23 | quickstart 24 | model_guidelines 25 | api/index 26 | 27 | 28 | Indices and tables 29 | ------------------ 30 | 31 | * :ref:`genindex` 32 | * :ref:`modindex` 33 | * :ref:`search` 34 | -------------------------------------------------------------------------------- /doc/_themes/flask_small/layout.html: -------------------------------------------------------------------------------- 1 | {% extends "basic/layout.html" %} 2 | {% block header %} 3 | {{ super() }} 4 | {% if pagename == 'index' %} 5 |
6 | {% endif %} 7 | {% endblock %} 8 | {% block footer %} 9 | {% if pagename == 'index' %} 10 |
11 | {% endif %} 12 | {% endblock %} 13 | {# do not display relbars #} 14 | {% block relbar1 %}{% endblock %} 15 | {% block relbar2 %} 16 | {% if theme_github_fork %} 17 | Fork me on GitHub 19 | {% endif %} 20 | {% endblock %} 21 | {% block sidebar1 %}{% endblock %} 22 | {% block sidebar2 %}{% endblock %} 23 | -------------------------------------------------------------------------------- /doc/api/mod_contrib_sqlamodel.rst: -------------------------------------------------------------------------------- 1 | ``flask.ext.superadmin.contrib.sqlamodel`` 2 | ===================================== 3 | 4 | .. automodule:: flask.ext.superadmin.contrib.sqlamodel 5 | 6 | .. autoclass:: ModelView 7 | :members: 8 | :inherited-members: 9 | :exclude-members: hide_backrefs, auto_select_related, list_select_related, 10 | searchable_columns, filter_converter 11 | 12 | Class inherits configuration options from :class:`~flask.ext.superadmin.model.BaseModelView` and they're not displayed here. 13 | 14 | .. autoattribute:: hide_backrefs 15 | .. autoattribute:: auto_select_related 16 | .. autoattribute:: list_select_related 17 | .. autoattribute:: searchable_columns 18 | .. autoattribute:: filter_converter 19 | -------------------------------------------------------------------------------- /doc/_themes/flask/layout.html: -------------------------------------------------------------------------------- 1 | {%- extends "basic/layout.html" %} 2 | {%- block extrahead %} 3 | {{ super() }} 4 | {% if theme_touch_icon %} 5 | 6 | {% endif %} 7 | 9 | {% endblock %} 10 | {%- block relbar2 %}{% endblock %} 11 | {% block header %} 12 | {{ super() }} 13 | {% if pagename == 'index' %} 14 |
15 | {% endif %} 16 | {% endblock %} 17 | {%- block footer %} 18 | 22 | {% if pagename == 'index' %} 23 |
24 | {% endif %} 25 | {%- endblock %} 26 | -------------------------------------------------------------------------------- /TODO.txt: -------------------------------------------------------------------------------- 1 | - Core 2 | - View Site button? 3 | - Localization 4 | - Create documentation 5 | - Model Admin 6 | - Reduce number of parameters passed to list view 7 | - Checkboxes and mass operations 8 | - Filters 9 | - Use table to draw filters so column names will line up? 10 | - Custom filters for date fields? 11 | - Change boolean filter to True/False instead of Yes/No 12 | - Ability to sort by fields that are not visible? 13 | - List display callables? 14 | - SQLA Model Admin 15 | - Postprocess sort columns - do not resolve to attributes in runtime 16 | - Many2Many support 17 | - Verify if it is working properly 18 | - WYSIWYG editor support? 19 | - File admin 20 | - Mass-delete functionality 21 | - File size restriction 22 | - Unit tests 23 | - Form generation tests 24 | - Documentation 25 | - Add all new stuff 26 | -------------------------------------------------------------------------------- /doc/api/mod_contrib_fileadmin.rst: -------------------------------------------------------------------------------- 1 | ``flask.ext.superadmin.contrib.fileadmin`` 2 | ===================================== 3 | 4 | .. automodule:: flask.ext.superadmin.contrib.fileadmin 5 | 6 | .. autoclass:: FileAdmin 7 | :members: 8 | :exclude-members: can_upload, can_delete, can_delete_dirs, can_mkdir, can_rename, 9 | allowed_extensions, list_template, upload_template, mkdir_template, 10 | rename_template 11 | 12 | .. autoattribute:: can_upload 13 | .. autoattribute:: can_delete 14 | .. autoattribute:: can_delete_dirs 15 | .. autoattribute:: can_mkdir 16 | .. autoattribute:: can_rename 17 | .. autoattribute:: allowed_extensions 18 | .. autoattribute:: list_template 19 | .. autoattribute:: upload_template 20 | .. autoattribute:: mkdir_template 21 | .. autoattribute:: rename_template 22 | -------------------------------------------------------------------------------- /examples/file/file.py: -------------------------------------------------------------------------------- 1 | import os 2 | import os.path as op 3 | 4 | from flask import Flask 5 | 6 | from flask.ext import superadmin 7 | from flask.ext.superadmin.contrib import fileadmin 8 | 9 | 10 | # Create flask app 11 | app = Flask(__name__, template_folder='templates', static_folder='files') 12 | 13 | # Create dummy secrey key so we can use flash 14 | app.config['SECRET_KEY'] = '123456790' 15 | 16 | 17 | # Flask views 18 | @app.route('/') 19 | def index(): 20 | return 'Click me to get to Admin!' 21 | 22 | 23 | if __name__ == '__main__': 24 | # Create directory 25 | path = op.join(op.dirname(__file__), 'files') 26 | try: 27 | os.mkdir(path) 28 | except OSError: 29 | pass 30 | 31 | # Create admin interface 32 | admin = superadmin.Admin(app) 33 | admin.add_view(fileadmin.FileAdmin(path, '/files/', name='Files')) 34 | 35 | # Start app 36 | app.debug = True 37 | app.run() 38 | -------------------------------------------------------------------------------- /doc/api/mod_model.rst: -------------------------------------------------------------------------------- 1 | ``flask.ext.superadmin.model`` 2 | ========================= 3 | 4 | .. automodule:: flask.ext.superadmin.model 5 | 6 | .. autoclass:: BaseModelAdmin 7 | :members: 8 | :exclude-members: can_create, can_edit, can_delete, list_template, edit_template, 9 | create_template, list_columns, excluded_list_columns, rename_columns, 10 | sortable_columns, searchable_columns, column_filters, form, form_columns, 11 | excluded_form_columns, form_args, form_overrides, page_size 12 | 13 | .. autoattribute:: can_create 14 | .. autoattribute:: can_edit 15 | .. autoattribute:: can_delete 16 | 17 | .. autoattribute:: list_template 18 | .. autoattribute:: edit_template 19 | .. autoattribute:: create_template 20 | 21 | .. autoattribute:: list_display 22 | 23 | .. autoattribute:: form 24 | 25 | .. autoattribute:: fields 26 | 27 | .. autoattribute:: page_size 28 | -------------------------------------------------------------------------------- /flask_superadmin/static/js/jquery.browser.js: -------------------------------------------------------------------------------- 1 | jQuery.uaMatch = function( ua ) { 2 | ua = ua.toLowerCase(); 3 | 4 | var match = /(chrome)[ \/]([\w.]+)/.exec( ua ) || 5 | /(webkit)[ \/]([\w.]+)/.exec( ua ) || 6 | /(opera)(?:.*version|)[ \/]([\w.]+)/.exec( ua ) || 7 | /(msie) ([\w.]+)/.exec( ua ) || 8 | ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec( ua ) || 9 | []; 10 | 11 | return { 12 | browser: match[ 1 ] || "", 13 | version: match[ 2 ] || "0" 14 | }; 15 | }; 16 | 17 | // Don't clobber any existing jQuery.browser in case it's different 18 | if ( !jQuery.browser ) { 19 | matched = jQuery.uaMatch( navigator.userAgent ); 20 | browser = {}; 21 | 22 | if ( matched.browser ) { 23 | browser[ matched.browser ] = true; 24 | browser.version = matched.version; 25 | } 26 | 27 | // Chrome is Webkit, but Webkit is also Safari. 28 | if ( browser.chrome ) { 29 | browser.webkit = true; 30 | } else if ( browser.webkit ) { 31 | browser.safari = true; 32 | } 33 | 34 | jQuery.browser = browser; 35 | } 36 | 37 | -------------------------------------------------------------------------------- /examples/simple/simple.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template 2 | 3 | from flask.ext import superadmin 4 | 5 | 6 | # Create custom admin view 7 | class MyAdminView(superadmin.BaseView): 8 | @superadmin.expose('/') 9 | def index(self): 10 | return self.render('myadmin.html') 11 | 12 | 13 | class AnotherAdminView(superadmin.BaseView): 14 | @superadmin.expose('/') 15 | def index(self): 16 | return self.render('anotheradmin.html') 17 | 18 | @superadmin.expose('/test/') 19 | def test(self): 20 | return self.render('test.html') 21 | 22 | 23 | # Create flask app 24 | app = Flask(__name__, template_folder='templates') 25 | 26 | 27 | # Flask views 28 | @app.route('/') 29 | def index(): 30 | return 'Click me to get to Admin!' 31 | 32 | 33 | if __name__ == '__main__': 34 | # Create admin interface 35 | admin = superadmin.Admin() 36 | admin.add_view(MyAdminView(category='Test')) 37 | admin.add_view(AnotherAdminView(category='Test')) 38 | admin.init_app(app) 39 | 40 | # Start app 41 | app.debug = True 42 | app.run() 43 | -------------------------------------------------------------------------------- /doc/model_guidelines.rst: -------------------------------------------------------------------------------- 1 | Adding new model backend 2 | ======================== 3 | 4 | If you want to implement new database backend to use with model views, follow steps found in this guideline. 5 | 6 | There are few assumptions about models: 7 | 8 | 1. Model has "primary key" - value which uniquely identifies 9 | one model in a data store. There's no restriction on the 10 | data type or field name. 11 | 2. Model has readable python properties 12 | 3. It is possible to get list of models (optionally - sorted, 13 | filtered, etc) from data store 14 | 4. It is possible to get one model by its primary key 15 | 16 | 17 | Steps to add new model backend: 18 | 19 | 1. Create new class and derive it from :class:`~flask.ext.superadmin.model.base.BaseModelAdmin`:: 20 | 21 | class MyDbModel(BaseModelView): 22 | pass 23 | 24 | By default, all model views accept model class and it 25 | will be stored as ``self.model``. 26 | 27 | 2. **PLEASE VIEW** :class:`~flask.ext.superadmin.model.backends.sqlalchemy.ModelAdmin` for how to do a new backend. 28 | 29 | Feel free ask questions if you have problem adding new model backend. 30 | -------------------------------------------------------------------------------- /flask_superadmin/babel.py: -------------------------------------------------------------------------------- 1 | try: 2 | try: 3 | from flask.ext.babelex import Domain 4 | except: 5 | from flask.ext.babel import Domain 6 | 7 | from flask.ext.superadmin import translations 8 | 9 | class CustomDomain(Domain): 10 | def __init__(self): 11 | super(CustomDomain, self).__init__(translations.__path__[0], 12 | domain='admin') 13 | 14 | def get_translations_path(self, ctx): 15 | print ctx 16 | 17 | dirname = ctx.app.extensions['admin'].translations_path 18 | if dirname is not None: 19 | return dirname 20 | 21 | return super(CustomDomain, self).get_translations_path(ctx) 22 | 23 | domain = CustomDomain() 24 | 25 | gettext = domain.gettext 26 | ngettext = domain.ngettext 27 | lazy_gettext = domain.lazy_gettext 28 | except ImportError: 29 | def gettext(string, **variables): 30 | return string % variables 31 | 32 | def ngettext(singular, plural, num, **variables): 33 | return (singular if num == 1 else plural) % variables 34 | 35 | def lazy_gettext(string, **variables): 36 | return gettext(string, **variables) 37 | -------------------------------------------------------------------------------- /doc/_themes/README: -------------------------------------------------------------------------------- 1 | Flask Sphinx Styles 2 | =================== 3 | 4 | This repository contains sphinx styles for Flask and Flask related 5 | projects. To use this style in your Sphinx documentation, follow 6 | this guide: 7 | 8 | 1. put this folder as _themes into your docs folder. Alternatively 9 | you can also use git submodules to check out the contents there. 10 | 2. add this to your conf.py: 11 | 12 | sys.path.append(os.path.abspath('_themes')) 13 | html_theme_path = ['_themes'] 14 | html_theme = 'flask' 15 | 16 | The following themes exist: 17 | 18 | - 'flask' - the standard flask documentation theme for large 19 | projects 20 | - 'flask_small' - small one-page theme. Intended to be used by 21 | very small addon libraries for flask. 22 | 23 | The following options exist for the flask_small theme: 24 | 25 | [options] 26 | index_logo = '' filename of a picture in _static 27 | to be used as replacement for the 28 | h1 in the index.rst file. 29 | index_logo_height = 120px height of the index logo 30 | github_fork = '' repository name on github for the 31 | "fork me" badge 32 | -------------------------------------------------------------------------------- /flask_superadmin/templates/admin/model/add.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/layout.html' %} 2 | {% import 'admin/_macros.html' as lib with context %} 3 | {% set name = admin_view.get_display_name() %} 4 | 5 | {% block head_css %} 6 | 7 | 8 | {{super()}} 9 | {% endblock %} 10 | 11 | {% block body %} 12 |

{{ _gettext('Add %(model)s',model=name|capitalize) }}

13 |
14 |
15 | 16 |
17 | {% macro extra() %} 18 | 19 | 20 | {% endmacro %} 21 | 22 | {{ lib.render_form(form, extra(), admin_view.can_edit, admin_view.can_delete) }} 23 |
24 | {% endblock %} 25 | 26 | {% block tail %} 27 | 28 | 29 | {% endblock %} 30 | -------------------------------------------------------------------------------- /doc/_themes/flask/static/small_flask.css: -------------------------------------------------------------------------------- 1 | /* 2 | * small_flask.css_t 3 | * ~~~~~~~~~~~~~~~~~ 4 | * 5 | * :copyright: Copyright 2010 by Armin Ronacher. 6 | * :license: Flask Design License, see LICENSE for details. 7 | */ 8 | 9 | body { 10 | margin: 0; 11 | padding: 20px 30px; 12 | } 13 | 14 | div.documentwrapper { 15 | float: none; 16 | background: white; 17 | } 18 | 19 | div.sphinxsidebar { 20 | display: block; 21 | float: none; 22 | width: 102.5%; 23 | margin: 50px -30px -20px -30px; 24 | padding: 10px 20px; 25 | background: #333; 26 | color: white; 27 | } 28 | 29 | div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p, 30 | div.sphinxsidebar h3 a { 31 | color: white; 32 | } 33 | 34 | div.sphinxsidebar a { 35 | color: #aaa; 36 | } 37 | 38 | div.sphinxsidebar p.logo { 39 | display: none; 40 | } 41 | 42 | div.document { 43 | width: 100%; 44 | margin: 0; 45 | } 46 | 47 | div.related { 48 | display: block; 49 | margin: 0; 50 | padding: 10px 0 20px 0; 51 | } 52 | 53 | div.related ul, 54 | div.related ul li { 55 | margin: 0; 56 | padding: 0; 57 | } 58 | 59 | div.footer { 60 | display: none; 61 | } 62 | 63 | div.bodywrapper { 64 | margin: 0; 65 | } 66 | 67 | div.body { 68 | min-height: 0; 69 | padding: 0; 70 | } 71 | -------------------------------------------------------------------------------- /flask_superadmin/templates/admin/model/edit.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/layout.html' %} 2 | {% import 'admin/_macros.html' as lib with context %} 3 | {% set name = admin_view.get_display_name() %} 4 | 5 | {% block head_css %} 6 | 7 | 8 | {{super()}} 9 | {% endblock %} 10 | 11 | {% block body %} 12 |

{{ _gettext('Edit %(model)s',model=name|capitalize) }}

13 |
14 |
15 | 16 |
17 | {% macro extra() %} 18 | {% if admin_view.can_delete %} 19 | {{ _gettext('Delete') }} 20 | {% endif %} 21 | {% if admin_view.can_edit %} 22 | 23 | {% endif %} 24 | {% endmacro %} 25 | 26 | {{ lib.render_form(form, extra(), admin_view.can_edit, admin_view.can_delete) }} 27 |
28 | {% endblock %} 29 | 30 | {% block tail %} 31 | 32 | 33 | {% endblock %} 34 | -------------------------------------------------------------------------------- /flask_superadmin/templates/admin/model/delete.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/layout.html' %} 2 | {% set name = admin_view.get_display_name() %} 3 | 4 | {% block body %} 5 |

{{ _gettext('Delete') }}

6 |
7 |
8 | 9 |
10 |
11 | {% if csrf_token %} 12 | 13 | {% endif %} 14 | 15 |

Do you really want to delete all this models?

16 |
    17 | {% for instance in instances %} 18 | {% set pk = admin_view.get_pk(instance) %} 19 |
  • 20 | 21 | {{instance|string or 'None'}} 22 |
  • 23 | {% endfor %} 24 |
25 |
26 | 27 | {{ _gettext('Cancel') }} 28 |
29 |
30 |
31 | {% endblock %} 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012, Syrus Akbary, Serge S. Koval and contributors. 2 | 3 | Some rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | * Names of the contributors may not be used to endorse or promote products 14 | derived from this software without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL SYRUS AKBARY OR SERGE KOVAL BE LIABLE FOR ANY 20 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Fix for older setuptools 2 | import multiprocessing, logging, os 3 | 4 | from setuptools import setup, find_packages 5 | 6 | 7 | def read(fname): 8 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 9 | 10 | 11 | def desc(): 12 | info = read('README.rst') 13 | try: 14 | return info + '\n\n' + read('doc/changelog.rst') 15 | except IOError: 16 | return info 17 | 18 | setup( 19 | name='Flask-SuperAdmin', 20 | version='1.7.1', 21 | url='https://github.com/syrusakbary/flask-superadmin/', 22 | license='BSD', 23 | author='Syrus Akbary', 24 | author_email='me@syrusakbary.com', 25 | description='The best admin interface framework for Python. With scaffolding for MongoEngine, Django and SQLAlchemy.', 26 | long_description=desc(), 27 | packages=find_packages(), 28 | include_package_data=True, 29 | zip_safe=False, 30 | platforms='any', 31 | install_requires=[ 32 | 'Flask>=0.7', 33 | 'Flask-WTF>=0.9' 34 | ], 35 | tests_require=[ 36 | 'nose>=1.0', 37 | 'Flask', 38 | 'flask-sqlalchemy', 39 | 'django', 40 | 'mongoengine' 41 | ], 42 | classifiers=[ 43 | 'Development Status :: 3 - Alpha', 44 | 'Environment :: Web Environment', 45 | 'Intended Audience :: Developers', 46 | 'License :: OSI Approved :: BSD License', 47 | 'Operating System :: OS Independent', 48 | 'Programming Language :: Python', 49 | 'Topic :: Software Development :: Libraries :: Python Modules' 50 | ], 51 | test_suite='nose.collector' 52 | ) 53 | -------------------------------------------------------------------------------- /examples/django/utils.py: -------------------------------------------------------------------------------- 1 | def install_models(*all_models): 2 | from django.core.management.color import no_style 3 | from django.core.management.sql import custom_sql_for_model, emit_post_sync_signal 4 | from django.db import connections, router, transaction, models, DEFAULT_DB_ALIAS 5 | db = 'default' 6 | connection = connections[db] 7 | cursor = connection.cursor() 8 | style = no_style() 9 | # Get a list of already installed *models* so that references work right. 10 | tables = connection.introspection.table_names() 11 | seen_models = connection.introspection.installed_models(tables) 12 | created_models = set() 13 | pending_references = {} 14 | def model_installed(model): 15 | opts = model._meta 16 | converter = connection.introspection.table_name_converter 17 | return not ((converter(opts.db_table) in tables) or 18 | (opts.auto_created and converter(opts.auto_created._meta.db_table) in tables)) 19 | 20 | for model in all_models: 21 | sql, references = connection.creation.sql_create_model(model, style, seen_models) 22 | seen_models.add(model) 23 | created_models.add(model) 24 | for refto, refs in references.items(): 25 | pending_references.setdefault(refto, []).extend(refs) 26 | if refto in seen_models: 27 | sql.extend(connection.creation.sql_for_pending_references(refto, style, pending_references)) 28 | sql.extend(connection.creation.sql_for_pending_references(model, style, pending_references)) 29 | for statement in sql: 30 | cursor.execute(statement) 31 | tables.append(connection.introspection.table_name_converter(model._meta.db_table)) 32 | 33 | transaction.commit_unless_managed(using=db) 34 | -------------------------------------------------------------------------------- /examples/django/simple.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask.ext.superadmin import Admin, model 3 | 4 | from utils import install_models 5 | 6 | 7 | #For using with django 8 | from django.conf import settings 9 | 10 | settings.configure( 11 | DATABASES = { 12 | 'default': { 13 | 'ENGINE': 'django.db.backends.sqlite3', 14 | 'NAME': 'mydatabase.sqlite', 15 | } 16 | } 17 | ) 18 | 19 | from django.db import models 20 | 21 | # Create application 22 | 23 | app = Flask(__name__) 24 | 25 | # Create dummy secrey key so we can use sessions 26 | app.config['SECRET_KEY'] = '123456790' 27 | 28 | 29 | class User(models.Model): 30 | class Meta: 31 | app_label = 'users' 32 | username = models.CharField(max_length=255,unique=True) 33 | email = models.CharField(max_length=255,unique=True) 34 | def __unicode__(self): 35 | return self.username 36 | 37 | 38 | class Post(models.Model): 39 | class Meta: 40 | app_label = 'posts' 41 | title = models.CharField(max_length=255) 42 | text = models.TextField() 43 | date = models.DateField() 44 | user = models.ForeignKey(User) 45 | def __unicode__(self): 46 | return self.title 47 | 48 | 49 | # Flask views 50 | @app.route('/') 51 | def index(): 52 | return 'Click me to get to Admin!' 53 | 54 | # Build the manifest of apps and models that are to be synchronized 55 | 56 | if __name__ == '__main__': 57 | # Create admin 58 | admin = Admin(app, 'Simple Models') 59 | 60 | # Add views 61 | admin.register(User) 62 | admin.register(Post) 63 | 64 | # Create tables in database if not exists 65 | try: 66 | install_models(User,Post) 67 | except: 68 | pass 69 | 70 | # Start app 71 | app.debug = True 72 | app.run('0.0.0.0', 8000) 73 | -------------------------------------------------------------------------------- /examples/mongoengine/simple.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask.ext.superadmin import Admin, model 3 | 4 | try: 5 | from mongoengine import * 6 | except ImportError: 7 | exit('You must have mongoengine installed. Install it with the command:\n\t$> easy_install mongoengine') 8 | 9 | # Create application 10 | app = Flask(__name__) 11 | 12 | # Create dummy secret key so we can use sessions 13 | app.config['SECRET_KEY'] = '123456790' 14 | 15 | mongodb_settings = { 16 | 'db':'test', 17 | # 'username':None, 18 | # 'password':None, 19 | # 'host':None, 20 | # 'port':None 21 | } 22 | 23 | # Connect to mongodb 24 | connect(**mongodb_settings) 25 | 26 | 27 | # Defining MongoEngine Documents 28 | 29 | class User(Document): 30 | username = StringField(unique=True) 31 | email = StringField(unique=True) 32 | def __unicode__(self): 33 | return self.username 34 | 35 | class ComplexEmbedded (EmbeddedDocument): 36 | complexstring = StringField() 37 | multiple_users = ListField(ReferenceField('User')) 38 | 39 | class Post(Document): 40 | user = ReferenceField(User) 41 | tags = ListField(StringField()) 42 | text = StringField() 43 | date = DateTimeField() 44 | complex = ListField(EmbeddedDocumentField(ComplexEmbedded)) 45 | 46 | 47 | # Flask views 48 | @app.route('/') 49 | def index(): 50 | return 'Click me to get to Admin!' 51 | 52 | if __name__ == '__main__': 53 | 54 | # Create admin 55 | admin = Admin(app, 'Simple Models') 56 | 57 | class UserModel(model.ModelAdmin): 58 | list_display = ('username','email') 59 | # only = ('username',) 60 | 61 | # Register the models 62 | admin.register(User, UserModel) 63 | admin.register(Post) 64 | 65 | # Start app 66 | app.debug = True 67 | app.run('0.0.0.0', 8000) 68 | -------------------------------------------------------------------------------- /doc/_themes/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 by Armin Ronacher. 2 | 3 | Some rights reserved. 4 | 5 | Redistribution and use in source and binary forms of the theme, with or 6 | without modification, are permitted provided that the following conditions 7 | are met: 8 | 9 | * Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above 13 | copyright notice, this list of conditions and the following 14 | disclaimer in the documentation and/or other materials provided 15 | with the distribution. 16 | 17 | * The names of the contributors may not be used to endorse or 18 | promote products derived from this software without specific 19 | prior written permission. 20 | 21 | We kindly ask you to only use these themes in an unmodified manner just 22 | for Flask and Flask-related products, not for unrelated projects. If you 23 | like the visual style and want to use it for your own projects, please 24 | consider making some larger changes to the themes (such as changing 25 | font faces, sizes, colors or margins). 26 | 27 | THIS THEME IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 28 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 29 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 30 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 31 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 32 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 33 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 34 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 35 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 36 | ARISING IN ANY WAY OUT OF THE USE OF THIS THEME, EVEN IF ADVISED OF THE 37 | POSSIBILITY OF SUCH DAMAGE. 38 | -------------------------------------------------------------------------------- /examples/babel/simple.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, session 2 | from flask.ext.sqlalchemy import SQLAlchemy 3 | 4 | from flask.ext.superadmin import Admin, model 5 | from flask.ext.babelex import Babel 6 | 7 | 8 | # Create application 9 | app = Flask(__name__) 10 | 11 | # Create dummy secrey key so we can use sessions 12 | app.config['SECRET_KEY'] = '12345678' 13 | 14 | # Create in-memory database 15 | app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///test.sqlite' 16 | app.config['SQLALCHEMY_ECHO'] = True 17 | db = SQLAlchemy(app) 18 | 19 | # Initialize babel 20 | babel = Babel(app) 21 | 22 | 23 | @babel.localeselector 24 | def get_locale(): 25 | override = request.args.get('lang') 26 | 27 | if override: 28 | session['lang'] = override 29 | 30 | return session.get('lang', 'en') 31 | 32 | 33 | # Create models 34 | class User(db.Model): 35 | id = db.Column(db.Integer, primary_key=True) 36 | username = db.Column(db.String(80), unique=True) 37 | email = db.Column(db.String(120), unique=True) 38 | 39 | # Required for administrative interface 40 | def __unicode__(self): 41 | return self.username 42 | 43 | 44 | class Post(db.Model): 45 | id = db.Column(db.Integer, primary_key=True) 46 | title = db.Column(db.String(120)) 47 | text = db.Column(db.Text, nullable=False) 48 | date = db.Column(db.DateTime) 49 | 50 | user_id = db.Column(db.Integer(), db.ForeignKey(User.id)) 51 | user = db.relationship(User, backref='posts') 52 | 53 | def __unicode__(self): 54 | return self.title 55 | 56 | 57 | # Flask views 58 | @app.route('/') 59 | def index(): 60 | return 'Click me to get to Admin!' 61 | 62 | if __name__ == '__main__': 63 | # Create admin 64 | admin = Admin(app, 'Simple Models') 65 | 66 | admin.locale_selector(get_locale) 67 | 68 | # Add views 69 | admin.register(User, session=db.session) 70 | admin.register(Post, session=db.session) 71 | 72 | # Create DB 73 | db.create_all() 74 | 75 | # Start app 76 | app.debug = True 77 | app.run('0.0.0.0', 8000) 78 | -------------------------------------------------------------------------------- /examples/sqlalchemy/simple.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask.ext.sqlalchemy import SQLAlchemy 3 | 4 | from flask.ext import wtf 5 | from flask.ext.superadmin import Admin, model 6 | 7 | # Create application 8 | app = Flask(__name__) 9 | 10 | # Create dummy secrey key so we can use sessions 11 | app.config['SECRET_KEY'] = '123456790' 12 | 13 | # Create in-memory database 14 | app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///test.sqlite' 15 | app.config['SQLALCHEMY_ECHO'] = True 16 | db = SQLAlchemy(app) 17 | 18 | 19 | # Create models 20 | class User(db.Model): 21 | id = db.Column(db.Integer, primary_key=True) 22 | username = db.Column(db.String(80), unique=True) 23 | email = db.Column(db.String(120), unique=True) 24 | 25 | # Required for administrative interface 26 | def __unicode__(self): 27 | return self.username 28 | 29 | 30 | # Create M2M table 31 | post_tags_table = db.Table('post_tags', db.Model.metadata, 32 | db.Column('post_id', db.Integer, db.ForeignKey('post.id')), 33 | db.Column('tag_id', db.Integer, db.ForeignKey('tag.id')) 34 | ) 35 | 36 | 37 | class Post(db.Model): 38 | id = db.Column(db.Integer, primary_key=True) 39 | title = db.Column(db.String(120)) 40 | text = db.Column(db.Text, nullable=False) 41 | date = db.Column(db.DateTime) 42 | 43 | user_id = db.Column(db.Integer(), db.ForeignKey(User.id)) 44 | user = db.relationship(User, backref='posts') 45 | 46 | tags = db.relationship('Tag', secondary=post_tags_table) 47 | 48 | def __unicode__(self): 49 | return self.title 50 | 51 | 52 | class Tag(db.Model): 53 | id = db.Column(db.Integer, primary_key=True) 54 | name = db.Column(db.Unicode(64)) 55 | 56 | def __unicode__(self): 57 | return self.name 58 | 59 | 60 | # Flask views 61 | @app.route('/') 62 | def index(): 63 | return 'Click me to get to Admin!' 64 | 65 | 66 | if __name__ == '__main__': 67 | # Create admin 68 | admin = Admin(app, 'Simple Models') 69 | 70 | # Add views 71 | admin.register(User, session=db.session) 72 | admin.register(Tag, session=db.session) 73 | admin.register(Post, session=db.session) 74 | # admin.add_view(sqlamodel.ModelView(Post, session=db.session)) 75 | 76 | # Create DB 77 | db.create_all() 78 | 79 | # Start app 80 | app.debug = True 81 | app.run('0.0.0.0', 8000) 82 | -------------------------------------------------------------------------------- /flask_superadmin/model/backends/django/view.py: -------------------------------------------------------------------------------- 1 | from flask_superadmin.model.base import BaseModelAdmin 2 | 3 | from orm import model_form, AdminModelConverter 4 | from django.db import models 5 | 6 | import operator 7 | 8 | class ModelAdmin(BaseModelAdmin): 9 | @staticmethod 10 | def model_detect(model): 11 | return issubclass(model, models.Model) 12 | 13 | def allow_pk(self): 14 | return False 15 | 16 | def get_model_form(self): 17 | return model_form 18 | 19 | def get_converter(self): 20 | return AdminModelConverter 21 | 22 | def get_queryset(self): 23 | return self.model.objects 24 | 25 | def get_objects(self, *pks): 26 | return self.get_queryset().filter(pk__in=pks) 27 | 28 | def get_object(self, pk): 29 | return self.model.objects.get(pk=pk) 30 | 31 | def get_pk(self, instance): 32 | return str(instance.id) 33 | 34 | def save_model(self, instance, form, adding=False): 35 | form.populate_obj(instance) 36 | instance.save() 37 | return instance 38 | 39 | def delete_models(self, *pks): 40 | self.get_objects(*pks).delete() 41 | return True 42 | 43 | def construct_search(self, field_name): 44 | if field_name.startswith('^'): 45 | return "%s__istartswith" % field_name[1:] 46 | elif field_name.startswith('='): 47 | return "%s__iexact" % field_name[1:] 48 | else: 49 | return "%s__icontains" % field_name 50 | 51 | def get_list(self, page=0, sort=None, sort_desc=None, execute=False, search_query=None): 52 | qs = self.get_queryset() 53 | 54 | # Filter by search query 55 | if search_query and self.search_fields: 56 | orm_lookups = [self.construct_search(str(search_field)) 57 | for search_field in self.search_fields] 58 | for bit in search_query.split(): 59 | or_queries = [models.Q(**{orm_lookup: bit}) 60 | for orm_lookup in orm_lookups] 61 | qs = qs.filter(reduce(operator.or_, or_queries)) 62 | 63 | #Calculate number of rows 64 | count = qs.count() 65 | 66 | #Order queryset 67 | if sort: 68 | qs = qs.order_by('%s%s' % ('-' if sort_desc else '', sort)) 69 | 70 | # Pagination 71 | if page is not None: 72 | qs = qs.all()[page * self.list_per_page:] 73 | qs = qs[:self.list_per_page] 74 | 75 | if execute: 76 | qs = list(qs) 77 | 78 | return count, qs 79 | -------------------------------------------------------------------------------- /flask_superadmin/static/js/form.js: -------------------------------------------------------------------------------- 1 | var AdminForm = function() { 2 | this.applyStyle = function(el, name) { 3 | switch (name) { 4 | case 'chosen': 5 | $(el).chosen(); 6 | break; 7 | case 'chosenblank': 8 | $(el).chosen({allow_single_deselect: true}); 9 | break; 10 | case 'datepicker': 11 | $(el).datepicker(); 12 | break; 13 | case 'datetimepicker': 14 | $(el).datepicker({displayTime: true}); 15 | break; 16 | } 17 | }; 18 | }; 19 | 20 | $(document).on('click', '.append', function(e) { 21 | e.preventDefault(); 22 | _this = $(this); 23 | parent = _this.parent(); 24 | len = parent.find('>.field').length; 25 | html = parent.find('>.append-list').html(); 26 | _new = $(html); 27 | _new.find('label,input,textarea,select').each(function(index) { 28 | __this = $(this); 29 | $.each(['id', 'name','for'], function(index, value) { 30 | v = __this.attr(value); 31 | if (v) { 32 | new_v = v.replace("__new__", len); 33 | __this.attr(value,new_v); 34 | } 35 | }); 36 | }); 37 | _this.before(_new); 38 | auto_apply(_new); 39 | _new.hide().fadeOut(0); 40 | _new.slideDown(200); 41 | _new.dequeue(); 42 | 43 | _new.fadeIn(200); 44 | return false; 45 | }); 46 | 47 | $(document).on('click', '.field>.delete',function() { 48 | var p = $(this).parent(); 49 | p.slideUp(200); 50 | p.dequeue(); 51 | p.fadeOut(200,function(){ p.remove(); }); 52 | }); 53 | 54 | $(document).on('click', 'legend > .delete',function() { 55 | $(this).parent().parent().remove(); 56 | }); 57 | 58 | $('.search-input').keydown(function(ev) { 59 | if (ev.keyCode === 13) { 60 | ev.preventDefault(); 61 | window.location.href = window.location.pathname + '?q=' + encodeURIComponent($(this).val()); 62 | } 63 | }); 64 | 65 | $('.search-input').on('input', function() { 66 | if ($(this).val().length) { 67 | $('.search .clear-btn').show(); 68 | } else { 69 | $('.search .clear-btn').hide(); 70 | } 71 | }); 72 | 73 | $('.search .clear-btn').click(function() { 74 | $('.search .search-input').val('').focus(); 75 | $(this).hide(); 76 | window.location.href = window.location.pathname; 77 | }); 78 | 79 | // Apply automatic styles 80 | function auto_apply(el) { 81 | el.find('[data-role=chosen]:visible').chosen(); 82 | el.find('[data-role=chosenblank]:visible').chosen({allow_single_deselect: true}); 83 | el.find('[data-role=datepicker]:visible').datepicker(); 84 | el.find('[data-role=datetimepicker]:visible').datepicker({displayTime: true}); 85 | } 86 | 87 | auto_apply($(document)); 88 | 89 | -------------------------------------------------------------------------------- /flask_superadmin/model/backends/mongoengine/view.py: -------------------------------------------------------------------------------- 1 | from flask_superadmin.model.base import BaseModelAdmin 2 | 3 | from orm import model_form, AdminModelConverter 4 | 5 | import operator 6 | import mongoengine 7 | 8 | from bson.objectid import ObjectId 9 | 10 | SORTABLE_FIELDS = ( 11 | mongoengine.BooleanField, 12 | mongoengine.DateTimeField, 13 | #mongoengine.DecimalField, 14 | mongoengine.FloatField, 15 | mongoengine.IntField, 16 | mongoengine.StringField, 17 | mongoengine.ReferenceField 18 | ) 19 | 20 | 21 | class ModelAdmin(BaseModelAdmin): 22 | @staticmethod 23 | def model_detect(model): 24 | return issubclass(model, mongoengine.Document) 25 | 26 | def allow_pk(self): 27 | return False 28 | 29 | def is_sortable(self, column): 30 | field = getattr(self.model, column, None) 31 | return isinstance(field, SORTABLE_FIELDS) 32 | 33 | def get_model_form(self): 34 | return model_form 35 | 36 | def get_converter(self): 37 | return AdminModelConverter 38 | 39 | def get_queryset(self): 40 | return self.model.objects 41 | 42 | def get_objects(self, *pks): 43 | return self.get_queryset().filter(pk__in=pks) 44 | 45 | def get_object(self, pk): 46 | return self.get_queryset().get(pk=pk) 47 | 48 | def get_pk(self, instance): 49 | return str(instance.id) 50 | 51 | def save_model(self, instance, form, adding=False): 52 | form.populate_obj(instance) 53 | instance.save() 54 | return instance 55 | 56 | def delete_models(self, *pks): 57 | for obj in self.get_objects(*pks): 58 | obj.delete() 59 | return True 60 | 61 | def construct_search(self, field_name): 62 | if field_name.startswith('^'): 63 | return "%s__istartswith" % field_name[1:] 64 | elif field_name.startswith('='): 65 | return "%s__iexact" % field_name[1:] 66 | else: 67 | return "%s__icontains" % field_name 68 | 69 | def get_list(self, page=0, sort=None, sort_desc=None, execute=False, search_query=None): 70 | qs = self.get_queryset() 71 | 72 | # Filter by search query 73 | if search_query and self.search_fields: 74 | orm_lookups = [self.construct_search(str(search_field)) 75 | for search_field in self.search_fields] 76 | for bit in search_query.split(): 77 | or_queries = [mongoengine.queryset.Q(**{orm_lookup: bit}) 78 | for orm_lookup in orm_lookups] 79 | qs = qs.filter(reduce(operator.or_, or_queries)) 80 | 81 | #Calculate number of documents 82 | count = qs.count() 83 | 84 | #Order queryset 85 | if sort: 86 | qs = qs.order_by('%s%s' % ('-' if sort_desc else '', sort)) 87 | 88 | # Pagination 89 | if page is not None: 90 | qs = qs.skip(page * self.list_per_page) 91 | qs = qs.limit(self.list_per_page) 92 | 93 | if execute: 94 | qs = qs.all() 95 | 96 | return count, qs 97 | 98 | -------------------------------------------------------------------------------- /flask_superadmin/tests/test_django.py: -------------------------------------------------------------------------------- 1 | from nose.tools import eq_, ok_, raises 2 | 3 | import wtforms 4 | 5 | from flask import Flask 6 | from flask_superadmin import Admin 7 | 8 | 9 | from django.conf import settings 10 | 11 | 12 | settings.configure( 13 | DATABASES = { 14 | 'default': { 15 | 'ENGINE': 'django.db.backends.sqlite3', 16 | 'NAME': 'mydatabase.sqlite', 17 | } 18 | } 19 | ) 20 | 21 | app = Flask(__name__) 22 | app.config['SECRET_KEY'] = '123456790' 23 | app.config['WTF_CSRF_ENABLED'] = False 24 | 25 | admin = Admin(app) 26 | 27 | from flask_superadmin.model.backends.django.view import ModelAdmin 28 | from django.db import models, DatabaseError 29 | from examples.django.utils import install_models 30 | 31 | 32 | class CustomModelView(ModelAdmin): 33 | def __init__(self, model, name=None, category=None, endpoint=None, 34 | url=None, **kwargs): 35 | for k, v in kwargs.iteritems(): 36 | setattr(self, k, v) 37 | 38 | super(CustomModelView, self).__init__(model, name, category, endpoint, 39 | url) 40 | 41 | def test_list(): 42 | class Person(models.Model): 43 | name = models.CharField(max_length=255) 44 | age = models.IntegerField() 45 | 46 | def __unicode__(self): 47 | return self.name 48 | 49 | # Create tables in the database if they don't exists 50 | try: 51 | install_models(Person) 52 | except DatabaseError, e: 53 | if 'already exists' not in e.message: 54 | raise 55 | 56 | Person.objects.all().delete() 57 | 58 | view = CustomModelView(Person) 59 | admin.add_view(view) 60 | 61 | eq_(view.model, Person) 62 | eq_(view.name, 'Person') 63 | eq_(view.endpoint, 'person') 64 | eq_(view.url, '/admin/person') 65 | 66 | # Verify form 67 | with app.test_request_context(): 68 | Form = view.get_form() 69 | ok_(isinstance(Form()._fields['name'], wtforms.TextField)) 70 | ok_(isinstance(Form()._fields['age'], wtforms.IntegerField)) 71 | 72 | # Make some test clients 73 | client = app.test_client() 74 | 75 | resp = client.get('/admin/person/') 76 | eq_(resp.status_code, 200) 77 | 78 | resp = client.get('/admin/person/add/') 79 | eq_(resp.status_code, 200) 80 | 81 | resp = client.post('/admin/person/add/', 82 | data=dict(name='name', age='18')) 83 | eq_(resp.status_code, 302) 84 | 85 | person = Person.objects.all()[0] 86 | eq_(person.name, 'name') 87 | eq_(person.age, 18) 88 | 89 | resp = client.get('/admin/person/') 90 | eq_(resp.status_code, 200) 91 | ok_(person.name in resp.data) 92 | 93 | resp = client.get('/admin/person/%s/' % person.pk) 94 | eq_(resp.status_code, 200) 95 | 96 | resp = client.post('/admin/person/%s/' % person.pk, data=dict(name='changed')) 97 | eq_(resp.status_code, 302) 98 | 99 | person = Person.objects.all()[0] 100 | eq_(person.name, 'changed') 101 | eq_(person.age, 18) 102 | 103 | resp = client.post('/admin/person/%s/delete/' % person.pk) 104 | eq_(resp.status_code, 200) 105 | eq_(Person.objects.count(), 1) 106 | 107 | resp = client.post('/admin/person/%s/delete/' % person.pk, data={'confirm_delete': True}) 108 | eq_(resp.status_code, 302) 109 | eq_(Person.objects.count(), 0) 110 | 111 | -------------------------------------------------------------------------------- /flask_superadmin/static/css/datepicker.css: -------------------------------------------------------------------------------- 1 | .datepicker { 2 | background-color: #ffffff; 3 | border-color: #999; 4 | border-color: rgba(0, 0, 0, 0.2); 5 | border-style: solid; 6 | border-width: 1px; 7 | -webkit-border-radius: 4px; 8 | -moz-border-radius: 4px; 9 | border-radius: 4px; 10 | -webkit-box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); 11 | -moz-box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); 12 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); 13 | -webkit-background-clip: padding-box; 14 | -moz-background-clip: padding-box; 15 | background-clip: padding-box; 16 | display: none; 17 | position: absolute; 18 | z-index: 900; 19 | margin-left: 0; 20 | margin-right: 0; 21 | margin-bottom: 18px; 22 | padding-bottom: 4px; 23 | width: 218px; 24 | } 25 | .datepicker .nav { 26 | font-weight: bold; 27 | width: 100%; 28 | padding: 4px 0; 29 | background-color: #f5f5f5; 30 | color: #808080; 31 | border-bottom: 1px solid #ddd; 32 | -webkit-box-shadow: inset 0 1px 0 #ffffff; 33 | -moz-box-shadow: inset 0 1px 0 #ffffff; 34 | box-shadow: inset 0 1px 0 #ffffff; 35 | zoom: 1; 36 | } 37 | .datepicker .nav:before, .datepicker .nav:after { 38 | display: table; 39 | content: ""; 40 | zoom: 1; 41 | *display: inline; 42 | } 43 | .datepicker .nav:after { 44 | clear: both; 45 | } 46 | .datepicker .nav span { 47 | display: block; 48 | float: left; 49 | text-align: center; 50 | height: 28px; 51 | line-height: 28px; 52 | position: relative; 53 | } 54 | .datepicker .nav .bg { 55 | width: 100%; 56 | background-color: #fdf5d9; 57 | height: 28px; 58 | position: absolute; 59 | top: 0; 60 | left: 0; 61 | -webkit-border-radius: 4px; 62 | -moz-border-radius: 4px; 63 | border-radius: 4px; 64 | } 65 | .datepicker .nav .fg { 66 | width: 100%; 67 | position: absolute; 68 | top: 0; 69 | left: 0; 70 | } 71 | .datepicker .button { 72 | cursor: pointer; 73 | padding: 0 4px; 74 | -webkit-border-radius: 4px; 75 | -moz-border-radius: 4px; 76 | border-radius: 4px; 77 | } 78 | .datepicker .button:hover { 79 | background-color: #808080; 80 | color: #ffffff; 81 | } 82 | .datepicker .months { 83 | float: left; 84 | margin-left: 4px; 85 | } 86 | .datepicker .months .name { 87 | width: 72px; 88 | padding: 0; 89 | } 90 | .datepicker .years { 91 | float: right; 92 | margin-right: 4px; 93 | } 94 | .datepicker .years .name { 95 | width: 36px; 96 | padding: 0; 97 | } 98 | .datepicker .dow, .datepicker .days div { 99 | float: left; 100 | width: 30px; 101 | line-height: 25px; 102 | text-align: center; 103 | } 104 | .datepicker .dow { 105 | font-weight: bold; 106 | color: #808080; 107 | } 108 | .datepicker .calendar { 109 | padding: 4px; 110 | } 111 | .datepicker .days div { 112 | cursor: pointer; 113 | -webkit-border-radius: 4px; 114 | -moz-border-radius: 4px; 115 | border-radius: 4px; 116 | } 117 | .datepicker .days div:hover { 118 | background-color: #0064cd; 119 | color: #ffffff; 120 | } 121 | .datepicker .overlap { 122 | color: #bfbfbf; 123 | } 124 | .datepicker .today { 125 | background-color: #fee9cc; 126 | } 127 | .datepicker .selected { 128 | background-color: #bfbfbf; 129 | color: #ffffff; 130 | } 131 | .datepicker .time { 132 | clear: both; 133 | padding-top: 8px; 134 | margin-left: 4px; 135 | } 136 | .datepicker .time label { 137 | text-align: center; 138 | } 139 | .datepicker .time input { 140 | width: 200px; 141 | } 142 | -------------------------------------------------------------------------------- /flask_superadmin/static/css/forms.css: -------------------------------------------------------------------------------- 1 | h3, label { 2 | width:120px; 3 | padding-top:6px; 4 | display:inline-block; 5 | line-height: 1em; 6 | font-weight:normal; 7 | font-size:inherit; 8 | color:#666; 9 | } 10 | 11 | .field { 12 | margin-bottom:10px; 13 | position: relative; 14 | } 15 | 16 | .field:hover > .delete { 17 | opacity: 1; 18 | } 19 | 20 | .delete { 21 | -webkit-transition: .15s opacity; 22 | background:transparent url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAiElEQVR42r2RsQrDMAxEBRdl8SDcX8lQPGg1GBI6lvz/h7QyRRXV0qUULwfvwZ1tenw5PxToRPWMC52eA9+WDnlh3HFQ/xBQl86NFYJqeGflkiogrOvVlIFhqURFVho3x1moGAa3deMs+LS30CAhBN5nNxeT5hbJ1zwmji2k+aF6NENIPf/hs54f0sZFUVAMigAAAABJRU5ErkJggg==") no-repeat; 23 | border: none; 24 | display: block; 25 | height: 16px; 26 | width: 16px; 27 | opacity: 0; 28 | text-indent:-2000px; 29 | position:absolute; 30 | 31 | left:-18px; 32 | top:6px; 33 | padding-right:4px; 34 | -webkit-user-select: none; 35 | } 36 | 37 | .delete:hover { 38 | background-image:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAqklEQVR4XqWRMQ6DMAxF/1Fyilyj2SmIBUG5QcTCyJA5Z8jGhlBPgRi4TmoDraVmKFJlWYrlp/g5QfwRlwEVNWVa4WzfH9jK6kCkEkBjwxOhLghheMWMELUAqqwQ4OCbnE4LJnhr5IYdqQt4DJQjhe9u4vBBmnxHHNzRFkDGjHDo0VuTAqy2vAG4NkvXXDHxbGsIGlj3e835VFNtdugma/Jk0eXq0lP//5svi4PtO01oFfYAAAAASUVORK5CYII='); 39 | opacity: 1; 40 | } 41 | 42 | .delete:active { 43 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAARElEQVQoz2P4z4AfMlBLAYMdwxkghgEwD1XBGTC0g0sDIaYJECVwFqoChBK4WegKkJWArSJZAQErCDqSKG/iCyhaRhYA9LDIbULDzlIAAAAASUVORK5CYII='); 44 | } 45 | 46 | section { 47 | -webkit-box-orient: horizontal; 48 | } 49 | 50 | section > h3, section > label, section > div, fieldset{ 51 | display: table-cell; 52 | vertical-align: top; 53 | padding-right: 10px; 54 | } 55 | 56 | fieldset { 57 | border:none; 58 | padding:0; 59 | margin:0; 60 | padding-left:10px; 61 | -webkit-transition: all .1s linear; 62 | border-left:1px solid #DDD; 63 | margin-bottom:6px ; 64 | } 65 | 66 | form > fieldset { 67 | padding:0; 68 | border:none; 69 | } 70 | 71 | form > .form-buttons { 72 | margin-top:24px; 73 | padding-top:18px; 74 | border-top:1px solid #EEE; 75 | } 76 | 77 | form > .form-buttons > .btn { 78 | margin-right:10px; 79 | padding:6px 12px; 80 | } 81 | 82 | .delete:hover ~ div fieldset, fieldset:hover{ 83 | opacity:1; 84 | border-color:#AAA; 85 | } 86 | 87 | input.error { 88 | border-color:#DD4B39!important; 89 | } 90 | 91 | input, textarea, select, .uneditable-input,.chzn-container-multi .chzn-choices { 92 | margin-bottom:0; 93 | border-radius: 0; 94 | border-color:#CCC; 95 | } 96 | 97 | input:focus, .chzn-container-active .chzn-choices, textarea:focus { 98 | border-color: rgba(56, 117, 215, 0.8); 99 | -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(56, 117, 215, 0.6); 100 | -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(56, 117, 215, 0.6); 101 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(56, 117, 215, 0.6); 102 | outline: 0; 103 | outline: thin dotted \9; 104 | /* IE6-9 */ 105 | } 106 | 107 | .btn, .chzn-container-single .chzn-single, .chzn-container-multi .chzn-choices .search-choice { 108 | font-size: .9em; 109 | } 110 | 111 | .chzn-container-single .chzn-search input { 112 | padding: 3px 20px 3px 5px; 113 | } 114 | 115 | .readonly-value { 116 | padding-top: 3px; 117 | } 118 | -------------------------------------------------------------------------------- /flask_superadmin/model/backends/sqlalchemy/filters.py: -------------------------------------------------------------------------------- 1 | from flask_superadmin.babel import gettext 2 | 3 | from flask_superadmin.model import filters 4 | from flask_superadmin.contrib.sqlamodel import tools 5 | 6 | 7 | class BaseSQLAFilter(filters.BaseFilter): 8 | """ 9 | Base SQLAlchemy filter. 10 | """ 11 | def __init__(self, column, name, options=None, data_type=None): 12 | """ 13 | Constructor. 14 | 15 | `column` 16 | Model field 17 | `name` 18 | Display name 19 | `options` 20 | Fixed set of options 21 | `data_type` 22 | Client data type 23 | """ 24 | super(BaseSQLAFilter, self).__init__(name, options, data_type) 25 | 26 | self.column = column 27 | 28 | 29 | # Common filters 30 | class FilterEqual(BaseSQLAFilter): 31 | def apply(self, query, value): 32 | return query.filter(self.column == value) 33 | 34 | def operation(self): 35 | return gettext('equals') 36 | 37 | 38 | class FilterNotEqual(BaseSQLAFilter): 39 | def apply(self, query, value): 40 | return query.filter(self.column != value) 41 | 42 | def operation(self): 43 | return gettext('not equal') 44 | 45 | 46 | class FilterLike(BaseSQLAFilter): 47 | def apply(self, query, value): 48 | stmt = tools.parse_like_term(value) 49 | return query.filter(self.column.ilike(stmt)) 50 | 51 | def operation(self): 52 | return gettext('contains') 53 | 54 | 55 | class FilterNotLike(BaseSQLAFilter): 56 | def apply(self, query, value): 57 | stmt = tools.parse_like_term(value) 58 | return query.filter(~self.column.ilike(stmt)) 59 | 60 | def operation(self): 61 | return gettext('not contains') 62 | 63 | 64 | class FilterGreater(BaseSQLAFilter): 65 | def apply(self, query, value): 66 | return query.filter(self.column > value) 67 | 68 | def operation(self): 69 | return gettext('greater than') 70 | 71 | 72 | class FilterSmaller(BaseSQLAFilter): 73 | def apply(self, query, value): 74 | return query.filter(self.column < value) 75 | 76 | def operation(self): 77 | return gettext('smaller than') 78 | 79 | 80 | # Customized type filters 81 | class BooleanEqualFilter(FilterEqual, filters.BaseBooleanFilter): 82 | pass 83 | 84 | 85 | class BooleanNotEqualFilter(FilterNotEqual, filters.BaseBooleanFilter): 86 | pass 87 | 88 | 89 | # Base SQLA filter field converter 90 | class FilterConverter(filters.BaseFilterConverter): 91 | strings = (FilterEqual, FilterNotEqual, FilterLike, FilterNotLike) 92 | numeric = (FilterEqual, FilterNotEqual, FilterGreater, FilterSmaller) 93 | 94 | def convert(self, type_name, column, name): 95 | if type_name in self.converters: 96 | return self.converters[type_name](column, name) 97 | 98 | return None 99 | 100 | @filters.convert('String', 'Unicode', 'Text', 'UnicodeText') 101 | def conv_string(self, column, name): 102 | return [f(column, name) for f in self.strings] 103 | 104 | @filters.convert('Boolean') 105 | def conv_bool(self, column, name): 106 | return [BooleanEqualFilter(column, name), 107 | BooleanNotEqualFilter(column, name)] 108 | 109 | @filters.convert('Integer', 'SmallInteger', 'Numeric', 'Float') 110 | def conv_int(self, column, name): 111 | return [f(column, name) for f in self.numeric] 112 | 113 | @filters.convert('Date') 114 | def conv_date(self, column, name): 115 | return [f(column, name, data_type='datepicker') for f in self.numeric] 116 | 117 | @filters.convert('DateTime') 118 | def conv_datetime(self, column, name): 119 | return [f(column, name, data_type='datetimepicker') for f in self.numeric] 120 | -------------------------------------------------------------------------------- /flask_superadmin/templates/admin/file/list.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/layout.html' %} 2 | {% import 'admin/_macros.html' as lib with context %} 3 | 4 | {% block head_css %} 5 | 6 | {{super()}} 7 | {% endblock %} 8 | 9 | 10 | {% block body %} 11 |

{{_gettext('Files')}}

12 | {% if admin_view.can_upload %} 13 | {{ _gettext('Upload File') }} 14 | {% endif %} 15 | {% if admin_view.can_mkdir %} 16 | {{ _gettext('Create Directory') }} 17 | {% endif %} 18 |
19 |
20 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | {% for name, path, is_dir, size in items %} 45 | 46 | 72 | {% if is_dir %} 73 | 78 | {% else %} 79 | 82 | 85 | {% endif %} 86 | 87 | {% endfor %} 88 |
 NameSize
47 | {% if admin_view.can_rename and path and name != '..' %} 48 | 49 | 50 | 51 | {% endif %} 52 | {%- if admin_view.can_delete and path -%} 53 | {% if is_dir %} 54 | {% if name != '..' and admin_view.can_delete_dirs %} 55 |
56 | 57 | 60 |
61 | {% endif %} 62 | {% else %} 63 |
64 | 65 | 68 |
69 | {% endif %} 70 | {%- endif -%} 71 |
74 | 75 | {{ name }} 76 | 77 | 80 | {{ name }} 81 | 83 | {{ size }} 84 |
89 | {% endblock %} 90 | -------------------------------------------------------------------------------- /flask_superadmin/model/backends/django/fields.py: -------------------------------------------------------------------------------- 1 | # """ 2 | # Useful form fields for use with the Django ORM. 3 | # """ 4 | # from __future__ import unicode_literals 5 | 6 | # import operator 7 | 8 | # from wtforms import widgets 9 | # from wtforms.fields import SelectFieldBase 10 | # from wtforms.validators import ValidationError 11 | 12 | 13 | # __all__ = ( 14 | # 'ModelSelectField', 'QuerySetSelectField', 15 | # ) 16 | 17 | 18 | # class QuerySetSelectField(SelectFieldBase): 19 | # """ 20 | # Given a QuerySet either at initialization or inside a view, will display a 21 | # select drop-down field of choices. The `data` property actually will 22 | # store/keep an ORM model instance, not the ID. Submitting a choice which is 23 | # not in the queryset will result in a validation error. 24 | 25 | # Specify `get_label` to customize the label associated with each option. If 26 | # a string, this is the name of an attribute on the model object to use as 27 | # the label text. If a one-argument callable, this callable will be passed 28 | # model instance and expected to return the label text. Otherwise, the model 29 | # object's `__str__` or `__unicode__` will be used. 30 | 31 | # If `allow_blank` is set to `True`, then a blank choice will be added to the 32 | # top of the list. Selecting this choice will result in the `data` property 33 | # being `None`. The label for the blank choice can be set by specifying the 34 | # `blank_text` parameter. 35 | # """ 36 | # widget = widgets.Select() 37 | 38 | # def __init__(self, label=None, validators=None, queryset=None, get_label=None, allow_blank=False, blank_text='', **kwargs): 39 | # super(QuerySetSelectField, self).__init__(label, validators, **kwargs) 40 | # self.allow_blank = allow_blank 41 | # self.blank_text = blank_text 42 | # self._set_data(None) 43 | # if queryset is not None: 44 | # self.queryset = queryset.all() # Make sure the queryset is fresh 45 | 46 | # if get_label is None: 47 | # self.get_label = lambda x: x 48 | # elif isinstance(get_label, (string,basestring)): 49 | # self.get_label = operator.attrgetter(get_label) 50 | # else: 51 | # self.get_label = get_label 52 | 53 | # def _get_data(self): 54 | # if self._formdata is not None: 55 | # for obj in self.queryset: 56 | # if obj.pk == self._formdata: 57 | # self._set_data(obj) 58 | # break 59 | # return self._data 60 | 61 | # def _set_data(self, data): 62 | # self._data = data 63 | # self._formdata = None 64 | 65 | # data = property(_get_data, _set_data) 66 | 67 | # def iter_choices(self): 68 | # if self.allow_blank: 69 | # yield ('__None', self.blank_text, self.data is None) 70 | 71 | # for obj in self.queryset: 72 | # yield (obj.pk, self.get_label(obj), obj == self.data) 73 | 74 | # def process_formdata(self, valuelist): 75 | # if valuelist: 76 | # if valuelist[0] == '__None': 77 | # self.data = None 78 | # else: 79 | # self._formdata = int(valuelist[0]) 80 | 81 | # def pre_validate(self, form): 82 | # if not self.allow_blank or self.data is not None: 83 | # for obj in self.queryset: 84 | # if self.data == obj: 85 | # break 86 | # else: 87 | # raise ValidationError(self.gettext('Not a valid choice')) 88 | 89 | 90 | # class ModelSelectField(QuerySetSelectField): 91 | # """ 92 | # Like a QuerySetSelectField, except takes a model class instead of a 93 | # queryset and lists everything in it. 94 | # """ 95 | # def __init__(self, label=None, validators=None, model=None, **kwargs): 96 | # super(ModelSelectField, self).__init__(label, validators, queryset=model._default_manager.all(), **kwargs) -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Flask-SuperAdmin 2 | ================ 3 | 4 | .. image:: https://badges.gitter.im/Join%20Chat.svg 5 | :alt: Join the chat at https://gitter.im/syrusakbary/Flask-SuperAdmin 6 | :target: https://gitter.im/syrusakbary/Flask-SuperAdmin?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge 7 | 8 | 9 | .. image:: https://travis-ci.org/SyrusAkbary/Flask-SuperAdmin.png?branch=master 10 | :target: https://travis-ci.org/SyrusAkbary/Flask-SuperAdmin 11 | 12 | Flask-Superadmin is the **best** admin interface framework for `Flask `_. As good as Django admin. 13 | 14 | Batteries included: 15 | 16 | * Admin interface 17 | * **Scaffolding for MongoEngine, Django and SQLAlchemy** 18 | * File administrator (optional) 19 | 20 | Requirements: 21 | 22 | * `Flask`_ 23 | * `WTForms `_ 24 | 25 | 26 | Admin interface 27 | --------------- 28 | 29 | Influenced heavily by the Django admin, **provides easy create/edit/delete functionality** for your 30 | project's models (MongoEngine, Django or SQLAlchemy). 31 | 32 | 33 | .. image:: https://raw.github.com/SyrusAkbary/Flask-SuperAdmin/master/screenshots/model-list.png 34 | :width: 480px 35 | :target: https://raw.github.com/SyrusAkbary/Flask-SuperAdmin/master/screenshots/model-list.png 36 | 37 | .. image:: https://raw.github.com/SyrusAkbary/Flask-SuperAdmin/master/screenshots/model-edit.png 38 | :width: 480px 39 | :target: https://raw.github.com/SyrusAkbary/Flask-SuperAdmin/master/screenshots/model-edit.png 40 | 41 | 42 | Introduction 43 | ------------ 44 | 45 | This is library for building administrative interface on top of Flask framework. 46 | 47 | Instead of providing simple scaffolding for SQLAlchemy, MongoEngine or Django models, Flask-SuperAdmin 48 | provides tools that can be used to build administrative interface of any complexity, 49 | using consistent look and feel. 50 | 51 | 52 | Small example (Flask initialization omitted):: 53 | 54 | from flask.ext.superadmin import Admin, model 55 | 56 | app = Flask(__name__) 57 | admin = Admin(app) 58 | 59 | # For SQLAlchemy (User is a SQLAlchemy Model/Table) 60 | admin.register(User, session=db.session) 61 | 62 | # For MongoEngine Documents (User is a MongoEngine Document) 63 | admin.register(User) 64 | 65 | # For Django Models (User is a Django Model) 66 | admin.register(User) 67 | 68 | 69 | # Adding a custom view 70 | admin.add_view(CustomView(name='Photos', category='Cats')) 71 | 72 | admin.setup_app(app) 73 | 74 | 75 | Installation 76 | ------------ 77 | 78 | For installing you have to do:: 79 | 80 | pip install Flask-SuperAdmin 81 | 82 | Or:: 83 | 84 | python setup.py install 85 | 86 | 87 | Examples 88 | -------- 89 | 90 | Library comes with a lot of examples, you can find them in `examples `_ directory. 91 | 92 | - `MongoEngine `_ 93 | - `SQLAlchemy `_ 94 | - `Django `_ 95 | - `Flask-Login integration `_ 96 | 97 | 98 | Documentation 99 | ------------- 100 | 101 | Flask-SuperAdmin is extensively documented, you can find `documentation here `_. 102 | 103 | 104 | 3rd Party Stuff 105 | --------------- 106 | 107 | Flask-SuperAdmin is built with help of `Twitter Bootstrap `_, `Chosen `_, and `jQuery `_. 108 | 109 | 110 | Kudos 111 | ----- 112 | 113 | This library is a supervitamined fork of the `Flask-Admin `_ package by Serge S. Koval. 114 | -------------------------------------------------------------------------------- /flask_superadmin/templates/admin/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% block head %} 4 | {% block title %}{% if admin_view.category %}{{ admin_view.category }} - {% endif %}{{ admin_view.name }} - {{ admin_view.admin.name }}{% endblock %} 5 | {% block head_meta %} 6 | 7 | 8 | 9 | {% endblock %} 10 | {% block head_css %} 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | {% endblock %} 19 | {% endblock %} 20 | 21 | {% block page_body %} 22 |
23 | 24 |
25 |
26 | 51 |
52 | 53 | 54 |
55 | {% with messages = get_flashed_messages(with_categories=True) %} 56 | {% if messages %} 57 | {% for category, m in messages %} 58 | {% if category == 'error' %} 59 |
60 | {% else %} 61 |
62 | {% endif %} 63 | x 64 | {{ m }} 65 |
66 | {% endfor %} 67 | {% endif %} 68 | {% endwith %} 69 | 70 | {% block body %}{% endblock %} 71 |
72 | 73 | {% endblock %} 74 |
75 |
76 | 77 | 78 | 79 | 80 | 81 | {% block tail %} 82 | {% endblock %} 83 | 84 | 85 | -------------------------------------------------------------------------------- /flask_superadmin/model/backends/sqlalchemy/view.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.sql.expression import desc, literal_column, or_ 2 | 3 | from orm import model_form, AdminModelConverter 4 | 5 | from flask_superadmin.model.base import BaseModelAdmin 6 | from sqlalchemy import schema 7 | 8 | 9 | class ModelAdmin(BaseModelAdmin): 10 | hide_backrefs = False 11 | 12 | def __init__(self, model, session=None, 13 | *args, **kwargs): 14 | super(ModelAdmin, self).__init__(model, *args, **kwargs) 15 | if session: 16 | self.session = session 17 | self._primary_key = self.pk_key 18 | 19 | @staticmethod 20 | def model_detect(model): 21 | return isinstance(getattr(model, 'metadata', None), schema.MetaData) 22 | 23 | def _get_model_iterator(self, model=None): 24 | """ 25 | Return property iterator for the model 26 | """ 27 | if model is None: 28 | model = self.model 29 | 30 | return model._sa_class_manager.mapper.iterate_properties 31 | 32 | @property 33 | def pk_key(self): 34 | for p in self._get_model_iterator(): 35 | if hasattr(p, 'columns'): 36 | for c in p.columns: 37 | if c.primary_key: 38 | return p.key 39 | 40 | def allow_pk(self): 41 | return False 42 | 43 | def get_model_form(self): 44 | return model_form 45 | 46 | def get_converter(self): 47 | return AdminModelConverter(self) 48 | 49 | @property 50 | def query(self): 51 | return self.get_queryset() # TODO remove eventually (kept for backwards compatibility) 52 | 53 | def get_queryset(self): 54 | return self.session.query(self.model) 55 | 56 | def get_objects(self, *pks): 57 | id = self.get_pk(self.model) 58 | return self.get_queryset().filter(id.in_(pks)) 59 | 60 | def get_object(self, pk): 61 | return self.get_queryset().get(pk) 62 | 63 | def get_pk(self, instance): 64 | return getattr(instance, self._primary_key) 65 | 66 | def save_model(self, instance, form, adding=False): 67 | form.populate_obj(instance) 68 | if adding: 69 | self.session.add(instance) 70 | self.session.commit() 71 | return instance 72 | 73 | def delete_models(self, *pks): 74 | objs = self.get_objects(*pks) 75 | [self.session.delete(x) for x in objs] 76 | self.session.commit() 77 | return True 78 | 79 | def construct_search(self, field_name, op=None): 80 | if op == '^': 81 | return literal_column(field_name).startswith 82 | elif op == '=': 83 | return literal_column(field_name).op('=') 84 | else: 85 | return literal_column(field_name).contains 86 | 87 | def apply_search(self, qs, search_query): 88 | or_queries = [] 89 | # treat spaces as if they were OR operators 90 | for word in search_query.split(): 91 | op = word[:1] 92 | if op in ['^', '=']: 93 | word = word[1:] 94 | orm_lookups = [self.construct_search(str(model_field), op) 95 | for model_field in self.search_fields] 96 | or_queries.extend([orm_lookup(word) for orm_lookup in orm_lookups]) 97 | if or_queries: 98 | qs = qs.filter(or_(*or_queries)) 99 | return qs 100 | 101 | def get_list(self, page=0, sort=None, sort_desc=None, execute=False, search_query=None): 102 | qs = self.get_queryset() 103 | 104 | # Filter by search query 105 | if search_query and self.search_fields: 106 | qs = self.apply_search(qs, search_query) 107 | 108 | #Calculate number of rows 109 | count = qs.count() 110 | 111 | #Order queryset 112 | if sort: 113 | if sort_desc: 114 | sort = desc(sort) 115 | qs = qs.order_by(sort) 116 | 117 | # Pagination 118 | if page is not None: 119 | qs = qs.offset(page * self.list_per_page) 120 | 121 | qs = qs.limit(self.list_per_page) 122 | 123 | if execute: 124 | qs = qs.all() 125 | 126 | return count, qs 127 | -------------------------------------------------------------------------------- /flask_superadmin/static/js/filters.js: -------------------------------------------------------------------------------- 1 | var AdminFilters = function(element, filters_element, adminForm, operations, options, types) { 2 | var $root = $(element); 3 | var $container = $('.filters', $root); 4 | var lastCount = 0; 5 | 6 | function getCount(name) { 7 | var idx = name.indexOf('_'); 8 | return parseInt(name.substr(3, idx - 3), 10); 9 | } 10 | 11 | function changeOperation() { 12 | var $parent = $(this).parent(); 13 | var $el = $('.filter-val', $parent); 14 | var count = getCount($el.attr('name')); 15 | $el.attr('name', 'flt' + count + '_' + $(this).val()); 16 | $('button', $root).show(); 17 | } 18 | 19 | function removeFilter() { 20 | $(this).parent().remove(); 21 | $('button', $root).show(); 22 | } 23 | 24 | function addFilter(name, op) { 25 | var $el = $('
').appendTo($container); 26 | 27 | $('') 28 | .append($('×')) 29 | .append(' ') 30 | .append(name) 31 | .appendTo($el) 32 | .click(removeFilter); 33 | 34 | var $select = $('') 50 | .attr('name', 'flt' + lastCount + '_' + optId) 51 | .appendTo($el); 52 | 53 | $(options[optId]).each(function() { 54 | $field.append($('
13 | {% if csrf_token %} 14 | 15 | {% endif %} 16 |

{{ _gettext('%(model)s model', model=name|capitalize) }}

17 | 18 | {% if admin_view.can_create %} 19 |
{{ _gettext('Add %(model)s', model=name) }} 20 | {% endif %} 21 | 22 | 28 | 29 |
30 |
31 | 32 |
Total count: {{ count }}
33 | 34 |
35 | {% if admin_view.search_fields %} 36 | 40 | {% endif %} 41 | 42 | 43 | 44 | 45 | 46 | {% for c in admin_view.list_display %} 47 | 66 | {% else %} 67 | 70 | {% endfor %} 71 | 72 | 73 | {% for instance in data %} 74 | 75 | 79 | {% for c in admin_view.list_display %} 80 | {% if loop.first %} 81 | 82 | {% else %} 83 | 84 | {% with reference = admin_view.get_reference(admin_view.get_column(instance, c)) %} 85 | {% if reference %} 86 | 87 | {% else %} 88 | 89 | {% endif %} 90 | {% endwith %} 91 | 92 | {% endif %} 93 | {% else %} 94 | 95 | {% endfor %} 96 | 97 | {% endfor %} 98 |
48 | {% set name = admin_view.field_name(c) %} 49 | {% if admin_view.is_sortable(c)%} 50 | {% if sort == c %} 51 | 52 | {{ name }} 53 | {% if sort_desc %} 54 | 55 | {% else %} 56 | 57 | {% endif %} 58 | 59 | {% else %} 60 | {{ name }} 61 | {% endif %} 62 | {% else %} 63 | {{ name }} 64 | {% endif %} 65 | 68 | {{ name|capitalize }} 69 |
76 | {% set pk = admin_view.get_pk(instance) %} 77 | 78 | {{ admin_view.get_column(instance, c) }}{{ admin_view.get_column(instance, c) }}{{ admin_view.get_column(instance, c) }}{{ instance|string or 'None' }}
99 | {{ lib.pager(page, total_pages, admin_view.page_url) }} 100 |
101 |
102 | {% endblock %} 103 | 104 | {% block tail %} 105 | 106 | 107 | 108 | {% endblock %} 109 | -------------------------------------------------------------------------------- /flask_superadmin/tests/test_base.py: -------------------------------------------------------------------------------- 1 | from nose.tools import ok_, eq_, raises 2 | 3 | from flask import Flask 4 | from flask_superadmin import base 5 | 6 | 7 | class MockView(base.BaseView): 8 | # Various properties 9 | allow_call = True 10 | allow_access = True 11 | 12 | @base.expose('/') 13 | def index(self): 14 | return 'Success!' 15 | 16 | @base.expose('/test/') 17 | def test(self): 18 | return self.render('mock.html') 19 | 20 | def _handle_view(self, name, **kwargs): 21 | if self.allow_call: 22 | return super(MockView, self)._handle_view(name, **kwargs) 23 | else: 24 | return 'Failure!' 25 | 26 | def is_accessible(self): 27 | if self.allow_access: 28 | return super(MockView, self).is_accessible() 29 | else: 30 | return False 31 | 32 | 33 | def test_baseview_defaults(): 34 | view = MockView() 35 | eq_(view.name, None) 36 | eq_(view.category, None) 37 | eq_(view.endpoint, None) 38 | eq_(view.url, None) 39 | eq_(view.static_folder, None) 40 | eq_(view.admin, None) 41 | eq_(view.blueprint, None) 42 | 43 | 44 | def test_base_defaults(): 45 | admin = base.Admin() 46 | eq_(admin.name, 'Admin') 47 | eq_(admin.url, '/admin') 48 | eq_(admin.app, None) 49 | ok_(admin.index_view is not None) 50 | 51 | # Check if default view was added 52 | eq_(len(admin._views), 1) 53 | eq_(admin._views[0], admin.index_view) 54 | 55 | 56 | def test_base_registration(): 57 | app = Flask(__name__) 58 | admin = base.Admin(app) 59 | 60 | eq_(admin.app, app) 61 | ok_(admin.index_view.blueprint is not None) 62 | 63 | 64 | def test_admin_customizations(): 65 | app = Flask(__name__) 66 | admin = base.Admin(app, name='Test', url='/foobar') 67 | eq_(admin.name, 'Test') 68 | eq_(admin.url, '/foobar') 69 | 70 | client = app.test_client() 71 | rv = client.get('/foobar/') 72 | eq_(rv.status_code, 200) 73 | 74 | 75 | def test_baseview_registration(): 76 | admin = base.Admin() 77 | 78 | view = MockView() 79 | bp = view.create_blueprint(admin) 80 | 81 | # Base properties 82 | eq_(view.admin, admin) 83 | ok_(view.blueprint is not None) 84 | 85 | # Calculated properties 86 | eq_(view.endpoint, 'mockview') 87 | eq_(view.url, '/admin/mockview') 88 | eq_(view.name, 'Mock View') 89 | 90 | # Verify generated blueprint properties 91 | eq_(bp.name, view.endpoint) 92 | eq_(bp.url_prefix, view.url) 93 | eq_(bp.template_folder, 'templates') 94 | eq_(bp.static_folder, view.static_folder) 95 | 96 | # Verify customizations 97 | view = MockView(name='Test', endpoint='foobar') 98 | view.create_blueprint(base.Admin()) 99 | 100 | eq_(view.name, 'Test') 101 | eq_(view.endpoint, 'foobar') 102 | eq_(view.url, '/admin/foobar') 103 | 104 | view = MockView(url='test') 105 | view.create_blueprint(base.Admin()) 106 | eq_(view.url, '/admin/test') 107 | 108 | view = MockView(url='/test/test') 109 | view.create_blueprint(base.Admin()) 110 | eq_(view.url, '/test/test') 111 | 112 | 113 | def test_baseview_urls(): 114 | app = Flask(__name__) 115 | admin = base.Admin(app) 116 | 117 | view = MockView() 118 | admin.add_view(view) 119 | 120 | eq_(len(view._urls), 2) 121 | 122 | 123 | @raises(Exception) 124 | def test_no_default(): 125 | app = Flask(__name__) 126 | admin = base.Admin(app) 127 | admin.add_view(base.BaseView()) 128 | 129 | 130 | def test_call(): 131 | app = Flask(__name__) 132 | admin = base.Admin(app) 133 | view = MockView() 134 | admin.add_view(view) 135 | client = app.test_client() 136 | 137 | rv = client.get('/admin/') 138 | eq_(rv.status_code, 200) 139 | 140 | rv = client.get('/admin/mockview/') 141 | eq_(rv.data, 'Success!') 142 | 143 | rv = client.get('/admin/mockview/test/') 144 | eq_(rv.data, 'Success!') 145 | 146 | # Check authentication failure 147 | view.allow_call = False 148 | rv = client.get('/admin/mockview/') 149 | eq_(rv.data, 'Failure!') 150 | 151 | 152 | def test_permissions(): 153 | app = Flask(__name__) 154 | admin = base.Admin(app) 155 | view = MockView() 156 | admin.add_view(view) 157 | client = app.test_client() 158 | 159 | view.allow_access = False 160 | 161 | rv = client.get('/admin/mockview/') 162 | eq_(rv.status_code, 403) 163 | 164 | 165 | def test_submenu(): 166 | app = Flask(__name__) 167 | admin = base.Admin(app) 168 | admin.add_view(MockView(name='Test 1', category='Test', endpoint='test1')) 169 | 170 | # Second view is not normally accessible 171 | view = MockView(name='Test 2', category='Test', endpoint='test2') 172 | view.allow_access = False 173 | admin.add_view(view) 174 | 175 | ok_('Test' in admin._menu_categories) 176 | eq_(len(admin._menu), 2) 177 | eq_(admin._menu[1].name, 'Test') 178 | eq_(len(admin._menu[1]._children), 2) 179 | 180 | # Categories don't have URLs and they're not accessible 181 | eq_(admin._menu[1].get_url(), None) 182 | eq_(admin._menu[1].is_accessible(), False) 183 | 184 | eq_(len(admin._menu[1].get_children()), 1) 185 | 186 | 187 | def test_delayed_init(): 188 | app = Flask(__name__) 189 | admin = base.Admin() 190 | admin.add_view(MockView()) 191 | admin.init_app(app) 192 | 193 | client = app.test_client() 194 | 195 | rv = client.get('/admin/mockview/') 196 | eq_(rv.data, 'Success!') 197 | 198 | 199 | @raises(Exception) 200 | def test_double_init(): 201 | app = Flask(__name__) 202 | admin = base.Admin(app) 203 | admin.init_app(app) 204 | 205 | -------------------------------------------------------------------------------- /doc/_themes/flask_small/static/flasky.css_t: -------------------------------------------------------------------------------- 1 | /* 2 | * flasky.css_t 3 | * ~~~~~~~~~~~~ 4 | * 5 | * Sphinx stylesheet -- flasky theme based on nature theme. 6 | * 7 | * :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. 8 | * :license: BSD, see LICENSE for details. 9 | * 10 | */ 11 | 12 | @import url("basic.css"); 13 | 14 | /* -- page layout ----------------------------------------------------------- */ 15 | 16 | body { 17 | font-family: 'Georgia', serif; 18 | font-size: 17px; 19 | color: #000; 20 | background: white; 21 | margin: 0; 22 | padding: 0; 23 | } 24 | 25 | div.documentwrapper { 26 | float: left; 27 | width: 100%; 28 | } 29 | 30 | div.bodywrapper { 31 | margin: 40px auto 0 auto; 32 | width: 700px; 33 | } 34 | 35 | hr { 36 | border: 1px solid #B1B4B6; 37 | } 38 | 39 | div.body { 40 | background-color: #ffffff; 41 | color: #3E4349; 42 | padding: 0 30px 30px 30px; 43 | } 44 | 45 | img.floatingflask { 46 | padding: 0 0 10px 10px; 47 | float: right; 48 | } 49 | 50 | div.footer { 51 | text-align: right; 52 | color: #888; 53 | padding: 10px; 54 | font-size: 14px; 55 | width: 650px; 56 | margin: 0 auto 40px auto; 57 | } 58 | 59 | div.footer a { 60 | color: #888; 61 | text-decoration: underline; 62 | } 63 | 64 | div.related { 65 | line-height: 32px; 66 | color: #888; 67 | } 68 | 69 | div.related ul { 70 | padding: 0 0 0 10px; 71 | } 72 | 73 | div.related a { 74 | color: #444; 75 | } 76 | 77 | /* -- body styles ----------------------------------------------------------- */ 78 | 79 | a { 80 | color: #004B6B; 81 | text-decoration: underline; 82 | } 83 | 84 | a:hover { 85 | color: #6D4100; 86 | text-decoration: underline; 87 | } 88 | 89 | div.body { 90 | padding-bottom: 40px; /* saved for footer */ 91 | } 92 | 93 | div.body h1, 94 | div.body h2, 95 | div.body h3, 96 | div.body h4, 97 | div.body h5, 98 | div.body h6 { 99 | font-family: 'Garamond', 'Georgia', serif; 100 | font-weight: normal; 101 | margin: 30px 0px 10px 0px; 102 | padding: 0; 103 | } 104 | 105 | {% if theme_index_logo %} 106 | div.indexwrapper h1 { 107 | text-indent: -999999px; 108 | background: url({{ theme_index_logo }}) no-repeat center center; 109 | height: {{ theme_index_logo_height }}; 110 | } 111 | {% endif %} 112 | 113 | div.body h2 { font-size: 180%; } 114 | div.body h3 { font-size: 150%; } 115 | div.body h4 { font-size: 130%; } 116 | div.body h5 { font-size: 100%; } 117 | div.body h6 { font-size: 100%; } 118 | 119 | a.headerlink { 120 | color: white; 121 | padding: 0 4px; 122 | text-decoration: none; 123 | } 124 | 125 | a.headerlink:hover { 126 | color: #444; 127 | background: #eaeaea; 128 | } 129 | 130 | div.body p, div.body dd, div.body li { 131 | line-height: 1.4em; 132 | } 133 | 134 | div.admonition { 135 | background: #fafafa; 136 | margin: 20px -30px; 137 | padding: 10px 30px; 138 | border-top: 1px solid #ccc; 139 | border-bottom: 1px solid #ccc; 140 | } 141 | 142 | div.admonition p.admonition-title { 143 | font-family: 'Garamond', 'Georgia', serif; 144 | font-weight: normal; 145 | font-size: 24px; 146 | margin: 0 0 10px 0; 147 | padding: 0; 148 | line-height: 1; 149 | } 150 | 151 | div.admonition p.last { 152 | margin-bottom: 0; 153 | } 154 | 155 | div.highlight{ 156 | background-color: white; 157 | } 158 | 159 | dt:target, .highlight { 160 | background: #FAF3E8; 161 | } 162 | 163 | div.note { 164 | background-color: #eee; 165 | border: 1px solid #ccc; 166 | } 167 | 168 | div.seealso { 169 | background-color: #ffc; 170 | border: 1px solid #ff6; 171 | } 172 | 173 | div.topic { 174 | background-color: #eee; 175 | } 176 | 177 | div.warning { 178 | background-color: #ffe4e4; 179 | border: 1px solid #f66; 180 | } 181 | 182 | p.admonition-title { 183 | display: inline; 184 | } 185 | 186 | p.admonition-title:after { 187 | content: ":"; 188 | } 189 | 190 | pre, tt { 191 | font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; 192 | font-size: 0.85em; 193 | } 194 | 195 | img.screenshot { 196 | } 197 | 198 | tt.descname, tt.descclassname { 199 | font-size: 0.95em; 200 | } 201 | 202 | tt.descname { 203 | padding-right: 0.08em; 204 | } 205 | 206 | img.screenshot { 207 | -moz-box-shadow: 2px 2px 4px #eee; 208 | -webkit-box-shadow: 2px 2px 4px #eee; 209 | box-shadow: 2px 2px 4px #eee; 210 | } 211 | 212 | table.docutils { 213 | border: 1px solid #888; 214 | -moz-box-shadow: 2px 2px 4px #eee; 215 | -webkit-box-shadow: 2px 2px 4px #eee; 216 | box-shadow: 2px 2px 4px #eee; 217 | } 218 | 219 | table.docutils td, table.docutils th { 220 | border: 1px solid #888; 221 | padding: 0.25em 0.7em; 222 | } 223 | 224 | table.field-list, table.footnote { 225 | border: none; 226 | -moz-box-shadow: none; 227 | -webkit-box-shadow: none; 228 | box-shadow: none; 229 | } 230 | 231 | table.footnote { 232 | margin: 15px 0; 233 | width: 100%; 234 | border: 1px solid #eee; 235 | } 236 | 237 | table.field-list th { 238 | padding: 0 0.8em 0 0; 239 | } 240 | 241 | table.field-list td { 242 | padding: 0; 243 | } 244 | 245 | table.footnote td { 246 | padding: 0.5em; 247 | } 248 | 249 | dl { 250 | margin: 0; 251 | padding: 0; 252 | } 253 | 254 | dl dd { 255 | margin-left: 30px; 256 | } 257 | 258 | pre { 259 | padding: 0; 260 | margin: 15px -30px; 261 | padding: 8px; 262 | line-height: 1.3em; 263 | padding: 7px 30px; 264 | background: #eee; 265 | border-radius: 2px; 266 | -moz-border-radius: 2px; 267 | -webkit-border-radius: 2px; 268 | } 269 | 270 | dl pre { 271 | margin-left: -60px; 272 | padding-left: 60px; 273 | } 274 | 275 | tt { 276 | background-color: #ecf0f3; 277 | color: #222; 278 | /* padding: 1px 2px; */ 279 | } 280 | 281 | tt.xref, a tt { 282 | background-color: #FBFBFB; 283 | } 284 | 285 | a:hover tt { 286 | background: #EEE; 287 | } 288 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Flask-SuperAdmin.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Flask-SuperAdmin.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Flask-SuperAdmin" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Flask-SuperAdmin" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /flask_superadmin/form.py: -------------------------------------------------------------------------------- 1 | import time 2 | import datetime 3 | 4 | from flask.ext import wtf 5 | from wtforms import fields, widgets 6 | 7 | from flask_superadmin.babel import gettext 8 | from flask import request 9 | 10 | class BaseForm(wtf.Form): 11 | """ 12 | Customized form class. 13 | """ 14 | def __init__(self, formdata=None, obj=None, prefix='', **kwargs): 15 | if formdata: 16 | super(BaseForm, self).__init__(formdata, obj, prefix, **kwargs) 17 | else: 18 | super(BaseForm, self).__init__(obj=obj, prefix=prefix, **kwargs) 19 | 20 | self._obj = obj 21 | 22 | @property 23 | def has_file_field(self): 24 | """ 25 | Return True if form contains at least one FileField. 26 | """ 27 | # TODO: Optimize me 28 | for f in self: 29 | if isinstance(f, fields.FileField): 30 | return True 31 | 32 | return False 33 | 34 | 35 | class TimeField(fields.Field): 36 | """ 37 | A text field which stores a `datetime.time` object. 38 | Accepts time string in multiple formats: 20:10, 20:10:00, 10:00 am, 9:30pm, etc. 39 | """ 40 | widget = widgets.TextInput() 41 | 42 | def __init__(self, label=None, validators=None, formats=None, **kwargs): 43 | """ 44 | Constructor 45 | 46 | `label` 47 | Label 48 | `validators` 49 | Field validators 50 | `formats` 51 | Supported time formats, as a enumerable. 52 | `kwargs` 53 | Any additional parameters 54 | """ 55 | super(TimeField, self).__init__(label, validators, **kwargs) 56 | 57 | self.formats = formats or ('%H:%M:%S', '%H:%M', 58 | '%I:%M:%S%p', '%I:%M%p', 59 | '%I:%M:%S %p', '%I:%M %p') 60 | 61 | def _value(self): 62 | if self.raw_data: 63 | return u' '.join(self.raw_data) 64 | else: 65 | return self.data and self.data.strftime(self.formats[0]) or u'' 66 | 67 | def process_formdata(self, valuelist): 68 | if valuelist: 69 | date_str = u' '.join(valuelist) 70 | 71 | for format in self.formats: 72 | try: 73 | timetuple = time.strptime(date_str, format) 74 | self.data = datetime.time(timetuple.tm_hour, 75 | timetuple.tm_min, 76 | timetuple.tm_sec) 77 | return 78 | except ValueError: 79 | pass 80 | 81 | raise ValueError(gettext('Invalid time format')) 82 | 83 | 84 | class ChosenSelectWidget(widgets.Select): 85 | """ 86 | `Chosen `_ styled select widget. 87 | 88 | You must include chosen.js and form.js for styling to work. 89 | """ 90 | def __call__(self, field, **kwargs): 91 | if getattr(field, 'allow_blank', False) and not self.multiple: 92 | kwargs['data-role'] = u'chosenblank' 93 | else: 94 | kwargs['data-role'] = u'chosen' 95 | 96 | return super(ChosenSelectWidget, self).__call__(field, **kwargs) 97 | 98 | 99 | class ChosenSelectField(fields.SelectField): 100 | """ 101 | `Chosen `_ styled select field. 102 | 103 | You must include chosen.js and form.js for styling to work. 104 | """ 105 | widget = ChosenSelectWidget 106 | 107 | class FileFieldWidget(object): 108 | # widget_file = widgets.FileInput() 109 | widget_checkbox = widgets.CheckboxInput() 110 | def __call__(self, field, **kwargs): 111 | from cgi import escape 112 | input_file = '' % widgets.html_params(name=field.name, type='file') 113 | return widgets.HTMLString('%s
Current: %s
%s '%(input_file, escape(field._value()), self.widget_checkbox(field._clear), field._clear.id)) 114 | 115 | class FileField(fields.FileField): 116 | widget = FileFieldWidget() 117 | def __init__(self,*args,**kwargs): 118 | self.clearable = kwargs.pop('clearable', True) 119 | super(FileField, self).__init__(*args, **kwargs) 120 | self._prefix = kwargs.get('_prefix', '') 121 | self.clear_field = fields.BooleanField(default=False) 122 | if self.clearable: 123 | self._clear_name = '%s-clear'%self.short_name 124 | self._clear_id = '%s-clear'%self.id 125 | self._clear = self.clear_field.bind(form=None, name=self._clear_name, prefix=self._prefix, id=self._clear_id) 126 | 127 | def process(self, formdata, data=fields._unset_value): 128 | super(FileField, self).process(formdata, data) 129 | if self.clearable: 130 | self._clear.process(formdata, data) 131 | self._clear.checked = False 132 | 133 | @property 134 | def clear(self): 135 | return (not self.clearable) or self._clear.data 136 | 137 | @property 138 | def data(self): 139 | data = self._data 140 | if data is not None: 141 | data.clear = self.clear 142 | return data 143 | 144 | @data.setter 145 | def data(self, data): 146 | self._data = data 147 | 148 | 149 | class DatePickerWidget(widgets.TextInput): 150 | """ 151 | Date picker widget. 152 | 153 | You must include bootstrap-datepicker.js and form.js for styling to work. 154 | """ 155 | def __call__(self, field, **kwargs): 156 | kwargs['data-role'] = u'datepicker' 157 | return super(DatePickerWidget, self).__call__(field, **kwargs) 158 | 159 | 160 | class DateTimePickerWidget(widgets.TextInput): 161 | """ 162 | Datetime picker widget. 163 | 164 | You must include bootstrap-datepicker.js and form.js for styling to work. 165 | """ 166 | def __call__(self, field, **kwargs): 167 | kwargs['data-role'] = u'datetimepicker' 168 | return super(DateTimePickerWidget, self).__call__(field, **kwargs) 169 | 170 | # def format_form(form): 171 | # for field in form: 172 | # if isinstance(field,fields.SelectField): 173 | # field.widget = ChosenSelectWidget(multiple=field.widget.multiple) 174 | # elif isinstance(field, fields.DateTimeField): 175 | # field.widget = DatePickerWidget() 176 | # elif isinstance(field, fields.FormField): 177 | # format_form(field.form) 178 | # return form 179 | # # elif isinstance(field, fields.FieldList): 180 | # # for f in field.entries: format_form 181 | -------------------------------------------------------------------------------- /babel/admin.pot: -------------------------------------------------------------------------------- 1 | # Translations template for Flask-SuperAdminEx. 2 | # Copyright (C) 2012 ORGANIZATION 3 | # This file is distributed under the same license as the Flask-SuperAdminEx 4 | # project. 5 | # FIRST AUTHOR , 2012. 6 | # 7 | #, fuzzy 8 | msgid "" 9 | msgstr "" 10 | "Project-Id-Version: Flask-SuperAdminEx VERSION\n" 11 | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" 12 | "POT-Creation-Date: 2012-04-11 18:47+0300\n" 13 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 14 | "Last-Translator: FULL NAME \n" 15 | "Language-Team: LANGUAGE \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=utf-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Generated-By: Babel 0.9.6\n" 20 | 21 | #: ../flask_superadminex/base.py:216 22 | msgid "Home" 23 | msgstr "" 24 | 25 | #: ../flask_superadminex/form.py:81 26 | msgid "Invalid time format" 27 | msgstr "" 28 | 29 | #: ../flask_superadminex/ext/fileadmin.py:32 30 | msgid "Invalid directory name" 31 | msgstr "" 32 | 33 | #: ../flask_superadminex/ext/fileadmin.py:40 34 | msgid "File to upload" 35 | msgstr "" 36 | 37 | #: ../flask_superadminex/ext/fileadmin.py:49 38 | msgid "File required." 39 | msgstr "" 40 | 41 | #: ../flask_superadminex/ext/fileadmin.py:54 42 | msgid "Invalid file type." 43 | msgstr "" 44 | 45 | #: ../flask_superadminex/ext/fileadmin.py:335 46 | msgid "File uploading is disabled." 47 | msgstr "" 48 | 49 | #: ../flask_superadminex/ext/fileadmin.py:344 50 | #, python-format 51 | msgid "File \"%(name)s\" already exists." 52 | msgstr "" 53 | 54 | #: ../flask_superadminex/ext/fileadmin.py:351 55 | #, python-format 56 | msgid "Failed to save file: %(error)s" 57 | msgstr "" 58 | 59 | #: ../flask_superadminex/ext/fileadmin.py:370 60 | msgid "Directory creation is disabled." 61 | msgstr "" 62 | 63 | #: ../flask_superadminex/ext/fileadmin.py:380 64 | #, python-format 65 | msgid "Failed to create directory: %(error)s" 66 | msgstr "" 67 | 68 | #: ../flask_superadminex/ext/fileadmin.py:402 69 | msgid "Deletion is disabled." 70 | msgstr "" 71 | 72 | #: ../flask_superadminex/ext/fileadmin.py:407 73 | msgid "Directory deletion is disabled." 74 | msgstr "" 75 | 76 | #: ../flask_superadminex/ext/fileadmin.py:412 77 | #, python-format 78 | msgid "Directory \"%s\" was successfully deleted." 79 | msgstr "" 80 | 81 | #: ../flask_superadminex/ext/fileadmin.py:414 82 | #, python-format 83 | msgid "Failed to delete directory: %(error)s" 84 | msgstr "" 85 | 86 | #: ../flask_superadminex/ext/fileadmin.py:418 87 | #, python-format 88 | msgid "File \"%(name)s\" was successfully deleted." 89 | msgstr "" 90 | 91 | #: ../flask_superadminex/ext/fileadmin.py:420 92 | #, python-format 93 | msgid "Failed to delete file: %(name)s" 94 | msgstr "" 95 | 96 | #: ../flask_superadminex/ext/fileadmin.py:439 97 | msgid "Renaming is disabled." 98 | msgstr "" 99 | 100 | #: ../flask_superadminex/ext/fileadmin.py:443 101 | msgid "Path does not exist." 102 | msgstr "" 103 | 104 | #: ../flask_superadminex/ext/fileadmin.py:454 105 | #, python-format 106 | msgid "Successfully renamed \"%(src)s\" to \"%(dst)s\"" 107 | msgstr "" 108 | 109 | #: ../flask_superadminex/ext/fileadmin.py:457 110 | #, python-format 111 | msgid "Failed to rename: %(error)s" 112 | msgstr "" 113 | 114 | #: ../flask_superadminex/ext/sqlamodel/filters.py:35 115 | msgid "equals" 116 | msgstr "" 117 | 118 | #: ../flask_superadminex/ext/sqlamodel/filters.py:43 119 | msgid "not equal" 120 | msgstr "" 121 | 122 | #: ../flask_superadminex/ext/sqlamodel/filters.py:52 123 | msgid "contains" 124 | msgstr "" 125 | 126 | #: ../flask_superadminex/ext/sqlamodel/filters.py:61 127 | msgid "not contains" 128 | msgstr "" 129 | 130 | #: ../flask_superadminex/ext/sqlamodel/filters.py:69 131 | msgid "greater than" 132 | msgstr "" 133 | 134 | #: ../flask_superadminex/ext/sqlamodel/filters.py:77 135 | msgid "smaller than" 136 | msgstr "" 137 | 138 | #: ../flask_superadminex/ext/sqlamodel/form.py:37 139 | msgid "Already exists." 140 | msgstr "" 141 | 142 | #: ../flask_superadminex/ext/sqlamodel/view.py:504 143 | #, python-format 144 | msgid "Failed to create model. %(error)s" 145 | msgstr "" 146 | 147 | #: ../flask_superadminex/ext/sqlamodel/view.py:519 148 | #, python-format 149 | msgid "Failed to update model. %(error)s" 150 | msgstr "" 151 | 152 | #: ../flask_superadminex/ext/sqlamodel/view.py:534 153 | #, python-format 154 | msgid "Failed to delete model. %(error)s" 155 | msgstr "" 156 | 157 | #: ../flask_superadminex/model/base.py:742 158 | msgid "Model was successfully created." 159 | msgstr "" 160 | 161 | #: ../flask_superadminex/model/filters.py:82 162 | msgid "Yes" 163 | msgstr "" 164 | 165 | #: ../flask_superadminex/model/filters.py:83 166 | msgid "No" 167 | msgstr "" 168 | 169 | #: ../flask_superadminex/templates/admin/lib.html:105 170 | msgid "Submit" 171 | msgstr "" 172 | 173 | #: ../flask_superadminex/templates/admin/lib.html:110 174 | msgid "Cancel" 175 | msgstr "" 176 | 177 | #: ../flask_superadminex/templates/admin/file/list.html:7 178 | msgid "Root" 179 | msgstr "" 180 | 181 | #: ../flask_superadminex/templates/admin/file/list.html:42 182 | #, python-format 183 | msgid "Are you sure you want to delete \\'%(name)s\\' recursively?" 184 | msgstr "" 185 | 186 | #: ../flask_superadminex/templates/admin/file/list.html:50 187 | #, python-format 188 | msgid "Are you sure you want to delete \\'%(name)s\\'?" 189 | msgstr "" 190 | 191 | #: ../flask_superadminex/templates/admin/file/list.html:75 192 | msgid "Upload File" 193 | msgstr "" 194 | 195 | #: ../flask_superadminex/templates/admin/file/list.html:78 196 | msgid "Create Directory" 197 | msgstr "" 198 | 199 | #: ../flask_superadminex/templates/admin/file/rename.html:5 200 | #, python-format 201 | msgid "Please provide new name for %(name)s" 202 | msgstr "" 203 | 204 | #: ../flask_superadminex/templates/admin/model/create.html:11 205 | msgid "Save and Add" 206 | msgstr "" 207 | 208 | #: ../flask_superadminex/templates/admin/model/create.html:16 209 | #: ../flask_superadminex/templates/admin/model/list.html:12 210 | msgid "List" 211 | msgstr "" 212 | 213 | #: ../flask_superadminex/templates/admin/model/create.html:19 214 | #: ../flask_superadminex/templates/admin/model/list.html:16 215 | msgid "Create" 216 | msgstr "" 217 | 218 | #: ../flask_superadminex/templates/admin/model/list.html:23 219 | msgid "Add Filter" 220 | msgstr "" 221 | 222 | #: ../flask_superadminex/templates/admin/model/list.html:44 223 | msgid "Search" 224 | msgstr "" 225 | 226 | #: ../flask_superadminex/templates/admin/model/list.html:57 227 | msgid "Apply" 228 | msgstr "" 229 | 230 | #: ../flask_superadminex/templates/admin/model/list.html:59 231 | msgid "Reset Filters" 232 | msgstr "" 233 | 234 | #: ../flask_superadminex/templates/admin/model/list.html:67 235 | msgid "Remove Filter" 236 | msgstr "" 237 | 238 | #: ../flask_superadminex/templates/admin/model/list.html:128 239 | msgid "You sure you want to delete this item?" 240 | msgstr "" 241 | 242 | -------------------------------------------------------------------------------- /flask_superadmin/translations/zh_CN/LC_MESSAGES/admin.po: -------------------------------------------------------------------------------- 1 | # Translations template for Flask-SuperAdminEx. 2 | # Copyright (C) 2012 ORGANIZATION 3 | # This file is distributed under the same license as the Flask-SuperAdminEx 4 | # project. 5 | # FIRST AUTHOR , 2012. 6 | # 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: Flask-SuperAdminEx\n" 10 | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" 11 | "POT-Creation-Date: 2012-04-11 18:47+0300\n" 12 | "PO-Revision-Date: 2012-04-11 18:48+0200\n" 13 | "Last-Translator: Jiangge Zhang \n" 14 | "Language-Team: \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Generated-By: Babel 0.9.6\n" 19 | "X-Poedit-Language: Simplified Chinese\n" 20 | "X-Poedit-Country: Chinese\n" 21 | "X-Poedit-SourceCharset: utf-8\n" 22 | 23 | #: ../flask_superadminex/base.py:216 24 | msgid "Home" 25 | msgstr "主页" 26 | 27 | #: ../flask_superadminex/form.py:81 28 | msgid "Invalid time format" 29 | msgstr "无效的时间格式" 30 | 31 | #: ../flask_superadminex/ext/fileadmin.py:32 32 | msgid "Invalid directory name" 33 | msgstr "无效的目录名" 34 | 35 | #: ../flask_superadminex/ext/fileadmin.py:40 36 | msgid "File to upload" 37 | msgstr "上传的文件" 38 | 39 | #: ../flask_superadminex/ext/fileadmin.py:49 40 | msgid "File required." 41 | msgstr "需要上传文件。" 42 | 43 | #: ../flask_superadminex/ext/fileadmin.py:54 44 | msgid "Invalid file type." 45 | msgstr "无效的文件格式。" 46 | 47 | #: ../flask_superadminex/ext/fileadmin.py:335 48 | msgid "File uploading is disabled." 49 | msgstr "文件上传功能没有启用。" 50 | 51 | #: ../flask_superadminex/ext/fileadmin.py:344 52 | #, python-format 53 | msgid "File \"%(name)s\" already exists." 54 | msgstr "文件 \"%(name)s\" 已经存在。" 55 | 56 | #: ../flask_superadminex/ext/fileadmin.py:351 57 | #, python-format 58 | msgid "Failed to save file: %(error)s" 59 | msgstr "保存文件失败: %(error)s" 60 | 61 | #: ../flask_superadminex/ext/fileadmin.py:370 62 | msgid "Directory creation is disabled." 63 | msgstr "目录创建功能没有启用。" 64 | 65 | #: ../flask_superadminex/ext/fileadmin.py:380 66 | #, python-format 67 | msgid "Failed to create directory: %(error)s" 68 | msgstr "创建目录失败: %(error)s" 69 | 70 | #: ../flask_superadminex/ext/fileadmin.py:402 71 | msgid "Deletion is disabled." 72 | msgstr "删除功能没有启用。" 73 | 74 | #: ../flask_superadminex/ext/fileadmin.py:407 75 | msgid "Directory deletion is disabled." 76 | msgstr "删除目录功能没有启用。" 77 | 78 | #: ../flask_superadminex/ext/fileadmin.py:412 79 | #, python-format 80 | msgid "Directory \"%s\" was successfully deleted." 81 | msgstr "目录 \"%s\" 已被成功地删除。" 82 | 83 | #: ../flask_superadminex/ext/fileadmin.py:414 84 | #, python-format 85 | msgid "Failed to delete directory: %(error)s" 86 | msgstr "删除目录失败: %(error)s" 87 | 88 | #: ../flask_superadminex/ext/fileadmin.py:418 89 | #, python-format 90 | msgid "File \"%(name)s\" was successfully deleted." 91 | msgstr "文件 \"%(name)s\" 已被成功地删除。" 92 | 93 | #: ../flask_superadminex/ext/fileadmin.py:420 94 | #, python-format 95 | msgid "Failed to delete file: %(name)s" 96 | msgstr "删除文件失败: %(name)s" 97 | 98 | #: ../flask_superadminex/ext/fileadmin.py:439 99 | msgid "Renaming is disabled." 100 | msgstr "重命名功能没有启用。" 101 | 102 | #: ../flask_superadminex/ext/fileadmin.py:443 103 | msgid "Path does not exist." 104 | msgstr "路径不存在。" 105 | 106 | #: ../flask_superadminex/ext/fileadmin.py:454 107 | #, python-format 108 | msgid "Successfully renamed \"%(src)s\" to \"%(dst)s\"" 109 | msgstr "\"%(src)s\" 已被成功地重命名为 \"%(dst)s\"" 110 | 111 | #: ../flask_superadminex/ext/fileadmin.py:457 112 | #, python-format 113 | msgid "Failed to rename: %(error)s" 114 | msgstr "重命名失败: %(error)s" 115 | 116 | #: ../flask_superadminex/ext/sqlamodel/filters.py:35 117 | msgid "equals" 118 | msgstr "相同" 119 | 120 | #: ../flask_superadminex/ext/sqlamodel/filters.py:43 121 | msgid "not equal" 122 | msgstr "不相同" 123 | 124 | #: ../flask_superadminex/ext/sqlamodel/filters.py:52 125 | msgid "contains" 126 | msgstr "包含" 127 | 128 | #: ../flask_superadminex/ext/sqlamodel/filters.py:61 129 | msgid "not contains" 130 | msgstr "不包含" 131 | 132 | #: ../flask_superadminex/ext/sqlamodel/filters.py:69 133 | msgid "greater than" 134 | msgstr "大于" 135 | 136 | #: ../flask_superadminex/ext/sqlamodel/filters.py:77 137 | msgid "smaller than" 138 | msgstr "小于" 139 | 140 | #: ../flask_superadminex/ext/sqlamodel/form.py:37 141 | msgid "Already exists." 142 | msgstr "已经存在." 143 | 144 | #: ../flask_superadminex/ext/sqlamodel/view.py:504 145 | #, python-format 146 | msgid "Failed to create model. %(error)s" 147 | msgstr "创建模型失败: %(error)s" 148 | 149 | #: ../flask_superadminex/ext/sqlamodel/view.py:519 150 | #, python-format 151 | msgid "Failed to update model. %(error)s" 152 | msgstr "更新模型失败: %(error)s" 153 | 154 | #: ../flask_superadminex/ext/sqlamodel/view.py:534 155 | #, python-format 156 | msgid "Failed to delete model. %(error)s" 157 | msgstr "删除模型失败: %(error)s" 158 | 159 | #: ../flask_superadminex/model/base.py:742 160 | msgid "Model was successfully created." 161 | msgstr "模型已被成功地创建。" 162 | 163 | #: ../flask_superadminex/model/filters.py:82 164 | msgid "Yes" 165 | msgstr "是" 166 | 167 | #: ../flask_superadminex/model/filters.py:83 168 | msgid "No" 169 | msgstr "否" 170 | 171 | #: ../flask_superadminex/templates/admin/lib.html:105 172 | msgid "Submit" 173 | msgstr "提交" 174 | 175 | #: ../flask_superadminex/templates/admin/lib.html:110 176 | msgid "Cancel" 177 | msgstr "取消" 178 | 179 | #: ../flask_superadminex/templates/admin/file/list.html:7 180 | msgid "Root" 181 | msgstr "根目录" 182 | 183 | #: ../flask_superadminex/templates/admin/file/list.html:42 184 | #, python-format 185 | msgid "Are you sure you want to delete \\'%(name)s\\' recursively?" 186 | msgstr "确定要递归删除 \\'%(name)s\\' 吗?" 187 | 188 | #: ../flask_superadminex/templates/admin/file/list.html:50 189 | #, python-format 190 | msgid "Are you sure you want to delete \\'%(name)s\\'?" 191 | msgstr "确定要删除 \\'%(name)s\\' 吗?" 192 | 193 | #: ../flask_superadminex/templates/admin/file/list.html:75 194 | msgid "Upload File" 195 | msgstr "上传文件" 196 | 197 | #: ../flask_superadminex/templates/admin/file/list.html:78 198 | msgid "Create Directory" 199 | msgstr "创建目录" 200 | 201 | #: ../flask_superadminex/templates/admin/file/rename.html:5 202 | #, python-format 203 | msgid "Please provide new name for %(name)s" 204 | msgstr "请为 %(name)s 提供新名字" 205 | 206 | #: ../flask_superadminex/templates/admin/model/create.html:11 207 | msgid "Save and Add" 208 | msgstr "保存并添加" 209 | 210 | #: ../flask_superadminex/templates/admin/model/create.html:16 211 | #: ../flask_superadminex/templates/admin/model/list.html:12 212 | msgid "List" 213 | msgstr "列表" 214 | 215 | #: ../flask_superadminex/templates/admin/model/create.html:19 216 | #: ../flask_superadminex/templates/admin/model/list.html:16 217 | msgid "Create" 218 | msgstr "创建" 219 | 220 | #: ../flask_superadminex/templates/admin/model/list.html:23 221 | msgid "Add Filter" 222 | msgstr "添加过滤规则" 223 | 224 | #: ../flask_superadminex/templates/admin/model/list.html:44 225 | msgid "Search" 226 | msgstr "搜索" 227 | 228 | #: ../flask_superadminex/templates/admin/model/list.html:57 229 | msgid "Apply" 230 | msgstr "应用" 231 | 232 | #: ../flask_superadminex/templates/admin/model/list.html:59 233 | msgid "Reset Filters" 234 | msgstr "重置所有过滤规则" 235 | 236 | #: ../flask_superadminex/templates/admin/model/list.html:67 237 | msgid "Remove Filter" 238 | msgstr "删除过滤规则" 239 | 240 | #: ../flask_superadminex/templates/admin/model/list.html:128 241 | msgid "You sure you want to delete this item?" 242 | msgstr "确定要删除该项吗?" 243 | 244 | -------------------------------------------------------------------------------- /flask_superadmin/model/backends/sqlalchemy/orm.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tools for generating forms based on SQLAlchemy Model schemas. 3 | """ 4 | 5 | from sqlalchemy import Column 6 | from sqlalchemy.orm.exc import NoResultFound 7 | 8 | from wtforms import Form, ValidationError, fields, validators 9 | from wtforms.ext.sqlalchemy.orm import converts, ModelConverter, model_form as original_model_form 10 | from wtforms.ext.sqlalchemy.fields import QuerySelectField, QuerySelectMultipleField 11 | 12 | from flask.ext.superadmin import form 13 | 14 | 15 | class Unique(object): 16 | """Checks field value unicity against specified table field. 17 | 18 | :param get_session: 19 | A function that return a SQAlchemy Session. 20 | :param model: 21 | The model to check unicity against. 22 | :param column: 23 | The unique column. 24 | :param message: 25 | The error message. 26 | """ 27 | field_flags = ('unique', ) 28 | 29 | def __init__(self, db_session, model, column, message=None): 30 | self.db_session = db_session 31 | self.model = model 32 | self.column = column 33 | self.message = message 34 | 35 | def __call__(self, form, field): 36 | try: 37 | obj = (self.db_session.query(self.model) 38 | .filter(self.column == field.data).one()) 39 | 40 | if not hasattr(form, '_obj') or not form._obj == obj: 41 | if self.message is None: 42 | self.message = field.gettext(u'Already exists.') 43 | raise ValidationError(self.message) 44 | except NoResultFound: 45 | pass 46 | 47 | 48 | class AdminModelConverter(ModelConverter): 49 | """ 50 | SQLAlchemy model to form converter 51 | """ 52 | def __init__(self, view): 53 | super(AdminModelConverter, self).__init__() 54 | 55 | self.view = view 56 | 57 | def _get_label(self, name, field_args): 58 | if 'label' in field_args: 59 | return field_args['label'] 60 | 61 | # if self.view.rename_columns: 62 | # return self.view.rename_columns.get(name) 63 | 64 | return None 65 | 66 | def _get_field_override(self, name): 67 | if self.view.field_overrides: 68 | return self.view.field_overrides.get(name) 69 | 70 | def convert(self, model, mapper, prop, field_args, *args): 71 | kwargs = { 72 | 'validators': [], 73 | 'filters': [] 74 | } 75 | 76 | if field_args: 77 | kwargs.update(field_args) 78 | 79 | if hasattr(prop, 'direction'): 80 | remote_model = prop.mapper.class_ 81 | local_column = prop.local_remote_pairs[0][0] 82 | 83 | kwargs.update({ 84 | 'allow_blank': local_column.nullable, 85 | 'label': self._get_label(prop.key, kwargs), 86 | 'query_factory': lambda: self.view.session.query(remote_model) 87 | }) 88 | if local_column.nullable: 89 | kwargs['validators'].append(validators.Optional()) 90 | elif prop.direction.name not in ('MANYTOMANY', 'ONETOMANY'): 91 | kwargs['validators'].append(validators.Required()) 92 | 93 | # Override field type if necessary 94 | override = self._get_field_override(prop.key) 95 | if override: 96 | return override(**kwargs) 97 | 98 | if prop.direction.name == 'MANYTOONE': 99 | return QuerySelectField(widget=form.ChosenSelectWidget(), 100 | **kwargs) 101 | elif prop.direction.name == 'ONETOMANY': 102 | # Skip backrefs 103 | if not local_column.foreign_keys and self.view.hide_backrefs: 104 | return None 105 | 106 | return QuerySelectMultipleField( 107 | widget=form.ChosenSelectWidget(multiple=True), 108 | **kwargs) 109 | elif prop.direction.name == 'MANYTOMANY': 110 | return QuerySelectMultipleField( 111 | widget=form.ChosenSelectWidget(multiple=True), 112 | **kwargs) 113 | else: 114 | # Ignore pk/fk 115 | if hasattr(prop, 'columns'): 116 | column = prop.columns[0] 117 | 118 | # Column can be SQL expressions 119 | # WTForms cannot convert them 120 | if not isinstance(column, Column): 121 | return None 122 | 123 | # Do not display foreign keys - use relations 124 | if column.foreign_keys: 125 | return None 126 | 127 | unique = False 128 | 129 | if column.primary_key: 130 | # By default, don't show primary keys either 131 | if self.view.fields is None: 132 | return None 133 | 134 | # If PK is not explicitly allowed, ignore it 135 | if prop.key not in self.view.fields: 136 | return None 137 | 138 | kwargs['validators'].append(Unique(self.view.session, 139 | model, 140 | column)) 141 | unique = True 142 | 143 | # If field is unique, validate it 144 | if column.unique and not unique: 145 | kwargs['validators'].append(Unique(self.view.session, 146 | model, 147 | column)) 148 | 149 | if column.nullable: 150 | kwargs['validators'].append(validators.Optional()) 151 | else: 152 | kwargs['validators'].append(validators.Required()) 153 | 154 | # Apply label 155 | kwargs['label'] = self._get_label(prop.key, kwargs) 156 | 157 | # Override field type if necessary 158 | override = self._get_field_override(prop.key) 159 | if override: 160 | return override(**kwargs) 161 | 162 | return super(AdminModelConverter, self).convert(model, mapper, 163 | prop, kwargs) 164 | 165 | @converts('Date') 166 | def convert_date(self, field_args, **extra): 167 | field_args['widget'] = form.DatePickerWidget() 168 | return fields.DateField(**field_args) 169 | 170 | @converts('DateTime') 171 | def convert_datetime(self, field_args, **extra): 172 | field_args['widget'] = form.DateTimePickerWidget() 173 | return fields.DateTimeField(**field_args) 174 | 175 | @converts('Time') 176 | def convert_time(self, field_args, **extra): 177 | return form.TimeField(**field_args) 178 | 179 | @converts('Text') 180 | def conv_Text_fix(self, field_args, **extra): 181 | return self.conv_Text(field_args, **extra) 182 | 183 | 184 | def model_form(model, base_class=Form, fields=None, readonly_fields=None, 185 | exclude=None, field_args=None, converter=None): 186 | only = tuple(set(fields or []) - set(readonly_fields or [])) 187 | return original_model_form(model, base_class=base_class, only=only, 188 | exclude=exclude, field_args=field_args, 189 | converter=converter) 190 | -------------------------------------------------------------------------------- /flask_superadmin/translations/ru/LC_MESSAGES/admin.po: -------------------------------------------------------------------------------- 1 | # Translations template for Flask-SuperAdminEx. 2 | # Copyright (C) 2012 ORGANIZATION 3 | # This file is distributed under the same license as the Flask-SuperAdminEx 4 | # project. 5 | # FIRST AUTHOR , 2012. 6 | # 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: Flask-SuperAdminEx\n" 10 | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" 11 | "POT-Creation-Date: 2012-04-11 18:47+0300\n" 12 | "PO-Revision-Date: 2012-04-11 18:48+0200\n" 13 | "Last-Translator: Serge S. Koval \n" 14 | "Language-Team: \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Generated-By: Babel 0.9.6\n" 19 | "X-Poedit-Language: Russian\n" 20 | "X-Poedit-Country: RUSSIAN FEDERATION\n" 21 | "X-Poedit-SourceCharset: utf-8\n" 22 | 23 | #: ../flask_superadminex/base.py:216 24 | msgid "Home" 25 | msgstr "Главная" 26 | 27 | #: ../flask_superadminex/form.py:81 28 | msgid "Invalid time format" 29 | msgstr "Неправильный формат времени." 30 | 31 | #: ../flask_superadminex/ext/fileadmin.py:32 32 | msgid "Invalid directory name" 33 | msgstr "Недопустимое имя директории" 34 | 35 | #: ../flask_superadminex/ext/fileadmin.py:40 36 | msgid "File to upload" 37 | msgstr "Файл" 38 | 39 | #: ../flask_superadminex/ext/fileadmin.py:49 40 | msgid "File required." 41 | msgstr "Необходимо выбрать файл" 42 | 43 | #: ../flask_superadminex/ext/fileadmin.py:54 44 | msgid "Invalid file type." 45 | msgstr "Недопустимый тип файла." 46 | 47 | #: ../flask_superadminex/ext/fileadmin.py:335 48 | msgid "File uploading is disabled." 49 | msgstr "Заливка файлов запрещена." 50 | 51 | #: ../flask_superadminex/ext/fileadmin.py:344 52 | #, python-format 53 | msgid "File \"%(name)s\" already exists." 54 | msgstr "Файл с именем \"%(name)s\" уже существует." 55 | 56 | #: ../flask_superadminex/ext/fileadmin.py:351 57 | #, python-format 58 | msgid "Failed to save file: %(error)s" 59 | msgstr "Ошибка сохранения файла: %(error)s" 60 | 61 | #: ../flask_superadminex/ext/fileadmin.py:370 62 | msgid "Directory creation is disabled." 63 | msgstr "Создание новых директорий запрещено." 64 | 65 | #: ../flask_superadminex/ext/fileadmin.py:380 66 | #, python-format 67 | msgid "Failed to create directory: %(error)s" 68 | msgstr "Ошибка создания директории: %(error)s" 69 | 70 | #: ../flask_superadminex/ext/fileadmin.py:402 71 | msgid "Deletion is disabled." 72 | msgstr "Удаление запрещено." 73 | 74 | #: ../flask_superadminex/ext/fileadmin.py:407 75 | msgid "Directory deletion is disabled." 76 | msgstr "Удаление директорий запрещено." 77 | 78 | #: ../flask_superadminex/ext/fileadmin.py:412 79 | #, python-format 80 | msgid "Directory \"%s\" was successfully deleted." 81 | msgstr "Директория \"%s\" была удалена." 82 | 83 | #: ../flask_superadminex/ext/fileadmin.py:414 84 | #, python-format 85 | msgid "Failed to delete directory: %(error)s" 86 | msgstr "Ошибка удаления директории: %(error)s" 87 | 88 | #: ../flask_superadminex/ext/fileadmin.py:418 89 | #, python-format 90 | msgid "File \"%(name)s\" was successfully deleted." 91 | msgstr "Файл \"%(name)s\" был удален." 92 | 93 | #: ../flask_superadminex/ext/fileadmin.py:420 94 | #, python-format 95 | msgid "Failed to delete file: %(name)s" 96 | msgstr "Ошибка удаления файла: %(name)s" 97 | 98 | #: ../flask_superadminex/ext/fileadmin.py:439 99 | msgid "Renaming is disabled." 100 | msgstr "Переименование запрещено." 101 | 102 | #: ../flask_superadminex/ext/fileadmin.py:443 103 | msgid "Path does not exist." 104 | msgstr "Путь не существует." 105 | 106 | #: ../flask_superadminex/ext/fileadmin.py:454 107 | #, python-format 108 | msgid "Successfully renamed \"%(src)s\" to \"%(dst)s\"" 109 | msgstr "\"%(src)s\" был переименован в \"%(dst)s\"" 110 | 111 | #: ../flask_superadminex/ext/fileadmin.py:457 112 | #, python-format 113 | msgid "Failed to rename: %(error)s" 114 | msgstr "Ошибка переименования: %(error)s" 115 | 116 | #: ../flask_superadminex/ext/sqlamodel/filters.py:35 117 | msgid "equals" 118 | msgstr "равно" 119 | 120 | #: ../flask_superadminex/ext/sqlamodel/filters.py:43 121 | msgid "not equal" 122 | msgstr "не равно" 123 | 124 | #: ../flask_superadminex/ext/sqlamodel/filters.py:52 125 | msgid "contains" 126 | msgstr "содержит" 127 | 128 | #: ../flask_superadminex/ext/sqlamodel/filters.py:61 129 | msgid "not contains" 130 | msgstr "не содержит" 131 | 132 | #: ../flask_superadminex/ext/sqlamodel/filters.py:69 133 | msgid "greater than" 134 | msgstr "больше чем" 135 | 136 | #: ../flask_superadminex/ext/sqlamodel/filters.py:77 137 | msgid "smaller than" 138 | msgstr "меньше чем" 139 | 140 | #: ../flask_superadminex/ext/sqlamodel/form.py:37 141 | msgid "Already exists." 142 | msgstr "Уже существует." 143 | 144 | #: ../flask_superadminex/ext/sqlamodel/view.py:504 145 | #, python-format 146 | msgid "Failed to create model. %(error)s" 147 | msgstr "Ошибка создания записи: %(error)s" 148 | 149 | #: ../flask_superadminex/ext/sqlamodel/view.py:519 150 | #, python-format 151 | msgid "Failed to update model. %(error)s" 152 | msgstr "Ошибка обновления записи: %(error)s" 153 | 154 | #: ../flask_superadminex/ext/sqlamodel/view.py:534 155 | #, python-format 156 | msgid "Failed to delete model. %(error)s" 157 | msgstr "Ошибка удаления записи: %(error)s" 158 | 159 | #: ../flask_superadminex/model/base.py:742 160 | msgid "Model was successfully created." 161 | msgstr "Запись была создана." 162 | 163 | #: ../flask_superadminex/model/filters.py:82 164 | msgid "Yes" 165 | msgstr "Да" 166 | 167 | #: ../flask_superadminex/model/filters.py:83 168 | msgid "No" 169 | msgstr "Нет" 170 | 171 | #: ../flask_superadminex/templates/admin/lib.html:105 172 | msgid "Submit" 173 | msgstr "Отправить" 174 | 175 | #: ../flask_superadminex/templates/admin/lib.html:110 176 | msgid "Cancel" 177 | msgstr "Отмена" 178 | 179 | #: ../flask_superadminex/templates/admin/file/list.html:7 180 | msgid "Root" 181 | msgstr "Корень" 182 | 183 | #: ../flask_superadminex/templates/admin/file/list.html:42 184 | #, python-format 185 | msgid "Are you sure you want to delete \\'%(name)s\\' recursively?" 186 | msgstr "Вы уверены что хотите рекурсивно удалить \\'%(name)s\\'?" 187 | 188 | #: ../flask_superadminex/templates/admin/file/list.html:50 189 | #, python-format 190 | msgid "Are you sure you want to delete \\'%(name)s\\'?" 191 | msgstr "Вы уверены что хотите удалить \\'%(name)s\\'?" 192 | 193 | #: ../flask_superadminex/templates/admin/file/list.html:75 194 | msgid "Upload File" 195 | msgstr "Залить файл" 196 | 197 | #: ../flask_superadminex/templates/admin/file/list.html:78 198 | msgid "Create Directory" 199 | msgstr "Создать директорию" 200 | 201 | #: ../flask_superadminex/templates/admin/file/rename.html:5 202 | #, python-format 203 | msgid "Please provide new name for %(name)s" 204 | msgstr "Введите новое имя для %(name)s" 205 | 206 | #: ../flask_superadminex/templates/admin/model/create.html:11 207 | msgid "Save and Add" 208 | msgstr "Сохранить и Добавить" 209 | 210 | #: ../flask_superadminex/templates/admin/model/create.html:16 211 | #: ../flask_superadminex/templates/admin/model/list.html:12 212 | msgid "List" 213 | msgstr "Список" 214 | 215 | #: ../flask_superadminex/templates/admin/model/create.html:19 216 | #: ../flask_superadminex/templates/admin/model/list.html:16 217 | msgid "Create" 218 | msgstr "Создать" 219 | 220 | #: ../flask_superadminex/templates/admin/model/list.html:23 221 | msgid "Add Filter" 222 | msgstr "Добавить Фильтр" 223 | 224 | #: ../flask_superadminex/templates/admin/model/list.html:44 225 | msgid "Search" 226 | msgstr "Поиск" 227 | 228 | #: ../flask_superadminex/templates/admin/model/list.html:57 229 | msgid "Apply" 230 | msgstr "Применить" 231 | 232 | #: ../flask_superadminex/templates/admin/model/list.html:59 233 | msgid "Reset Filters" 234 | msgstr "Сброс Фильтров" 235 | 236 | #: ../flask_superadminex/templates/admin/model/list.html:67 237 | msgid "Remove Filter" 238 | msgstr "Убрать Фильтр" 239 | 240 | #: ../flask_superadminex/templates/admin/model/list.html:128 241 | msgid "You sure you want to delete this item?" 242 | msgstr "Вы уверены что хотите удалить эту запись?" 243 | 244 | -------------------------------------------------------------------------------- /flask_superadmin/model/backends/django/orm.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tools for generating forms based on Django Model schemas. 3 | """ 4 | 5 | from wtforms import fields as f 6 | from wtforms import Form 7 | from wtforms import validators 8 | from wtforms.ext.django.fields import ModelSelectField 9 | 10 | from flask_superadmin import form 11 | 12 | __all__ = ( 13 | 'AdminModelConverter', 'model_fields', 'model_form' 14 | ) 15 | 16 | 17 | class ModelConverterBase(object): 18 | def __init__(self, converters): 19 | self.converters = converters 20 | 21 | def convert(self, model, field, field_args): 22 | kwargs = { 23 | 'label': field.verbose_name, 24 | 'description': field.help_text, 25 | 'validators': [], 26 | 'filters': [], 27 | 'default': field.default, 28 | } 29 | if field_args: 30 | kwargs.update(field_args) 31 | 32 | if field.blank: 33 | kwargs['validators'].append(validators.Optional()) 34 | if field.max_length is not None and field.max_length > 0: 35 | kwargs['validators'].append(validators.Length(max=field.max_length)) 36 | 37 | ftype = type(field).__name__ 38 | if field.choices: 39 | kwargs['choices'] = field.choices 40 | return f.SelectField(widget=form.ChosenSelectWidget(), **kwargs) 41 | elif ftype in self.converters: 42 | return self.converters[ftype](model, field, kwargs) 43 | else: 44 | converter = getattr(self, 'conv_%s' % ftype, None) 45 | if converter is not None: 46 | return converter(model, field, kwargs) 47 | 48 | 49 | class AdminModelConverter(ModelConverterBase): 50 | DEFAULT_SIMPLE_CONVERSIONS = { 51 | f.IntegerField: ['AutoField', 'IntegerField', 'SmallIntegerField', 52 | 'PositiveIntegerField', 'PositiveSmallIntegerField'], 53 | f.DecimalField: ['DecimalField', 'FloatField'], 54 | f.FileField: ['FileField', 'FilePathField', 'ImageField'], 55 | f.BooleanField: ['BooleanField'], 56 | f.TextField: ['CharField', 'PhoneNumberField', 'SlugField'], 57 | f.TextAreaField: ['TextField', 'XMLField'], 58 | } 59 | 60 | def __init__(self, extra_converters=None, simple_conversions=None): 61 | converters = {} 62 | if simple_conversions is None: 63 | simple_conversions = self.DEFAULT_SIMPLE_CONVERSIONS 64 | for field_type, django_fields in simple_conversions.iteritems(): 65 | converter = self.make_simple_converter(field_type) 66 | for name in django_fields: 67 | converters[name] = converter 68 | 69 | if extra_converters: 70 | converters.update(extra_converters) 71 | super(AdminModelConverter, self).__init__(converters) 72 | 73 | def make_simple_converter(self, field_type): 74 | def _converter(model, field, kwargs): 75 | return field_type(**kwargs) 76 | return _converter 77 | 78 | def conv_ForeignKey(self, model, field, kwargs): 79 | return ModelSelectField(widget=form.ChosenSelectWidget(), 80 | model=field.rel.to, **kwargs) 81 | 82 | def conv_TimeField(self, model, field, kwargs): 83 | def time_only(obj): 84 | try: 85 | return obj.time() 86 | except AttributeError: 87 | return obj 88 | kwargs['filters'].append(time_only) 89 | return f.DateTimeField(widget=form.DateTimePickerWidget(), 90 | format='%H:%M:%S', **kwargs) 91 | 92 | def conv_DateTimeField(self, model, field, kwargs): 93 | def time_only(obj): 94 | try: 95 | return obj.time() 96 | except AttributeError: 97 | return obj 98 | kwargs['filters'].append(time_only) 99 | return f.DateTimeField(widget=form.DateTimePickerWidget(), 100 | format='%H:%M:%S', **kwargs) 101 | 102 | def conv_DateField(self, model, field, kwargs): 103 | def time_only(obj): 104 | try: 105 | return obj.date() 106 | except AttributeError: 107 | return obj 108 | kwargs['filters'].append(time_only) 109 | return f.DateField(widget=form.DatePickerWidget(), **kwargs) 110 | 111 | def conv_EmailField(self, model, field, kwargs): 112 | kwargs['validators'].append(validators.email()) 113 | return f.TextField(**kwargs) 114 | 115 | def conv_IPAddressField(self, model, field, kwargs): 116 | kwargs['validators'].append(validators.ip_address()) 117 | return f.TextField(**kwargs) 118 | 119 | def conv_URLField(self, model, field, kwargs): 120 | kwargs['validators'].append(validators.url()) 121 | return f.TextField(**kwargs) 122 | 123 | def conv_USStateField(self, model, field, kwargs): 124 | try: 125 | from django.contrib.localflavor.us.us_states import STATE_CHOICES 126 | except ImportError: 127 | STATE_CHOICES = [] 128 | 129 | return f.SelectField(choices=STATE_CHOICES, **kwargs) 130 | 131 | def conv_NullBooleanField(self, model, field, kwargs): 132 | def coerce_nullbool(value): 133 | d = {'None': None, None: None, 'True': True, 'False': False} 134 | if value in d: 135 | return d[value] 136 | else: 137 | return bool(int(value)) 138 | 139 | choices = ((None, 'Unknown'), (True, 'Yes'), (False, 'No')) 140 | return f.SelectField(choices=choices, coerce=coerce_nullbool, **kwargs) 141 | 142 | 143 | def model_fields(model, fields=None, readonly_fields=None, exclude=None, 144 | field_args=None, converter=None): 145 | """ 146 | Generate a dictionary of fields for a given Django model. 147 | 148 | See `model_form` docstring for description of parameters. 149 | """ 150 | converter = converter or ModelConverter() 151 | field_args = field_args or {} 152 | 153 | model_fields = ((f.name, f) for f in model._meta.fields) 154 | if fields: 155 | model_fields = (x for x in model_fields if x[0] in fields) 156 | elif exclude: 157 | model_fields = (x for x in model_fields if x[0] not in exclude) 158 | 159 | field_dict = {} 160 | for name, model_field in model_fields: 161 | field = converter.convert(model, model_field, field_args.get(name)) 162 | if field is not None: 163 | field_dict[name] = field 164 | 165 | return field_dict 166 | 167 | 168 | def model_form(model, base_class=Form, fields=None, readonly_fields=None, 169 | exclude=None, field_args=None, converter=None): 170 | """ 171 | Create a wtforms Form for a given Django model class:: 172 | 173 | from wtforms.ext.django.orm import model_form 174 | from myproject.myapp.models import User 175 | UserForm = model_form(User) 176 | 177 | :param model: 178 | A Django ORM model class 179 | :param base_class: 180 | Base form class to extend from. Must be a ``wtforms.Form`` subclass. 181 | :param fields: 182 | An optional iterable with the property names that should be included 183 | in the form. Only these properties will have fields. It also 184 | determines the order of the fields. 185 | :param exclude: 186 | An optional iterable with the property names that should be excluded 187 | from the form. All other properties will have fields. 188 | :param field_args: 189 | An optional dictionary of field names mapping to keyword arguments 190 | used to construct each field object. 191 | :param converter: 192 | A converter to generate the fields based on the model properties. If 193 | not set, ``ModelConverter`` is used. 194 | """ 195 | exclude = ([f for f in exclude] if exclude else []) + ['id'] 196 | field_dict = model_fields(model, fields, readonly_fields, exclude, 197 | field_args, converter) 198 | return type(model._meta.object_name + 'Form', (base_class, ), field_dict) 199 | 200 | -------------------------------------------------------------------------------- /flask_superadmin/templates/admin/_macros.html: -------------------------------------------------------------------------------- 1 | {% macro pager(page, pages, generator) -%} 2 | {% if pages > 1 %} 3 | 73 | {% endif %} 74 | {%- endmacro %} 75 | 76 | {% macro render_field(field, show_error_list=True) %} 77 |
78 | {{ field.label }} 79 | {{ field(**kwargs)|safe }} 80 | {% if show_error_list and field.errors %} 81 |
    82 | {% for error in field.errors %}
  • {{ error }}{% endfor %} 83 |
84 | {% endif %} 85 |
86 | {% endmacro %} 87 | 88 | {% macro render_ff (ff, delete) %} 89 |
90 | {% if ff.type == "FormField" %} 91 | 92 | {% if delete %} 93 | Delete 94 | {% else %} 95 |

{{ admin_view.field_name(ff.short_name) }}

96 | {% endif %} 97 | 98 |
{{ render_formfield(ff.form) }}
99 | {% elif ff.type == "ListField" %} 100 |

{{ admin_view.field_name(ff.short_name) }}

101 | {% if delete %} 102 | Delete 103 | {% endif %} 104 | {% set a = ff.new_generic() %} 105 |
106 | 107 | {% for field in ff %} 108 | {{ render_ff(field, admin_view.can_edit or not instance) }} 109 | {% endfor %} 110 | {% if admin_view.can_edit or not instance %} 111 | 112 | {% endif %} 113 |
114 |
115 | {% else %} 116 | 117 | {% if delete %} 118 | Delete 119 | {% else %} 120 | 121 | {% endif %} 122 | 123 |
124 | {% set class='' %} 125 | {% if ff.type == "DateTimeField" %} 126 | {% set data_type="datetimepicker" %} 127 | {% endif %} 128 | 129 | {% if ff.errors|length>0 %} {% set class=class+' error' %}{% endif %} 130 | 131 | {% if not admin_view.can_edit and instance %} 132 | 133 | {% with reference = admin_view.get_reference(ff.data) %} 134 | {% if reference %} 135 | 136 | {% else %} 137 |
{{ ff.data }}
138 | {% endif %} 139 | {% endwith %} 140 | 141 | {% else %} 142 | {{ ff(class=class) }} 143 | {% endif %} 144 | 145 | {% for error in ff.errors %} 146 |  {{ error }} 147 | {% endfor %} 148 | 149 | {% if ff.description %} 150 |

{{ ff.description }}

151 | {% endif %} 152 |
153 | {% endif %} 154 |
155 | {% endmacro %} 156 | 157 | {% macro render_formfield(form) %} 158 |
159 | {% set readonly_fields = admin_view.get_readonly_fields(instance) %} 160 | {% set field_names = admin_view.fields or form._fields.keys() %} 161 | 162 | {% for field_name in field_names %} 163 | {% if field_name in readonly_fields %} 164 | {% with f = readonly_fields[field_name] %} 165 |
166 | 167 |
168 |
169 | {% if f.url %} 170 | {{ f.value }} 171 | {% else %} 172 | {{ f.value }} 173 | {% endif %} 174 |
175 |
176 |
177 | {% endwith %} 178 | {% else %} 179 | {% if field_name != 'csrf_token' and field_name != 'csrf' %} 180 | {{ render_ff(form._fields[field_name]) }} 181 | {% endif %} 182 | {% endif %} 183 | {% endfor %} 184 | 185 |
186 | {% endmacro %} 187 | 188 | {% macro render_form(form, extra=None, can_edit=True, can_delete=True) -%} 189 |
190 | {{ form.hidden_tag() if form.hidden_tag is defined }} 191 | 192 | {{ render_formfield(form) }} 193 | 194 | {# if the view is not editable nor deletable, there's no point to show any buttons #} 195 | {% if can_edit or can_delete %} 196 |
197 | {% if can_edit or not instance %} 198 | 199 | {% endif %} 200 | {% if extra %} 201 | {{ extra }} 202 | {% endif %} 203 |
204 | {% endif %} 205 |
206 | {% endmacro %} 207 | 208 | -------------------------------------------------------------------------------- /doc/_themes/flask/static/flasky.css_t: -------------------------------------------------------------------------------- 1 | /* 2 | * flasky.css_t 3 | * ~~~~~~~~~~~~ 4 | * 5 | * :copyright: Copyright 2010 by Armin Ronacher. 6 | * :license: Flask Design License, see LICENSE for details. 7 | */ 8 | 9 | {% set page_width = '940px' %} 10 | {% set sidebar_width = '220px' %} 11 | 12 | @import url("basic.css"); 13 | 14 | /* -- page layout ----------------------------------------------------------- */ 15 | 16 | body { 17 | font-family: 'Georgia', serif; 18 | font-size: 17px; 19 | background-color: white; 20 | color: #000; 21 | margin: 0; 22 | padding: 0; 23 | } 24 | 25 | div.document { 26 | width: {{ page_width }}; 27 | margin: 30px auto 0 auto; 28 | } 29 | 30 | div.documentwrapper { 31 | float: left; 32 | width: 100%; 33 | } 34 | 35 | div.bodywrapper { 36 | margin: 0 0 0 {{ sidebar_width }}; 37 | } 38 | 39 | div.sphinxsidebar { 40 | width: {{ sidebar_width }}; 41 | } 42 | 43 | hr { 44 | border: 1px solid #B1B4B6; 45 | } 46 | 47 | div.body { 48 | background-color: #ffffff; 49 | color: #3E4349; 50 | padding: 0 30px 0 30px; 51 | } 52 | 53 | img.floatingflask { 54 | padding: 0 0 10px 10px; 55 | float: right; 56 | } 57 | 58 | div.footer { 59 | width: {{ page_width }}; 60 | margin: 20px auto 30px auto; 61 | font-size: 14px; 62 | color: #888; 63 | text-align: right; 64 | } 65 | 66 | div.footer a { 67 | color: #888; 68 | } 69 | 70 | div.related { 71 | display: none; 72 | } 73 | 74 | div.sphinxsidebar a { 75 | color: #444; 76 | text-decoration: none; 77 | border-bottom: 1px dotted #999; 78 | } 79 | 80 | div.sphinxsidebar a:hover { 81 | border-bottom: 1px solid #999; 82 | } 83 | 84 | div.sphinxsidebar { 85 | font-size: 14px; 86 | line-height: 1.5; 87 | } 88 | 89 | div.sphinxsidebarwrapper { 90 | padding: 18px 10px; 91 | } 92 | 93 | div.sphinxsidebarwrapper p.logo { 94 | padding: 0 0 20px 0; 95 | margin: 0; 96 | text-align: center; 97 | } 98 | 99 | div.sphinxsidebar h3, 100 | div.sphinxsidebar h4 { 101 | font-family: 'Garamond', 'Georgia', serif; 102 | color: #444; 103 | font-size: 24px; 104 | font-weight: normal; 105 | margin: 0 0 5px 0; 106 | padding: 0; 107 | } 108 | 109 | div.sphinxsidebar h4 { 110 | font-size: 20px; 111 | } 112 | 113 | div.sphinxsidebar h3 a { 114 | color: #444; 115 | } 116 | 117 | div.sphinxsidebar p.logo a, 118 | div.sphinxsidebar h3 a, 119 | div.sphinxsidebar p.logo a:hover, 120 | div.sphinxsidebar h3 a:hover { 121 | border: none; 122 | } 123 | 124 | div.sphinxsidebar p { 125 | color: #555; 126 | margin: 10px 0; 127 | } 128 | 129 | div.sphinxsidebar ul { 130 | margin: 10px 0; 131 | padding: 0; 132 | color: #000; 133 | } 134 | 135 | div.sphinxsidebar input { 136 | border: 1px solid #ccc; 137 | font-family: 'Georgia', serif; 138 | font-size: 1em; 139 | } 140 | 141 | /* -- body styles ----------------------------------------------------------- */ 142 | 143 | a { 144 | color: #004B6B; 145 | text-decoration: underline; 146 | } 147 | 148 | a:hover { 149 | color: #6D4100; 150 | text-decoration: underline; 151 | } 152 | 153 | div.body h1, 154 | div.body h2, 155 | div.body h3, 156 | div.body h4, 157 | div.body h5, 158 | div.body h6 { 159 | font-family: 'Garamond', 'Georgia', serif; 160 | font-weight: normal; 161 | margin: 30px 0px 10px 0px; 162 | padding: 0; 163 | } 164 | 165 | {% if theme_index_logo %} 166 | div.indexwrapper h1 { 167 | text-indent: -999999px; 168 | background: url({{ theme_index_logo }}) no-repeat center center; 169 | height: {{ theme_index_logo_height }}; 170 | } 171 | {% endif %} 172 | 173 | div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; } 174 | div.body h2 { font-size: 180%; } 175 | div.body h3 { font-size: 150%; } 176 | div.body h4 { font-size: 130%; } 177 | div.body h5 { font-size: 100%; } 178 | div.body h6 { font-size: 100%; } 179 | 180 | a.headerlink { 181 | color: #ddd; 182 | padding: 0 4px; 183 | text-decoration: none; 184 | } 185 | 186 | a.headerlink:hover { 187 | color: #444; 188 | background: #eaeaea; 189 | } 190 | 191 | div.body p, div.body dd, div.body li { 192 | line-height: 1.4em; 193 | } 194 | 195 | div.admonition { 196 | background: #fafafa; 197 | margin: 20px -30px; 198 | padding: 10px 30px; 199 | border-top: 1px solid #ccc; 200 | border-bottom: 1px solid #ccc; 201 | } 202 | 203 | div.admonition tt.xref, div.admonition a tt { 204 | border-bottom: 1px solid #fafafa; 205 | } 206 | 207 | dd div.admonition { 208 | margin-left: -60px; 209 | padding-left: 60px; 210 | } 211 | 212 | div.admonition p.admonition-title { 213 | font-family: 'Garamond', 'Georgia', serif; 214 | font-weight: normal; 215 | font-size: 24px; 216 | margin: 0 0 10px 0; 217 | padding: 0; 218 | line-height: 1; 219 | } 220 | 221 | div.admonition p.last { 222 | margin-bottom: 0; 223 | } 224 | 225 | div.highlight { 226 | background-color: white; 227 | } 228 | 229 | dt:target, .highlight { 230 | background: #FAF3E8; 231 | } 232 | 233 | div.note { 234 | background-color: #eee; 235 | border: 1px solid #ccc; 236 | } 237 | 238 | div.seealso { 239 | background-color: #ffc; 240 | border: 1px solid #ff6; 241 | } 242 | 243 | div.topic { 244 | background-color: #eee; 245 | } 246 | 247 | p.admonition-title { 248 | display: inline; 249 | } 250 | 251 | p.admonition-title:after { 252 | content: ":"; 253 | } 254 | 255 | pre, tt { 256 | font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; 257 | font-size: 0.9em; 258 | } 259 | 260 | img.screenshot { 261 | } 262 | 263 | tt.descname, tt.descclassname { 264 | font-size: 0.95em; 265 | } 266 | 267 | tt.descname { 268 | padding-right: 0.08em; 269 | } 270 | 271 | img.screenshot { 272 | -moz-box-shadow: 2px 2px 4px #eee; 273 | -webkit-box-shadow: 2px 2px 4px #eee; 274 | box-shadow: 2px 2px 4px #eee; 275 | } 276 | 277 | table.docutils { 278 | border: 1px solid #888; 279 | -moz-box-shadow: 2px 2px 4px #eee; 280 | -webkit-box-shadow: 2px 2px 4px #eee; 281 | box-shadow: 2px 2px 4px #eee; 282 | } 283 | 284 | table.docutils td, table.docutils th { 285 | border: 1px solid #888; 286 | padding: 0.25em 0.7em; 287 | } 288 | 289 | table.field-list, table.footnote { 290 | border: none; 291 | -moz-box-shadow: none; 292 | -webkit-box-shadow: none; 293 | box-shadow: none; 294 | } 295 | 296 | table.footnote { 297 | margin: 15px 0; 298 | width: 100%; 299 | border: 1px solid #eee; 300 | background: #fdfdfd; 301 | font-size: 0.9em; 302 | } 303 | 304 | table.footnote + table.footnote { 305 | margin-top: -15px; 306 | border-top: none; 307 | } 308 | 309 | table.field-list th { 310 | padding: 0 0.8em 0 0; 311 | } 312 | 313 | table.field-list td { 314 | padding: 0; 315 | } 316 | 317 | table.footnote td.label { 318 | width: 0px; 319 | padding: 0.3em 0 0.3em 0.5em; 320 | } 321 | 322 | table.footnote td { 323 | padding: 0.3em 0.5em; 324 | } 325 | 326 | dl { 327 | margin: 0; 328 | padding: 0; 329 | } 330 | 331 | dl dd { 332 | margin-left: 30px; 333 | } 334 | 335 | blockquote { 336 | margin: 0 0 0 30px; 337 | padding: 0; 338 | } 339 | 340 | ul, ol { 341 | margin: 10px 0 10px 30px; 342 | padding: 0; 343 | } 344 | 345 | pre { 346 | background: #eee; 347 | padding: 7px 30px; 348 | margin: 15px -30px; 349 | line-height: 1.3em; 350 | } 351 | 352 | dl pre, blockquote pre, li pre { 353 | margin-left: -60px; 354 | padding-left: 60px; 355 | } 356 | 357 | dl dl pre { 358 | margin-left: -90px; 359 | padding-left: 90px; 360 | } 361 | 362 | tt { 363 | background-color: #ecf0f3; 364 | color: #222; 365 | /* padding: 1px 2px; */ 366 | } 367 | 368 | tt.xref, a tt { 369 | background-color: #FBFBFB; 370 | border-bottom: 1px solid white; 371 | } 372 | 373 | a.reference { 374 | text-decoration: none; 375 | border-bottom: 1px dotted #004B6B; 376 | } 377 | 378 | a.reference:hover { 379 | border-bottom: 1px solid #6D4100; 380 | } 381 | 382 | a.footnote-reference { 383 | text-decoration: none; 384 | font-size: 0.7em; 385 | vertical-align: top; 386 | border-bottom: 1px dotted #004B6B; 387 | } 388 | 389 | a.footnote-reference:hover { 390 | border-bottom: 1px solid #6D4100; 391 | } 392 | 393 | a:hover tt { 394 | background: #EEE; 395 | } 396 | -------------------------------------------------------------------------------- /flask_superadmin/static/css/admin.css: -------------------------------------------------------------------------------- 1 | /* Global styles */ 2 | body 3 | { 4 | padding-top:20px; 5 | background: #F6F6F6 url(../img/background.png); 6 | font-family:'Open Sans',sans-serif; 7 | } 8 | #main-nav a { 9 | font-size:15px; 10 | color: #999; 11 | font-weight: 400; 12 | text-shadow: 0 1px rgba(255, 255, 255, .75); 13 | } 14 | @media (min-width: 767px) { 15 | body { 16 | padding-top:48px; 17 | padding-bottom:36px; 18 | } 19 | } 20 | input { 21 | line-height: 1em; 22 | } 23 | #main-nav > .active > a, #main-nav > .active > a:hover { 24 | background: none; 25 | color:#717E93; 26 | border-color:#717E93; 27 | border-radius: 0; 28 | /* text-shadow: none; 29 | background-color:#717E93; 30 | color:white; 31 | border-radius: 4px; 32 | */ } 33 | #main-nav a { 34 | /* border-left:2px solid transparent;; 35 | *//* border-top:2px solid transparent; 36 | border-bottom:2px solid transparent; 37 | */ font-size:13px; 38 | line-height: 1.3em; 39 | font-weight: 600; 40 | padding:8px 8px; 41 | margin:0; 42 | text-transform: uppercase;; 43 | } 44 | #main-nav li { 45 | border-top: 1px solid rgba(113, 126, 147, 0.1); 46 | line-height: 1em; 47 | } 48 | #main-nav { 49 | border-bottom: 1px solid rgba(113, 126, 147, 0.1); 50 | 51 | } 52 | /* #main-nav > .active > a, #main-nav > .active > a:hover { 53 | background-color:#717E93; 54 | border-radius:3px; 55 | text-shadow: white 0 1px 0 rgba(0, 0, 0, .1); 56 | color:white; 57 | } 58 | #main-nav a:hover { 59 | color:#717E93; 60 | }*/ 61 | 62 | .navbar-inner { 63 | background: none; 64 | box-shadow: none; 65 | } 66 | #content { 67 | border-radius: 2px; 68 | background: white; 69 | border: 1px solid #CCC; 70 | padding: 22px 32px; 71 | box-sizing: border-box; 72 | -moz-box-sizing:border-box; 73 | -webkit-box-sizing: border-box; 74 | } 75 | .model-check { 76 | width:15px; 77 | } 78 | .table tbody tr:hover td, 79 | .table tbody tr:hover th { 80 | background: #F9F9F9; 81 | } 82 | .table tbody tr.checked td{ 83 | background:rgb(255, 255, 230)!important; 84 | } 85 | .table th, .table td { 86 | border-top-color:#F2F2F2; 87 | } 88 | a { 89 | color:#3F6EC2; 90 | text-decoration: none; 91 | } 92 | a:hover { 93 | color:#717E93; 94 | } 95 | 96 | #main-title { 97 | float: left; 98 | } 99 | 100 | a.btn-title,input.btn-title, .btn-title .btn { 101 | padding: 8px 16px; 102 | text-transform: uppercase; 103 | } 104 | 105 | .btn-title.chzn-container-single .chzn-single { 106 | padding: 8px 16px 3px; 107 | text-transform: uppercase; 108 | } 109 | 110 | .btn-title.btn-group,a.btn-title,input.btn-title, .btn-title { 111 | float: right; 112 | margin-right:0; 113 | margin-left:10px; 114 | } 115 | 116 | .actions { 117 | width:160px; 118 | } 119 | 120 | form > .form-buttons > .btn.btn-primary { 121 | margin: 0; 122 | margin-left: 10px; 123 | float: right; 124 | } 125 | 126 | hr { 127 | clear:both; 128 | border-top:1px solid #CCC; 129 | margin-top:12px; 130 | margin-bottom:5px; 131 | } 132 | 133 | .page-content { 134 | margin-top: 30px; 135 | } 136 | 137 | h1,h2,h3,h4,h5,h6{ 138 | font-weight: 300; 139 | } 140 | 141 | .btn,.chzn-container-single .chzn-single,.chzn-container-multi .chzn-choices .search-choice { 142 | border: 1px solid rgba(0, 0, 0, 0.25); 143 | border-radius: 1px; 144 | box-shadow: 0 1px 0 rgba(0, 0, 0, 0.08), 145 | inset 0 1px 2px rgba(255, 255, 255, 0.75); 146 | font: inherit; 147 | margin: 0; 148 | } 149 | 150 | .btn-group .btn:first-child { 151 | border-bottom-left-radius:1px; 152 | border-top-left-radius:1px; 153 | } 154 | 155 | .btn-group .btn:last-child { 156 | border-bottom-right-radius:1px; 157 | border-top-right-radius:1px; 158 | } 159 | 160 | .chzn-container-multi .chzn-choices .search-choice { 161 | margin: 3px 0 0 3px; 162 | padding: 1px 19px 0 4px; 163 | } 164 | 165 | .chzn-container-single .chzn-single { 166 | padding-top:4px; 167 | } 168 | 169 | .datepicker { 170 | border:1px solid #AAA; 171 | } 172 | 173 | .datepicker .nav { 174 | box-shadow: none; 175 | border-radius: 3px 3px 0 0; 176 | } 177 | 178 | .chzn-container-single .chzn-search { 179 | background: #e6e6e6; 180 | border-bottom: 1px solid rgba(0, 0, 0, 0.25); 181 | } 182 | 183 | .chzn-container .chzn-results { 184 | margin: 0; 185 | padding: 0; 186 | } 187 | 188 | .chzn-container-single .chzn-search input:focus { 189 | box-shadow: none; 190 | } 191 | 192 | .btn:hover { 193 | box-shadow: 0 1px 0 rgba(0, 0, 0, 0.12), 194 | inset 0 1px 2px rgba(255, 255, 255, 0.95); 195 | color: black; 196 | } 197 | 198 | .btn-primary, .btn-danger { 199 | box-shadow: 0 1px 0 rgba(0, 0, 0, 0.08); 200 | text-shadow: 0 1px 0 #717E93; 201 | } 202 | 203 | .btn-primary:hover, .btn-danger:hover { 204 | color:white; 205 | box-shadow: 0 1px 0 rgba(0, 0, 0, 0.12); 206 | } 207 | 208 | /* Form customizations */ 209 | form.icon { 210 | display: inline; 211 | } 212 | 213 | form.icon button { 214 | border: none; 215 | background: transparent; 216 | text-decoration: none; 217 | padding: 0; 218 | line-height: normal; 219 | } 220 | 221 | a.icon { 222 | text-decoration: none; 223 | } 224 | 225 | /* Model search form */ 226 | form.search-form { 227 | margin: 4px 0 0 0; 228 | } 229 | 230 | form.search-form a.clear i { 231 | margin: 2px 0 0 0; 232 | } 233 | 234 | /* Filters */ 235 | .filter-row { 236 | margin: 4px; 237 | } 238 | 239 | .filter-row a, .filter-row select { 240 | margin-right: 4px; 241 | } 242 | 243 | .filter-row input 244 | { 245 | margin-bottom: 0px; 246 | width: 208px; 247 | } 248 | 249 | .filter-row .remove-filter 250 | { 251 | vertical-align: middle; 252 | } 253 | 254 | .filter-row .remove-filter .close-icon 255 | { 256 | font-size: 16px; 257 | } 258 | 259 | .filter-row .remove-filter .close-icon:hover 260 | { 261 | color: black; 262 | opacity: 0.4; 263 | } 264 | 265 | .dropdown-menu input { 266 | width:100%; 267 | background: none; 268 | text-align: left; 269 | border: none; 270 | box-shadow:none; 271 | } 272 | 273 | .dropdown-menu li > a:hover, 274 | .dropdown-menu .active > a, 275 | .dropdown-menu .active > a:hover, 276 | .dropdown-menu li > input:hover, 277 | .dropdown-menu .active > input, 278 | .dropdown-menu .active > input:hover { 279 | color: #ffffff; 280 | text-decoration: none; 281 | background-color: #3875D7; 282 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#3875d7', endColorstr='#2a62bc', GradientType=0 ); 283 | background-image: -webkit-gradient(linear, 0 0, 0 100%, color-stop(20%, #3875D7), color-stop(90%, #2A62BC)); 284 | background-image: -webkit-linear-gradient(top, #3875D7 20%, #2A62BC 90%); 285 | background-image: -moz-linear-gradient(top, #3875D7 20%, #2A62BC 90%); 286 | background-image: -o-linear-gradient(top, #3875D7 20%, #2A62BC 90%); 287 | background-image: linear-gradient(#3875D7 20%, #2A62BC 90%); 288 | color: white; 289 | } 290 | 291 | .search { 292 | position: relative; 293 | } 294 | 295 | .search .search-input { 296 | width: 250px; 297 | margin-bottom: 10px; 298 | padding: 4px 12px 4px 25px; 299 | border: solid 1px #999; 300 | background: #fff url("../img/search-input-icon.png") 10px 10px no-repeat; 301 | resize: none; 302 | -webkit-border-radius: 16px; 303 | -moz-border-radius: 16px; 304 | border-radius: 16px; 305 | -webkit-background-clip: padding-box; 306 | -moz-background-clip: padding; 307 | background-clip: padding-box; 308 | -webkit-box-sizing: border-box; 309 | -moz-box-sizing: border-box; 310 | box-sizing: border-box; 311 | height: 30px; 312 | } 313 | 314 | .search .clear-btn { 315 | position: absolute; 316 | right: auto; 317 | left: 225px; 318 | top: 7px; 319 | background: #ddd; 320 | border-radius: 16px; 321 | width: 16px; 322 | height: 16px; 323 | color: white; 324 | line-height: 0; 325 | cursor: pointer; 326 | -webkit-touch-callout: none; 327 | -webkit-user-select: none; 328 | -moz-user-select: none; 329 | -ms-user-select: none; 330 | -o-user-select: none; 331 | user-select: none; 332 | } 333 | 334 | .search .clear-btn:before { 335 | content: '×'; 336 | padding: 9px 0 0 3px; 337 | display: inline-block; 338 | font-weight: bold; 339 | font-size: 17px; 340 | font-family: arial; 341 | } 342 | 343 | --------------------------------------------------------------------------------