├── .gitignore ├── .gitmodules ├── AUTHORS ├── CHANGES ├── LICENSE ├── MANIFEST.in ├── README ├── docs ├── Makefile ├── api.rst ├── conf.py ├── contents.rst ├── index.rst ├── make.bat └── overview.rst ├── example ├── __init__.py ├── authentication │ ├── __init__.py │ ├── templates │ │ ├── admin │ │ │ └── extra_base.html │ │ └── login.html │ └── view_decorator.py ├── declarative │ ├── __init__.py │ ├── composite_keys.py │ ├── custom_form.py │ ├── multiple.py │ └── simple.py ├── flask_sqlalchemy │ ├── __init__.py │ ├── flaskext_sa_example.py │ ├── flaskext_sa_multi_pk.py │ └── flaskext_sa_simple.py └── mongoalchemy │ ├── __init__.py │ └── simple.py ├── flask_admin ├── __init__.py ├── datastore │ ├── __init__.py │ ├── core.py │ ├── mongoalchemy.py │ └── sqlalchemy.py ├── static │ ├── css │ │ ├── Aristo │ │ │ ├── Aristo.css │ │ │ ├── images │ │ │ │ ├── bg_fallback.png │ │ │ │ ├── icon_sprite.png │ │ │ │ ├── progress_bar.gif │ │ │ │ ├── slider_handles.png │ │ │ │ ├── ui-icons_222222_256x240.png │ │ │ │ └── ui-icons_454545_256x240.png │ │ │ └── jquery-ui-1.8.7.custom.css │ │ ├── bootstrap.min.css │ │ ├── chosen-sprite.png │ │ ├── chosen.css │ │ ├── jquery-ui │ │ │ ├── jquery.crossSelect.css │ │ │ └── jquery.crossSelect.custom.css │ │ └── style.css │ ├── img │ │ ├── glyphicons-halflings-white.png │ │ └── glyphicons-halflings.png │ └── js │ │ ├── admin.js │ │ ├── libs │ │ ├── bootstrap.min.js │ │ ├── chosen.jquery.min.js │ │ ├── dd_belatedpng.js │ │ ├── jquery-1.7.1.min.js │ │ ├── jquery-ui-1.8.17.custom.min.js │ │ ├── jquery-ui-1.8.9.custom.min.js │ │ ├── jquery-ui-timepicker-addon.js │ │ └── modernizr-1.6.min.js │ │ ├── mylibs │ │ └── .gitignore │ │ └── plugins.js ├── templates │ └── admin │ │ ├── _formhelpers.html │ │ ├── _helpers.html │ │ ├── _paginationhelpers.html │ │ ├── _statichelpers.html │ │ ├── add.html │ │ ├── base.html │ │ ├── edit.html │ │ ├── extra_base.html │ │ ├── index.html │ │ └── list.html ├── util.py └── wtforms.py ├── setup.cfg ├── setup.py ├── test ├── __init__.py ├── custom_form.py ├── deprecation.py ├── filefield.py ├── mongoalchemy_datastore.py └── sqlalchemy_with_defaults.py ├── test_admin.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | *.egg-info/ 4 | *.egg/ 5 | docs/_build/ 6 | *.pyc 7 | *~ 8 | *.swp -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "docs/_themes"] 2 | path = docs/_themes 3 | url = git://github.com/mitsuhiko/flask-sphinx-themes.git 4 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | 2 | 3 | Flask-Admin was initially developed by Andy Wilson. The current list 4 | of authors and contributors includes: 5 | 6 | - mfa (Andreas Madsack) 7 | - ralfonso (Ryan Roemmich) 8 | - reite 9 | - delai 10 | - mgood (Matt Good) -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | 0.4.2 2 | - fix bug with converting SQLAlchemy models to forms 3 | 4 | 0.4.1 5 | - fix Flask-SQLAlchemy import path 6 | 7 | 0.4.0 8 | - front end redesign 9 | - fix bug with displaying default value from SQLAlchemy models 10 | 11 | 0.3.0 12 | - added datastore API to support additional datastores more easily 13 | - added MongoAlchemy support 14 | - added composite primary key support 15 | - changed `admin.list_view` endpoint to `admin.list` for consistency 16 | 17 | 0.2.x 18 | - move from flaskext namespace to flask_admin 19 | - miscellaneous fixes and frontend tweaks 20 | 21 | 0.1.x 22 | - initial release 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 by Andy Wilson. 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 7 | 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 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 22 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 23 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 24 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 25 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 26 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 27 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 28 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 29 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | ------------------------------------------------------------------------ 33 | 34 | 35 | 36 | 37 | This distribution of Flask-Admin includes code from a number of open 38 | source projects. Listed below are the licenses and attributions for 39 | those projects. The source code of these projects containing copyright 40 | notices and more information can be found in the flask_admin/static/ 41 | directory. 42 | 43 | 44 | 45 | ------------------------------------------------------------------------ 46 | jQuery JavaScript Library v1.5 47 | http://jquery.com/ 48 | 49 | Copyright 2011, John Resig 50 | Dual licensed under the MIT or GPL Version 2 licenses. 51 | http://jquery.org/license 52 | 53 | Includes Sizzle.js 54 | http://sizzlejs.com/ 55 | Copyright 2011, The Dojo Foundation 56 | Released under the MIT, BSD, and GPL Licenses. 57 | 58 | Date: Mon Jan 31 08:31:29 2011 -0500 59 | 60 | 61 | 62 | ------------------------------------------------------------------------ 63 | jQuery UI 1.8.9 64 | 65 | Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) 66 | Dual licensed under the MIT or GPL Version 2 licenses. 67 | 68 | http://jquery.org/license 69 | http://docs.jquery.com/UI 70 | 71 | 72 | 73 | ------------------------------------------------------------------------ 74 | Modernizr v1.6 75 | http://www.modernizr.com 76 | 77 | Developed by: 78 | - Faruk Ates http://farukat.es/ 79 | - Paul Irish http://paulirish.com/ 80 | 81 | Copyright (c) 2009-2010 82 | Dual-licensed under the BSD or MIT licenses. 83 | http://www.modernizr.com/license/ 84 | 85 | 86 | 87 | ------------------------------------------------------------------------ 88 | jQuery timepicker addon 89 | By: Trent Richardson [http://trentrichardson.com] 90 | Version 0.9.5 91 | Last Modified: 05/25/2011 92 | 93 | Copyright 2011 Trent Richardson 94 | Dual licensed under the MIT and GPL licenses. 95 | http://trentrichardson.com/Impromptu/GPL-LICENSE.txt 96 | http://trentrichardson.com/Impromptu/MIT-LICENSE.txt 97 | 98 | 99 | 100 | ------------------------------------------------------------------------ 101 | 102 | Chosen, a Select Box Enhancer for jQuery and Protoype 103 | by Patrick Filler for Harvest 104 | 105 | Available for use under the MIT License 106 | 107 | Copyright (c) 2011 by Harvest 108 | 109 | Permission is hereby granted, free of charge, to any person obtaining 110 | a copy of this software and associated documentation files (the 111 | "Software"), to deal in the Software without restriction, including 112 | without limitation the rights to use, copy, modify, merge, publish, 113 | distribute, sublicense, and/or sell copies of the Software, and to 114 | permit persons to whom the Software is furnished to do so, subject to 115 | the following conditions: 116 | 117 | The above copyright notice and this permission notice shall be 118 | included in all copies or substantial portions of the Software. 119 | 120 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 121 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 122 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 123 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 124 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 125 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 126 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 127 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE *.py 2 | recursive-include flask_admin * 3 | recursive-include docs * 4 | recursive-exclude docs *.pyc 5 | recursive-exclude docs *.pyo 6 | prune docs/_build 7 | prune docs/_themes/.git 8 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | This is the code for the deprecated Flask-Admin 0.x branch. 2 | 3 | Flask-Admin 1.x is way better. Go check it out at: 4 | http://github.com/mrjoes/flask-admin 5 | -------------------------------------------------------------------------------- /docs/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) . 14 | 15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " singlehtml to make a single large HTML file" 22 | @echo " pickle to make pickle files" 23 | @echo " json to make JSON files" 24 | @echo " htmlhelp to make HTML files and a HTML help project" 25 | @echo " qthelp to make HTML files and a qthelp project" 26 | @echo " devhelp to make HTML files and a Devhelp project" 27 | @echo " epub to make an epub" 28 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 29 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 30 | @echo " text to make text files" 31 | @echo " man to make manual pages" 32 | @echo " changes to make an overview of all changed/added/deprecated items" 33 | @echo " linkcheck to check all external links for integrity" 34 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 35 | 36 | clean: 37 | -rm -rf $(BUILDDIR)/* 38 | 39 | html: 40 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 41 | @echo 42 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 43 | 44 | dirhtml: 45 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 48 | 49 | singlehtml: 50 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 51 | @echo 52 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 53 | 54 | pickle: 55 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 56 | @echo 57 | @echo "Build finished; now you can process the pickle files." 58 | 59 | json: 60 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 61 | @echo 62 | @echo "Build finished; now you can process the JSON files." 63 | 64 | htmlhelp: 65 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 66 | @echo 67 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 68 | ".hhp project file in $(BUILDDIR)/htmlhelp." 69 | 70 | qthelp: 71 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 72 | @echo 73 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 74 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 75 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Flask-Admin.qhcp" 76 | @echo "To view the help file:" 77 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Flask-Admin.qhc" 78 | 79 | devhelp: 80 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 81 | @echo 82 | @echo "Build finished." 83 | @echo "To view the help file:" 84 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Flask-Admin" 85 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Flask-Admin" 86 | @echo "# devhelp" 87 | 88 | epub: 89 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 90 | @echo 91 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 92 | 93 | latex: 94 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 95 | @echo 96 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 97 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 98 | "(use \`make latexpdf' here to do that automatically)." 99 | 100 | latexpdf: 101 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 102 | @echo "Running LaTeX files through pdflatex..." 103 | make -C $(BUILDDIR)/latex all-pdf 104 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 105 | 106 | text: 107 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 108 | @echo 109 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 110 | 111 | man: 112 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 113 | @echo 114 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 115 | 116 | changes: 117 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 118 | @echo 119 | @echo "The overview file is in $(BUILDDIR)/changes." 120 | 121 | linkcheck: 122 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 123 | @echo 124 | @echo "Link check complete; look for any errors in the above output " \ 125 | "or in $(BUILDDIR)/linkcheck/output.txt." 126 | 127 | doctest: 128 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 129 | @echo "Testing of doctests in the sources finished, look at the " \ 130 | "results in $(BUILDDIR)/doctest/output.txt." 131 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | .. module:: flask.ext.admin 2 | 3 | API 4 | --- 5 | 6 | .. autofunction:: create_admin_blueprint(datastore, name='admin', list_view_pagination=25, view_decorator=None, empty_sequence=u'\x1a',**kwargs) 7 | 8 | 9 | Datastores 10 | ---------- 11 | 12 | .. autoclass:: flask.ext.admin.datastore.core.AdminDatastore 13 | :members: 14 | 15 | .. autoclass:: flask.ext.admin.datastore.sqlalchemy.SQLAlchemyDatastore 16 | 17 | .. autoclass:: flask.ext.admin.datastore.mongoalchemy.MongoAlchemyDatastore 18 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Flask-Admin documentation build configuration file, created by 4 | # sphinx-quickstart on Sat Feb 12 13:20:00 2011. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) 20 | sys.path.append(os.path.abspath('_themes')) 21 | 22 | # -- General configuration ----------------------------------------------------- 23 | 24 | # If your documentation needs a minimal Sphinx version, state it here. 25 | #needs_sphinx = '1.0' 26 | 27 | # Add any Sphinx extension module names here, as strings. They can be extensions 28 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 29 | extensions = ['sphinx.ext.autodoc'] 30 | 31 | # Add any paths that contain templates here, relative to this directory. 32 | templates_path = ['_templates'] 33 | 34 | # The suffix of source filenames. 35 | source_suffix = '.rst' 36 | 37 | # The encoding of source files. 38 | #source_encoding = 'utf-8-sig' 39 | 40 | # The master toctree document. 41 | master_doc = 'index' 42 | 43 | # General information about the project. 44 | project = u'Flask-Admin' 45 | copyright = u'2011, Andy Wilson' 46 | 47 | # The version info for the project you're documenting, acts as replacement for 48 | # |version| and |release|, also used in various other places throughout the 49 | # built documents. 50 | # 51 | # The short X.Y version. 52 | version = '0.1' 53 | # The full version, including alpha/beta/rc tags. 54 | release = '0.1' 55 | 56 | # The language for content autogenerated by Sphinx. Refer to documentation 57 | # for a list of supported languages. 58 | #language = None 59 | 60 | # There are two options for replacing |today|: either, you set today to some 61 | # non-false value, then it is used: 62 | #today = '' 63 | # Else, today_fmt is used as the format for a strftime call. 64 | #today_fmt = '%B %d, %Y' 65 | 66 | # List of patterns, relative to source directory, that match files and 67 | # directories to ignore when looking for source files. 68 | exclude_patterns = ['_build'] 69 | 70 | # The reST default role (used for this markup: `text`) to use for all documents. 71 | #default_role = None 72 | 73 | # If true, '()' will be appended to :func: etc. cross-reference text. 74 | add_function_parentheses = False 75 | 76 | # If true, the current module name will be prepended to all description 77 | # unit titles (such as .. function::). 78 | #add_module_names = True 79 | 80 | # If true, sectionauthor and moduleauthor directives will be shown in the 81 | # output. They are ignored by default. 82 | #show_authors = False 83 | 84 | # The name of the Pygments (syntax highlighting) style to use. 85 | #pygments_style = 'sphinx' 86 | 87 | # A list of ignored prefixes for module index sorting. 88 | #modindex_common_prefix = [] 89 | 90 | 91 | # -- Options for HTML output --------------------------------------------------- 92 | 93 | # The theme to use for HTML and HTML Help pages. See the documentation for 94 | # a list of builtin themes. 95 | html_theme = 'flask_small' 96 | 97 | # Theme options are theme-specific and customize the look and feel of a theme 98 | # further. For a list of options available for each theme, see the 99 | # documentation. 100 | html_theme_options = {'index_logo': False, 101 | 'github_fork': 'wilsaj/flask-admin' 102 | } 103 | 104 | # Add any paths that contain custom themes here, relative to this directory. 105 | html_theme_path = ['_themes'] 106 | 107 | # The name for this set of Sphinx documents. If None, it defaults to 108 | # " v documentation". 109 | #html_title = None 110 | 111 | # A shorter title for the navigation bar. Default is the same as html_title. 112 | #html_short_title = None 113 | 114 | # The name of an image file (relative to this directory) to place at the top 115 | # of the sidebar. 116 | #html_logo = None 117 | 118 | # The name of an image file (within the static path) to use as favicon of the 119 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 120 | # pixels large. 121 | #html_favicon = None 122 | 123 | # Add any paths that contain custom static files (such as style sheets) here, 124 | # relative to this directory. They are copied after the builtin static files, 125 | # so a file named "default.css" will overwrite the builtin "default.css". 126 | html_static_path = ['_static'] 127 | 128 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 129 | # using the given strftime format. 130 | #html_last_updated_fmt = '%b %d, %Y' 131 | 132 | # If true, SmartyPants will be used to convert quotes and dashes to 133 | # typographically correct entities. 134 | #html_use_smartypants = True 135 | 136 | # Custom sidebar templates, maps document names to template names. 137 | #html_sidebars = {} 138 | 139 | # Additional templates that should be rendered to pages, maps page names to 140 | # template names. 141 | #html_additional_pages = {} 142 | 143 | # If false, no module index is generated. 144 | #html_domain_indices = True 145 | 146 | # If false, no index is generated. 147 | #html_use_index = True 148 | 149 | # If true, the index is split into individual pages for each letter. 150 | #html_split_index = False 151 | 152 | # If true, links to the reST sources are added to the pages. 153 | #html_show_sourcelink = True 154 | 155 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 156 | #html_show_sphinx = True 157 | 158 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 159 | #html_show_copyright = True 160 | 161 | # If true, an OpenSearch description file will be output, and all pages will 162 | # contain a tag referring to it. The value of this option must be the 163 | # base URL from which the finished HTML is served. 164 | #html_use_opensearch = '' 165 | 166 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 167 | #html_file_suffix = None 168 | 169 | # Output file base name for HTML help builder. 170 | htmlhelp_basename = 'Flask-Admindoc' 171 | 172 | 173 | # -- Options for LaTeX output -------------------------------------------------- 174 | 175 | # The paper size ('letter' or 'a4'). 176 | #latex_paper_size = 'letter' 177 | 178 | # The font size ('10pt', '11pt' or '12pt'). 179 | #latex_font_size = '10pt' 180 | 181 | # Grouping the document tree into LaTeX files. List of tuples 182 | # (source start file, target name, title, author, documentclass [howto/manual]). 183 | latex_documents = [ 184 | ('index', 'Flask-Admin.tex', u'Flask-Admin Documentation', 185 | u'Andy Wilson', 'manual'), 186 | ] 187 | 188 | # The name of an image file (relative to this directory) to place at the top of 189 | # the title page. 190 | #latex_logo = None 191 | 192 | # For "manual" documents, if this is true, then toplevel headings are parts, 193 | # not chapters. 194 | #latex_use_parts = False 195 | 196 | # If true, show page references after internal links. 197 | #latex_show_pagerefs = False 198 | 199 | # If true, show URL addresses after external links. 200 | #latex_show_urls = False 201 | 202 | # Additional stuff for the LaTeX preamble. 203 | #latex_preamble = '' 204 | 205 | # Documents to append as an appendix to all manuals. 206 | #latex_appendices = [] 207 | 208 | # If false, no module index is generated. 209 | #latex_domain_indices = True 210 | 211 | 212 | # -- Options for manual page output -------------------------------------------- 213 | 214 | # One entry per manual page. List of tuples 215 | # (source start file, name, description, authors, manual section). 216 | man_pages = [ 217 | ('index', 'flask-admin', u'Flask-Admin Documentation', 218 | [u'Andy Wilson'], 1) 219 | ] 220 | 221 | -------------------------------------------------------------------------------- /docs/contents.rst: -------------------------------------------------------------------------------- 1 | User's Guide 2 | ------------ 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | overview 8 | 9 | 10 | API Reference 11 | ------------- 12 | 13 | .. toctree:: 14 | :maxdepth: 2 15 | 16 | api 17 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | :orphan: 2 | 3 | Flask-Admin 4 | =========== 5 | 6 | Flask-Admin is a `Flask`_ extension that aims to be a flexible, 7 | customizable web-based interface to your datastore. 8 | 9 | 10 | .. note:: 11 | 12 | Flask-Admin will only work with versions of Flask 0.7 or above. As 13 | of Flask-Admin version 0.2, Flask-Admin uses the new extension 14 | namespace so if you are using Flask 0.7, you will need to use the 15 | `extension compatability module`_. 16 | 17 | .. _Flask: http://flask.pocoo.org/ 18 | .. _extension compatability module: http://flask.pocoo.org/docs/extensions/#flask-before-0-8 19 | 20 | .. include:: contents.rst 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | if NOT "%PAPER%" == "" ( 11 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 12 | ) 13 | 14 | if "%1" == "" goto help 15 | 16 | if "%1" == "help" ( 17 | :help 18 | echo.Please use `make ^` where ^ is one of 19 | echo. html to make standalone HTML files 20 | echo. dirhtml to make HTML files named index.html in directories 21 | echo. singlehtml to make a single large HTML file 22 | echo. pickle to make pickle files 23 | echo. json to make JSON files 24 | echo. htmlhelp to make HTML files and a HTML help project 25 | echo. qthelp to make HTML files and a qthelp project 26 | echo. devhelp to make HTML files and a Devhelp project 27 | echo. epub to make an epub 28 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 29 | echo. text to make text files 30 | echo. man to make manual pages 31 | echo. changes to make an overview over all changed/added/deprecated items 32 | echo. linkcheck to check all external links for integrity 33 | echo. doctest to run all doctests embedded in the documentation if enabled 34 | goto end 35 | ) 36 | 37 | if "%1" == "clean" ( 38 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 39 | del /q /s %BUILDDIR%\* 40 | goto end 41 | ) 42 | 43 | if "%1" == "html" ( 44 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 45 | if errorlevel 1 exit /b 1 46 | echo. 47 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 48 | goto end 49 | ) 50 | 51 | if "%1" == "dirhtml" ( 52 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 53 | if errorlevel 1 exit /b 1 54 | echo. 55 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 56 | goto end 57 | ) 58 | 59 | if "%1" == "singlehtml" ( 60 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 61 | if errorlevel 1 exit /b 1 62 | echo. 63 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 64 | goto end 65 | ) 66 | 67 | if "%1" == "pickle" ( 68 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 69 | if errorlevel 1 exit /b 1 70 | echo. 71 | echo.Build finished; now you can process the pickle files. 72 | goto end 73 | ) 74 | 75 | if "%1" == "json" ( 76 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 77 | if errorlevel 1 exit /b 1 78 | echo. 79 | echo.Build finished; now you can process the JSON files. 80 | goto end 81 | ) 82 | 83 | if "%1" == "htmlhelp" ( 84 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 85 | if errorlevel 1 exit /b 1 86 | echo. 87 | echo.Build finished; now you can run HTML Help Workshop with the ^ 88 | .hhp project file in %BUILDDIR%/htmlhelp. 89 | goto end 90 | ) 91 | 92 | if "%1" == "qthelp" ( 93 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 94 | if errorlevel 1 exit /b 1 95 | echo. 96 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 97 | .qhcp project file in %BUILDDIR%/qthelp, like this: 98 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Flask-Admin.qhcp 99 | echo.To view the help file: 100 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Flask-Admin.ghc 101 | goto end 102 | ) 103 | 104 | if "%1" == "devhelp" ( 105 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 106 | if errorlevel 1 exit /b 1 107 | echo. 108 | echo.Build finished. 109 | goto end 110 | ) 111 | 112 | if "%1" == "epub" ( 113 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 117 | goto end 118 | ) 119 | 120 | if "%1" == "latex" ( 121 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 122 | if errorlevel 1 exit /b 1 123 | echo. 124 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 125 | goto end 126 | ) 127 | 128 | if "%1" == "text" ( 129 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 130 | if errorlevel 1 exit /b 1 131 | echo. 132 | echo.Build finished. The text files are in %BUILDDIR%/text. 133 | goto end 134 | ) 135 | 136 | if "%1" == "man" ( 137 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 138 | if errorlevel 1 exit /b 1 139 | echo. 140 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 141 | goto end 142 | ) 143 | 144 | if "%1" == "changes" ( 145 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 146 | if errorlevel 1 exit /b 1 147 | echo. 148 | echo.The overview file is in %BUILDDIR%/changes. 149 | goto end 150 | ) 151 | 152 | if "%1" == "linkcheck" ( 153 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 154 | if errorlevel 1 exit /b 1 155 | echo. 156 | echo.Link check complete; look for any errors in the above output ^ 157 | or in %BUILDDIR%/linkcheck/output.txt. 158 | goto end 159 | ) 160 | 161 | if "%1" == "doctest" ( 162 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 163 | if errorlevel 1 exit /b 1 164 | echo. 165 | echo.Testing of doctests in the sources finished, look at the ^ 166 | results in %BUILDDIR%/doctest/output.txt. 167 | goto end 168 | ) 169 | 170 | :end 171 | -------------------------------------------------------------------------------- /docs/overview.rst: -------------------------------------------------------------------------------- 1 | Quick start 2 | ----------- 3 | 4 | Start off by creating some data models. In this example we'll use 5 | `SQLAlchemy`_ declarative models. `Flask-SQLAlchemy`_ and 6 | `MongoAlchemy`_ models are also supported. So here are our models:: 7 | 8 | from sqlalchemy import create_engine, Column, Integer, String 9 | from sqlalchemy.ext.declarative import declarative_base 10 | 11 | engine = create_engine('sqlite://', convert_unicode=True) 12 | Base = declarative_base(bind=engine) 13 | 14 | class Student(Base): 15 | __tablename__ = 'student' 16 | 17 | id = Column(Integer, primary_key=True) 18 | name = Column(String(120), unique=True) 19 | 20 | def __repr__(self): 21 | return self.name 22 | 23 | class Teacher(Base): 24 | __tablename__ = 'teacher' 25 | 26 | id = Column(Integer, primary_key=True) 27 | name = Column(String(120), unique=True) 28 | 29 | def __repr__(self): 30 | return self.name 31 | 32 | 33 | Then create a datastore object using those models and your sqlalchemy 34 | session:: 35 | 36 | from flask.ext.admin.datastore.sqlalchemy import SQLAlchemyDatastore 37 | from sqlalchemy.orm import scoped_session, sessionmaker 38 | 39 | db_session = scoped_session(sessionmaker(bind=engine)) 40 | 41 | admin_datastore = SQLAlchemyDatastore((Student, Teacher), db_session) 42 | 43 | 44 | And create a blueprint using this datastore object:: 45 | 46 | admin_blueprint = admin.create_admin_blueprint(admin_datastore) 47 | app = Flask(__name__) 48 | app.register_blueprint(admin_blueprint, url_prefix='/admin') 49 | 50 | Now the admin interface is set up. If you are running the app with the 51 | built-in development server via :meth:`app.run()`, then it should be 52 | available at http://localhost:5000/admin . 53 | 54 | 55 | 56 | Some notes on model classes 57 | --------------------------- 58 | 59 | The __repr__ method of your model class will be used to describe 60 | specific instances of your models models in things like the list 61 | view. If you don't set it, the default __repr__ method will look 62 | something like `<__main__.Student object at 0x1bb1490>`, which won't 63 | be very useful for distinguishing model instances. 64 | 65 | 66 | Also, your model classes must be able to be initialized without any 67 | arguments. For example, the following works because in 68 | :meth:`__init__`, name is a keyword argument and is therefore 69 | optional:: 70 | 71 | class Person(Base): 72 | id = Column(Integer, primary_key=True) 73 | name = Column(String(120), unique=True) 74 | 75 | def __init__(self, name=None): 76 | self.name = name 77 | 78 | def __repr__(self): 79 | return self.name 80 | 81 | 82 | But the following will not work because in this case, the __init__ 83 | method of :class:`User` `requires` a name:: 84 | 85 | class Person(Base): 86 | id = Column(Integer, primary_key=True) 87 | name = Column(String(120), unique=True) 88 | 89 | def __init__(self, name): 90 | self.name = name 91 | 92 | def __repr__(self): 93 | return self.name 94 | 95 | 96 | 97 | Flask-Admin Endpoints 98 | --------------------- 99 | If you want to refer to views in Flask-Admin, the following endpoints 100 | are available: 101 | 102 | :meth:`url_for('admin.index')` 103 | returns the url for the index view 104 | 105 | :meth:`url_for('admin.list', model_name='some_model')` 106 | returns the list view for a given model 107 | 108 | :meth:`url_for('admin.edit', model_name='some_model', model_key=model_key)` 109 | returns the url for the page used for editing a specific model 110 | instance. The model_key is a key that allows the model to be 111 | uniquely identified. For example, with the SQLAlchemy datastore, 112 | the model_key is the set primary key value(s) for a model 113 | instance. In cases where multiple values identify a model, the 114 | values will be separated by a ``'/'`` 115 | 116 | :meth:`url_for('admin.add', model_name='some_model')` 117 | returns the url for the adding a new model instance 118 | 119 | :meth:`url_for('admin.delete', model_name='some_model', model_key=model_key)` 120 | returns the url for the page used for deleting a specific model 121 | instance; see the note about the model_key on the 'admin.edit' 122 | endpoint above 123 | 124 | 125 | .. note:: 126 | 127 | You can use the ``name`` argument in 128 | :func:`create_admin_blueprint()` to set the name of the 129 | blueprint. For example if ``name="my_named_admin"``, then the 130 | endpoint for the index becomes ``'my_named_admin.index'``. This is 131 | necessary if you are going to use more than one admin blueprint 132 | within the same app. 133 | 134 | Custom Templates and Static Files 135 | --------------------------------- 136 | 137 | Using Flask blueprints makes customizing the admin interface 138 | easy. Flask-Admin comes with a default set of templates and static 139 | files. It's possible to customize as much of the interface as you'd 140 | like by overriding the files you'd like to change. To do this, just 141 | create your own version of the files in the templates and/or static 142 | directories used by your Flask application. 143 | 144 | The following templates are defined in Flask-Admin: 145 | 146 | ``admin/base.html`` - This is the primary base template that defines the 147 | bulk of the look and feel of the Admin. If you are using any of the 148 | default admin view templates, the base templates should provide the 149 | following blocks: 150 | 151 | **title** - The title of the page (in the html title element) 152 | 153 | **main** - This is where the main content of each of the admin 154 | views is placed (like editing forms) 155 | 156 | ``admin/extra_base.html`` - This is the template that is actually 157 | inheritted by the default admin view templates. By extending 158 | base.html, this template allows you to override some of behaviors 159 | provided in the `base.html` template (e.g. navigation) while 160 | maintaining the most of base template behavior (like setting up 161 | Javascript-enhanced UI elements). 162 | 163 | ``admin/index.html`` - The template used by the ``admin.index`` view. 164 | 165 | ``admin/list.html`` - The template used by the ``admin.list`` view. 166 | 167 | ``admin/add.html`` - The template used by the ``admin.add`` view. 168 | 169 | ``admin/edit.html`` - The template used by the ``admin.edit`` view. 170 | 171 | 172 | In addition, the following "helper" templates are defined. These 173 | define Jinja macros that are used for rendering things like the 174 | pagination and forms: 175 | 176 | ``admin/_formhelpers.html`` - The template that defines the 177 | ``render_field`` macro which is used for rendering fields. 178 | 179 | ``admin/_paginationhelpers.html`` - The template that defines the 180 | ``render_pagination`` macro used for creating pagination element in 181 | the list view. 182 | 183 | ``admin/_statichelpers.html`` - The template that defines the 184 | ``static`` macro. You probably won't need to override this one: it 185 | just points to the ``admin.static`` endpoint, which may be easier to 186 | override directly. 187 | 188 | 189 | 190 | Refer to the `Flask documentation on blueprints`_ for specifics on how 191 | blueprints effect the template search path. There is also an example 192 | of extending the default admin templates in the `view decorator 193 | example`_. 194 | 195 | 196 | Custom Forms 197 | ------------ 198 | 199 | Flask-Admin uses the WTForms library to automatically generate the 200 | form that will be used to add a new instance of a model or edit an 201 | existing model instance. There may be cases where the automatically 202 | generated form isn't what you want, so you can also create a custom 203 | form for Flask-Admin to use for a given model. 204 | 205 | For example, consider the following model of a User that stores hashed 206 | passwords (originally from http://flask.pocoo.org/snippets/54/):: 207 | 208 | from sqlalchemy import Boolean, Column, Integer, String 209 | from sqlalchemy.ext.declarative import declarative_base 210 | 211 | Base = declarative_base() 212 | 213 | class User(Base): 214 | __tablename__ = 'user' 215 | 216 | id = Column(Integer, primary_key=True) 217 | username = Column(String(80), unique=True) 218 | _password_hash = Column('password', String(80), nullable=False) 219 | is_active = Column(Boolean, default=True) 220 | 221 | def __init__(self, username="", password="", is_active=True): 222 | self.username = username 223 | self.password = password 224 | self.is_active = is_active 225 | 226 | def check_password(self, password): 227 | return check_password_hash(self.pw_hash, password) 228 | 229 | @property 230 | def password(self): 231 | return self._password_hash 232 | 233 | @password.setter 234 | def password(self, password): 235 | self._password_hash = generate_password_hash(password) 236 | 237 | password = synonym('_password_hash', descriptor=password) 238 | 239 | def __repr__(self): 240 | return self.username 241 | 242 | 243 | To allow this model to be used with a typical password and 244 | confirmation field form, you could create the following form:: 245 | 246 | from wtforms import Form, validators 247 | from wtforms.fields import BooleanField, TextField, PasswordField 248 | 249 | class UserForm(Form): 250 | """ 251 | Form for creating or editting User object (via the admin). Define 252 | any handling of fields here. This form class also has precedence 253 | when rendering forms to a webpage, so the model-generated fields 254 | will come after it. 255 | """ 256 | username = TextField(u'User name', 257 | [validators.required(), 258 | validators.length(max=80)]) 259 | password = PasswordField('Change Password', 260 | [validators.optional(), 261 | validators.equal_to('confirm_password')]) 262 | confirm_password = PasswordField() 263 | is_active = BooleanField(default=True) 264 | 265 | 266 | And just use the model_forms argument when calling 267 | :func:`SQLAlchemyDatastore` to associate this form with the User 268 | model:: 269 | 270 | admin_blueprint = admin.datastore.SQLAlchemyDatastore( 271 | (User,), db_session, model_forms={'User': UserForm}) 272 | 273 | 274 | Now the :class:`UserForm` will be used for editing and adding a new 275 | user. If the form passes the validation checks, then password will 276 | propagate to the User model and will be hashed and stored the password 277 | in the database. 278 | 279 | .. note:: 280 | Due to the way that forms are generated, the order of input fields 281 | is difficult to control. This is something that is expected to 282 | improve in future versions, but for now a custom form is also the 283 | only way to specify the order of form fields. 284 | 285 | 286 | 287 | More examples 288 | ------------- 289 | 290 | The Flask-Admin `example directory`_ contains some sample applications 291 | that demonstrate all of the patterns above, plus some additional ideas 292 | on how you can configure the admin. 293 | 294 | 295 | Authors 296 | ------- 297 | 298 | .. include:: ../AUTHORS 299 | 300 | 301 | Changelog 302 | --------- 303 | 304 | .. include:: ../CHANGES 305 | 306 | 307 | .. _example directory: https://github.com/wilsaj/flask-admin/tree/master/example 308 | .. _Flask-SQLAlchemy: http://packages.python.org/Flask-SQLAlchemy/ 309 | .. _Flask documentation on blueprints: http://flask.pocoo.org/docs/blueprints/ 310 | .. _MongoAlchemy: http://www.mongoalchemy.org/ 311 | .. _SQLAlchemy: http://www.sqlalchemy.org/ 312 | .. _view decorator example: https://github.com/wilsaj/flask-admin/tree/master/example/authentication/view_decorator.py 313 | -------------------------------------------------------------------------------- /example/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wilsaj/flask-admin-old/aab2fe94e0641932ebd1c8f8dc500ba2daf5731c/example/__init__.py -------------------------------------------------------------------------------- /example/authentication/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wilsaj/flask-admin-old/aab2fe94e0641932ebd1c8f8dc500ba2daf5731c/example/authentication/__init__.py -------------------------------------------------------------------------------- /example/authentication/templates/admin/extra_base.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base.html" %} 2 | {% block header %} 3 |

Flask Admin

4 | {% if session['user'] %} 5 |

logged in as: {{ session['user'] }} log out

6 | {% endif %} 7 | {% endblock header %} 8 | 9 | -------------------------------------------------------------------------------- /example/authentication/templates/login.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/extra_base.html" %} 2 | 3 | {% block left_nav %} 4 | {% endblock left_nav %} 5 | 6 | {% block main %} 7 |
8 |
9 | 10 | 11 |
12 |
13 | 14 | 15 |
16 | 17 |
18 | {% endblock main %} 19 | -------------------------------------------------------------------------------- /example/authentication/view_decorator.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | import sys 3 | 4 | from flask import Flask, g, redirect, render_template, request, session, url_for 5 | from flask.ext import admin 6 | from flask.ext.admin.datastore.sqlalchemy import SQLAlchemyDatastore 7 | from sqlalchemy import create_engine, Table 8 | from sqlalchemy.orm import scoped_session, sessionmaker 9 | from sqlalchemy.ext.declarative import declarative_base 10 | from sqlalchemy import Column, Integer, Text, String, Float, Time, Enum 11 | from sqlalchemy.orm import relationship 12 | from sqlalchemy.schema import ForeignKey 13 | 14 | Base = declarative_base() 15 | 16 | # ---------------------------------------------------------------------- 17 | # Association tables 18 | # ---------------------------------------------------------------------- 19 | course_student_association_table = Table( 20 | 'course_student_association', 21 | Base.metadata, 22 | Column('student_id', Integer, ForeignKey('student.id')), 23 | Column('course_id', Integer, ForeignKey('course.id'))) 24 | 25 | 26 | # ---------------------------------------------------------------------- 27 | # Models 28 | # ---------------------------------------------------------------------- 29 | class Course(Base): 30 | __tablename__ = 'course' 31 | 32 | id = Column(Integer, primary_key=True) 33 | subject = Column(String) 34 | teacher_id = Column(Integer, ForeignKey('teacher.id'), nullable=False) 35 | start_time = Column(Time) 36 | end_time = Column(Time) 37 | 38 | teacher = relationship('Teacher', backref='courses') 39 | students = relationship('Student', 40 | secondary=course_student_association_table, 41 | backref='courses') 42 | 43 | def __repr__(self): 44 | return self.subject 45 | 46 | 47 | class Student(Base): 48 | __tablename__ = 'student' 49 | 50 | id = Column(Integer, primary_key=True) 51 | name = Column(String(120), unique=True) 52 | 53 | def __repr__(self): 54 | return self.name 55 | 56 | 57 | class Teacher(Base): 58 | __tablename__ = 'teacher' 59 | 60 | id = Column(Integer, primary_key=True) 61 | name = Column(String(120), unique=True) 62 | 63 | def __repr__(self): 64 | return self.name 65 | 66 | 67 | def login_required(f): 68 | @wraps(f) 69 | def decorated_function(*args, **kwargs): 70 | if 'user' not in session: 71 | return redirect(url_for('.login', next=request.url)) 72 | return f(*args, **kwargs) 73 | return decorated_function 74 | 75 | 76 | def create_app(database_uri='sqlite://'): 77 | app = Flask(__name__) 78 | app.config['SECRET_KEY'] = 'not secure' 79 | 80 | app.engine = create_engine(database_uri, convert_unicode=True) 81 | db_session = scoped_session(sessionmaker( 82 | autocommit=False, autoflush=False, bind=app.engine)) 83 | datastore = SQLAlchemyDatastore( 84 | (Course, Student, Teacher), db_session) 85 | admin_blueprint = admin.create_admin_blueprint( 86 | datastore, view_decorator=login_required) 87 | 88 | @admin_blueprint.route('/login/', methods=('GET', 'POST')) 89 | def login(): 90 | if request.form.get('username', None): 91 | session['user'] = request.form['username'] 92 | return redirect(request.args.get('next', url_for('admin.index'))) 93 | else: 94 | if request.method == 'POST': 95 | return render_template("login.html", 96 | bad_login=True) 97 | else: 98 | return render_template("login.html") 99 | 100 | @admin_blueprint.route('/logout/') 101 | def logout(): 102 | del session['user'] 103 | return redirect('/') 104 | 105 | @app.route('/') 106 | def go_to_admin(): 107 | return redirect('/admin/') 108 | 109 | app.register_blueprint(admin_blueprint, url_prefix='/admin') 110 | 111 | return app 112 | 113 | 114 | if __name__ == '__main__': 115 | app = create_app('sqlite:///simple.db') 116 | Base.metadata.create_all(bind=app.engine) 117 | app.run(debug=True) 118 | -------------------------------------------------------------------------------- /example/declarative/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wilsaj/flask-admin-old/aab2fe94e0641932ebd1c8f8dc500ba2daf5731c/example/declarative/__init__.py -------------------------------------------------------------------------------- /example/declarative/composite_keys.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from flask import Flask, redirect 4 | from flask.ext import admin 5 | from flask.ext.admin.datastore.sqlalchemy import SQLAlchemyDatastore 6 | from sqlalchemy import create_engine, Table 7 | from sqlalchemy.orm import scoped_session, sessionmaker 8 | from sqlalchemy.ext.declarative import declarative_base 9 | from sqlalchemy import Column, Integer, String, Time 10 | from sqlalchemy.orm import relationship 11 | from sqlalchemy.schema import ForeignKey 12 | 13 | 14 | Base = declarative_base() 15 | 16 | 17 | class Student(Base): 18 | __tablename__ = 'student' 19 | 20 | student_id = Column(Integer, primary_key=True) 21 | name = Column(String(120), primary_key=True) 22 | 23 | def __repr__(self): 24 | return self.name 25 | 26 | 27 | class Teacher(Base): 28 | __tablename__ = 'teacher' 29 | 30 | id = Column(Integer, primary_key=True) 31 | name = Column(String(120), unique=True) 32 | 33 | def __repr__(self): 34 | return self.name 35 | 36 | 37 | def create_app(database_uri='sqlite://', pagination=25): 38 | app = Flask(__name__) 39 | app.config['SECRET_KEY'] = 'not secure' 40 | engine = create_engine(database_uri, convert_unicode=True) 41 | app.db_session = scoped_session(sessionmaker( 42 | autocommit=False, autoflush=False, 43 | bind=engine)) 44 | datastore = SQLAlchemyDatastore( 45 | (Student, Teacher), app.db_session, exclude_pks=False) 46 | admin_blueprint = admin.create_admin_blueprint( 47 | datastore, list_view_pagination=pagination) 48 | app.register_blueprint(admin_blueprint, url_prefix='/admin') 49 | Base.metadata.create_all(bind=engine) 50 | 51 | @app.route('/') 52 | def go_to_admin(): 53 | return redirect('/admin') 54 | 55 | return app 56 | 57 | 58 | if __name__ == '__main__': 59 | app = create_app('sqlite:///composite.db') 60 | app.run(debug=True) 61 | -------------------------------------------------------------------------------- /example/declarative/custom_form.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from flask import Flask, redirect 4 | from flask.ext import admin 5 | from flask.ext.admin.datastore.sqlalchemy import SQLAlchemyDatastore 6 | from sqlalchemy import create_engine 7 | from sqlalchemy.orm import scoped_session, sessionmaker 8 | from sqlalchemy.ext.declarative import declarative_base 9 | from sqlalchemy import Boolean, Column, Integer, Text, String, Float, Time 10 | from sqlalchemy.orm import synonym 11 | from werkzeug import check_password_hash, generate_password_hash 12 | from wtforms import Form, validators 13 | from wtforms.fields import BooleanField, TextField, PasswordField 14 | 15 | 16 | Base = declarative_base() 17 | 18 | 19 | class User(Base): 20 | __tablename__ = 'user' 21 | 22 | id = Column(Integer, primary_key=True) 23 | username = Column(String(80), unique=True) 24 | _password_hash = Column('password', String(80), nullable=False) 25 | is_active = Column(Boolean, default=True) 26 | 27 | def __init__(self, username="", password="", is_active=True): 28 | self.username = username 29 | self.password = password 30 | self.is_active = is_active 31 | 32 | def check_password(self, password): 33 | return check_password_hash(self.pw_hash, password) 34 | 35 | @property 36 | def password(self): 37 | return self._password_hash 38 | 39 | @password.setter 40 | def password(self, password): 41 | self._password_hash = generate_password_hash(password) 42 | 43 | password = synonym('_password_hash', descriptor=password) 44 | 45 | def __repr__(self): 46 | return self.username 47 | 48 | __mapper_args__ = { 49 | 'order_by': username 50 | } 51 | 52 | 53 | class UserForm(Form): 54 | """ 55 | Form for creating or editting User object (via the admin). Define 56 | any handling of fields here. This form class also has precedence 57 | when rendering forms to a webpage, so the model-generated fields 58 | will come after it. 59 | """ 60 | username = TextField(u'User name', 61 | [validators.required(), validators.length(max=80)]) 62 | password = PasswordField('Change Password', 63 | [validators.optional(), 64 | validators.equal_to('confirm_password')]) 65 | confirm_password = PasswordField() 66 | is_active = BooleanField(default=True) 67 | 68 | 69 | def create_app(database_uri='sqlite://'): 70 | app = Flask(__name__) 71 | app.config['SECRET_KEY'] = 'not secure' 72 | engine = create_engine(database_uri, convert_unicode=True) 73 | db_session = scoped_session(sessionmaker( 74 | autocommit=False, autoflush=False, 75 | bind=engine)) 76 | datastore = SQLAlchemyDatastore( 77 | (User,), db_session, model_forms={'User': UserForm}) 78 | admin_blueprint = admin.create_admin_blueprint( 79 | datastore) 80 | app.register_blueprint(admin_blueprint, url_prefix='/admin') 81 | Base.metadata.create_all(bind=engine) 82 | 83 | @app.route('/') 84 | def go_to_admin(): 85 | return redirect('/admin') 86 | 87 | return app 88 | 89 | 90 | if __name__ == '__main__': 91 | app = create_app('sqlite:///simple.db') 92 | app.run(debug=True) 93 | -------------------------------------------------------------------------------- /example/declarative/multiple.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from flask import Flask, redirect 4 | from flask.ext import admin 5 | from flask.ext.admin.datastore.sqlalchemy import SQLAlchemyDatastore 6 | from sqlalchemy import create_engine, Table 7 | from sqlalchemy.orm import scoped_session, sessionmaker 8 | from sqlalchemy.ext.declarative import declarative_base 9 | from sqlalchemy import Column, Integer, Text, String, Float, Time, Enum 10 | from sqlalchemy.orm import relationship 11 | from sqlalchemy.schema import ForeignKey 12 | 13 | 14 | Base = declarative_base() 15 | 16 | # ---------------------------------------------------------------------- 17 | # Association tables 18 | # ---------------------------------------------------------------------- 19 | course_student_association_table = Table( 20 | 'course_student_association', 21 | Base.metadata, 22 | Column('student_id', Integer, ForeignKey('student.id')), 23 | Column('course_id', Integer, ForeignKey('course.id'))) 24 | 25 | 26 | # ---------------------------------------------------------------------- 27 | # Models 28 | # ---------------------------------------------------------------------- 29 | class Course(Base): 30 | __tablename__ = 'course' 31 | 32 | id = Column(Integer, primary_key=True) 33 | subject = Column(String) 34 | teacher_id = Column(Integer, ForeignKey('teacher.id'), nullable=False) 35 | start_time = Column(Time) 36 | end_time = Column(Time) 37 | 38 | teacher = relationship('Teacher', backref='courses') 39 | students = relationship('Student', 40 | secondary=course_student_association_table, 41 | backref='courses') 42 | 43 | def __repr__(self): 44 | return self.subject 45 | 46 | 47 | class Student(Base): 48 | __tablename__ = 'student' 49 | 50 | id = Column(Integer, primary_key=True) 51 | name = Column(String(120), unique=True) 52 | 53 | def __repr__(self): 54 | return self.name 55 | 56 | 57 | class Teacher(Base): 58 | __tablename__ = 'teacher' 59 | 60 | id = Column(Integer, primary_key=True) 61 | name = Column(String(120), unique=True) 62 | 63 | def __repr__(self): 64 | return self.name 65 | 66 | 67 | def create_app(database_uri='sqlite://'): 68 | app = Flask(__name__) 69 | app.config['SECRET_KEY'] = 'not secure' 70 | 71 | app.engine = create_engine(database_uri, convert_unicode=True) 72 | app.db_session = scoped_session(sessionmaker( 73 | autocommit=False, autoflush=False, bind=app.engine)) 74 | datastore1 = SQLAlchemyDatastore( 75 | (Student, Teacher), app.db_session) 76 | admin_blueprint1 = admin.create_admin_blueprint( 77 | datastore1, name='admin1') 78 | datastore2 = SQLAlchemyDatastore( 79 | (Course,), app.db_session) 80 | admin_blueprint2 = admin.create_admin_blueprint( 81 | datastore2, name='admin2') 82 | app.register_blueprint(admin_blueprint1, url_prefix='/admin1') 83 | app.register_blueprint(admin_blueprint2, url_prefix='/admin2') 84 | Base.metadata.create_all(bind=app.engine) 85 | return app 86 | 87 | 88 | if __name__ == '__main__': 89 | app = create_app('sqlite:///simple.db') 90 | app.run(debug=True) 91 | -------------------------------------------------------------------------------- /example/declarative/simple.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from flask import Flask, redirect 4 | from flask.ext import admin 5 | from flask.ext.admin.datastore.sqlalchemy import SQLAlchemyDatastore 6 | from sqlalchemy import create_engine, Table 7 | from sqlalchemy.orm import scoped_session, sessionmaker 8 | from sqlalchemy.ext.declarative import declarative_base 9 | from sqlalchemy import Column, Integer, String, Time 10 | from sqlalchemy.orm import relationship 11 | from sqlalchemy.schema import ForeignKey 12 | 13 | 14 | Base = declarative_base() 15 | 16 | # ---------------------------------------------------------------------- 17 | # Association tables 18 | # ---------------------------------------------------------------------- 19 | course_student_association_table = Table( 20 | 'course_student_association', 21 | Base.metadata, 22 | Column('student_id', Integer, ForeignKey('student.id')), 23 | Column('course_id', Integer, ForeignKey('course.id'))) 24 | 25 | 26 | # ---------------------------------------------------------------------- 27 | # Models 28 | # ---------------------------------------------------------------------- 29 | class Course(Base): 30 | __tablename__ = 'course' 31 | 32 | id = Column(Integer, primary_key=True) 33 | subject = Column(String) 34 | teacher_id = Column(Integer, ForeignKey('teacher.id'), nullable=False) 35 | start_time = Column(Time) 36 | end_time = Column(Time) 37 | 38 | teacher = relationship('Teacher', backref='courses') 39 | students = relationship('Student', 40 | secondary=course_student_association_table, 41 | backref='courses') 42 | 43 | def __repr__(self): 44 | return self.subject 45 | 46 | 47 | class Student(Base): 48 | __tablename__ = 'student' 49 | 50 | id = Column(Integer, primary_key=True) 51 | name = Column(String(120), unique=True) 52 | 53 | def __repr__(self): 54 | return self.name 55 | 56 | 57 | class Teacher(Base): 58 | __tablename__ = 'teacher' 59 | 60 | id = Column(Integer, primary_key=True) 61 | name = Column(String(120), unique=True) 62 | 63 | def __repr__(self): 64 | return self.name 65 | 66 | 67 | def create_app(database_uri='sqlite://', pagination=25): 68 | app = Flask(__name__) 69 | app.config['SECRET_KEY'] = 'not secure' 70 | engine = create_engine(database_uri, convert_unicode=True) 71 | app.db_session = scoped_session(sessionmaker( 72 | autocommit=False, autoflush=False, 73 | bind=engine)) 74 | datastore = SQLAlchemyDatastore( 75 | (Course, Student, Teacher), app.db_session) 76 | admin_blueprint = admin.create_admin_blueprint( 77 | datastore, list_view_pagination=pagination) 78 | app.register_blueprint(admin_blueprint, url_prefix='/admin') 79 | Base.metadata.create_all(bind=engine) 80 | 81 | @app.route('/') 82 | def go_to_admin(): 83 | return redirect('/admin') 84 | 85 | return app 86 | 87 | 88 | if __name__ == '__main__': 89 | app = create_app('sqlite:///simple.db') 90 | app.run(debug=True) 91 | -------------------------------------------------------------------------------- /example/flask_sqlalchemy/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wilsaj/flask-admin-old/aab2fe94e0641932ebd1c8f8dc500ba2daf5731c/example/flask_sqlalchemy/__init__.py -------------------------------------------------------------------------------- /example/flask_sqlalchemy/flaskext_sa_example.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, redirect 2 | from flask.ext import admin 3 | from flask.ext.admin.datastore.sqlalchemy import SQLAlchemyDatastore 4 | from flaskext.sqlalchemy import SQLAlchemy 5 | 6 | from datetime import datetime 7 | 8 | db = SQLAlchemy() 9 | 10 | 11 | class User(db.Model): 12 | id = db.Column(db.Integer, primary_key=True) 13 | username = db.Column(db.String(80), unique=True) 14 | email = db.Column(db.String(120), unique=True) 15 | 16 | def __init__(self, username=None, email=None): 17 | self.username = username 18 | self.email = email 19 | 20 | def __repr__(self): 21 | return '' % self.username 22 | 23 | 24 | class Post(db.Model): 25 | id = db.Column(db.Integer, primary_key=True) 26 | title = db.Column(db.String(80)) 27 | body = db.Column(db.Text) 28 | pub_date = db.Column(db.DateTime) 29 | 30 | category_id = db.Column(db.Integer, db.ForeignKey('category.id')) 31 | category = db.relationship('Category', 32 | backref=db.backref('posts', lazy='dynamic')) 33 | 34 | def __init__(self, title=None, body=None, category=None, pub_date=None): 35 | self.title = title 36 | self.body = body 37 | if pub_date is None: 38 | pub_date = datetime.utcnow() 39 | self.pub_date = pub_date 40 | self.category = category 41 | 42 | def __repr__(self): 43 | return '' % self.title 44 | 45 | 46 | class Category(db.Model): 47 | id = db.Column(db.Integer, primary_key=True) 48 | name = db.Column(db.String(50)) 49 | 50 | def __init__(self, name=None): 51 | self.name = name 52 | 53 | def __repr__(self): 54 | return '' % self.name 55 | 56 | 57 | def create_app(database_uri='sqlite://'): 58 | app = Flask(__name__) 59 | app.config['SQLALCHEMY_DATABASE_URI'] = database_uri 60 | app.config['SECRET_KEY'] = 'seeeeecret' 61 | 62 | db.init_app(app) 63 | datastore = SQLAlchemyDatastore( 64 | (User, Post, Category), db.session) 65 | admin_blueprint = admin.create_admin_blueprint(datastore) 66 | app.register_blueprint(admin_blueprint, url_prefix='/admin') 67 | db.create_all(app=app) 68 | 69 | @app.route('/') 70 | def go_to_admin(): 71 | return redirect('/admin') 72 | return app 73 | 74 | if __name__ == '__main__': 75 | app = create_app('sqlite:///flask_sqlalchemy_example.db') 76 | app.run(debug=True) 77 | -------------------------------------------------------------------------------- /example/flask_sqlalchemy/flaskext_sa_multi_pk.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, redirect 2 | from flask.ext import admin 3 | from flask.ext.admin.datastore.sqlalchemy import SQLAlchemyDatastore 4 | from flaskext.sqlalchemy import SQLAlchemy 5 | 6 | db = SQLAlchemy() 7 | 8 | 9 | # ---------------------------------------------------------------------- 10 | # Models 11 | # ---------------------------------------------------------------------- 12 | class Address(db.Model): 13 | __tablename__ = 'address' 14 | 15 | shortname = db.Column(db.Unicode(100), primary_key=True, unique=True, 16 | index=True) 17 | 18 | name = db.Column(db.UnicodeText) 19 | street = db.Column(db.UnicodeText) 20 | zipcode = db.Column(db.Integer) 21 | city = db.Column(db.UnicodeText) 22 | country = db.Column(db.UnicodeText) 23 | 24 | # backref 25 | location = db.relationship('Location', backref='address') 26 | 27 | def __repr__(self): 28 | return self.shortname 29 | 30 | 31 | class Location(db.Model): 32 | __tablename__ = 'location' 33 | 34 | address_shortname = db.Column(db.Unicode(100), 35 | db.ForeignKey('address.shortname'), 36 | primary_key=True) 37 | room = db.Column(db.Unicode(100), primary_key=True) 38 | position = db.Column(db.Unicode(100), primary_key=True) 39 | 40 | db.PrimaryKeyConstraint('address_shortname', 'room', 'position', 41 | name='location_key') 42 | 43 | asset = db.relationship( 44 | 'Asset', backref='location', 45 | lazy='select', 46 | primaryjoin='(Location.room==Asset.location_room) &'\ 47 | '(Location.address_shortname==Asset.address_shortname) &'\ 48 | '(Location.position==Asset.location_position)') 49 | 50 | # additional fields in real world example 51 | 52 | def __repr__(self): 53 | return u"%s|%s|%s" % (self.address_shortname, self.room, 54 | self.position) 55 | 56 | 57 | class Asset(db.Model): 58 | __tablename__ = 'asset' 59 | 60 | id = db.Column(db.Integer, primary_key=True) 61 | 62 | name = db.Column(db.UnicodeText) 63 | 64 | # 1*n relation to location-table 65 | address_shortname = db.Column(db.Unicode(100)) 66 | location_room = db.Column(db.Unicode(100)) 67 | location_position = db.Column(db.Unicode(100)) 68 | 69 | __table_args__ = ( 70 | db.ForeignKeyConstraint(['address_shortname', 'location_room', 71 | 'location_position'], 72 | ['location.address_shortname', 73 | 'location.room', 74 | 'location.position'], 75 | use_alter=True, 76 | name='fk_location_asset'),) 77 | 78 | def __repr__(self): 79 | return self.name 80 | 81 | 82 | def create_app(database_uri='sqlite://'): 83 | app = Flask(__name__) 84 | app.config['SQLALCHEMY_DATABASE_URI'] = database_uri 85 | app.config['SECRET_KEY'] = 'not secure' 86 | db.init_app(app) 87 | datastore = SQLAlchemyDatastore( 88 | (Address, Location, Asset), db.session, exclude_pks=False) 89 | admin_blueprint = admin.create_admin_blueprint(datastore) 90 | app.register_blueprint(admin_blueprint, url_prefix='/admin') 91 | db.create_all(app=app) 92 | 93 | @app.route('/') 94 | def go_to_admin(): 95 | return redirect('/admin') 96 | return app 97 | 98 | 99 | if __name__ == '__main__': 100 | app = create_app('sqlite:///multi_pks.db') 101 | app.run(debug=True) 102 | -------------------------------------------------------------------------------- /example/flask_sqlalchemy/flaskext_sa_simple.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, redirect 2 | from flask.ext import admin 3 | from flask.ext.admin.datastore.sqlalchemy import SQLAlchemyDatastore 4 | from flaskext.sqlalchemy import SQLAlchemy 5 | 6 | db = SQLAlchemy() 7 | 8 | # ---------------------------------------------------------------------- 9 | # Association tables 10 | # ---------------------------------------------------------------------- 11 | course_student_association_table = db.Table( 12 | 'course_student_association', 13 | db.Column('student_id', db.Integer, db.ForeignKey('student.id')), 14 | db.Column('course_id', db.Integer, db.ForeignKey('course.id'))) 15 | 16 | 17 | # ---------------------------------------------------------------------- 18 | # Models 19 | # ---------------------------------------------------------------------- 20 | class Course(db.Model): 21 | __tablename__ = 'course' 22 | 23 | id = db.Column(db.Integer, primary_key=True) 24 | subject = db.Column(db.String) 25 | teacher_id = db.Column(db.Integer, 26 | db.ForeignKey('teacher.id'), nullable=False) 27 | start_time = db.Column(db.Time) 28 | end_time = db.Column(db.Time) 29 | 30 | teacher = db.relationship('Teacher', backref='courses') 31 | students = db.relationship('Student', 32 | secondary=course_student_association_table, 33 | backref='courses') 34 | 35 | def __repr__(self): 36 | return self.subject 37 | 38 | 39 | class Student(db.Model): 40 | __tablename__ = 'student' 41 | 42 | id = db.Column(db.Integer, primary_key=True) 43 | name = db.Column(db.String(120), unique=True) 44 | 45 | def __repr__(self): 46 | return self.name 47 | 48 | 49 | class Teacher(db.Model): 50 | __tablename__ = 'teacher' 51 | 52 | id = db.Column(db.Integer, primary_key=True) 53 | name = db.Column(db.String(120), unique=True) 54 | 55 | def __repr__(self): 56 | return self.name 57 | 58 | 59 | def create_app(database_uri='sqlite://'): 60 | app = Flask(__name__) 61 | app.config['SQLALCHEMY_DATABASE_URI'] = database_uri 62 | app.config['SECRET_KEY'] = 'not secure' 63 | db.init_app(app) 64 | datastore = SQLAlchemyDatastore( 65 | (Course, Student, Teacher), db.session) 66 | admin_blueprint = admin.create_admin_blueprint(datastore) 67 | app.register_blueprint(admin_blueprint, url_prefix='/admin') 68 | db.create_all(app=app) 69 | 70 | @app.route('/') 71 | def go_to_admin(): 72 | return redirect('/admin') 73 | return app 74 | 75 | 76 | if __name__ == '__main__': 77 | app = create_app('sqlite:///simple.db') 78 | app.run(debug=True) 79 | -------------------------------------------------------------------------------- /example/mongoalchemy/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wilsaj/flask-admin-old/aab2fe94e0641932ebd1c8f8dc500ba2daf5731c/example/mongoalchemy/__init__.py -------------------------------------------------------------------------------- /example/mongoalchemy/simple.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from flask import Flask, redirect 4 | from flask.ext import admin 5 | from flask.ext.admin.datastore.mongoalchemy import MongoAlchemyDatastore 6 | from mongoalchemy.document import Document 7 | from mongoalchemy import fields, session 8 | from wtforms import fields as wtfields 9 | from wtforms import Form 10 | 11 | 12 | # ---------------------------------------------------------------------- 13 | # Models 14 | # ---------------------------------------------------------------------- 15 | class Course(Document): 16 | subject = fields.StringField() 17 | start_date = fields.DateTimeField() 18 | end_date = fields.DateTimeField() 19 | 20 | def __repr__(self): 21 | return self.subject 22 | 23 | 24 | class Student(Document): 25 | name = fields.StringField() 26 | 27 | def __repr__(self): 28 | return self.name 29 | 30 | 31 | class Teacher(Document): 32 | name = fields.StringField() 33 | 34 | def __repr__(self): 35 | return self.name 36 | 37 | 38 | # ---------------------------------------------------------------------- 39 | # Forms 40 | # ---------------------------------------------------------------------- 41 | class CourseForm(Form): 42 | subject = wtfields.TextField(u'Subject') 43 | 44 | 45 | def create_app(mongo_database='simple-example', pagination=25): 46 | app = Flask(__name__) 47 | app.config['SECRET_KEY'] = 'not secure' 48 | app.db_session = session.Session.connect(mongo_database) 49 | datastore = MongoAlchemyDatastore( 50 | (Course, Student, Teacher), app.db_session) 51 | admin_blueprint = admin.create_admin_blueprint( 52 | datastore, list_view_pagination=pagination) 53 | app.register_blueprint(admin_blueprint, url_prefix='/admin') 54 | 55 | @app.route('/') 56 | def go_to_admin(): 57 | return redirect('/admin') 58 | 59 | return app 60 | 61 | 62 | if __name__ == '__main__': 63 | app = create_app() 64 | app.run(debug=True) 65 | -------------------------------------------------------------------------------- /flask_admin/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | flask.ext.admin 4 | ~~~~~~~~~~~~~~ 5 | 6 | Flask-Admin is a Flask extension module that aims to be a 7 | flexible, customizable web-based interface to your datastore. 8 | 9 | :copyright: (c) 2011 by wilsaj. 10 | :license: BSD, see LICENSE for more details. 11 | """ 12 | from __future__ import absolute_import 13 | 14 | import datetime 15 | from functools import wraps 16 | import inspect 17 | import os 18 | import time 19 | import types 20 | 21 | import flask 22 | from flask import flash, render_template, redirect, request, url_for 23 | 24 | from flask.ext.admin.wtforms import has_file_field 25 | from flask.ext.admin.datastore import AdminDatastore 26 | 27 | 28 | def create_admin_blueprint(*args, **kwargs): 29 | """Returns a Flask blueprint that provides the admin interface 30 | views. This blueprint will need to be registered to your flask 31 | application. The `datastore` parameter should be an set to be an 32 | instantiated :class:`AdminDatastore`. 33 | 34 | For example, typical usage would look something like this:: 35 | 36 | from flask import Flask 37 | from flask.ext.admin.datastore.sqlalchemy import SQLAlchemyDatastore 38 | 39 | from my_application import models, db_session 40 | 41 | app = Flask(__name__) 42 | datastore = SQLAlchemyDatastore(models, db_session) 43 | admin = create_admin_blueprint(datastore) 44 | app.register_blueprint(admin, url_prefix='/admin') 45 | 46 | 47 | You can optionally specify the `name` to be used for your 48 | blueprint. The blueprint name preceeds the view names in the 49 | endpoints, if for example you want to refer to the views using 50 | :func:`flask.url_for()`. 51 | 52 | .. note:: 53 | 54 | If you are using more than one admin blueprint from within the 55 | same app, it is necessary to give each admin blueprint a 56 | different name so the admin blueprints will have distinct 57 | endpoints. 58 | 59 | The `view_decorator` parameter can be set to a decorator that will 60 | be applied to each admin view function. For example, you might 61 | want to set this to a decorator that handles authentication 62 | (e.g. login_required). See the authentication/view_decorator.py 63 | for an example of this. 64 | 65 | The `list_view_pagination` parameter sets the number of items that 66 | will be listed per page in the list view. 67 | 68 | Finally, the `empty_sequence` keyword can be used to designate a 69 | sequence of characters that can be used as a substitute for cases 70 | where part of the key url may be empty. This should be a rare 71 | occurance, but might comes up when using composite keys which can 72 | contain empty parts. By default, `empty_sequence` is set to %1A, 73 | the substitute control character. 74 | """ 75 | if not isinstance(args[0], AdminDatastore): 76 | from warnings import warn 77 | warn(DeprecationWarning( 78 | 'The interface for creating admin blueprints has changed ' 79 | 'as of Flask-Admin 0.3. In order to support alternative ' 80 | 'datasores, you now need to configure an admin datastore ' 81 | 'object before calling create_admin_blueprint(). See the ' 82 | 'Flask-Admin documentation for more information.'), 83 | stacklevel=2) 84 | return create_admin_blueprint_deprecated(*args, **kwargs) 85 | 86 | else: 87 | return create_admin_blueprint_new(*args, **kwargs) 88 | 89 | 90 | def create_admin_blueprint_deprecated( 91 | models, db_session, name='admin', model_forms=None, exclude_pks=True, 92 | list_view_pagination=25, view_decorator=None, **kwargs): 93 | 94 | from flask.ext.admin.datastore.sqlalchemy import SQLAlchemyDatastore 95 | datastore = SQLAlchemyDatastore(models, db_session, model_forms, 96 | exclude_pks) 97 | 98 | return create_admin_blueprint_new(datastore, name, list_view_pagination, 99 | view_decorator) 100 | 101 | 102 | def create_admin_blueprint_new( 103 | datastore, name='admin', list_view_pagination=25, view_decorator=None, 104 | empty_sequence=u'\x1a', template_folder=None, static_folder=None, 105 | **kwargs): 106 | if not template_folder: 107 | template_folder = os.path.join( 108 | _get_admin_extension_dir(), 'templates') 109 | if not static_folder: 110 | static_folder = os.path.join( 111 | _get_admin_extension_dir(), 'static') 112 | 113 | admin_blueprint = flask.Blueprint( 114 | name, 'flask.ext.admin', 115 | static_folder=static_folder, template_folder=template_folder, 116 | **kwargs) 117 | 118 | # if no view decorator was assigned, let view_decorator be a dummy 119 | # decorator that doesn't really do anything 120 | if not view_decorator: 121 | def view_decorator(f): 122 | @wraps(f) 123 | def wrapper(*args, **kwds): 124 | return f(*args, **kwds) 125 | return wrapper 126 | 127 | def get_model_url_key(model_instance): 128 | """Helper function that turns a set of model keys into a 129 | unique key for a url. 130 | """ 131 | values = datastore.get_model_keys(model_instance) 132 | return '/'.join([unicode(value) if value else empty_sequence 133 | for value in values]) 134 | 135 | def create_index_view(): 136 | @view_decorator 137 | def index(): 138 | """Landing page view for admin module 139 | """ 140 | return render_template( 141 | 'admin/index.html', 142 | model_names=datastore.list_model_names()) 143 | return index 144 | 145 | def create_list_view(): 146 | @view_decorator 147 | def list_view(model_name): 148 | """Lists instances of a given model, so they can 149 | beselected for editing or deletion. 150 | """ 151 | if not model_name in datastore.list_model_names(): 152 | return "%s cannot be accessed through this admin page" % ( 153 | model_name,) 154 | per_page = list_view_pagination 155 | page = int(request.args.get('page', '1')) 156 | pagination = datastore.create_model_pagination( 157 | model_name, page, per_page) 158 | 159 | return render_template( 160 | 'admin/list.html', 161 | model_names=datastore.list_model_names(), 162 | get_model_url_key=get_model_url_key, 163 | model_name=model_name, 164 | pagination=pagination) 165 | return list_view 166 | 167 | def create_edit_view(): 168 | @view_decorator 169 | def edit(model_name, model_url_key): 170 | """Edit a particular instance of a model.""" 171 | model_keys = [key if key != empty_sequence else u'' 172 | for key in model_url_key.split('/')] 173 | 174 | if not model_name in datastore.list_model_names(): 175 | return "%s cannot be accessed through this admin page" % ( 176 | model_name,) 177 | 178 | model_form = datastore.get_model_form(model_name) 179 | model_instance = datastore.find_model_instance( 180 | model_name, model_keys) 181 | 182 | if not model_instance: 183 | return "%s not found: %s" % (model_name, model_key) 184 | 185 | if request.method == 'GET': 186 | form = model_form(obj=model_instance) 187 | form._has_file_field = has_file_field(form) 188 | return render_template( 189 | 'admin/edit.html', 190 | model_names=datastore.list_model_names(), 191 | model_instance=model_instance, 192 | model_name=model_name, form=form) 193 | 194 | elif request.method == 'POST': 195 | form = model_form(request.form, obj=model_instance) 196 | form._has_file_field = has_file_field(form) 197 | if form.validate(): 198 | model_instance = datastore.update_from_form( 199 | model_instance, form) 200 | datastore.save_model(model_instance) 201 | flash('%s updated: %s' % (model_name, model_instance), 202 | 'success') 203 | return redirect( 204 | url_for('.list', 205 | model_name=model_name)) 206 | else: 207 | flash('There was an error processing your form. ' 208 | 'This %s has not been saved.' % model_name, 209 | 'error') 210 | return render_template( 211 | 'admin/edit.html', 212 | model_names=datastore.list_model_names(), 213 | model_instance=model_instance, 214 | model_name=model_name, form=form) 215 | return edit 216 | 217 | def create_add_view(): 218 | @view_decorator 219 | def add(model_name): 220 | """Create a new instance of a model.""" 221 | if not model_name in datastore.list_model_names(): 222 | return "%s cannot be accessed through this admin page" % ( 223 | model_name) 224 | model_class = datastore.get_model_class(model_name) 225 | model_form = datastore.get_model_form(model_name) 226 | model_instance = model_class() 227 | if request.method == 'GET': 228 | form = model_form() 229 | form._has_file_field = has_file_field(form) 230 | return render_template( 231 | 'admin/add.html', 232 | model_names=datastore.list_model_names(), 233 | model_name=model_name, 234 | form=form) 235 | elif request.method == 'POST': 236 | form = model_form(request.form) 237 | form._has_file_field = has_file_field(form) 238 | if form.validate(): 239 | model_instance = datastore.update_from_form( 240 | model_instance, form) 241 | datastore.save_model(model_instance) 242 | flash('%s added: %s' % (model_name, model_instance), 243 | 'success') 244 | return redirect(url_for('.list', 245 | model_name=model_name)) 246 | else: 247 | flash('There was an error processing your form. This ' 248 | '%s has not been saved.' % model_name, 'error') 249 | return render_template( 250 | 'admin/add.html', 251 | model_names=datastore.list_model_names(), 252 | model_name=model_name, 253 | form=form) 254 | return add 255 | 256 | def create_delete_view(): 257 | @view_decorator 258 | def delete(model_name, model_url_key): 259 | """Delete an instance of a model.""" 260 | model_keys = [key if key != empty_sequence else u'' 261 | for key in model_url_key.split('/')] 262 | 263 | if not model_name in datastore.list_model_names(): 264 | return "%s cannot be accessed through this admin page" % ( 265 | model_name,) 266 | model_instance = datastore.delete_model_instance( 267 | model_name, model_keys) 268 | if not model_instance: 269 | return "%s not found: %s" % (model_name, model_keys) 270 | flash('%s deleted: %s' % (model_name, model_instance), 271 | 'success') 272 | return redirect( 273 | url_for('.list', model_name=model_name)) 274 | 275 | return delete 276 | 277 | admin_blueprint.add_url_rule('/', 'index', 278 | view_func=create_index_view()) 279 | list_view = create_list_view() 280 | admin_blueprint.add_url_rule('/list//', 281 | 'list', view_func=list_view) 282 | admin_blueprint.add_url_rule('/list//', 283 | 'list_view', view_func=list_view) 284 | admin_blueprint.add_url_rule('/edit///', 285 | 'edit', view_func=create_edit_view(), 286 | methods=['GET', 'POST']) 287 | admin_blueprint.add_url_rule('/delete///', 288 | 'delete', view_func=create_delete_view()) 289 | admin_blueprint.add_url_rule('/add//', 290 | 'add', view_func=create_add_view(), 291 | methods=['GET', 'POST']) 292 | 293 | return admin_blueprint 294 | 295 | 296 | def _get_admin_extension_dir(): 297 | """Returns the directory path of this admin extension. This is 298 | necessary for setting the static_folder and templates_folder 299 | arguments when creating the blueprint. 300 | """ 301 | return os.path.dirname(inspect.getfile(inspect.currentframe())) 302 | -------------------------------------------------------------------------------- /flask_admin/datastore/__init__.py: -------------------------------------------------------------------------------- 1 | from .core import AdminDatastore 2 | -------------------------------------------------------------------------------- /flask_admin/datastore/core.py: -------------------------------------------------------------------------------- 1 | class AdminDatastore(object): 2 | """A base class for admin datastore objects. All datastores used 3 | in Flask-Admin should subclass this object and define the 4 | following methods. 5 | """ 6 | 7 | def create_model_pagination(self, model_name, page, per_page=25): 8 | """Returns a pagination object for the list view.""" 9 | raise NotImplementedError() 10 | 11 | def delete_model_instance(self, model_name, model_keys): 12 | """Deletes a model instance. Returns True if model instance 13 | was successfully deleted, returns False otherwise. 14 | """ 15 | raise NotImplementedError() 16 | 17 | def find_model_instance(self, model_name, model_keys): 18 | """Returns a model instance, if one exists, that matches 19 | model_name and model_keys. Returns None if no such model 20 | instance exists. 21 | """ 22 | raise NotImplementedError() 23 | 24 | def get_model_class(self, model_name): 25 | """Returns a model class, given a model name.""" 26 | raise NotImplementedError() 27 | 28 | def get_model_form(self, model_name): 29 | """Returns a form, given a model name.""" 30 | raise NotImplementedError() 31 | 32 | def get_model_keys(self, model_instance): 33 | """Returns the keys for a given a model instance. This should 34 | be an iterable (e.g. list or tuple) containing the keys. 35 | """ 36 | raise NotImplementedError() 37 | 38 | def list_model_names(self): 39 | """Returns a list of model names available in the datastore.""" 40 | raise NotImplementedError() 41 | 42 | def save_model(self, model_instance): 43 | """Persists a model instance to the datastore. Note: this 44 | could be called when a model instance is added or edited. 45 | """ 46 | raise NotImplementedError() 47 | 48 | def update_from_form(self, model_instance, form): 49 | """Returns a model instance whose values have been updated 50 | with the values from a given form. 51 | """ 52 | raise NotImplementedError() 53 | -------------------------------------------------------------------------------- /flask_admin/datastore/mongoalchemy.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | flask.ext.datastore.mongoalchemy 4 | ~~~~~~~~~~~~~~ 5 | 6 | 7 | :copyright: (c) 2011 by wilsaj. 8 | :license: BSD, see LICENSE for more details. 9 | """ 10 | from __future__ import absolute_import 11 | 12 | import types 13 | 14 | import mongoalchemy as ma 15 | from mongoalchemy.document import Document 16 | from wtforms import fields as f 17 | from wtforms import form, validators, widgets 18 | from wtforms.form import Form 19 | 20 | from flask.ext.admin.datastore import AdminDatastore 21 | from flask.ext.admin import wtforms as admin_wtf 22 | from flask.ext.admin import util 23 | 24 | 25 | class MongoAlchemyDatastore(AdminDatastore): 26 | """A datastore for accessing MongoAlchemy document models. 27 | 28 | The `models` parameter should be either a module or an iterable 29 | that contains the MongoAlchemy models that will be made available 30 | through the admin interface. 31 | 32 | `db_session` should be an initialized MongoAlchemy session 33 | object. See the `MongoAlchemy documentation`_ for information on 34 | how to do that. 35 | 36 | By default, a form for adding and editing data will be 37 | automatically generated for each MongoAlchemy model. Only 38 | primitive MongoAlchemy types are supported so if you need to 39 | support other fields you will need to create custom forms. You can 40 | also use custom forms if you want more control over form behavior. 41 | To use custom forms, set the `model_forms` parameter to be a dict 42 | with model names as keys matched to custom forms for the forms you 43 | want to override. Forms should be WTForms form objects; see the 44 | `WTForms documentation`_ for more information on how to configure 45 | forms. 46 | 47 | A dict with model names as keys, mapped to WTForm Form objects 48 | that should be used as forms for creating and editing instances of 49 | these models. 50 | 51 | .. _MongoAlchemy documentation: http://www.mongoalchemy.org/api/session.html 52 | .. _WTForms documentation: http://wtforms.simplecodes.com/ 53 | """ 54 | def __init__(self, models, db_session, model_forms=None): 55 | self.model_classes = {} 56 | self.model_forms = model_forms 57 | self.db_session = db_session 58 | 59 | if not self.model_forms: 60 | self.model_forms = {} 61 | 62 | if type(models) == types.ModuleType: 63 | self.model_classes = dict( 64 | [(k, v) for k, v in models.__dict__.items() 65 | if issubclass(v, Document)]) 66 | else: 67 | self.model_classes = dict( 68 | [(model.__name__, model) 69 | for model in models 70 | if issubclass(model, Document)]) 71 | 72 | if self.model_classes: 73 | self.form_dict = dict( 74 | [(k, _form_for_model(v, db_session,)) 75 | for k, v in self.model_classes.items()]) 76 | for model_name, form in self.model_forms.items(): 77 | if model_name in self.form_dict: 78 | self.form_dict[model_name] = form 79 | 80 | def create_model_pagination(self, model_name, page, per_page=25): 81 | """Returns a pagination object for the list view.""" 82 | model_class = self.get_model_class(model_name) 83 | query = self.db_session.query(model_class).skip( 84 | (page - 1) * per_page).limit(per_page) 85 | return MongoAlchemyPagination(page, per_page, query) 86 | 87 | def delete_model_instance(self, model_name, model_keys): 88 | """Deletes a model instance. Returns True if model instance 89 | was successfully deleted, returns False otherwise. 90 | """ 91 | model_class = self.get_model_class(model_name) 92 | try: 93 | model_instance = self.find_model_instance(model_name, model_keys) 94 | self.db_session.remove(model_instance) 95 | return True 96 | except ma.query.BadResultException: 97 | return False 98 | 99 | def find_model_instance(self, model_name, model_keys): 100 | """Returns a model instance, if one exists, that matches 101 | model_name and model_keys. Returns None if no such model 102 | instance exists. 103 | """ 104 | model_key = model_keys[0] 105 | model_class = self.get_model_class(model_name) 106 | return self.db_session.query(model_class).filter( 107 | model_class.mongo_id == model_key).one() 108 | 109 | def get_model_class(self, model_name): 110 | """Returns a model class, given a model name.""" 111 | return self.model_classes.get(model_name, None) 112 | 113 | def get_model_form(self, model_name): 114 | """Returns a form, given a model name.""" 115 | return self.form_dict.get(model_name, None) 116 | 117 | def get_model_keys(self, model_instance): 118 | """Returns the keys for a given a model instance.""" 119 | return [model_instance.mongo_id] 120 | 121 | def list_model_names(self): 122 | """Returns a list of model names available in the datastore.""" 123 | return self.model_classes.keys() 124 | 125 | def save_model(self, model_instance): 126 | """Persists a model instance to the datastore. Note: this 127 | could be called when a model instance is added or edited. 128 | """ 129 | return model_instance.commit(self.db_session.db) 130 | 131 | def update_from_form(self, model_instance, form): 132 | """Returns a model instance whose values have been updated 133 | with the values from a given form. 134 | """ 135 | for field in form: 136 | # handle FormFields that were generated for mongoalchemy 137 | # TupleFields as a special case 138 | if field.__class__ == f.FormField: 139 | data_tuple = tuple([subfield.data for subfield in field]) 140 | setattr(model_instance, field.name, data_tuple) 141 | continue 142 | 143 | # don't use the mongo id from the form - it comes from the 144 | # key/url and if someone tampers with the form somehow, we 145 | # should ignore that 146 | elif field.name != 'mongo_id': 147 | setattr(model_instance, field.name, field.data) 148 | return model_instance 149 | 150 | 151 | class MongoAlchemyPagination(util.Pagination): 152 | def __init__(self, page, per_page, query, *args, **kwargs): 153 | super(MongoAlchemyPagination, self).__init__( 154 | page, per_page, total=query.count(), items=query.all(), 155 | *args, **kwargs) 156 | 157 | 158 | def _form_for_model(document_class, db_session): 159 | """returns a wtform Form object for a given document model class. 160 | """ 161 | #XXX: needs to be implemented 162 | return model_form(document_class) 163 | 164 | 165 | #----------------------------------------------------------------------- 166 | # mongo alchemy form generation: to be pushed upstream 167 | #----------------------------------------------------------------------- 168 | class DisabledTextInput(widgets.TextInput): 169 | def __call__(self, field, **kwargs): 170 | kwargs['disabled'] = 'disabled' 171 | return super(DisabledTextInput, self).__call__(field, **kwargs) 172 | 173 | 174 | def converts(*args): 175 | def _inner(func): 176 | func._converter_for = frozenset(args) 177 | return func 178 | return _inner 179 | 180 | 181 | class ModelConverterBase(object): 182 | def __init__(self, converters, use_mro=True): 183 | self.use_mro = use_mro 184 | 185 | if not converters: 186 | converters = {} 187 | 188 | for name in dir(self): 189 | obj = getattr(self, name) 190 | if hasattr(obj, '_converter_for'): 191 | for classname in obj._converter_for: 192 | converters[classname] = obj 193 | 194 | self.converters = converters 195 | 196 | def convert(self, model, ma_field, field_args): 197 | default = getattr(ma_field, 'default', None) 198 | 199 | if default == ma.util.UNSET: 200 | default = None 201 | 202 | kwargs = { 203 | 'validators': [], 204 | 'filters': [], 205 | 'default': default, 206 | } 207 | 208 | if field_args: 209 | kwargs.update(field_args) 210 | 211 | if not ma_field.required: 212 | kwargs['validators'].append(validators.Optional()) 213 | 214 | types = [type(ma_field)] 215 | 216 | converter = None 217 | for ma_field_type in types: 218 | type_string = '%s.%s' % ( 219 | ma_field_type.__module__, ma_field_type.__name__) 220 | if type_string.startswith('mongoalchemy.fields'): 221 | type_string = type_string[20:] 222 | 223 | if type_string in self.converters: 224 | converter = self.converters[type_string] 225 | break 226 | else: 227 | for ma_field_type in types: 228 | if ma_field_type.__name__ in self.converters: 229 | converter = self.converters[ma_field_type.__name__] 230 | break 231 | else: 232 | return 233 | return converter(model=model, ma_field=ma_field, field_args=kwargs) 234 | 235 | 236 | class ModelConverter(ModelConverterBase): 237 | def __init__(self, extra_converters=None): 238 | super(ModelConverter, self).__init__(extra_converters) 239 | 240 | @converts('BoolField') 241 | def conv_Bool(self, ma_field, field_args, **extra): 242 | return f.BooleanField(**field_args) 243 | 244 | @converts('DateTimeField') 245 | def conv_DateTime(self, ma_field, field_args, **extra): 246 | # TODO: add custom validator for date range 247 | field_args['widget'] = admin_wtf.DateTimePickerWidget() 248 | return f.DateTimeField(**field_args) 249 | 250 | @converts('EnumField') 251 | def conv_Enum(self, model, ma_field, field_args, **extra): 252 | converted_field = self.convert(model, ma_field.item_type, {}) 253 | converted_field.kwargs['validators'].append( 254 | validators.AnyOf(ma_field.values, values_formatter=str)) 255 | return converted_field 256 | 257 | @converts('FloatField') 258 | def conv_Float(self, ma_field, field_args, **extra): 259 | if ma_field.min or ma_field.max: 260 | field_args['validators'].append( 261 | validators.NumberRange(min=ma_field.min, max=ma_field.max)) 262 | return f.FloatField(**field_args) 263 | 264 | @converts('IntField') 265 | def conv_Int(self, ma_field, field_args, **extra): 266 | if ma_field.min or ma_field.max: 267 | field_args['validators'].append( 268 | validators.NumberRange(min=ma_field.min, max=ma_field.max)) 269 | return f.IntegerField(**field_args) 270 | 271 | @converts('ObjectIdField') 272 | def conv_ObjectId(self, field_args, **extra): 273 | widget = DisabledTextInput() 274 | return f.TextField(widget=widget, **field_args) 275 | 276 | @converts('StringField') 277 | def conv_String(self, ma_field, field_args, **extra): 278 | if ma_field.min or ma_field.max: 279 | min = ma_field.min or -1 280 | max = ma_field.max or -1 281 | field_args['validators'].append( 282 | validators.Length(min=min, max=max)) 283 | return f.TextField(**field_args) 284 | 285 | @converts('TupleField') 286 | def conv_Tuple(self, model, ma_field, field_args, **extra): 287 | def convert_field(field): 288 | return self.convert(model, field, {}) 289 | fields = map(convert_field, ma_field.types) 290 | fields_dict = dict([('%s_%s' % (ma_field._name, i), field) 291 | for i, field in enumerate(fields)]) 292 | 293 | class ConvertedTupleForm(Form): 294 | def process(self, formdata=None, obj=None, **kwargs): 295 | # if the field is being populated from a mongoalchemy 296 | # TupleField, obj will be a tuple object so we can set 297 | # the fields by reversing the field name to get the 298 | # index and then passing that along to wtforms in the 299 | # kwargs dict 300 | if type(obj) == tuple: 301 | for name, field in self._fields.items(): 302 | tuple_index = int(name.split('_')[-1]) 303 | kwargs[name] = obj[tuple_index] 304 | super(ConvertedTupleForm, self).process( 305 | formdata, obj, **kwargs) 306 | 307 | fields_form = type(ma_field._name + 'Form', (ConvertedTupleForm,), fields_dict) 308 | return f.FormField(fields_form) 309 | 310 | 311 | def model_fields(model, only=None, exclude=None, field_args=None, 312 | converter=None): 313 | """ 314 | Generate a dictionary of fields for a given MongoAlchemy model. 315 | 316 | See `model_form` docstring for description of parameters. 317 | """ 318 | if not issubclass(model, Document): 319 | raise TypeError('model must be a mongoalchemy document model') 320 | 321 | converter = converter or ModelConverter() 322 | field_args = field_args or {} 323 | 324 | ma_fields = ((name, field) for name, field in model.get_fields().items()) 325 | if only: 326 | ma_fields = (x for x in ma_fields if x[0] in only) 327 | elif exclude: 328 | ma_fields = (x for x in ma_fields if x[0] not in exclude) 329 | 330 | field_dict = {} 331 | for name, field in ma_fields: 332 | wtfield = converter.convert(model, field, field_args.get(name)) 333 | if wtfield is not None: 334 | field_dict[name] = wtfield 335 | 336 | return field_dict 337 | 338 | 339 | def model_form(model, base_class=Form, only=None, exclude=None, 340 | field_args=None, converter=None): 341 | """ 342 | Create a wtforms Form for a given MongoAlchemy model class:: 343 | 344 | from wtforms.ext.mongoalchemy.orm import model_form 345 | from myapp.models import User 346 | UserForm = model_form(User) 347 | 348 | :param model: 349 | A MongoAlchemy mapped model class. 350 | :param base_class: 351 | Base form class to extend from. Must be a ``wtforms.Form`` subclass. 352 | :param only: 353 | An optional iterable with the property names that should be included in 354 | the form. Only these properties will have fields. 355 | :param exclude: 356 | An optional iterable with the property names that should be excluded 357 | from the form. All other properties will have fields. 358 | :param field_args: 359 | An optional dictionary of field names mapping to keyword arguments used 360 | to construct each field object. 361 | :param converter: 362 | A converter to generate the fields based on the model properties. If 363 | not set, ``ModelConverter`` is used. 364 | """ 365 | field_dict = model_fields(model, only, exclude, field_args, converter) 366 | return type(model.__name__ + 'Form', (base_class, ), field_dict) 367 | -------------------------------------------------------------------------------- /flask_admin/datastore/sqlalchemy.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | flask.ext.datastore.sqlalchemy 4 | ~~~~~~~~~~~~~~ 5 | 6 | Defines a SQLAlchemy datastore. 7 | 8 | :copyright: (c) 2011 by wilsaj. 9 | :license: BSD, see LICENSE for more details. 10 | """ 11 | from __future__ import absolute_import 12 | 13 | import datetime 14 | from functools import wraps 15 | import inspect 16 | import os 17 | import time 18 | import types 19 | 20 | import flask 21 | from flask import flash, render_template, redirect, request, url_for 22 | from flask.ext.sqlalchemy import Pagination 23 | import sqlalchemy as sa 24 | from sqlalchemy.orm.exc import NoResultFound 25 | from wtforms import validators, widgets 26 | from wtforms.ext.sqlalchemy.orm import model_form, converts, ModelConverter 27 | from wtforms.ext.sqlalchemy import fields as sa_fields 28 | 29 | from flask.ext.admin.wtforms import * 30 | from flask.ext.admin.datastore import AdminDatastore 31 | 32 | 33 | class SQLAlchemyDatastore(AdminDatastore): 34 | """A datastore class for accessing SQLAlchemy models. 35 | 36 | The `models` parameter should be either a module or an iterable 37 | (like a tuple or a list) that contains the SQLAlchemy models that 38 | will be made available through the admin interface. 39 | 40 | `db_session` should be the SQLAlchemy session that the datastore 41 | will use to access the database. The session should already be 42 | bound to an engine. See the `SQLAlchemy in Flask`_ documentation 43 | for more information on how to configure the session. 44 | 45 | By default, a form for adding and editing data will be 46 | automatically generated for each SQLAlchemy model. You can also 47 | create custom forms if you need more control over what the forms 48 | look like or how they behave. To use custom forms, set the 49 | `model_forms` parameter to be a dict with model names as keys 50 | matched to custom forms for the forms you want to override. Forms 51 | should be WTForms form objects; see the `WTForms documentation`_ 52 | for more information on how to configure forms. 53 | 54 | Finally, the `exclude_pks` parameter can be used to specify 55 | whether or not to automatically exclude fields representing the 56 | primary key in auto-generated forms. The default is True, so the 57 | generated forms will not expose the primary keys of your 58 | models. This is usually a good idea if you are using a primary key 59 | that doesn't have any meaning outside of the database, like an 60 | auto-incrementing integer, because changing a primary key changes 61 | the nature of foreign key relationships. If you want to expose the 62 | primary key, set this to False. 63 | 64 | .. _SQLAlchemy in Flask: http://flask.pocoo.org/docs/patterns/sqlalchemy/ 65 | .. _WTForms documentation: http://wtforms.simplecodes.com/ 66 | """ 67 | def __init__(self, models, db_session, model_forms=None, exclude_pks=True): 68 | self.model_classes = {} 69 | self.model_forms = model_forms 70 | self.db_session = db_session 71 | 72 | if not self.model_forms: 73 | self.model_forms = {} 74 | 75 | #XXX: fix base handling so it will work with non-Declarative models 76 | if type(models) == types.ModuleType: 77 | self.model_classes = dict( 78 | [(k, v) for k, v in models.__dict__.items() 79 | if isinstance(v, sa.ext.declarative.DeclarativeMeta) 80 | and k != 'Base']) 81 | else: 82 | self.model_classes = dict( 83 | [(model.__name__, model) 84 | for model in models 85 | if isinstance(model, sa.ext.declarative.DeclarativeMeta) 86 | and model.__name__ != 'Base']) 87 | 88 | if self.model_classes: 89 | self.form_dict = dict( 90 | [(k, _form_for_model(v, db_session, 91 | exclude_pk=exclude_pks)) 92 | for k, v in self.model_classes.items()]) 93 | for model_name, form in self.model_forms.items(): 94 | if model_name in self.form_dict: 95 | self.form_dict[model_name] = form 96 | 97 | def create_model_pagination(self, model_name, page, per_page=25): 98 | """Returns a pagination object for the list view.""" 99 | model_class = self.model_classes[model_name] 100 | model_instances = self.db_session.query(model_class) 101 | offset = (page - 1) * per_page 102 | items = model_instances.limit(per_page).offset(offset).all() 103 | return Pagination(model_instances, page, per_page, 104 | model_instances.count(), items) 105 | 106 | def delete_model_instance(self, model_name, model_keys): 107 | """Deletes a model instance. Returns True if model instance 108 | was successfully deleted, returns False otherwise. 109 | """ 110 | model_instance = self.find_model_instance(model_name, model_keys) 111 | if not model_instance: 112 | return False 113 | self.db_session.delete(model_instance) 114 | self.db_session.commit() 115 | return True 116 | 117 | def find_model_instance(self, model_name, model_keys): 118 | """Returns a model instance, if one exists, that matches 119 | model_name and model_keys. Returns None if no such model 120 | instance exists. 121 | """ 122 | model_class = self.get_model_class(model_name) 123 | pk_query_dict = {} 124 | 125 | for key, value in zip(_get_pk_names(model_class), model_keys): 126 | pk_query_dict[key] = value 127 | 128 | try: 129 | return self.db_session.query(model_class).filter_by( 130 | **pk_query_dict).one() 131 | except NoResultFound: 132 | return None 133 | 134 | def get_model_class(self, model_name): 135 | """Returns a model class, given a model name.""" 136 | return self.model_classes[model_name] 137 | 138 | def get_model_form(self, model_name): 139 | """Returns a form, given a model name.""" 140 | return self.form_dict[model_name] 141 | 142 | def get_model_keys(self, model_instance): 143 | """Returns the keys for a given a model instance.""" 144 | return [getattr(model_instance, value) 145 | for value in _get_pk_names(model_instance)] 146 | 147 | def list_model_names(self): 148 | """Returns a list of model names available in the datastore.""" 149 | return self.model_classes.keys() 150 | 151 | def save_model(self, model_instance): 152 | """Persists a model instance to the datastore. Note: this 153 | could be called when a model instance is added or edited. 154 | """ 155 | self.db_session.add(model_instance) 156 | self.db_session.commit() 157 | 158 | def update_from_form(self, model_instance, form): 159 | """Returns a model instance whose values have been updated 160 | with the values from a given form. 161 | """ 162 | for name, field in form._fields.iteritems(): 163 | field.populate_obj(model_instance, name) 164 | 165 | return model_instance 166 | 167 | 168 | def _form_for_model(model_class, db_session, exclude=None, exclude_pk=True): 169 | """Return a form for a given model. This will be a form generated 170 | by wtforms.ext.sqlalchemy.model_form, but decorated with a 171 | QuerySelectField for foreign keys. 172 | """ 173 | if not exclude: 174 | exclude = [] 175 | 176 | model_mapper = sa.orm.class_mapper(model_class) 177 | relationship_fields = [] 178 | 179 | pk_names = _get_pk_names(model_class) 180 | 181 | if exclude_pk: 182 | exclude.extend(pk_names) 183 | 184 | # exclude any foreign_keys that we have relationships for; 185 | # relationships will be mapped to select fields by the 186 | # AdminConverter 187 | exclude.extend([relationship.local_side[0].name 188 | for relationship in model_mapper.iterate_properties 189 | if isinstance(relationship, 190 | sa.orm.properties.RelationshipProperty) 191 | and relationship.local_side[0].name not in pk_names]) 192 | form = model_form(model_class, exclude=exclude, 193 | converter=AdminConverter(db_session)) 194 | 195 | return form 196 | 197 | 198 | def _get_pk_names(model): 199 | """Return the primary key attribute names for a given model 200 | (either instance or class). 201 | """ 202 | model_mapper = model.__mapper__ 203 | 204 | return [prop.key for prop in model_mapper.iterate_properties 205 | if isinstance(prop, sa.orm.properties.ColumnProperty) and \ 206 | prop.columns[0].primary_key] 207 | 208 | 209 | def _query_factory_for(model_class, db_session): 210 | """Return a query factory for a given model_class. This gives us 211 | an all-purpose way of generating query factories for 212 | QuerySelectFields. 213 | """ 214 | def query_factory(): 215 | return sorted(db_session.query(model_class).all(), key=repr) 216 | 217 | return query_factory 218 | 219 | 220 | class AdminConverter(ModelConverter): 221 | """Subclass of the wtforms sqlalchemy Model Converter that handles 222 | relationship properties and uses custom widgets for date and 223 | datetime objects. 224 | """ 225 | def __init__(self, db_session, *args, **kwargs): 226 | self.db_session = db_session 227 | super(AdminConverter, self).__init__(*args, **kwargs) 228 | 229 | def convert(self, model, mapper, prop, field_args): 230 | if not isinstance(prop, sa.orm.properties.ColumnProperty) and \ 231 | not isinstance(prop, sa.orm.properties.RelationshipProperty): 232 | # XXX We don't support anything but ColumnProperty and 233 | # RelationshipProperty at the moment. 234 | return 235 | 236 | if isinstance(prop, sa.orm.properties.ColumnProperty): 237 | if len(prop.columns) != 1: 238 | raise TypeError('Do not know how to convert multiple-' 239 | 'column properties currently') 240 | 241 | column = prop.columns[0] 242 | 243 | # default_value = None 244 | # if hasattr(column, 'default'): #always is True 245 | # default_value = column.default 246 | 247 | default_value = getattr(column, 'default', None) 248 | 249 | if default_value is not None and \ 250 | not callable(default_value.arg): 251 | # for the default value such as number or string 252 | default_value = default_value.arg 253 | 254 | kwargs = { 255 | 'validators': [], 256 | 'filters': [], 257 | 'default': default_value, 258 | } 259 | if field_args: 260 | kwargs.update(field_args) 261 | if hasattr(column, 'nullable') and column.nullable: 262 | kwargs['validators'].append(validators.Optional()) 263 | if self.use_mro: 264 | types = inspect.getmro(type(column.type)) 265 | else: 266 | types = [type(column.type)] 267 | 268 | converter = None 269 | for col_type in types: 270 | type_string = '%s.%s' % (col_type.__module__, 271 | col_type.__name__) 272 | if type_string.startswith('sqlalchemy'): 273 | type_string = type_string[11:] 274 | if type_string in self.converters: 275 | converter = self.converters[type_string] 276 | break 277 | else: 278 | for col_type in types: 279 | if col_type.__name__ in self.converters: 280 | converter = self.converters[col_type.__name__] 281 | break 282 | else: 283 | return 284 | return converter(model=model, mapper=mapper, prop=prop, 285 | column=column, field_args=kwargs) 286 | 287 | if isinstance(prop, sa.orm.properties.RelationshipProperty): 288 | # if prop.direction == sa.orm.interfaces.MANYTOONE and \ 289 | # len(prop.local_remote_pairs) != 1: 290 | # raise TypeError('Do not know how to convert multiple' 291 | # '-column properties currently') 292 | # elif prop.direction == sa.orm.interfaces.MANYTOMANY and \ 293 | # len(prop.local_remote_pairs) != 2: 294 | # raise TypeError('Do not know how to convert multiple' 295 | # '-column properties currently') 296 | 297 | local_column = prop.local_remote_pairs[0][0] 298 | foreign_model = prop.mapper.class_ 299 | 300 | if prop.direction == sa.orm.properties.MANYTOONE: 301 | return sa_fields.QuerySelectField( 302 | prop.key, 303 | query_factory=_query_factory_for(foreign_model, 304 | self.db_session), 305 | allow_blank=local_column.nullable) 306 | if prop.direction == sa.orm.properties.MANYTOMANY: 307 | return sa_fields.QuerySelectMultipleField( 308 | prop.key, 309 | query_factory=_query_factory_for(foreign_model, 310 | self.db_session), 311 | allow_blank=local_column.nullable) 312 | 313 | @converts('Date') 314 | def conv_Date(self, field_args, **extra): 315 | field_args['widget'] = DatePickerWidget() 316 | return wtf_fields.DateField(**field_args) 317 | 318 | @converts('DateTime') 319 | def conv_DateTime(self, field_args, **extra): 320 | # XXX: should show disabled (greyed out) w/current value, 321 | # indicating it is updated internally? 322 | if hasattr(field_args['default'], 'arg'): 323 | if inspect.isfunction(field_args['default'].arg): 324 | return None 325 | field_args['widget'] = DateTimePickerWidget() 326 | return wtf_fields.DateTimeField(**field_args) 327 | 328 | @converts('Time') 329 | def conv_Time(self, field_args, **extra): 330 | field_args['widget'] = TimePickerWidget() 331 | return TimeField(**field_args) 332 | -------------------------------------------------------------------------------- /flask_admin/static/css/Aristo/images/bg_fallback.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wilsaj/flask-admin-old/aab2fe94e0641932ebd1c8f8dc500ba2daf5731c/flask_admin/static/css/Aristo/images/bg_fallback.png -------------------------------------------------------------------------------- /flask_admin/static/css/Aristo/images/icon_sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wilsaj/flask-admin-old/aab2fe94e0641932ebd1c8f8dc500ba2daf5731c/flask_admin/static/css/Aristo/images/icon_sprite.png -------------------------------------------------------------------------------- /flask_admin/static/css/Aristo/images/progress_bar.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wilsaj/flask-admin-old/aab2fe94e0641932ebd1c8f8dc500ba2daf5731c/flask_admin/static/css/Aristo/images/progress_bar.gif -------------------------------------------------------------------------------- /flask_admin/static/css/Aristo/images/slider_handles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wilsaj/flask-admin-old/aab2fe94e0641932ebd1c8f8dc500ba2daf5731c/flask_admin/static/css/Aristo/images/slider_handles.png -------------------------------------------------------------------------------- /flask_admin/static/css/Aristo/images/ui-icons_222222_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wilsaj/flask-admin-old/aab2fe94e0641932ebd1c8f8dc500ba2daf5731c/flask_admin/static/css/Aristo/images/ui-icons_222222_256x240.png -------------------------------------------------------------------------------- /flask_admin/static/css/Aristo/images/ui-icons_454545_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wilsaj/flask-admin-old/aab2fe94e0641932ebd1c8f8dc500ba2daf5731c/flask_admin/static/css/Aristo/images/ui-icons_454545_256x240.png -------------------------------------------------------------------------------- /flask_admin/static/css/chosen-sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wilsaj/flask-admin-old/aab2fe94e0641932ebd1c8f8dc500ba2daf5731c/flask_admin/static/css/chosen-sprite.png -------------------------------------------------------------------------------- /flask_admin/static/css/chosen.css: -------------------------------------------------------------------------------- 1 | /* @group Base */ 2 | .chzn-container { 3 | font-size: 13px; 4 | position: relative; 5 | display: inline-block; 6 | zoom: 1; 7 | *display: inline; 8 | } 9 | .chzn-container .chzn-drop { 10 | background: #fff; 11 | border: 1px solid #aaa; 12 | border-top: 0; 13 | position: absolute; 14 | top: 29px; 15 | left: 0; 16 | -webkit-box-shadow: 0 4px 5px rgba(0,0,0,.15); 17 | -moz-box-shadow : 0 4px 5px rgba(0,0,0,.15); 18 | -o-box-shadow : 0 4px 5px rgba(0,0,0,.15); 19 | box-shadow : 0 4px 5px rgba(0,0,0,.15); 20 | z-index: 999; 21 | } 22 | /* @end */ 23 | 24 | /* @group Single Chosen */ 25 | .chzn-container-single .chzn-single { 26 | background-color: #fff; 27 | background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #eeeeee), color-stop(0.5, white)); 28 | background-image: -webkit-linear-gradient(center bottom, #eeeeee 0%, white 50%); 29 | background-image: -moz-linear-gradient(center bottom, #eeeeee 0%, white 50%); 30 | background-image: -o-linear-gradient(top, #eeeeee 0%,#ffffff 50%); 31 | background-image: -ms-linear-gradient(top, #eeeeee 0%,#ffffff 50%); 32 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#eeeeee', endColorstr='#ffffff',GradientType=0 ); 33 | background-image: linear-gradient(top, #eeeeee 0%,#ffffff 50%); 34 | -webkit-border-radius: 4px; 35 | -moz-border-radius : 4px; 36 | border-radius : 4px; 37 | -moz-background-clip : padding; 38 | -webkit-background-clip: padding-box; 39 | background-clip : padding-box; 40 | border: 1px solid #aaa; 41 | display: block; 42 | overflow: hidden; 43 | white-space: nowrap; 44 | position: relative; 45 | height: 26px; 46 | line-height: 26px; 47 | padding: 0 0 0 8px; 48 | color: #444; 49 | text-decoration: none; 50 | } 51 | .chzn-container-single .chzn-single span { 52 | margin-right: 26px; 53 | display: block; 54 | overflow: hidden; 55 | white-space: nowrap; 56 | -o-text-overflow: ellipsis; 57 | -ms-text-overflow: ellipsis; 58 | text-overflow: ellipsis; 59 | } 60 | .chzn-container-single .chzn-single abbr { 61 | display: block; 62 | position: absolute; 63 | right: 26px; 64 | top: 8px; 65 | width: 12px; 66 | height: 13px; 67 | font-size: 1px; 68 | background: url(chosen-sprite.png) right top no-repeat; 69 | } 70 | .chzn-container-single .chzn-single abbr:hover { 71 | background-position: right -11px; 72 | } 73 | .chzn-container-single .chzn-single div { 74 | -webkit-border-radius: 0 4px 4px 0; 75 | -moz-border-radius : 0 4px 4px 0; 76 | border-radius : 0 4px 4px 0; 77 | -moz-background-clip : padding; 78 | -webkit-background-clip: padding-box; 79 | background-clip : padding-box; 80 | background: #ccc; 81 | background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #ccc), color-stop(0.6, #eee)); 82 | background-image: -webkit-linear-gradient(center bottom, #ccc 0%, #eee 60%); 83 | background-image: -moz-linear-gradient(center bottom, #ccc 0%, #eee 60%); 84 | background-image: -o-linear-gradient(bottom, #ccc 0%, #eee 60%); 85 | background-image: -ms-linear-gradient(top, #cccccc 0%,#eeeeee 60%); 86 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#cccccc', endColorstr='#eeeeee',GradientType=0 ); 87 | background-image: linear-gradient(top, #cccccc 0%,#eeeeee 60%); 88 | border-left: 1px solid #aaa; 89 | position: absolute; 90 | right: 0; 91 | top: 0; 92 | display: block; 93 | height: 100%; 94 | width: 18px; 95 | } 96 | .chzn-container-single .chzn-single div b { 97 | background: url('chosen-sprite.png') no-repeat 0 1px; 98 | display: block; 99 | width: 100%; 100 | height: 100%; 101 | } 102 | .chzn-container-single .chzn-search { 103 | padding: 3px 4px; 104 | position: relative; 105 | margin: 0; 106 | white-space: nowrap; 107 | z-index: 1010; 108 | } 109 | .chzn-container-single .chzn-search input { 110 | background: #fff url('chosen-sprite.png') no-repeat 100% -22px; 111 | background: url('chosen-sprite.png') no-repeat 100% -22px, -webkit-gradient(linear, left bottom, left top, color-stop(0.85, white), color-stop(0.99, #eeeeee)); 112 | background: url('chosen-sprite.png') no-repeat 100% -22px, -webkit-linear-gradient(center bottom, white 85%, #eeeeee 99%); 113 | background: url('chosen-sprite.png') no-repeat 100% -22px, -moz-linear-gradient(center bottom, white 85%, #eeeeee 99%); 114 | background: url('chosen-sprite.png') no-repeat 100% -22px, -o-linear-gradient(bottom, white 85%, #eeeeee 99%); 115 | background: url('chosen-sprite.png') no-repeat 100% -22px, -ms-linear-gradient(top, #ffffff 85%,#eeeeee 99%); 116 | background: url('chosen-sprite.png') no-repeat 100% -22px, -ms-linear-gradient(top, #ffffff 85%,#eeeeee 99%); 117 | background: url('chosen-sprite.png') no-repeat 100% -22px, linear-gradient(top, #ffffff 85%,#eeeeee 99%); 118 | margin: 1px 0; 119 | padding: 4px 20px 4px 5px; 120 | outline: 0; 121 | border: 1px solid #aaa; 122 | font-family: sans-serif; 123 | font-size: 1em; 124 | } 125 | .chzn-container-single .chzn-drop { 126 | -webkit-border-radius: 0 0 4px 4px; 127 | -moz-border-radius : 0 0 4px 4px; 128 | border-radius : 0 0 4px 4px; 129 | -moz-background-clip : padding; 130 | -webkit-background-clip: padding-box; 131 | background-clip : padding-box; 132 | } 133 | /* @end */ 134 | 135 | .chzn-container-single-nosearch .chzn-search input { 136 | position: absolute; 137 | left: -9000px; 138 | } 139 | 140 | /* @group Multi Chosen */ 141 | .chzn-container-multi .chzn-choices { 142 | background-color: #fff; 143 | background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0.85, white), color-stop(0.99, #eeeeee)); 144 | background-image: -webkit-linear-gradient(center bottom, white 85%, #eeeeee 99%); 145 | background-image: -moz-linear-gradient(center bottom, white 85%, #eeeeee 99%); 146 | background-image: -o-linear-gradient(bottom, white 85%, #eeeeee 99%); 147 | background-image: -ms-linear-gradient(top, #ffffff 85%,#eeeeee 99%); 148 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#eeeeee',GradientType=0 ); 149 | background-image: linear-gradient(top, #ffffff 85%,#eeeeee 99%); 150 | border: 1px solid #aaa; 151 | margin: 0; 152 | padding: 0; 153 | cursor: text; 154 | overflow: hidden; 155 | height: auto !important; 156 | height: 1%; 157 | position: relative; 158 | } 159 | .chzn-container-multi .chzn-choices li { 160 | float: left; 161 | list-style: none; 162 | } 163 | .chzn-container-multi .chzn-choices .search-field { 164 | white-space: nowrap; 165 | margin: 0; 166 | padding: 0; 167 | } 168 | .chzn-container-multi .chzn-choices .search-field input { 169 | color: #666; 170 | background: transparent !important; 171 | border: 0 !important; 172 | padding: 5px; 173 | margin: 1px 0; 174 | outline: 0; 175 | -webkit-box-shadow: none; 176 | -moz-box-shadow : none; 177 | -o-box-shadow : none; 178 | box-shadow : none; 179 | } 180 | .chzn-container-multi .chzn-choices .search-field .default { 181 | color: #999; 182 | } 183 | .chzn-container-multi .chzn-choices .search-choice { 184 | -webkit-border-radius: 3px; 185 | -moz-border-radius : 3px; 186 | border-radius : 3px; 187 | -moz-background-clip : padding; 188 | -webkit-background-clip: padding-box; 189 | background-clip : padding-box; 190 | background-color: #e4e4e4; 191 | background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #e4e4e4), color-stop(0.7, #eeeeee)); 192 | background-image: -webkit-linear-gradient(center bottom, #e4e4e4 0%, #eeeeee 70%); 193 | background-image: -moz-linear-gradient(center bottom, #e4e4e4 0%, #eeeeee 70%); 194 | background-image: -o-linear-gradient(bottom, #e4e4e4 0%, #eeeeee 70%); 195 | background-image: -ms-linear-gradient(top, #e4e4e4 0%,#eeeeee 70%); 196 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#e4e4e4', endColorstr='#eeeeee',GradientType=0 ); 197 | background-image: linear-gradient(top, #e4e4e4 0%,#eeeeee 70%); 198 | color: #333; 199 | border: 1px solid #b4b4b4; 200 | line-height: 13px; 201 | padding: 3px 19px 3px 6px; 202 | margin: 3px 0 3px 5px; 203 | position: relative; 204 | } 205 | .chzn-container-multi .chzn-choices .search-choice span { 206 | cursor: default; 207 | } 208 | .chzn-container-multi .chzn-choices .search-choice-focus { 209 | background: #d4d4d4; 210 | } 211 | .chzn-container-multi .chzn-choices .search-choice .search-choice-close { 212 | display: block; 213 | position: absolute; 214 | right: 3px; 215 | top: 4px; 216 | width: 12px; 217 | height: 13px; 218 | font-size: 1px; 219 | background: url(chosen-sprite.png) right top no-repeat; 220 | } 221 | .chzn-container-multi .chzn-choices .search-choice .search-choice-close:hover { 222 | background-position: right -11px; 223 | } 224 | .chzn-container-multi .chzn-choices .search-choice-focus .search-choice-close { 225 | background-position: right -11px; 226 | } 227 | /* @end */ 228 | 229 | /* @group Results */ 230 | .chzn-container .chzn-results { 231 | margin: 0 4px 4px 0; 232 | max-height: 190px; 233 | padding: 0 0 0 4px; 234 | position: relative; 235 | overflow-x: hidden; 236 | overflow-y: auto; 237 | } 238 | .chzn-container-multi .chzn-results { 239 | margin: -1px 0 0; 240 | padding: 0; 241 | } 242 | .chzn-container .chzn-results li { 243 | display: none; 244 | line-height: 80%; 245 | padding: 7px 7px 8px; 246 | margin: 0; 247 | list-style: none; 248 | } 249 | .chzn-container .chzn-results .active-result { 250 | cursor: pointer; 251 | display: list-item; 252 | } 253 | .chzn-container .chzn-results .highlighted { 254 | background: #3875d7; 255 | color: #fff; 256 | } 257 | .chzn-container .chzn-results li em { 258 | background: #feffde; 259 | font-style: normal; 260 | } 261 | .chzn-container .chzn-results .highlighted em { 262 | background: transparent; 263 | } 264 | .chzn-container .chzn-results .no-results { 265 | background: #f4f4f4; 266 | display: list-item; 267 | } 268 | .chzn-container .chzn-results .group-result { 269 | cursor: default; 270 | color: #999; 271 | font-weight: bold; 272 | } 273 | .chzn-container .chzn-results .group-option { 274 | padding-left: 20px; 275 | } 276 | .chzn-container-multi .chzn-drop .result-selected { 277 | display: none; 278 | } 279 | /* @end */ 280 | 281 | /* @group Active */ 282 | .chzn-container-active .chzn-single { 283 | -webkit-box-shadow: 0 0 5px rgba(0,0,0,.3); 284 | -moz-box-shadow : 0 0 5px rgba(0,0,0,.3); 285 | -o-box-shadow : 0 0 5px rgba(0,0,0,.3); 286 | box-shadow : 0 0 5px rgba(0,0,0,.3); 287 | border: 1px solid #5897fb; 288 | } 289 | .chzn-container-active .chzn-single-with-drop { 290 | border: 1px solid #aaa; 291 | -webkit-box-shadow: 0 1px 0 #fff inset; 292 | -moz-box-shadow : 0 1px 0 #fff inset; 293 | -o-box-shadow : 0 1px 0 #fff inset; 294 | box-shadow : 0 1px 0 #fff inset; 295 | background-color: #eee; 296 | background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, white), color-stop(0.5, #eeeeee)); 297 | background-image: -webkit-linear-gradient(center bottom, white 0%, #eeeeee 50%); 298 | background-image: -moz-linear-gradient(center bottom, white 0%, #eeeeee 50%); 299 | background-image: -o-linear-gradient(bottom, white 0%, #eeeeee 50%); 300 | background-image: -ms-linear-gradient(top, #ffffff 0%,#eeeeee 50%); 301 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#eeeeee',GradientType=0 ); 302 | background-image: linear-gradient(top, #ffffff 0%,#eeeeee 50%); 303 | -webkit-border-bottom-left-radius : 0; 304 | -webkit-border-bottom-right-radius: 0; 305 | -moz-border-radius-bottomleft : 0; 306 | -moz-border-radius-bottomright: 0; 307 | border-bottom-left-radius : 0; 308 | border-bottom-right-radius: 0; 309 | } 310 | .chzn-container-active .chzn-single-with-drop div { 311 | background: transparent; 312 | border-left: none; 313 | } 314 | .chzn-container-active .chzn-single-with-drop div b { 315 | background-position: -18px 1px; 316 | } 317 | .chzn-container-active .chzn-choices { 318 | -webkit-box-shadow: 0 0 5px rgba(0,0,0,.3); 319 | -moz-box-shadow : 0 0 5px rgba(0,0,0,.3); 320 | -o-box-shadow : 0 0 5px rgba(0,0,0,.3); 321 | box-shadow : 0 0 5px rgba(0,0,0,.3); 322 | border: 1px solid #5897fb; 323 | } 324 | .chzn-container-active .chzn-choices .search-field input { 325 | color: #111 !important; 326 | } 327 | /* @end */ 328 | 329 | /* @group Disabled Support */ 330 | .chzn-disabled { 331 | cursor: default; 332 | opacity:0.5 !important; 333 | } 334 | .chzn-disabled .chzn-single { 335 | cursor: default; 336 | } 337 | .chzn-disabled .chzn-choices .search-choice .search-choice-close { 338 | cursor: default; 339 | } 340 | 341 | /* @group Right to Left */ 342 | .chzn-rtl { direction:rtl;text-align: right; } 343 | .chzn-rtl .chzn-single { padding-left: 0; padding-right: 8px; } 344 | .chzn-rtl .chzn-single span { margin-left: 26px; margin-right: 0; } 345 | .chzn-rtl .chzn-single div { 346 | left: 0; right: auto; 347 | border-left: none; border-right: 1px solid #aaaaaa; 348 | -webkit-border-radius: 4px 0 0 4px; 349 | -moz-border-radius : 4px 0 0 4px; 350 | border-radius : 4px 0 0 4px; 351 | } 352 | .chzn-rtl .chzn-choices li { float: right; } 353 | .chzn-rtl .chzn-choices .search-choice { padding: 3px 6px 3px 19px; margin: 3px 5px 3px 0; } 354 | .chzn-rtl .chzn-choices .search-choice .search-choice-close { left: 5px; right: auto; background-position: right top;} 355 | .chzn-rtl.chzn-container-single .chzn-results { margin-left: 4px; margin-right: 0; padding-left: 0; padding-right: 4px; } 356 | .chzn-rtl .chzn-results .group-option { padding-left: 0; padding-right: 20px; } 357 | .chzn-rtl.chzn-container-active .chzn-single-with-drop div { border-right: none; } 358 | .chzn-rtl .chzn-search input { 359 | background: url('chosen-sprite.png') no-repeat -38px -22px, #ffffff; 360 | background: url('chosen-sprite.png') no-repeat -38px -22px, -webkit-gradient(linear, left bottom, left top, color-stop(0.85, white), color-stop(0.99, #eeeeee)); 361 | background: url('chosen-sprite.png') no-repeat -38px -22px, -webkit-linear-gradient(center bottom, white 85%, #eeeeee 99%); 362 | background: url('chosen-sprite.png') no-repeat -38px -22px, -moz-linear-gradient(center bottom, white 85%, #eeeeee 99%); 363 | background: url('chosen-sprite.png') no-repeat -38px -22px, -o-linear-gradient(bottom, white 85%, #eeeeee 99%); 364 | background: url('chosen-sprite.png') no-repeat -38px -22px, -ms-linear-gradient(top, #ffffff 85%,#eeeeee 99%); 365 | background: url('chosen-sprite.png') no-repeat -38px -22px, -ms-linear-gradient(top, #ffffff 85%,#eeeeee 99%); 366 | background: url('chosen-sprite.png') no-repeat -38px -22px, linear-gradient(top, #ffffff 85%,#eeeeee 99%); 367 | padding: 4px 5px 4px 20px; 368 | } 369 | /* @end */ 370 | -------------------------------------------------------------------------------- /flask_admin/static/css/jquery-ui/jquery.crossSelect.css: -------------------------------------------------------------------------------- 1 | /* ruler for measuring item width should be invisible */ 2 | .jqxs span 3 | { 4 | visibility:none; 5 | /* position:absolute; */ 6 | } 7 | 8 | .jqxs ul, 9 | .jqxs div 10 | { 11 | float:left; 12 | margin:4px; 13 | padding:0; 14 | background:#ffffff; 15 | } 16 | 17 | 18 | div.jqxs 19 | { 20 | float: both; 21 | } 22 | 23 | /* ie6 fix for float margin bug */ 24 | * html .jqxs ul, 25 | * html .jqxs div 26 | { 27 | margin:4px 2px; 28 | } 29 | 30 | .jqxs ul 31 | { 32 | border-top:2px solid gray; 33 | border-left:2px solid gray; 34 | border-bottom:2px solid silver; 35 | border-right:2px solid silver; 36 | overflow: auto; 37 | } 38 | 39 | .jqxs ul, 40 | .jqxs li 41 | { 42 | padding:0; 43 | list-style:none; 44 | text-indent:0; 45 | } 46 | 47 | .jqxs li 48 | { 49 | padding: 0 2px; 50 | color:black; 51 | background:white; 52 | margin:0; 53 | cursor: pointer; 54 | overflow:hidden; 55 | } 56 | 57 | .jqxs li span 58 | { 59 | display:none; 60 | } 61 | 62 | .jqxs_optionsList li.jqxs_selected 63 | { 64 | display:none; 65 | } 66 | 67 | .jqxs li.jqxs_focused 68 | { 69 | color:white; 70 | background:navy; 71 | } 72 | 73 | .jqxs input 74 | { 75 | width:90px; 76 | display:block; 77 | } 78 | 79 | /* not used in default style, but useful for applying custom button designs */ 80 | .jqxs input.jqxs_active 81 | { 82 | 83 | } -------------------------------------------------------------------------------- /flask_admin/static/css/jquery-ui/jquery.crossSelect.custom.css: -------------------------------------------------------------------------------- 1 | /* ruler for measuring item width should be invisible */ 2 | .jqxs span 3 | { 4 | visibility:none; 5 | /* position:absolute; */ 6 | } 7 | 8 | .jqxs ul, 9 | .jqxs div 10 | { 11 | float:left; 12 | margin:4px; 13 | padding:0; 14 | background:#ffffff; 15 | } 16 | 17 | 18 | div.jqxs 19 | { 20 | overflow: auto; 21 | } 22 | 23 | /* ie6 fix for float margin bug */ 24 | * html .jqxs ul, 25 | * html .jqxs div 26 | { 27 | margin:4px 2px; 28 | } 29 | 30 | .jqxs ul 31 | { 32 | border-top:2px solid gray; 33 | border-left:2px solid gray; 34 | border-bottom:2px solid silver; 35 | border-right:2px solid silver; 36 | overflow: auto; 37 | } 38 | 39 | .jqxs ul, 40 | .jqxs li 41 | { 42 | padding:0; 43 | list-style:none; 44 | text-indent:0; 45 | } 46 | 47 | .jqxs li 48 | { 49 | padding: 0 2px; 50 | color:black; 51 | background:white; 52 | margin:0; 53 | cursor: pointer; 54 | overflow:hidden; 55 | } 56 | 57 | .jqxs li span 58 | { 59 | display:none; 60 | } 61 | 62 | .jqxs_optionsList li.jqxs_selected 63 | { 64 | display:none; 65 | } 66 | 67 | .jqxs li.jqxs_focused 68 | { 69 | color:white; 70 | background:navy; 71 | } 72 | 73 | .jqxs input 74 | { 75 | width:90px; 76 | display:block; 77 | } 78 | 79 | /* not used in default style, but useful for applying custom button designs */ 80 | .jqxs input.jqxs_active 81 | { 82 | 83 | } -------------------------------------------------------------------------------- /flask_admin/static/css/style.css: -------------------------------------------------------------------------------- 1 | /* 2 | * timepicker css 3 | * see copyright notice and licensing terms in: 4 | * ../js/libs/jquery-ui-timepicker-addon.js 5 | */ 6 | .ui-timepicker-div .ui-widget-header{ margin-bottom: 8px; } 7 | .ui-timepicker-div dl{ text-align: left; } 8 | .ui-timepicker-div dl dt{ height: 25px; } 9 | .ui-timepicker-div dl dd{ margin: -25px 10px 10px 65px; } 10 | .ui-timepicker-div td { font-size: 90%; } 11 | 12 | 13 | 14 | 15 | /** 16 | * Primary styles 17 | */ 18 | 19 | body { 20 | padding-top: 60px; 21 | } 22 | 23 | header h3 { 24 | font-size: 2em; 25 | font-family: 'Vollkorn', 'Georgia', serif; 26 | font-weight: normal; 27 | } 28 | 29 | #list-table tr.listed-highlight td { 30 | background-color: #fffbde; 31 | } 32 | 33 | #list-table td a.edit-link { 34 | text-decoration:none; 35 | display:block; 36 | padding:0px; 37 | height:100%; 38 | } 39 | 40 | #list-table a.delete-link i { 41 | border-radius:10px; 42 | -moz-border-radius:10px; 43 | -webkit-border-radius:10px; 44 | -khtml-border-radius:10px; 45 | } 46 | 47 | #list-table a.delete-link i:hover { 48 | background-color: #444; 49 | background-image: url(../img/glyphicons-halflings-white.png); 50 | } 51 | 52 | -------------------------------------------------------------------------------- /flask_admin/static/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wilsaj/flask-admin-old/aab2fe94e0641932ebd1c8f8dc500ba2daf5731c/flask_admin/static/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /flask_admin/static/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wilsaj/flask-admin-old/aab2fe94e0641932ebd1c8f8dc500ba2daf5731c/flask_admin/static/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /flask_admin/static/js/admin.js: -------------------------------------------------------------------------------- 1 | // set up jQuery UI widgets 2 | $(function(){ 3 | $('.ui-widget').hover( 4 | function() { $(this).addClass('ui-state-hover'); }, 5 | function() { $(this).removeClass('ui-state-hover'); } 6 | ); 7 | 8 | $('input.datepicker').datepicker({ 9 | dateFormat: 'yy-mm-dd' 10 | }); 11 | 12 | $('input.datetimepicker').datetimepicker({ 13 | showSecond: true, 14 | dateFormat: 'yy-mm-dd', 15 | timeFormat: 'hh:mm:ss' 16 | }); 17 | 18 | $('input.timepicker').timepicker({ 19 | timeFormat: 'hh:mm:ss', 20 | showSecond: true 21 | }); 22 | 23 | function getLabelFor(id){ 24 | return $('label[for="'+id+'"]').text(); 25 | }; 26 | 27 | $('.edit-form select:empty') 28 | .append(''); 29 | 30 | $('.edit-form select > option[value="__None"]:only-child').parent() 31 | .attr('disabled', 'disabled') 32 | .attr('data-placeholder', ( 33 | function(index, attr){ 34 | if (!attr){ 35 | return 'No '+getLabelFor(this.id)+' available to choose'; 36 | } 37 | })); 38 | 39 | $('.edit-form select') 40 | .attr('data-placeholder', ( 41 | function(index, attr){ 42 | if (!attr){ 43 | return 'Choose a '+getLabelFor(this.id)+'...'; 44 | } 45 | })) 46 | .chosen({no_results_text: "No results matched", 47 | allow_single_deselect: true}); 48 | 49 | $('tr.listed').on('click', function(){ 50 | window.location = $(this).find('a.edit-link').attr('href'); 51 | }); 52 | 53 | $('tr.listed').on('mouseover', function(){ 54 | $(this).toggleClass('listed-highlight'); 55 | }); 56 | 57 | $('tr.listed').on('mouseout', function(){ 58 | $(this).toggleClass('listed-highlight'); 59 | }); 60 | 61 | }); -------------------------------------------------------------------------------- /flask_admin/static/js/libs/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | !function(a){a(function(){"use strict",a.support.transition=function(){var b=document.body||document.documentElement,c=b.style,d=c.transition!==undefined||c.WebkitTransition!==undefined||c.MozTransition!==undefined||c.MsTransition!==undefined||c.OTransition!==undefined;return d&&{end:function(){var b="TransitionEnd";return a.browser.webkit?b="webkitTransitionEnd":a.browser.mozilla?b="transitionend":a.browser.opera&&(b="oTransitionEnd"),b}()}}()})}(window.jQuery),!function(a){"use strict";var b='[data-dismiss="alert"]',c=function(c){a(c).on("click",b,this.close)};c.prototype={constructor:c,close:function(b){function f(){e.remove(),e.trigger("closed")}var c=a(this),d=c.attr("data-target"),e;d||(d=c.attr("href"),d=d&&d.replace(/.*(?=#[^\s]*$)/,"")),e=a(d),e.trigger("close"),b&&b.preventDefault(),e.length||(e=c.hasClass("alert")?c:c.parent()),e.removeClass("in"),a.support.transition&&e.hasClass("fade")?e.on(a.support.transition.end,f):f()}},a.fn.alert=function(b){return this.each(function(){var d=a(this),e=d.data("alert");e||d.data("alert",e=new c(this)),typeof b=="string"&&e[b].call(d)})},a.fn.alert.Constructor=c,a(function(){a("body").on("click.alert.data-api",b,c.prototype.close)})}(window.jQuery),!function(a){"use strict";var b=function(b,c){this.$element=a(b),this.options=a.extend({},a.fn.button.defaults,c)};b.prototype={constructor:b,setState:function(a){var b="disabled",c=this.$element,d=c.data(),e=c.is("input")?"val":"html";a+="Text",d.resetText||c.data("resetText",c[e]()),c[e](d[a]||this.options[a]),setTimeout(function(){a=="loadingText"?c.addClass(b).attr(b,b):c.removeClass(b).removeAttr(b)},0)},toggle:function(){var a=this.$element.parent('[data-toggle="buttons-radio"]');a&&a.find(".active").removeClass("active"),this.$element.toggleClass("active")}},a.fn.button=function(c){return this.each(function(){var d=a(this),e=d.data("button"),f=typeof c=="object"&&c;e||d.data("button",e=new b(this,f)),c=="toggle"?e.toggle():c&&e.setState(c)})},a.fn.button.defaults={loadingText:"loading..."},a.fn.button.Constructor=b,a(function(){a("body").on("click.button.data-api","[data-toggle^=button]",function(b){a(b.target).button("toggle")})})}(window.jQuery),!function(a){"use strict";var b=function(b,c){this.$element=a(b),this.options=a.extend({},a.fn.carousel.defaults,c),this.options.slide&&this.slide(this.options.slide)};b.prototype={cycle:function(){return this.interval=setInterval(a.proxy(this.next,this),this.options.interval),this},to:function(b){var c=this.$element.find(".active"),d=c.parent().children(),e=d.index(c),f=this;if(b>d.length-1||b<0)return;return this.sliding?this.$element.one("slid",function(){f.to(b)}):e==b?this.pause().cycle():this.slide(b>e?"next":"prev",a(d[b]))},pause:function(){return clearInterval(this.interval),this},next:function(){if(this.sliding)return;return this.slide("next")},prev:function(){if(this.sliding)return;return this.slide("prev")},slide:function(b,c){var d=this.$element.find(".active"),e=c||d[b](),f=this.interval,g=b=="next"?"left":"right",h=b=="next"?"first":"last",i=this;return this.sliding=!0,f&&this.pause(),e=e.length?e:this.$element.find(".item")[h](),!a.support.transition&&this.$element.hasClass("slide")?(this.$element.trigger("slide"),d.removeClass("active"),e.addClass("active"),this.sliding=!1,this.$element.trigger("slid")):(e.addClass(b),e[0].offsetWidth,d.addClass(g),e.addClass(g),this.$element.trigger("slide"),this.$element.one(a.support.transition.end,function(){e.removeClass([b,g].join(" ")).addClass("active"),d.removeClass(["active",g].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger("slid")},0)})),f&&this.cycle(),this}},a.fn.carousel=function(c){return this.each(function(){var d=a(this),e=d.data("carousel"),f=typeof c=="object"&&c;e||d.data("carousel",e=new b(this,f)),typeof c=="number"?e.to(c):typeof c=="string"||(c=f.slide)?e[c]():e.cycle()})},a.fn.carousel.defaults={interval:5e3},a.fn.carousel.Constructor=b,a(function(){a("body").on("click.carousel.data-api","[data-slide]",function(b){var c=a(this),d,e=a(c.attr("data-target")||(d=c.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,"")),f=!e.data("modal")&&a.extend({},e.data(),c.data());e.carousel(f),b.preventDefault()})})}(window.jQuery),!function(a){"use strict";var b=function(b,c){this.$element=a(b),this.options=a.extend({},a.fn.collapse.defaults,c),this.options.parent&&(this.$parent=a(this.options.parent)),this.options.toggle&&this.toggle()};b.prototype={constructor:b,dimension:function(){var a=this.$element.hasClass("width");return a?"width":"height"},show:function(){var b=this.dimension(),c=a.camelCase(["scroll",b].join("-")),d=this.$parent&&this.$parent.find(".in"),e;d&&d.length&&(e=d.data("collapse"),d.collapse("hide"),e||d.data("collapse",null)),this.$element[b](0),this.transition("addClass","show","shown"),this.$element[b](this.$element[0][c])},hide:function(){var a=this.dimension();this.reset(this.$element[a]()),this.transition("removeClass","hide","hidden"),this.$element[a](0)},reset:function(a){var b=this.dimension();this.$element.removeClass("collapse")[b](a||"auto")[0].offsetWidth,this.$element.addClass("collapse")},transition:function(b,c,d){var e=this,f=function(){c=="show"&&e.reset(),e.$element.trigger(d)};this.$element.trigger(c)[b]("in"),a.support.transition&&this.$element.hasClass("collapse")?this.$element.one(a.support.transition.end,f):f()},toggle:function(){this[this.$element.hasClass("in")?"hide":"show"]()}},a.fn.collapse=function(c){return this.each(function(){var d=a(this),e=d.data("collapse"),f=typeof c=="object"&&c;e||d.data("collapse",e=new b(this,f)),typeof c=="string"&&e[c]()})},a.fn.collapse.defaults={toggle:!0},a.fn.collapse.Constructor=b,a(function(){a("body").on("click.collapse.data-api","[data-toggle=collapse]",function(b){var c=a(this),d,e=c.attr("data-target")||b.preventDefault()||(d=c.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""),f=a(e).data("collapse")?"toggle":c.data();a(e).collapse(f)})})}(window.jQuery),!function(a){function d(){a(b).parent().removeClass("open")}"use strict";var b='[data-toggle="dropdown"]',c=function(b){var c=a(b).on("click.dropdown.data-api",this.toggle);a("html").on("click.dropdown.data-api",function(){c.parent().removeClass("open")})};c.prototype={constructor:c,toggle:function(b){var c=a(this),e=c.attr("data-target"),f,g;return e||(e=c.attr("href"),e=e&&e.replace(/.*(?=#[^\s]*$)/,"")),f=a(e),f.length||(f=c.parent()),g=f.hasClass("open"),d(),!g&&f.toggleClass("open"),!1}},a.fn.dropdown=function(b){return this.each(function(){var d=a(this),e=d.data("dropdown");e||d.data("dropdown",e=new c(this)),typeof b=="string"&&e[b].call(d)})},a.fn.dropdown.Constructor=c,a(function(){a("html").on("click.dropdown.data-api",d),a("body").on("click.dropdown.data-api",b,c.prototype.toggle)})}(window.jQuery),!function(a){function c(){var b=this,c=setTimeout(function(){b.$element.off(a.support.transition.end),d.call(b)},500);this.$element.one(a.support.transition.end,function(){clearTimeout(c),d.call(b)})}function d(a){this.$element.hide().trigger("hidden"),e.call(this)}function e(b){var c=this,d=this.$element.hasClass("fade")?"fade":"";if(this.isShown&&this.options.backdrop){var e=a.support.transition&&d;this.$backdrop=a(' 13 | {% endmacro %} 14 | -------------------------------------------------------------------------------- /flask_admin/templates/admin/_helpers.html: -------------------------------------------------------------------------------- 1 | {% macro render_pagination(pagination, endpoint) %} 2 | 29 | {% endmacro %} 30 | -------------------------------------------------------------------------------- /flask_admin/templates/admin/_paginationhelpers.html: -------------------------------------------------------------------------------- 1 | {% macro render_pagination(pagination, endpoint) %} 2 | 31 | {% endmacro %} 32 | -------------------------------------------------------------------------------- /flask_admin/templates/admin/_statichelpers.html: -------------------------------------------------------------------------------- 1 | {%- macro static(some_file) -%} 2 | {{ url_for('.static', filename=some_file) }} 3 | {%- endmacro -%} 4 | -------------------------------------------------------------------------------- /flask_admin/templates/admin/add.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/extra_base.html" %} 2 | {% from "admin/_formhelpers.html" import render_field %} 3 | 4 | {% block title %} 5 | add new {{ model_name|lower }} 6 | {% endblock %} 7 | 8 | 9 | {% block main %} 10 | 11 |
12 |
13 | 14 | new {{ model_name|lower }} 15 | 16 | {% for field in form %} 17 | {{ render_field(field) }} 18 | {% endfor %} 19 | {{ form.csrf }} 20 |
21 | 22 | 24 |
25 |
26 |
27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /flask_admin/templates/admin/base.html: -------------------------------------------------------------------------------- 1 | {% from "admin/_statichelpers.html" import static %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | {% block title %}{% endblock title %} 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | {% block extra_head %} 32 | {% endblock extra_head %} 33 | 34 | 35 | 36 |
37 | {% block nav %} 38 | 64 | {% endblock nav %} 65 |
66 | 67 |
68 |
69 | {% if get_flashed_messages(with_categories=true) %} 70 | {% for category, msg in get_flashed_messages(with_categories=true) %} 71 |
72 | 73 | {{ msg }} 74 |
75 | {% endfor %} 76 | {% endif %} 77 | {% block main %} 78 | {% endblock main %} 79 |
80 |
81 |
82 |
83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /flask_admin/templates/admin/edit.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/extra_base.html" %} 2 | {% from "admin/_formhelpers.html" import render_field %} 3 | {% block title %} 4 | Editing {{model_name}} 5 | {% endblock %} 6 | 7 | {% block main %} 8 | 9 |
10 |
11 | 12 | edit {{ model_name | lower}} 13 | 14 | {% for field in form %} 15 | {{ render_field(field) }} 16 | {% endfor %} 17 | {{ form.csrf }} 18 |
19 | 20 | 22 |
23 |
24 |
25 | 26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /flask_admin/templates/admin/extra_base.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base.html" %} -------------------------------------------------------------------------------- /flask_admin/templates/admin/index.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/extra_base.html" %} 2 | 3 | {% block title %} 4 | Models 5 | {% endblock title %} 6 | 7 | {% block main %} 8 | {% endblock main %} 9 | -------------------------------------------------------------------------------- /flask_admin/templates/admin/list.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/extra_base.html" %} 2 | {% from "admin/_paginationhelpers.html" import render_pagination %} 3 | 4 | {%- block title -%} 5 | {{ model_name|lower }} list 6 | {%- endblock -%} 7 | 8 | {% block main %} 9 | {% if not pagination.total %} 10 |
11 |
12 |
13 | Not a single {{ model_name|lower }} was found. 14 |
15 | 20 |
21 |
22 | 23 | {% else %} 24 | 25 | {{ render_pagination(pagination, '.list', model_name=model_name) }} 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | {% for model_instance in pagination.items %} 35 | {% set model_url_key = get_model_url_key(model_instance) %} 36 | 37 | 40 | 45 | 46 | {% endfor %} 47 | 48 |
{{ model_name|lower }}delete
38 | {{ model_instance }} 39 | 41 | 42 | 43 | 44 |
49 | {{ render_pagination(pagination, '.list', model_name=model_name) }} 50 | 51 | add new {{ model_name|lower }} 52 | 53 | {% endif %} 54 | {% endblock %} 55 | -------------------------------------------------------------------------------- /flask_admin/util.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | 4 | # original source: http://flask.pocoo.org/snippets/44/ 5 | class Pagination(object): 6 | def __init__(self, page, per_page, total, items): 7 | self.page = page 8 | self.per_page = per_page 9 | self.total = total 10 | self.items = items 11 | 12 | @property 13 | def pages(self): 14 | return int(math.ceil(self.total / float(self.per_page))) 15 | 16 | @property 17 | def has_prev(self): 18 | return self.page > 1 19 | 20 | @property 21 | def has_next(self): 22 | return self.page < self.pages 23 | 24 | def iter_pages(self, left_edge=2, left_current=2, 25 | right_current=5, right_edge=2): 26 | last = 0 27 | for num in xrange(1, self.pages + 1): 28 | if num <= left_edge or \ 29 | (num > self.page - left_current - 1 and \ 30 | num < self.page + right_current) or \ 31 | num > self.pages - right_edge: 32 | if last + 1 != num: 33 | yield None 34 | yield num 35 | last = num 36 | -------------------------------------------------------------------------------- /flask_admin/wtforms.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | flask.ext.wtforms 4 | ~~~~~~~~~~~~~~ 5 | 6 | 7 | :copyright: (c) 2011 by wilsaj. 8 | :license: BSD, see LICENSE for more details. 9 | """ 10 | from __future__ import absolute_import 11 | 12 | import datetime 13 | import time 14 | 15 | from wtforms import fields as wtf_fields 16 | from wtforms import widgets, validators 17 | 18 | 19 | class TimeField(wtf_fields.Field): 20 | """A text field which stores a `time.time` matching a format.""" 21 | widget = widgets.TextInput() 22 | 23 | def __init__(self, label=None, validators=None, 24 | format='%H:%M:%S', **kwargs): 25 | super(TimeField, self).__init__(label, validators, **kwargs) 26 | self.format = format 27 | 28 | def _value(self): 29 | if self.raw_data: 30 | return u' '.join(self.raw_data) 31 | else: 32 | return self.data and self.data.strftime(self.format) or u'' 33 | 34 | def process_formdata(self, valuelist): 35 | if valuelist: 36 | time_str = u' '.join(valuelist) 37 | try: 38 | timetuple = time.strptime(time_str, self.format) 39 | self.data = datetime.time(*timetuple[3:6]) 40 | except ValueError: 41 | self.data = None 42 | raise 43 | 44 | 45 | class DatePickerWidget(widgets.TextInput): 46 | """ 47 | TextInput widget that adds a 'datepicker' class to the html input 48 | element; this makes it easy to write a jQuery selector that adds a 49 | UI widget for date picking. 50 | """ 51 | def __call__(self, field, **kwargs): 52 | c = kwargs.pop('class', '') or kwargs.pop('class_', '') 53 | kwargs['class'] = u'datepicker %s' % c 54 | return super(DatePickerWidget, self).__call__(field, **kwargs) 55 | 56 | 57 | class DateTimePickerWidget(widgets.TextInput): 58 | """TextInput widget that adds a 'datetimepicker' class to the html 59 | adds a UI widget for datetime picking. 60 | """ 61 | def __call__(self, field, **kwargs): 62 | c = kwargs.pop('class', '') or kwargs.pop('class_', '') 63 | kwargs['class'] = u'datetimepicker %s' % c 64 | return super(DateTimePickerWidget, self).__call__(field, **kwargs) 65 | 66 | 67 | class TimePickerWidget(widgets.TextInput): 68 | """TextInput widget that adds a 'timepicker' class to the html 69 | input element; this makes it easy to write a jQuery selector that 70 | adds a UI widget for time picking. 71 | """ 72 | def __call__(self, field, **kwargs): 73 | c = kwargs.pop('class', '') or kwargs.pop('class_', '') 74 | kwargs['class'] = u'timepicker %s' % c 75 | return super(TimePickerWidget, self).__call__(field, **kwargs) 76 | 77 | 78 | def has_file_field(form): 79 | """Test whether or not a form has a FileField in it. This is used 80 | to know whether or not we need to set enctype to 81 | multipart/form-data. 82 | """ 83 | for field in form: 84 | if isinstance(field, wtf_fields.FileField): 85 | return True 86 | 87 | return False 88 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [build_sphinx] 2 | source-dir = docs/ 3 | build-dir = docs/build 4 | all_files = 1 5 | 6 | [upload_sphinx] 7 | upload-dir = docs/build/html -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Flask-Admin 3 | ----------- 4 | 5 | Flask-Admin is a Flask extension module that aims to be a flexible, 6 | customizable web-based interface to your datastore. 7 | 8 | Links 9 | ````` 10 | 11 | * `documentation `_ 12 | * `development version 13 | `_ 14 | 15 | """ 16 | from setuptools import setup 17 | 18 | 19 | setup( 20 | name='Flask-Admin', 21 | version='0.4.2', 22 | url='https://github.com/wilsaj/flask-admin/', 23 | license='BSD', 24 | author='Andy Wilson', 25 | author_email='wilson.andrew.j@gmail.com', 26 | description='Flask extenstion module that provides an admin interface', 27 | long_description=__doc__, 28 | packages=['flask_admin'], 29 | include_package_data=True, 30 | zip_safe=False, 31 | platforms='any', 32 | install_requires=[ 33 | 'Flask>=0.7', 34 | 'wtforms>=0.6.3', 35 | ], 36 | test_suite='test_admin.suite', 37 | tests_require=[ 38 | 'Flask-SQLAlchemy>=0.12', 39 | 'Flask-Testing>=0.3', 40 | 'MongoAlchemy>=0.10', 41 | ], 42 | classifiers=[ 43 | 'Development Status :: 4 - Beta', 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 :: Internet :: WWW/HTTP :: Dynamic Content', 50 | 'Topic :: Software Development :: Libraries :: Python Modules' 51 | ] 52 | ) 53 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wilsaj/flask-admin-old/aab2fe94e0641932ebd1c8f8dc500ba2daf5731c/test/__init__.py -------------------------------------------------------------------------------- /test/custom_form.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from flask import Flask, redirect 4 | from flask.ext import admin 5 | from flask.ext.admin.datastore.sqlalchemy import SQLAlchemyDatastore 6 | from sqlalchemy import create_engine 7 | from sqlalchemy.orm import scoped_session, sessionmaker 8 | from sqlalchemy.ext.declarative import declarative_base 9 | from sqlalchemy import Boolean, Column, Integer, Text, String, Float, Time 10 | from sqlalchemy.orm import synonym 11 | from werkzeug import check_password_hash, generate_password_hash 12 | from wtforms import Form, validators 13 | from wtforms.fields import BooleanField, TextField, PasswordField 14 | 15 | 16 | Base = declarative_base() 17 | 18 | 19 | class User(Base): 20 | __tablename__ = 'user' 21 | 22 | id = Column(Integer, primary_key=True) 23 | username = Column(String(80), unique=True) 24 | _password_hash = Column('password', String(80), nullable=False) 25 | is_active = Column(Boolean, default=True) 26 | 27 | def __init__(self, username="", password="", is_active=True): 28 | self.username = username 29 | self.password = password 30 | self.is_active = is_active 31 | 32 | # use 2.5-compatible properties 33 | def check_password(self, password): 34 | return check_password_hash(self.pw_hash, password) 35 | 36 | def get_password(self): 37 | return self._password_hash 38 | 39 | def set_password(self, value): 40 | self._password = value 41 | 42 | password = property(get_password, set_password, None, "password property") 43 | 44 | # @property 45 | # def password(self): 46 | # return self._password_hash 47 | 48 | # @password.setter 49 | # def password(self, password): 50 | # self._password_hash = generate_password_hash(password) 51 | 52 | password = synonym('_password_hash', descriptor=password) 53 | 54 | def __repr__(self): 55 | return self.username 56 | 57 | __mapper_args__ = { 58 | 'order_by': username 59 | } 60 | 61 | 62 | class UserForm(Form): 63 | """ 64 | Form for creating or editting User object (via the admin). Define 65 | any handling of fields here. This form class also has precedence 66 | when rendering forms to a webpage, so the model-generated fields 67 | will come after it. 68 | """ 69 | username = TextField(u'User name', 70 | [validators.required(), validators.length(max=80)]) 71 | password = PasswordField('Change Password', 72 | [validators.optional(), 73 | validators.equal_to('confirm_password')]) 74 | confirm_password = PasswordField() 75 | is_active = BooleanField(default=True) 76 | 77 | 78 | def create_app(database_uri='sqlite://'): 79 | app = Flask(__name__) 80 | app.config['SECRET_KEY'] = 'not secure' 81 | engine = create_engine(database_uri, convert_unicode=True) 82 | db_session = scoped_session(sessionmaker( 83 | autocommit=False, autoflush=False, 84 | bind=engine)) 85 | datastore = SQLAlchemyDatastore( 86 | (User,), db_session, model_forms={'User': UserForm}) 87 | admin_blueprint = admin.create_admin_blueprint( 88 | datastore) 89 | app.register_blueprint(admin_blueprint, url_prefix='/admin') 90 | Base.metadata.create_all(bind=engine) 91 | 92 | @app.route('/') 93 | def go_to_admin(): 94 | return redirect('/admin') 95 | 96 | return app 97 | 98 | 99 | if __name__ == '__main__': 100 | app = create_app('sqlite:///simple.db') 101 | app.run(debug=True) 102 | -------------------------------------------------------------------------------- /test/deprecation.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from flask import Flask, redirect 4 | from flask.ext import admin 5 | from sqlalchemy import create_engine, Table 6 | from sqlalchemy.orm import scoped_session, sessionmaker 7 | from sqlalchemy.ext.declarative import declarative_base 8 | from sqlalchemy import Column, Integer, String, Time 9 | from sqlalchemy.orm import relationship 10 | from sqlalchemy.schema import ForeignKey 11 | import wtforms as wtf 12 | 13 | Base = declarative_base() 14 | 15 | 16 | # ---------------------------------------------------------------------- 17 | # Models 18 | # ---------------------------------------------------------------------- 19 | class TestModel(Base): 20 | __tablename__ = 'test' 21 | 22 | id = Column(Integer, primary_key=True) 23 | 24 | 25 | def create_app(database_uri='sqlite://'): 26 | app = Flask(__name__) 27 | app.config['SECRET_KEY'] = 'not secure' 28 | engine = create_engine(database_uri, convert_unicode=True) 29 | app.db_session = scoped_session(sessionmaker( 30 | autocommit=False, autoflush=False, 31 | bind=engine)) 32 | admin_blueprint = admin.create_admin_blueprint( 33 | (TestModel,), app.db_session) 34 | app.register_blueprint(admin_blueprint, url_prefix='/admin') 35 | Base.metadata.create_all(bind=engine) 36 | 37 | @app.route('/') 38 | def go_to_admin(): 39 | return redirect('/admin') 40 | 41 | return app 42 | 43 | 44 | if __name__ == '__main__': 45 | app = create_app('sqlite://') 46 | app.run(debug=True) 47 | -------------------------------------------------------------------------------- /test/filefield.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from flask import Flask, redirect 4 | from flask.ext import admin 5 | from flask.ext.admin.datastore.sqlalchemy import SQLAlchemyDatastore 6 | from sqlalchemy import create_engine, Table 7 | from sqlalchemy.orm import scoped_session, sessionmaker 8 | from sqlalchemy.ext.declarative import declarative_base 9 | from sqlalchemy import Column, Integer, String, Time 10 | from sqlalchemy.orm import relationship 11 | from sqlalchemy.schema import ForeignKey 12 | import wtforms as wtf 13 | 14 | Base = declarative_base() 15 | 16 | 17 | # ---------------------------------------------------------------------- 18 | # Models 19 | # ---------------------------------------------------------------------- 20 | class TestModel(Base): 21 | __tablename__ = 'test' 22 | 23 | id = Column(Integer, primary_key=True) 24 | 25 | 26 | class FileForm(wtf.Form): 27 | """ 28 | A form with a filefield on it. 29 | """ 30 | filefield = wtf.fields.FileField() 31 | 32 | 33 | def create_app(database_uri='sqlite://'): 34 | app = Flask(__name__) 35 | app.config['SECRET_KEY'] = 'not secure' 36 | engine = create_engine(database_uri, convert_unicode=True) 37 | app.db_session = scoped_session(sessionmaker( 38 | autocommit=False, autoflush=False, 39 | bind=engine)) 40 | datastore = SQLAlchemyDatastore( 41 | (TestModel,), app.db_session, model_forms={'TestModel': FileForm}) 42 | admin_blueprint = admin.create_admin_blueprint(datastore) 43 | app.register_blueprint(admin_blueprint, url_prefix='/admin') 44 | Base.metadata.create_all(bind=engine) 45 | 46 | @app.route('/') 47 | def go_to_admin(): 48 | return redirect('/admin') 49 | 50 | return app 51 | 52 | 53 | if __name__ == '__main__': 54 | app = create_app('sqlite://') 55 | app.run(debug=True) 56 | -------------------------------------------------------------------------------- /test/mongoalchemy_datastore.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import absolute_import 3 | 4 | from unittest import TestCase 5 | from mongoalchemy import fields as ma_fields 6 | from mongoalchemy.document import Document 7 | from flask.ext.admin.datastore.mongoalchemy import model_form 8 | from wtforms import fields as wtf_fields 9 | from wtforms.form import Form 10 | 11 | 12 | def assert_convert(ma_field, wtf_field): 13 | """helper function for testing that MongoAlchemy fields get 14 | converted to proper wtforms fields 15 | """ 16 | class TestModel(Document): 17 | required_field = ma_field(required=True) 18 | not_required_field = ma_field(required=False) 19 | 20 | form = model_form(TestModel) 21 | 22 | assert form.required_field.field_class == wtf_field 23 | 24 | not_required_validator_names = [ 25 | validator.__class__.__name__ 26 | for validator in form.not_required_field.kwargs['validators']] 27 | 28 | assert 'Optional' in not_required_validator_names 29 | 30 | 31 | def assert_min_max_number_range(ma_field): 32 | class TestModel(Document): 33 | min_field = ma_field(min_value=5) 34 | max_field = ma_field(max_value=19) 35 | min_max_field = ma_field(min_value=11, max_value=21) 36 | 37 | form = model_form(TestModel) 38 | min_validator = [ 39 | validator for validator in form.min_field.kwargs['validators'] 40 | if validator.__class__.__name__ == 'NumberRange'][0] 41 | max_validator = [ 42 | validator for validator in form.max_field.kwargs['validators'] 43 | if validator.__class__.__name__ == 'NumberRange'][0] 44 | min_max_validator = [ 45 | validator for validator in form.min_max_field.kwargs['validators'] 46 | if validator.__class__.__name__ == 'NumberRange'][0] 47 | 48 | assert min_validator.min == 5 49 | assert not min_validator.max 50 | assert max_validator.max == 19 51 | assert not max_validator.min 52 | assert min_max_validator.min == 11 53 | assert min_max_validator.max == 21 54 | 55 | 56 | def assert_min_max_length(ma_field): 57 | class TestModel(Document): 58 | min_field = ma_field(min_length=5) 59 | max_field = ma_field(max_length=19) 60 | min_max_field = ma_field(min_length=11, max_length=21) 61 | 62 | form = model_form(TestModel) 63 | min_validator = [ 64 | validator for validator in form.min_field.kwargs['validators'] 65 | if validator.__class__.__name__ == 'Length'][0] 66 | max_validator = [ 67 | validator for validator in form.max_field.kwargs['validators'] 68 | if validator.__class__.__name__ == 'Length'][0] 69 | min_max_validator = [ 70 | validator for validator in form.min_max_field.kwargs['validators'] 71 | if validator.__class__.__name__ == 'Length'][0] 72 | 73 | assert min_validator.min == 5 74 | assert min_validator.max == -1 75 | assert max_validator.max == 19 76 | assert max_validator.min == -1 77 | assert min_max_validator.min == 11 78 | assert min_max_validator.max == 21 79 | 80 | 81 | class ConversionTest(TestCase): 82 | def test_bool_field_conversion(self): 83 | assert_convert(ma_fields.BoolField, wtf_fields.BooleanField) 84 | 85 | def test_datetime_field_conversion(self): 86 | assert_convert(ma_fields.DateTimeField, wtf_fields.DateTimeField) 87 | 88 | def test_enum_field_conversion(self): 89 | class TestModel(Document): 90 | int_field = ma_fields.EnumField(ma_fields.IntField(), 4, 6, 7) 91 | 92 | form = model_form(TestModel) 93 | assert form.int_field.field_class == wtf_fields.IntegerField 94 | assert form.int_field.kwargs['validators'][0].values == (4, 6, 7) 95 | 96 | def test_float_field_conversion(self): 97 | assert_convert(ma_fields.FloatField, wtf_fields.FloatField) 98 | assert_min_max_number_range(ma_fields.IntField) 99 | 100 | def test_int_field_conversion(self): 101 | assert_convert(ma_fields.IntField, wtf_fields.IntegerField) 102 | assert_min_max_number_range(ma_fields.IntField) 103 | 104 | def test_objectid_field_conversion(self): 105 | assert_convert(ma_fields.ObjectIdField, wtf_fields.TextField) 106 | 107 | def test_string_field_conversion(self): 108 | assert_convert(ma_fields.StringField, wtf_fields.TextField) 109 | assert_min_max_length(ma_fields.StringField) 110 | 111 | def test_tuple_field_conversion(self): 112 | class TestModel(Document): 113 | tuple_field = ma_fields.TupleField( 114 | ma_fields.IntField(), ma_fields.BoolField(), 115 | ma_fields.StringField()) 116 | 117 | unbound_form = model_form(TestModel) 118 | form = unbound_form() 119 | assert form.tuple_field.tuple_field_0.__class__ == wtf_fields.IntegerField 120 | assert form.tuple_field.tuple_field_1.__class__ == wtf_fields.BooleanField 121 | assert form.tuple_field.tuple_field_2.__class__ == wtf_fields.TextField 122 | 123 | 124 | if __name__ == '__main__': 125 | from unittest import main 126 | main() 127 | -------------------------------------------------------------------------------- /test/sqlalchemy_with_defaults.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from flask import Flask, redirect 4 | from flask.ext import admin 5 | from flask.ext.admin.datastore.sqlalchemy import SQLAlchemyDatastore 6 | from sqlalchemy import create_engine, Table 7 | from sqlalchemy.orm import scoped_session, sessionmaker 8 | from sqlalchemy.ext.declarative import declarative_base 9 | from sqlalchemy import Column, Integer, String, Numeric 10 | from sqlalchemy.orm import relationship 11 | from sqlalchemy.schema import ForeignKey 12 | import wtforms as wtf 13 | 14 | Base = declarative_base() 15 | 16 | 17 | # ---------------------------------------------------------------------- 18 | # Models 19 | # ---------------------------------------------------------------------- 20 | class TestModel(Base): 21 | __tablename__ = 'test' 22 | 23 | id = Column(Integer, primary_key=True) 24 | int_value = Column(Integer, default="2194112") 25 | str_value = Column(String, default="128uasdn1uinvuio12ioj!!@Rfja") 26 | num_value = Column(Numeric, default=22341.29) 27 | 28 | 29 | def create_app(database_uri='sqlite://'): 30 | app = Flask(__name__) 31 | app.config['SECRET_KEY'] = 'not secure' 32 | engine = create_engine(database_uri, convert_unicode=True) 33 | app.db_session = scoped_session(sessionmaker( 34 | autocommit=False, autoflush=False, bind=engine)) 35 | datastore = SQLAlchemyDatastore( 36 | (TestModel,), app.db_session) 37 | admin_blueprint = admin.create_admin_blueprint(datastore) 38 | app.register_blueprint(admin_blueprint, url_prefix='/admin') 39 | Base.metadata.create_all(bind=engine) 40 | 41 | @app.route('/') 42 | def go_to_admin(): 43 | return redirect('/admin') 44 | 45 | return app 46 | 47 | 48 | if __name__ == '__main__': 49 | app = create_app('sqlite://') 50 | app.run(debug=True) 51 | -------------------------------------------------------------------------------- /test_admin.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | 3 | from datetime import datetime 4 | import sys 5 | import unittest 6 | 7 | from flask import Flask 8 | import sqlalchemy as sa 9 | 10 | from flask.ext import admin 11 | from flask.ext.testing import TestCase 12 | 13 | sys.path.append('./example/') 14 | 15 | from example.declarative import simple 16 | from example.declarative import multiple 17 | from example.authentication import view_decorator 18 | from example.flask_sqlalchemy import flaskext_sa_simple 19 | from example.flask_sqlalchemy import flaskext_sa_example 20 | from example.flask_sqlalchemy import flaskext_sa_multi_pk 21 | from example.mongoalchemy import simple as ma_simple 22 | import test.custom_form 23 | import test.deprecation 24 | import test.filefield 25 | import test.sqlalchemy_with_defaults 26 | from test.mongoalchemy_datastore import ConversionTest 27 | 28 | 29 | class SimpleTest(TestCase): 30 | TESTING = True 31 | 32 | def create_app(self): 33 | app = simple.create_app('sqlite://') 34 | teacher = simple.Teacher(name="Mrs. Jones") 35 | app.db_session.add(teacher) 36 | app.db_session.add(simple.Student(name="Stewart")) 37 | app.db_session.add(simple.Student(name="Mike")) 38 | app.db_session.add(simple.Student(name="Jason")) 39 | app.db_session.add(simple.Course(subject="maths", teacher=teacher)) 40 | app.db_session.commit() 41 | return app 42 | 43 | def test_basic(self): 44 | rv = self.client.get('/') 45 | self.assert_redirects(rv, '/admin') 46 | 47 | def test_index(self): 48 | rv = self.client.get('/admin/') 49 | self.assert_200(rv) 50 | 51 | def test_list(self): 52 | rv = self.client.get('/admin/list/Student/?page=1') 53 | self.assert_200(rv) 54 | 55 | def test_edit(self): 56 | rv = self.client.post('/admin/edit/Course/1/', 57 | data=dict(students=[1])) 58 | course = self.app.db_session.query(simple.Course).filter_by(id=1).one() 59 | self.assertEqual(len(course.students), 1) 60 | student = self.app.db_session.\ 61 | query(simple.Student).filter_by(id=1).one() 62 | self.assertEqual(len(student.courses), 1) 63 | self.assert_redirects(rv, '/admin/list/Course/') 64 | 65 | def test_add(self): 66 | self.assertEqual(self.app.db_session.query(simple.Teacher).count(), 1) 67 | rv = self.client.post('/admin/add/Teacher/', 68 | data=dict(name='Mr. Kohleffel')) 69 | self.assertEqual(self.app.db_session.query(simple.Teacher).count(), 2) 70 | self.assert_redirects(rv, '/admin/list/Teacher/') 71 | 72 | def test_delete(self): 73 | self.assertEqual(self.app.db_session.query(simple.Student).count(), 3) 74 | rv = self.client.get('/admin/delete/Student/2/') 75 | self.assertEqual(self.app.db_session.query(simple.Student).count(), 2) 76 | self.assert_redirects(rv, '/admin/list/Student/') 77 | 78 | rv = self.client.get('/admin/delete/Student/2/') 79 | self.assert_200(rv) 80 | assert "Student not found" in rv.data 81 | 82 | 83 | class MultipleTest(TestCase): 84 | TESTING = True 85 | 86 | def create_app(self): 87 | app = multiple.create_app('sqlite://') 88 | return app 89 | 90 | def test_admin1(self): 91 | rv = self.client.get('/admin1/') 92 | assert "Student" in rv.data 93 | assert "Course" not in rv.data 94 | 95 | def test_admin2(self): 96 | rv = self.client.get('/admin2/') 97 | assert "Student" not in rv.data 98 | assert "Course" in rv.data 99 | 100 | 101 | class ViewDecoratorTest(TestCase): 102 | TESTING = True 103 | 104 | def create_app(self): 105 | self.app = view_decorator.create_app('sqlite://') 106 | return self.app 107 | 108 | def test_add_redirect(self): 109 | rv = self.client.get('/admin/add/Student/') 110 | self.assert_redirects(rv, "/admin/login/?next=http%3A%2F%2Flocalhost%2Fadmin%2Fadd%2FStudent%2F") 111 | 112 | def test_delete_redirect(self): 113 | rv = self.client.get('/admin/delete/Student/1/') 114 | self.assert_redirects(rv, "/admin/login/?next=http%3A%2F%2Flocalhost%2Fadmin%2Fdelete%2FStudent%2F1%2F") 115 | 116 | def test_edit_redirect(self): 117 | rv = self.client.get('/admin/edit/Student/1/') 118 | self.assert_redirects(rv, "/admin/login/?next=http%3A%2F%2Flocalhost%2Fadmin%2Fedit%2FStudent%2F1%2F") 119 | 120 | def test_index_redirect(self): 121 | rv = self.client.get('/admin/') 122 | self.assert_redirects(rv, "/admin/login/?next=http%3A%2F%2Flocalhost%2Fadmin%2F") 123 | 124 | def test_list_redirect(self): 125 | rv = self.client.get('/admin/list/Student/') 126 | self.assert_redirects(rv, "/admin/login/?next=http%3A%2F%2Flocalhost%2Fadmin%2Flist%2FStudent%2F") 127 | 128 | def test_login_logout(self): 129 | rv = self.client.post('/admin/login/', 130 | data=dict(username='test', 131 | password='test')) 132 | self.assert_redirects(rv, '/admin/') 133 | 134 | rv = self.client.get('/admin/') 135 | self.assert200(rv) 136 | 137 | rv = self.client.get('/admin/logout/') 138 | self.assert_redirects(rv, '/') 139 | 140 | rv = self.client.get('/admin/') 141 | self.assert_redirects(rv, "/admin/login/?next=http%3A%2F%2Flocalhost%2Fadmin%2F") 142 | 143 | 144 | class CustomFormTest(TestCase): 145 | TESTING = True 146 | 147 | def create_app(self): 148 | app = test.custom_form.create_app('sqlite://') 149 | return app 150 | 151 | def test_custom_form(self): 152 | rv = self.client.get('/admin/add/User/') 153 | assert "User name" in rv.data 154 | assert "Change Password" in rv.data 155 | assert "Confirm Password" in rv.data 156 | assert "Is Active" in rv.data 157 | assert "_password_hash" not in rv.data 158 | 159 | 160 | class SQLAlchemyWithDefaultsTest(TestCase): 161 | TESTING = True 162 | 163 | def create_app(self): 164 | app = test.sqlalchemy_with_defaults.create_app('sqlite://') 165 | return app 166 | 167 | def test_defaults_work(self): 168 | rv = self.client.get('/admin/add/TestModel/') 169 | assert "2194112" in rv.data 170 | assert "128uasdn1uinvuio12ioj!!@Rfja" in rv.data 171 | assert "22341.29" in rv.data 172 | 173 | 174 | class FlaskSQLAlchemySimpleTest(SimpleTest): 175 | TESTING = True 176 | 177 | def create_app(self): 178 | app = flaskext_sa_simple.create_app('sqlite://') 179 | 180 | # set app.db_session to the db.session so the SimpleTest tests 181 | # will work 182 | app.db_session = flaskext_sa_simple.db.session 183 | 184 | # need to grab a request context since we use db.init_app() in 185 | # our application 186 | with app.test_request_context(): 187 | teacher = flaskext_sa_simple.Teacher(name="Mrs. Jones") 188 | flaskext_sa_simple.db.session.add(teacher) 189 | flaskext_sa_simple.db.session.add(flaskext_sa_simple.Student(name="Stewart")) 190 | flaskext_sa_simple.db.session.add(flaskext_sa_simple.Student(name="Mike")) 191 | flaskext_sa_simple.db.session.add(flaskext_sa_simple.Student(name="Jason")) 192 | flaskext_sa_simple.db.session.add(flaskext_sa_simple.Course(subject="maths", teacher=teacher)) 193 | flaskext_sa_simple.db.session.commit() 194 | return app 195 | 196 | 197 | class FlaskSQLAlchemyExampleTest(TestCase): 198 | TESTING = True 199 | 200 | def create_app(self): 201 | app = flaskext_sa_example.create_app('sqlite://') 202 | return app 203 | 204 | def test_index(self): 205 | # just make sure the app is initialized and works 206 | rv = self.client.get('/admin/') 207 | self.assert_200(rv) 208 | 209 | 210 | class FlaskSQLAlchemyMultiPKsTest(TestCase): 211 | TESTING = True 212 | 213 | def create_app(self): 214 | app = flaskext_sa_multi_pk.create_app('sqlite://') 215 | 216 | # set app.db_session to the db.session so the SimpleTest tests 217 | # will work 218 | app.db_session = flaskext_sa_multi_pk.db.session 219 | 220 | # need to grab a request context since we use db.init_app() in 221 | # our application 222 | with app.test_request_context(): 223 | address = flaskext_sa_multi_pk.Address( 224 | shortname=u'K2', 225 | name=u'K-II', 226 | street=u'Hauptstrasse 1', 227 | zipcode='10000', 228 | city=u'Berlin', 229 | country=u'Germany') 230 | flaskext_sa_multi_pk.db.session.add(address) 231 | flaskext_sa_multi_pk.db.session.flush() 232 | location = flaskext_sa_multi_pk.Location( 233 | address_shortname=address.shortname, 234 | room=u'2.01', 235 | position=u'left side') 236 | flaskext_sa_multi_pk.db.session.add(location) 237 | flaskext_sa_multi_pk.db.session.flush() 238 | flaskext_sa_multi_pk.db.session.add( 239 | flaskext_sa_multi_pk.Asset(name=u'asset1', 240 | address_shortname=address.shortname, 241 | location_room=location.room, 242 | location_position=location.position)) 243 | flaskext_sa_multi_pk.db.session.commit() 244 | return app 245 | 246 | def test_index(self): 247 | # just make sure the app is initialized and works 248 | rv = self.client.get('/admin/') 249 | self.assert_200(rv) 250 | 251 | def test_list_asset(self): 252 | rv = self.client.get('/admin/list/Asset/?page=1') 253 | self.assert_200(rv) 254 | 255 | def test_list_location(self): 256 | rv = self.client.get('/admin/list/Location/') 257 | self.assert_200(rv) 258 | 259 | def test_view_location(self): 260 | rv = self.client.get('/admin/edit/Location/K2/2.01/left%20side/') 261 | self.assert_200(rv) 262 | 263 | def test_add_location(self): 264 | self.assertEqual(self.app.db_session.query( 265 | flaskext_sa_multi_pk.Location).count(), 1) 266 | rv = self.client.post('/admin/add/Location/', 267 | data=dict(address=u'K2', 268 | address_shortname=u'K2', 269 | room=u'2.03', 270 | position=u'')) 271 | self.assertEqual(self.app.db_session.query( 272 | flaskext_sa_multi_pk.Location).count(), 2) 273 | rv = self.client.get('/admin/edit/Location/K2/2.03/%1A/') 274 | assert 'edit-form' in rv.data 275 | 276 | def test_edit_location(self): 277 | rv = self.client.post('/admin/edit/Location/K2/2.01/left%20side/', 278 | data=dict(address=u'K2', 279 | address_shortname=u'K2', 280 | room=u'2.01', 281 | position='right side')) 282 | rv = self.client.get('/admin/edit/Location/K2/2.01/right%20side/') 283 | self.assert_200(rv) 284 | 285 | 286 | class ExcludePKsTrueTest(TestCase): 287 | TESTING = True 288 | 289 | def create_app(self): 290 | app = Flask(__name__) 291 | app.config['SECRET_KEY'] = 'not secure' 292 | engine = sa.create_engine('sqlite://', convert_unicode=True) 293 | app.db_session = sa.orm.scoped_session(sa.orm.sessionmaker( 294 | autocommit=False, autoflush=False, 295 | bind=engine)) 296 | admin_blueprint = admin.create_admin_blueprint( 297 | (simple.Course, simple.Student, simple.Teacher), 298 | app.db_session, exclude_pks=True) 299 | app.register_blueprint(admin_blueprint, url_prefix='/admin') 300 | simple.Base.metadata.create_all(bind=engine) 301 | return app 302 | 303 | def test_exclude_pks(self): 304 | rv = self.client.get('/admin/add/Student/') 305 | assert "Id" not in rv.data 306 | 307 | 308 | class ExcludePKsFalseTest(TestCase): 309 | TESTING = True 310 | 311 | def create_app(self): 312 | app = Flask(__name__) 313 | app.config['SECRET_KEY'] = 'not secure' 314 | engine = sa.create_engine('sqlite://', convert_unicode=True) 315 | app.db_session = sa.orm.scoped_session(sa.orm.sessionmaker( 316 | autocommit=False, autoflush=False, 317 | bind=engine)) 318 | admin_blueprint = admin.create_admin_blueprint( 319 | (simple.Course, simple.Student, simple.Teacher), 320 | app.db_session, exclude_pks=False) 321 | app.register_blueprint(admin_blueprint, url_prefix='/admin') 322 | simple.Base.metadata.create_all(bind=engine) 323 | return app 324 | 325 | def test_exclude_pks(self): 326 | rv = self.client.get('/admin/add/Student/') 327 | assert "Id" in rv.data 328 | 329 | 330 | class SmallPaginationTest(TestCase): 331 | TESTING = True 332 | 333 | def create_app(self): 334 | app = simple.create_app('sqlite://', pagination=25) 335 | for i in range(500): 336 | app.db_session.add(simple.Student(name="Student%s" % i)) 337 | app.db_session.commit() 338 | return app 339 | 340 | def test_low_list_view_pagination(self): 341 | rv = self.client.get('/admin/list/Student/?page=1') 342 | assert '>' in rv.data 343 | 344 | 345 | class LargePaginationTest(TestCase): 346 | TESTING = True 347 | 348 | def create_app(self): 349 | app = simple.create_app('sqlite://', pagination=1000) 350 | for i in range(50): 351 | app.db_session.add(simple.Student(name="Student%s" % i)) 352 | app.db_session.commit() 353 | return app 354 | 355 | def test_high_list_view_pagination(self): 356 | rv = self.client.get('/admin/list/Student/') 357 | assert '>' not in rv.data 358 | 359 | 360 | class FileFieldTest(TestCase): 361 | TESTING = True 362 | 363 | def create_app(self): 364 | app = test.filefield.create_app('sqlite://') 365 | test_model = test.filefield.TestModel() 366 | app.db_session.add(test_model) 367 | app.db_session.commit() 368 | return app 369 | 370 | def test_file_field_enctype_rendered_on_add(self): 371 | rv = self.client.get('/admin/add/TestModel/') 372 | assert 'enctype="multipart/form-data"' in rv.data 373 | 374 | def test_file_field_enctype_rendered_on_edit(self): 375 | rv = self.client.get('/admin/edit/TestModel/1/') 376 | assert 'enctype="multipart/form-data"' in rv.data 377 | 378 | 379 | class DeprecationTest(TestCase): 380 | """test that the old deprecated method of calling 381 | create_admin_blueprint still works 382 | """ 383 | TESTING = True 384 | 385 | def create_app(self): 386 | app = test.deprecation.create_app('sqlite://') 387 | return app 388 | 389 | def test_index(self): 390 | rv = self.client.get('/admin/') 391 | self.assert_200(rv) 392 | 393 | 394 | class MASimpleTest(TestCase): 395 | TESTING = True 396 | 397 | def create_app(self): 398 | app = ma_simple.create_app('masimple-test') 399 | # clear db of test objects first 400 | app.db_session.remove_query(ma_simple.Course).execute() 401 | app.db_session.remove_query(ma_simple.Teacher).execute() 402 | app.db_session.remove_query(ma_simple.Student).execute() 403 | app.db_session.insert(ma_simple.Course( 404 | subject="Maths", 405 | start_date=datetime(2011, 8, 12), 406 | end_date=datetime(2011,12,16))) 407 | app.db_session.insert(ma_simple.Student(name="Stewart")) 408 | app.db_session.insert(ma_simple.Student(name="Mike")) 409 | app.db_session.insert(ma_simple.Student(name="Jason")) 410 | return app 411 | 412 | def test_basic(self): 413 | rv = self.client.get('/') 414 | self.assert_redirects(rv, '/admin') 415 | 416 | def test_index(self): 417 | rv = self.client.get('/admin/') 418 | self.assert_200(rv) 419 | 420 | def test_list(self): 421 | rv = self.client.get('/admin/list/Student/?page=1') 422 | self.assert_200(rv) 423 | 424 | def test_edit(self): 425 | course = self.app.db_session.query(ma_simple.Course).\ 426 | filter(ma_simple.Course.subject == 'Maths').one() 427 | course_dict = dict([(key, str(getattr(course, key))) 428 | for key in course.get_fields()]) 429 | course_dict['end_date'] = "2012-05-31 00:00:00" 430 | rv = self.client.post('/admin/edit/Course/%s/' % course.mongo_id, 431 | data=course_dict) 432 | new_course = self.app.db_session.query(ma_simple.Course).\ 433 | filter(ma_simple.Course.subject == 'Maths').one() 434 | 435 | self.assertEqual(new_course.end_date, datetime(2012, 5, 31)) 436 | self.assert_redirects(rv, '/admin/list/Course/') 437 | 438 | def test_add(self): 439 | self.assertEqual(self.app.db_session.query(ma_simple.Teacher).count(), 0) 440 | rv = self.client.post('/admin/add/Teacher/', 441 | data=dict(name='Mr. Kohleffel')) 442 | self.assertEqual(self.app.db_session.query(ma_simple.Teacher).count(), 1) 443 | self.assert_redirects(rv, '/admin/list/Teacher/') 444 | 445 | def test_delete(self): 446 | student_query = self.app.db_session.query(ma_simple.Student) 447 | self.assertEqual(student_query.count(), 3) 448 | student = student_query.first() 449 | rv = self.client.get('/admin/delete/Student/%s/' % student.mongo_id) 450 | self.assertEqual(student_query.count(), 2) 451 | self.assert_redirects(rv, '/admin/list/Student/') 452 | 453 | rv = self.client.get('/admin/delete/Student/%s/' % student.mongo_id) 454 | self.assert_200(rv) 455 | assert "Student not found" in rv.data 456 | 457 | 458 | def suite(): 459 | suite = unittest.TestSuite() 460 | suite.addTest(unittest.makeSuite(SimpleTest)) 461 | suite.addTest(unittest.makeSuite(MultipleTest)) 462 | suite.addTest(unittest.makeSuite(ViewDecoratorTest)) 463 | suite.addTest(unittest.makeSuite(CustomFormTest)) 464 | suite.addTest(unittest.makeSuite(FlaskSQLAlchemySimpleTest)) 465 | suite.addTest(unittest.makeSuite(FlaskSQLAlchemyExampleTest)) 466 | suite.addTest(unittest.makeSuite(FlaskSQLAlchemyMultiPKsTest)) 467 | suite.addTest(unittest.makeSuite(SQLAlchemyWithDefaultsTest)) 468 | suite.addTest(unittest.makeSuite(ExcludePKsTrueTest)) 469 | suite.addTest(unittest.makeSuite(ExcludePKsFalseTest)) 470 | suite.addTest(unittest.makeSuite(SmallPaginationTest)) 471 | suite.addTest(unittest.makeSuite(LargePaginationTest)) 472 | suite.addTest(unittest.makeSuite(FileFieldTest)) 473 | suite.addTest(unittest.makeSuite(DeprecationTest)) 474 | suite.addTest(unittest.makeSuite(ConversionTest)) 475 | suite.addTest(unittest.makeSuite(MASimpleTest)) 476 | return suite 477 | 478 | if __name__ == '__main__': 479 | test_suite = suite() 480 | unittest.TextTestRunner(verbosity=2).run(test_suite) 481 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # content of: tox.ini , put in same dir as setup.py 2 | [tox] 3 | envlist = py25,py26,py27 4 | [testenv] 5 | deps = 6 | flask-sqlalchemy 7 | flask-testing 8 | mongoalchemy 9 | commands=python test_admin.py --------------------------------------------------------------------------------