├── .gitignore ├── .gitmodules ├── .travis.yml ├── MANIFEST.in ├── Makefile ├── README ├── README.rst ├── docs ├── Makefile ├── _static │ ├── favicon.ico │ ├── screen-edit.png │ └── screen.png ├── api.rst ├── build │ └── doctrees │ │ ├── api.doctree │ │ ├── environment.pickle │ │ └── index.doctree ├── conf.py ├── index.rst └── make.bat ├── examples └── sqlalchemy_backend.py ├── flask_dashed ├── __init__.py ├── admin.py ├── dashboard.py ├── ext │ ├── __init__.py │ └── sqlalchemy.py ├── static │ ├── css │ │ ├── normalize.css │ │ ├── style.css │ │ └── style.styl │ └── images │ │ └── background.png ├── templates │ └── flask_dashed │ │ ├── base.html │ │ ├── breadcrumbs.html │ │ ├── dashboard.html │ │ ├── edit.html │ │ ├── footer.html │ │ ├── form.html │ │ ├── header.html │ │ ├── list.html │ │ └── navigation.html └── views.py ├── setup.cfg ├── setup.py └── tests ├── __init__.py ├── admin.py ├── all.py ├── flask_dashed ├── requirements.txt └── sqlalchemy_backend.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | /build 3 | dist 4 | Flask_Dashed.egg-info 5 | temp 6 | *.swp 7 | mixup 8 | docs/build 9 | .sass* 10 | .env 11 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "docs/_themes"] 2 | path = docs/_themes 3 | url = git://github.com/mitsuhiko/flask-sphinx-themes.git 4 | [submodule "flask_dashed/static/css/vendor/nib"] 5 | path = flask_dashed/static/css/vendor/nib 6 | url = git://github.com/visionmedia/nib.git 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | install: 5 | - pip install -r tests/requirements.txt --use-mirrors 6 | - python setup.py install 7 | script: make test 8 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst *.py 2 | recursive-include flask_dashed * 3 | recursive-include docs * 4 | recursive-exclude docs *.pyc 5 | recursive-exclude docs *.pyo 6 | prune docs/_build 7 | prune docs/_themes/.git 8 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | python tests/all.py 3 | 4 | styles: 5 | stylus flask_dashed/static/css/style.styl -o flask_dashed/static/css/ 6 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | README.rst -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Introduction 2 | ------------ 3 | 4 | .. image:: https://secure.travis-ci.org/jeanphix/Flask-Dashed.png 5 | 6 | Flask-Dashed provides tools for build simple and extensible admin interfaces. 7 | 8 | Online demonstration: http://flask-dashed.jeanphi.fr/ (Github account required). 9 | 10 | List view: 11 | 12 | .. image:: https://github.com/jeanphix/Flask-Dashed/raw/dev/docs/_static/screen.png 13 | 14 | Form view: 15 | 16 | .. image:: https://github.com/jeanphix/Flask-Dashed/raw/dev/docs/_static/screen-edit.png 17 | 18 | 19 | 20 | Installation 21 | ------------ 22 | 23 | pip install Flask-Dashed 24 | 25 | 26 | Minimal usage 27 | ------------- 28 | 29 | Code:: 30 | 31 | from flask import Flask 32 | from flask_dashed.admin import Admin 33 | 34 | app = Flask(__name__) 35 | admin = Admin(app) 36 | 37 | if __name__ == '__main__': 38 | app.run() 39 | 40 | 41 | Sample application: http://github.com/jeanphix/flask-dashed-demo 42 | 43 | 44 | Deal with security 45 | ------------------ 46 | 47 | Securing all module endpoints:: 48 | 49 | from flask import session 50 | 51 | book_module = admin.register_module(BookModule, '/books', 'books', 52 | 'book management') 53 | 54 | @book_module.secure(http_code=401) 55 | def login_required(): 56 | return "user" in session 57 | 58 | Securing specific module endpoint:: 59 | 60 | @book_module.secure_endpoint('edit', http_code=403) 61 | def check_edit_credential(view): 62 | # I'm now signed in, may I modify the ressource? 63 | return session.user.can_edit_book(view.object) 64 | 65 | 66 | Organize modules 67 | ---------------- 68 | 69 | As admin nodes are registered into a "tree" it's quite easy to organize them.:: 70 | 71 | library = admin.register_node('/library', 'library', my library) 72 | book_module = admin.register_module(BookModule, '/books', 'books', 73 | 'book management', parent=library) 74 | 75 | Navigation and breadcrumbs are automatically builds to feet your needs. Child module security will be inherited from parent one. 76 | 77 | 78 | SQLALchemy extension 79 | -------------------- 80 | 81 | Code:: 82 | 83 | from flask_dashed.ext.sqlalchemy import ModelAdminModule 84 | 85 | 86 | class BookModule(ModelAdminModule): 87 | model = Book 88 | db_session = db.session 89 | 90 | book_module = admin.register_module(BookModule, '/books', 'books', 91 | 'book management') 92 | -------------------------------------------------------------------------------- /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_Dashed.qhcp" 76 | @echo "To view the help file:" 77 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Flask_Dashed.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_Dashed" 85 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Flask_Dashed" 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/_static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeanphix/Flask-Dashed/40e13e8557ca1a4a0515ff96c2356167b9ac71b4/docs/_static/favicon.ico -------------------------------------------------------------------------------- /docs/_static/screen-edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeanphix/Flask-Dashed/40e13e8557ca1a4a0515ff96c2356167b9ac71b4/docs/_static/screen-edit.png -------------------------------------------------------------------------------- /docs/_static/screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeanphix/Flask-Dashed/40e13e8557ca1a4a0515ff96c2356167b9ac71b4/docs/_static/screen.png -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | Api 2 | === 3 | 4 | Admin Object 5 | ------------ 6 | 7 | .. currentmodule:: None 8 | 9 | .. autoclass:: admin.Admin 10 | :members: 11 | 12 | 13 | Admin Modules 14 | ------------- 15 | 16 | .. autofunction:: admin.recursive_getattr 17 | 18 | 19 | .. autoclass:: admin.AdminNode 20 | :members: 21 | 22 | .. autoclass:: admin.AdminModule 23 | :members: 24 | 25 | 26 | SQLAlchemy extension 27 | -------------------- 28 | .. autoclass:: ext.sqlalchemy.ModelAdminModule 29 | :members: 30 | -------------------------------------------------------------------------------- /docs/build/doctrees/api.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeanphix/Flask-Dashed/40e13e8557ca1a4a0515ff96c2356167b9ac71b4/docs/build/doctrees/api.doctree -------------------------------------------------------------------------------- /docs/build/doctrees/environment.pickle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeanphix/Flask-Dashed/40e13e8557ca1a4a0515ff96c2356167b9ac71b4/docs/build/doctrees/environment.pickle -------------------------------------------------------------------------------- /docs/build/doctrees/index.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeanphix/Flask-Dashed/40e13e8557ca1a4a0515ff96c2356167b9ac71b4/docs/build/doctrees/index.doctree -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Flask_Dashed documentation build configuration file, created by 4 | # sphinx-quickstart on Sun Sep 18 03:32:57 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 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../flask_dashed'))) 17 | sys.path.append(os.path.abspath('_themes')) 18 | # -- General configuration ----------------------------------------------------- 19 | 20 | # If your documentation needs a minimal Sphinx version, state it here. 21 | #needs_sphinx = '1.0' 22 | 23 | # Add any Sphinx extension module names here, as strings. They can be extensions 24 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 25 | extensions = ['sphinx.ext.viewcode', 'sphinx.ext.autodoc'] 26 | 27 | # Add any paths that contain templates here, relative to this directory. 28 | templates_path = ['_templates'] 29 | 30 | # The suffix of source filenames. 31 | source_suffix = '.rst' 32 | 33 | # The encoding of source files. 34 | #source_encoding = 'utf-8-sig' 35 | 36 | # The master toctree document. 37 | master_doc = 'index' 38 | 39 | # General information about the project. 40 | project = u'Flask_Dashed' 41 | copyright = u'2011, jean-philippe serafin' 42 | 43 | # The version info for the project you're documenting, acts as replacement for 44 | # |version| and |release|, also used in various other places throughout the 45 | # built documents. 46 | # 47 | # The short X.Y version. 48 | version = '0.1b' 49 | # The full version, including alpha/beta/rc tags. 50 | release = '0.1b' 51 | 52 | # The language for content autogenerated by Sphinx. Refer to documentation 53 | # for a list of supported languages. 54 | #language = None 55 | 56 | # There are two options for replacing |today|: either, you set today to some 57 | # non-false value, then it is used: 58 | #today = '' 59 | # Else, today_fmt is used as the format for a strftime call. 60 | #today_fmt = '%B %d, %Y' 61 | 62 | # List of patterns, relative to source directory, that match files and 63 | # directories to ignore when looking for source files. 64 | exclude_patterns = ['_build', 'build'] 65 | 66 | # The reST default role (used for this markup: `text`) to use for all documents. 67 | #default_role = None 68 | 69 | # If true, '()' will be appended to :func: etc. cross-reference text. 70 | #add_function_parentheses = True 71 | 72 | # If true, the current module name will be prepended to all description 73 | # unit titles (such as .. function::). 74 | #add_module_names = True 75 | 76 | # If true, sectionauthor and moduleauthor directives will be shown in the 77 | # output. They are ignored by default. 78 | #show_authors = False 79 | 80 | # The name of the Pygments (syntax highlighting) style to use. 81 | pygments_style = 'sphinx' 82 | 83 | # A list of ignored prefixes for module index sorting. 84 | #modindex_common_prefix = [] 85 | 86 | 87 | # -- Options for HTML output --------------------------------------------------- 88 | 89 | # The theme to use for HTML and HTML Help pages. See the documentation for 90 | # a list of builtin themes. 91 | html_theme = 'flask_small' 92 | 93 | # Theme options are theme-specific and customize the look and feel of a theme 94 | # further. For a list of options available for each theme, see the 95 | # documentation. 96 | #html_theme_options = {} 97 | 98 | # Add any paths that contain custom themes here, relative to this directory. 99 | html_theme_path = ['_themes'] 100 | 101 | # The name for this set of Sphinx documents. If None, it defaults to 102 | # " v documentation". 103 | #html_title = None 104 | 105 | # A shorter title for the navigation bar. Default is the same as html_title. 106 | #html_short_title = None 107 | 108 | # The name of an image file (relative to this directory) to place at the top 109 | # of the sidebar. 110 | #html_logo = None 111 | 112 | # The name of an image file (within the static path) to use as favicon of the 113 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 114 | # pixels large. 115 | html_favicon = 'favicon.ico' 116 | 117 | # Add any paths that contain custom static files (such as style sheets) here, 118 | # relative to this directory. They are copied after the builtin static files, 119 | # so a file named "default.css" will overwrite the builtin "default.css". 120 | html_static_path = ['_static'] 121 | 122 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 123 | # using the given strftime format. 124 | #html_last_updated_fmt = '%b %d, %Y' 125 | 126 | # If true, SmartyPants will be used to convert quotes and dashes to 127 | # typographically correct entities. 128 | #html_use_smartypants = True 129 | 130 | # Custom sidebar templates, maps document names to template names. 131 | #html_sidebars = {} 132 | 133 | # Additional templates that should be rendered to pages, maps page names to 134 | # template names. 135 | #html_additional_pages = {} 136 | 137 | # If false, no module index is generated. 138 | #html_domain_indices = True 139 | 140 | # If false, no index is generated. 141 | #html_use_index = True 142 | 143 | # If true, the index is split into individual pages for each letter. 144 | #html_split_index = False 145 | 146 | # If true, links to the reST sources are added to the pages. 147 | #html_show_sourcelink = True 148 | 149 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 150 | #html_show_sphinx = True 151 | 152 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 153 | #html_show_copyright = True 154 | 155 | # If true, an OpenSearch description file will be output, and all pages will 156 | # contain a tag referring to it. The value of this option must be the 157 | # base URL from which the finished HTML is served. 158 | #html_use_opensearch = '' 159 | 160 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 161 | #html_file_suffix = None 162 | 163 | # Output file base name for HTML help builder. 164 | htmlhelp_basename = 'Flask_Dasheddoc' 165 | 166 | 167 | # -- Options for LaTeX output -------------------------------------------------- 168 | 169 | # The paper size ('letter' or 'a4'). 170 | #latex_paper_size = 'letter' 171 | 172 | # The font size ('10pt', '11pt' or '12pt'). 173 | #latex_font_size = '10pt' 174 | 175 | # Grouping the document tree into LaTeX files. List of tuples 176 | # (source start file, target name, title, author, documentclass [howto/manual]). 177 | latex_documents = [ 178 | ('index', 'Flask_Dashed.tex', u'Flask\\_Dashed Documentation', 179 | u'jean-philippe serafin', 'manual'), 180 | ] 181 | 182 | # The name of an image file (relative to this directory) to place at the top of 183 | # the title page. 184 | #latex_logo = None 185 | 186 | # For "manual" documents, if this is true, then toplevel headings are parts, 187 | # not chapters. 188 | #latex_use_parts = False 189 | 190 | # If true, show page references after internal links. 191 | #latex_show_pagerefs = False 192 | 193 | # If true, show URL addresses after external links. 194 | #latex_show_urls = False 195 | 196 | # Additional stuff for the LaTeX preamble. 197 | #latex_preamble = '' 198 | 199 | # Documents to append as an appendix to all manuals. 200 | #latex_appendices = [] 201 | 202 | # If false, no module index is generated. 203 | #latex_domain_indices = True 204 | 205 | 206 | # -- Options for manual page output -------------------------------------------- 207 | 208 | # One entry per manual page. List of tuples 209 | # (source start file, name, description, authors, manual section). 210 | man_pages = [ 211 | ('index', 'flask_dashed', u'Flask_Dashed Documentation', 212 | [u'jean-philippe serafin'], 1) 213 | ] 214 | 215 | html_theme_options = {'index_logo': False, 216 | 'github_fork': 'jeanphix/Flask-Dashed'} 217 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Flask_Dashed documentation master file, created by 2 | sphinx-quickstart on Sun Sep 18 03:32:57 2011. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to Flask_Dashed's documentation! 7 | ======================================== 8 | 9 | 10 | .. include:: ../README.rst 11 | 12 | 13 | API Reference 14 | ------------- 15 | 16 | .. toctree:: 17 | :maxdepth: 3 18 | 19 | api 20 | 21 | Indices and tables 22 | ================== 23 | 24 | * :ref:`genindex` 25 | * :ref:`modindex` 26 | * :ref:`search` 27 | 28 | -------------------------------------------------------------------------------- /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_Dashed.qhcp 99 | echo.To view the help file: 100 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Flask_Dashed.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 | -------------------------------------------------------------------------------- /examples/sqlalchemy_backend.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import wtforms 3 | from werkzeug import OrderedMultiDict 4 | 5 | from flask import Flask, redirect 6 | 7 | from flask_dashed.admin import Admin 8 | from flask_dashed.ext.sqlalchemy import ModelAdminModule, model_form 9 | 10 | from flaskext.sqlalchemy import SQLAlchemy 11 | 12 | from sqlalchemy.orm import aliased, contains_eager 13 | 14 | 15 | app = Flask(__name__) 16 | app.config['SECRET_KEY'] = 'secret' 17 | app.debug = True 18 | 19 | app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db' 20 | app.jinja_env.trim_blocks = True 21 | 22 | 23 | db = SQLAlchemy(app) 24 | db_session = db.session 25 | 26 | 27 | class Company(db.Model): 28 | id = db.Column(db.Integer, primary_key=True) 29 | name = db.Column(db.String(255), unique=True, nullable=False) 30 | 31 | def __unicode__(self): 32 | return unicode(self.name) 33 | 34 | def __repr__(self): 35 | return '' % self.name 36 | 37 | 38 | class Warehouse(db.Model): 39 | id = db.Column(db.Integer, primary_key=True) 40 | name = db.Column(db.String(255), nullable=False) 41 | company_id = db.Column(db.Integer, db.ForeignKey(Company.id)) 42 | 43 | company = db.relationship(Company, backref=db.backref("warehouses")) 44 | 45 | def __repr__(self): 46 | return '' % self.name 47 | 48 | 49 | class User(db.Model): 50 | id = db.Column(db.Integer, primary_key=True) 51 | username = db.Column(db.String(255), unique=True, nullable=False) 52 | password = db.Column(db.String(255)) 53 | is_active = db.Column(db.Boolean()) 54 | 55 | 56 | class Profile(db.Model): 57 | id = db.Column(db.Integer, db.ForeignKey(User.id), primary_key=True) 58 | name = db.Column(db.String(255), nullable=False) 59 | location = db.Column(db.String(255)) 60 | company_id = db.Column(db.Integer, db.ForeignKey(Company.id), 61 | nullable=True) 62 | 63 | user = db.relationship(User, backref=db.backref("profile", 64 | remote_side=id, uselist=False, cascade="all, delete-orphan")) 65 | 66 | company = db.relationship(Company, backref=db.backref("staff")) 67 | 68 | 69 | user_group = db.Table( 70 | 'user_group', db.Model.metadata, 71 | db.Column('user_id', db.Integer, db.ForeignKey('user.id')), 72 | db.Column('group_id', db.Integer, db.ForeignKey('group.id')) 73 | ) 74 | 75 | 76 | class Group(db.Model): 77 | id = db.Column(db.Integer, primary_key=True) 78 | name = db.Column(db.String(255), unique=True, nullable=False) 79 | 80 | users = db.relationship("User", secondary=user_group, 81 | backref=db.backref("groups", lazy='dynamic')) 82 | 83 | def __unicode__(self): 84 | return unicode(self.name) 85 | 86 | def __repr__(self): 87 | return '' % self.name 88 | 89 | 90 | db.drop_all() 91 | db.create_all() 92 | 93 | group = Group(name="admin") 94 | db_session.add(group) 95 | company = Company(name="My company") 96 | db_session.add(company) 97 | db_session.commit() 98 | 99 | 100 | UserForm = model_form(User, db_session, exclude=['password']) 101 | 102 | 103 | class UserForm(UserForm): 104 | # Embeds OneToOne as FormField 105 | profile = wtforms.FormField( 106 | model_form(Profile, db_session, exclude=['user'], 107 | base_class=wtforms.Form)) 108 | 109 | 110 | class UserModule(ModelAdminModule): 111 | model = User 112 | db_session = db_session 113 | profile_alias = aliased(Profile) 114 | 115 | list_fields = OrderedMultiDict(( 116 | ('id', {'label': 'id', 'column': User.id}), 117 | ('username', {'label': 'username', 'column': User.username}), 118 | ('profile.name', {'label': 'name', 'column': profile_alias.name}), 119 | ('profile.location', {'label': 'location', 120 | 'column': profile_alias.location}), 121 | )) 122 | 123 | list_title = 'user list' 124 | 125 | searchable_fields = ['username', 'profile.name', 'profile.location'] 126 | 127 | order_by = ('id', 'desc') 128 | 129 | list_query_factory = model.query\ 130 | .outerjoin(profile_alias, 'profile')\ 131 | .options(contains_eager('profile', alias=profile_alias))\ 132 | 133 | form_class = UserForm 134 | 135 | def create_object(self): 136 | user = self.model() 137 | user.profile = Profile() 138 | return user 139 | 140 | 141 | class GroupModule(ModelAdminModule): 142 | model = Group 143 | db_session = db_session 144 | form_class = model_form(Group, db_session, only=['name']) 145 | 146 | 147 | class WarehouseModule(ModelAdminModule): 148 | model = Warehouse 149 | db_session = db_session 150 | 151 | 152 | class CompanyModule(ModelAdminModule): 153 | model = Company 154 | db_session = db_session 155 | form_class = model_form(Company, db_session, only=['name']) 156 | 157 | 158 | admin = Admin(app, title="my business administration") 159 | 160 | security = admin.register_node('/security', 'security', 'security management') 161 | 162 | user_module = admin.register_module(UserModule, '/users', 'users', 163 | 'users', parent=security) 164 | 165 | group_module = admin.register_module(GroupModule, '/groups', 'groups', 166 | 'groups', parent=security) 167 | 168 | company_module = admin.register_module(CompanyModule, '/companies', 169 | 'companies', 'companies') 170 | 171 | warehouse_module = admin.register_module(WarehouseModule, '/warehouses', 172 | 'warehouses', 'warehouses', parent=company_module) 173 | 174 | 175 | @app.route('/') 176 | def redirect_to_admin(): 177 | return redirect('/admin') 178 | 179 | if __name__ == '__main__': 180 | app.run() 181 | -------------------------------------------------------------------------------- /flask_dashed/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeanphix/Flask-Dashed/40e13e8557ca1a4a0515ff96c2356167b9ac71b4/flask_dashed/__init__.py -------------------------------------------------------------------------------- /flask_dashed/admin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from werkzeug import OrderedMultiDict 3 | 4 | from flask import Blueprint, url_for, request, abort 5 | from views import ObjectListView, ObjectFormView 6 | from views import ObjectDeleteView, secure 7 | 8 | 9 | def recursive_getattr(obj, attr): 10 | """Returns object related attributes, as it's a template filter None 11 | is return when attribute doesn't exists. 12 | 13 | eg:: 14 | 15 | a = object() 16 | a.b = object() 17 | a.b.c = 1 18 | recursive_getattr(a, 'b.c') => 1 19 | recursive_getattr(a, 'b.d') => None 20 | """ 21 | try: 22 | if "." not in attr: 23 | return getattr(obj, attr) 24 | else: 25 | l = attr.split('.') 26 | return recursive_getattr(getattr(obj, l[0]), '.'.join(l[1:])) 27 | except AttributeError: 28 | return None 29 | 30 | 31 | class AdminNode(object): 32 | """An AdminNode just act as navigation container, it doesn't provide any 33 | rules. 34 | 35 | :param admin: The parent admin object 36 | :param url_prefix: The url prefix 37 | :param enpoint: The endpoint 38 | :param short_title: The short module title use on navigation 39 | & breadcrumbs 40 | :param title: The long title 41 | :param parent: The parent node 42 | """ 43 | def __init__(self, admin, url_prefix, endpoint, short_title, title=None, 44 | parent=None): 45 | self.admin = admin 46 | self.parent = parent 47 | self.url_prefix = url_prefix 48 | self.endpoint = endpoint 49 | self.short_title = short_title 50 | self.title = title 51 | self.children = [] 52 | 53 | @property 54 | def url_path(self): 55 | """Returns the url path relative to admin one. 56 | """ 57 | if self.parent: 58 | return self.parent.url_path + self.url_prefix 59 | else: 60 | return self.url_prefix 61 | 62 | @property 63 | def parents(self): 64 | """Returns all parent hierarchy as list. Usefull for breadcrumbs. 65 | """ 66 | if self.parent: 67 | parents = list(self.parent.parents) 68 | parents.append(self.parent) 69 | return parents 70 | else: 71 | return [] 72 | 73 | def secure(self, http_code=403): 74 | """Gives a way to secure specific url path. 75 | 76 | :param http_code: The response http code when False 77 | """ 78 | def decorator(f): 79 | self.admin.add_path_security(self.url_path, f, http_code) 80 | return f 81 | return decorator 82 | 83 | 84 | class Admin(object): 85 | """Class that provides a way to add admin interface to Flask applications. 86 | 87 | :param app: The Flask application 88 | :param url_prefix: The url prefix 89 | :param main_dashboard: The main dashboard object 90 | :param endpoint: The endpoint 91 | """ 92 | def __init__(self, app, url_prefix="/admin", title="flask-dashed", 93 | main_dashboard=None, endpoint='admin'): 94 | 95 | if not main_dashboard: 96 | from dashboard import DefaultDashboard 97 | main_dashboard = DefaultDashboard 98 | 99 | self.blueprint = Blueprint(endpoint, __name__, 100 | static_folder='static', template_folder='templates') 101 | self.app = app 102 | self.url_prefix = url_prefix 103 | self.endpoint = endpoint 104 | self.title = title 105 | self.secure_functions = OrderedMultiDict() 106 | # Checks security for current path 107 | self.blueprint.before_request( 108 | lambda: self.check_path_security(request.path)) 109 | 110 | self.app.register_blueprint(self.blueprint, url_prefix=url_prefix) 111 | self.root_nodes = [] 112 | 113 | self._add_node(main_dashboard, '/', 'main-dashboard', 'dashboard') 114 | # Registers recursive_getattr filter 115 | self.app.jinja_env.filters['recursive_getattr'] = recursive_getattr 116 | 117 | def register_node(self, url_prefix, endpoint, short_title, title=None, 118 | parent=None, node_class=AdminNode): 119 | """Registers admin node. 120 | 121 | :param url_prefix: The url prefix 122 | :param endpoint: The endpoint 123 | :param short_title: The short title 124 | :param title: The long title 125 | :param parent: The parent node path 126 | :param node_class: The class for node objects 127 | """ 128 | return self._add_node(node_class, url_prefix, endpoint, short_title, 129 | title=title, parent=parent) 130 | 131 | def register_module(self, module_class, url_prefix, endpoint, short_title, 132 | title=None, parent=None): 133 | """Registers new module to current admin. 134 | """ 135 | return self._add_node(module_class, url_prefix, endpoint, short_title, 136 | title=title, parent=parent) 137 | 138 | def _add_node(self, node_class, url_prefix, endpoint, short_title, 139 | title=None, parent=None): 140 | """Registers new node object to current admin object. 141 | """ 142 | title = short_title if not title else title 143 | if parent and not issubclass(parent.__class__, AdminNode): 144 | raise Exception('`parent` class must be AdminNode subclass') 145 | new_node = node_class(self, url_prefix, endpoint, short_title, 146 | title=title, parent=parent) 147 | if parent: 148 | parent.children.append(new_node) 149 | else: 150 | self.root_nodes.append(new_node) 151 | return new_node 152 | 153 | @property 154 | def main_dashboard(self): 155 | return self.root_nodes[0] 156 | 157 | def add_path_security(self, path, function, http_code=403): 158 | """Registers security function for given path. 159 | 160 | :param path: The endpoint to secure 161 | :param function: The security function 162 | :param http_code: The response http code 163 | """ 164 | self.secure_functions.add(path, (function, http_code)) 165 | 166 | def check_path_security(self, path): 167 | """Checks security for specific and path. 168 | 169 | :param path: The path to check 170 | """ 171 | for key in self.secure_functions.iterkeys(): 172 | if path.startswith("%s%s" % (self.url_prefix, key)): 173 | for function, http_code in self.secure_functions.getlist(key): 174 | if not function(): 175 | return abort(http_code) 176 | 177 | 178 | class AdminModule(AdminNode): 179 | """Class that provides a way to create simple admin module. 180 | 181 | :param admin: The parent admin object 182 | :param url_prefix: The url prefix 183 | :param enpoint: The endpoint 184 | :param short_title: the short module title use on navigation 185 | & breadcrumbs 186 | :param title: The long title 187 | :param parent: The parent node 188 | """ 189 | def __init__(self, *args, **kwargs): 190 | super(AdminModule, self).__init__(*args, **kwargs) 191 | self.rules = OrderedMultiDict() 192 | self._register_rules() 193 | 194 | def add_url_rule(self, rule, endpoint, view_func, **options): 195 | """Adds a routing rule to the application from relative endpoint. 196 | `view_class` is copied as we need to dynamically apply decorators. 197 | 198 | :param rule: The rule 199 | :param endpoint: The endpoint 200 | :param view_func: The view 201 | """ 202 | class ViewClass(view_func.view_class): 203 | pass 204 | 205 | ViewClass.__name__ = "%s_%s" % (self.endpoint, endpoint) 206 | ViewClass.__module__ = view_func.__module__ 207 | view_func.view_class = ViewClass 208 | full_endpoint = "%s.%s_%s" % (self.admin.endpoint, 209 | self.endpoint, endpoint) 210 | self.admin.app.add_url_rule("%s%s%s" % (self.admin.url_prefix, 211 | self.url_path, rule), full_endpoint, view_func, **options) 212 | self.rules.setlist(endpoint, [(rule, endpoint, view_func)]) 213 | 214 | def _register_rules(self): 215 | """Registers all module rules after initialization. 216 | """ 217 | if not hasattr(self, 'default_rules'): 218 | raise NotImplementedError('Admin module class must provide' 219 | + ' default_rules') 220 | for rule, endpoint, view_func in self.default_rules: 221 | self.add_url_rule(rule, endpoint, view_func) 222 | 223 | @property 224 | def url(self): 225 | """Returns first registered (main) rule as url. 226 | """ 227 | try: 228 | return url_for("%s.%s_%s" % (self.admin.endpoint, 229 | self.endpoint, self.rules.lists()[0][0])) 230 | # Cause OrderedMultiDict.keys() doesn't preserve order... 231 | except IndexError: 232 | raise Exception('`AdminModule` must provide at list one rule.') 233 | 234 | def secure_endpoint(self, endpoint, http_code=403): 235 | """Gives a way to secure specific url path. 236 | 237 | :param endpoint: The endpoint to protect 238 | :param http_code: The response http code when False 239 | """ 240 | def decorator(f): 241 | self._secure_enpoint(endpoint, f, http_code) 242 | return f 243 | return decorator 244 | 245 | def _secure_enpoint(self, endpoint, secure_function, http_code): 246 | """Secure enpoint view function via `secure` decorator. 247 | 248 | :param enpoint: The endpoint to secure 249 | :param secure_function: The function to check 250 | :param http_code: The response http code when False. 251 | """ 252 | rule, endpoint, view_func = self.rules.get(endpoint) 253 | view_func.view_class.dispatch_request =\ 254 | secure(endpoint, secure_function, http_code)( 255 | view_func.view_class.dispatch_request) 256 | 257 | 258 | class ObjectAdminModule(AdminModule): 259 | """Base class for object admin modules backends. 260 | Provides all required methods to retrieve, create, update and delete 261 | objects. 262 | """ 263 | # List relateds 264 | list_view = ObjectListView 265 | list_template = 'flask_dashed/list.html' 266 | list_fields = None 267 | list_title = 'list' 268 | list_per_page = 10 269 | searchable_fields = None 270 | order_by = None 271 | # Edit relateds 272 | edit_template = 'flask_dashed/edit.html' 273 | form_view = ObjectFormView 274 | form_class = None 275 | edit_title = 'edit object' 276 | # New relateds 277 | new_title = 'new object' 278 | # Delete relateds 279 | delete_view = ObjectDeleteView 280 | 281 | def __new__(cls, *args, **kwargs): 282 | if not cls.list_fields: 283 | raise NotImplementedError() 284 | return super(ObjectAdminModule, cls).__new__(cls, *args, **kwargs) 285 | 286 | @property 287 | def default_rules(self): 288 | """Adds object list rule to current app. 289 | """ 290 | return [ 291 | ('/', 'list', self.list_view.as_view('short_title', self)), 292 | ('/page/', 'listpaged', self.list_view.as_view('short_title', 293 | self)), 294 | ('/new', 'new', self.form_view.as_view('short_title', self)), 295 | ('//edit', 'edit', self.form_view.as_view('short_title', 296 | self)), 297 | ('//delete', 'delete', self.delete_view.as_view('short_title', 298 | self)), 299 | ] 300 | 301 | def get_object_list(self, search=None, order_by_field=None, 302 | order_by_direction=None, offset=None, limit=None): 303 | """Returns objects list ordered and filtered. 304 | 305 | :param search: The search string for quick filtering 306 | :param order_by_field: The ordering field 307 | :param order_by_direction: The ordering direction 308 | :param offset: The pagintation offset 309 | :param limit: The pagination limit 310 | """ 311 | raise NotImplementedError() 312 | 313 | def count_list(self, search=None): 314 | """Counts filtered object list. 315 | 316 | :param search: The search string for quick filtering. 317 | """ 318 | raise NotImplementedError() 319 | 320 | def get_action_for_field(self, field, obj): 321 | """Returns title and link for given list field and object. 322 | 323 | :param field: The field path. 324 | :param object: The line object. 325 | """ 326 | title, url = None, None 327 | field = self.list_fields[field] 328 | if 'action' in field: 329 | title = field['action'].get('title', None) 330 | if callable(title): 331 | title = title(obj) 332 | url = field['action'].get('url', None) 333 | if callable(url): 334 | url = url(obj) 335 | return title, url 336 | 337 | def get_actions_for_object(self, object): 338 | """Returns action available for each object. 339 | 340 | :param object: The raw object 341 | """ 342 | raise NotImplementedError() 343 | 344 | def get_form(self, obj): 345 | """Returns form initialy populate from object instance. 346 | 347 | :param obj: The object 348 | """ 349 | return self.form_class(obj=obj) 350 | 351 | def get_object(self, pk=None): 352 | """Returns object retrieve by primary key. 353 | 354 | :param pk: The object primary key 355 | """ 356 | raise NotImplementedError() 357 | 358 | def create_object(self): 359 | """Returns new object instance.""" 360 | raise NotImplementedError() 361 | 362 | def save_object(self, object): 363 | """Persits object. 364 | 365 | :param object: The object to persist 366 | """ 367 | raise NotImplementedError() 368 | 369 | def delete_object(self, object): 370 | """Deletes object. 371 | 372 | :param object: The object to delete 373 | """ 374 | raise NotImplementedError() 375 | -------------------------------------------------------------------------------- /flask_dashed/dashboard.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from admin import AdminModule 3 | from views import DashboardView 4 | 5 | 6 | class Dashboard(AdminModule): 7 | """A dashboard is a Widget holder usually used as admin entry point. 8 | """ 9 | widgets = [] 10 | 11 | @property 12 | def default_rules(self): 13 | return [('/', 'show', DashboardView.as_view( 14 | 'dashboard', self))] 15 | 16 | 17 | class DashboardWidget(): 18 | """Dashboard widget builder. 19 | """ 20 | def __init__(self, title): 21 | """Initialize a new widget instance. 22 | 23 | :param title: The widget title 24 | """ 25 | self.title = title 26 | 27 | def render(self): 28 | """Returns html content to display. 29 | """ 30 | raise NotImplementedError() 31 | 32 | 33 | class HelloWorldWidget(DashboardWidget): 34 | def render(self): 35 | return '

Hello world!

' 36 | 37 | 38 | class DefaultDashboard(Dashboard): 39 | """Default dashboard.""" 40 | widgets = [HelloWorldWidget('my first dashboard widget')] 41 | -------------------------------------------------------------------------------- /flask_dashed/ext/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeanphix/Flask-Dashed/40e13e8557ca1a4a0515ff96c2356167b9ac71b4/flask_dashed/ext/__init__.py -------------------------------------------------------------------------------- /flask_dashed/ext/sqlalchemy.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import 3 | 4 | from werkzeug import OrderedMultiDict 5 | from flask import url_for 6 | from flask_dashed.admin import ObjectAdminModule 7 | from flask_dashed.views import ObjectFormView 8 | from sqlalchemy.sql.expression import or_ 9 | from wtforms.ext.sqlalchemy.orm import model_form as mf 10 | from flask.ext.wtf import Form 11 | 12 | 13 | def model_form(*args, **kwargs): 14 | """Returns form class for model. 15 | """ 16 | if not 'base_class' in kwargs: 17 | kwargs['base_class'] = Form 18 | return mf(*args, **kwargs) 19 | 20 | 21 | class ModelAdminModule(ObjectAdminModule): 22 | """SQLAlchemy model admin module builder. 23 | """ 24 | model = None 25 | form_view = ObjectFormView 26 | form_class = None 27 | db_session = None 28 | 29 | def __new__(cls, *args, **kwargs): 30 | if not cls.model: 31 | raise Exception('ModelAdminModule must provide `model` attribute') 32 | if not cls.list_fields: 33 | cls.list_fields = OrderedMultiDict() 34 | for column in cls.model.__table__._columns: 35 | cls.list_fields[column.name] = {'label': column.name, 36 | 'column': getattr(cls.model, column.name)} 37 | if not cls.form_class: 38 | cls.form_class = model_form(cls.model, cls.db_session) 39 | return super(ModelAdminModule, cls).__new__(cls, *args, **kwargs) 40 | 41 | def get_object_list(self, search=None, order_by_name=None, 42 | order_by_direction=None, offset=None, limit=None): 43 | """Returns ordered, filtered and limited query. 44 | 45 | :param search: The string for search filter 46 | :param order_by_name: The field name to order by 47 | :param order_by_direction: The field direction 48 | :param offset: The offset position 49 | :param limit: The limit 50 | """ 51 | limit = limit if limit else self.list_per_page 52 | query = self._get_filtered_query(self.list_query_factory, search) 53 | if not (order_by_name and order_by_direction)\ 54 | and self.order_by is not None: 55 | order_by_name = self.order_by[0] 56 | order_by_direction = self.order_by[1] 57 | if order_by_name and order_by_direction: 58 | try: 59 | query = query.order_by( 60 | getattr(self.list_fields[order_by_name]['column'], 61 | order_by_direction)() 62 | ) 63 | except KeyError: 64 | raise Exception('Order by field must be provided in ' + 65 | 'list_fields with a column key') 66 | return query.limit(limit).offset(offset).all() 67 | 68 | def count_list(self, search=None): 69 | """Counts filtered list. 70 | 71 | :param search: The string for quick search 72 | """ 73 | query = self._get_filtered_query(self.list_query_factory, search) 74 | return query.count() 75 | 76 | @property 77 | def list_query_factory(self): 78 | """Returns non filtered list query. 79 | """ 80 | return self.db_session.query(self.model) 81 | 82 | @property 83 | def edit_query_factory(self): 84 | """Returns query for object edition. 85 | """ 86 | return self.db_session.query(self.model).get 87 | 88 | def get_actions_for_object(self, object): 89 | """"Returns actions for object as and tuple list. 90 | 91 | :param object: The object 92 | """ 93 | return [ 94 | ('edit', 'edit', 'Edit object', url_for( 95 | "%s.%s_edit" % (self.admin.blueprint.name, self.endpoint), 96 | pk=object.id)), 97 | ('delete', 'delete', 'Delete object', url_for( 98 | "%s.%s_delete" % (self.admin.blueprint.name, self.endpoint), 99 | pk=object.id)), 100 | ] 101 | 102 | def get_object(self, pk): 103 | """Gets back object by primary key. 104 | 105 | :param pk: The object primary key 106 | """ 107 | obj = self.edit_query_factory(pk) 108 | return obj 109 | 110 | def create_object(self): 111 | """New object instance new object.""" 112 | return self.model() 113 | 114 | def save_object(self, obj): 115 | """Saves object. 116 | 117 | :param object: The object to save 118 | """ 119 | self.db_session.add(obj) 120 | self.db_session.commit() 121 | 122 | def delete_object(self, object): 123 | """Deletes object. 124 | 125 | :param object: The object to delete 126 | """ 127 | self.db_session.delete(object) 128 | self.db_session.commit() 129 | 130 | def _get_filtered_query(self, query, search=None): 131 | """Filters query. 132 | 133 | :param query: The non filtered query 134 | :param search: The string for quick search 135 | """ 136 | if search and self.searchable_fields: 137 | condition = None 138 | for field in self.searchable_fields: 139 | if field in self.list_fields\ 140 | and 'column' in self.list_fields[field]: 141 | if condition is None: 142 | condition = self.list_fields[field]['column'].\ 143 | contains(search) 144 | else: 145 | condition = or_(condition, self.\ 146 | list_fields[field]['column'].contains(search)) 147 | else: 148 | raise Exception('Searchables fields must be in ' + 149 | 'list_fields with specified column.') 150 | query = query.filter(condition) 151 | return query 152 | -------------------------------------------------------------------------------- /flask_dashed/static/css/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css 2012-03-06T10:21 UTC - http://github.com/necolas/normalize.css */ 2 | 3 | /* ============================================================================= 4 | HTML5 display definitions 5 | ========================================================================== */ 6 | 7 | /* 8 | * Corrects block display not defined in IE6/7/8/9 & FF3 9 | */ 10 | 11 | article, 12 | aside, 13 | details, 14 | figcaption, 15 | figure, 16 | footer, 17 | header, 18 | hgroup, 19 | nav, 20 | section, 21 | summary { 22 | display: block; 23 | } 24 | 25 | /* 26 | * Corrects inline-block display not defined in IE6/7/8/9 & FF3 27 | */ 28 | 29 | audio, 30 | canvas, 31 | video { 32 | display: inline-block; 33 | *display: inline; 34 | *zoom: 1; 35 | } 36 | 37 | /* 38 | * Prevents modern browsers from displaying 'audio' without controls 39 | * Remove excess height in iOS5 devices 40 | */ 41 | 42 | audio:not([controls]) { 43 | display: none; 44 | height: 0; 45 | } 46 | 47 | /* 48 | * Addresses styling for 'hidden' attribute not present in IE7/8/9, FF3, S4 49 | * Known issue: no IE6 support 50 | */ 51 | 52 | [hidden] { 53 | display: none; 54 | } 55 | 56 | 57 | /* ============================================================================= 58 | Base 59 | ========================================================================== */ 60 | 61 | /* 62 | * 1. Corrects text resizing oddly in IE6/7 when body font-size is set using em units 63 | * http://clagnut.com/blog/348/#c790 64 | * 2. Prevents iOS text size adjust after orientation change, without disabling user zoom 65 | * www.456bereastreet.com/archive/201012/controlling_text_size_in_safari_for_ios_without_disabling_user_zoom/ 66 | */ 67 | 68 | html { 69 | font-size: 100%; /* 1 */ 70 | -webkit-text-size-adjust: 100%; /* 2 */ 71 | -ms-text-size-adjust: 100%; /* 2 */ 72 | } 73 | 74 | /* 75 | * Addresses font-family inconsistency between 'textarea' and other form elements. 76 | */ 77 | 78 | html, 79 | button, 80 | input, 81 | select, 82 | textarea { 83 | font-family: sans-serif; 84 | } 85 | 86 | /* 87 | * Addresses margins handled incorrectly in IE6/7 88 | */ 89 | 90 | body { 91 | margin: 0; 92 | } 93 | 94 | 95 | /* ============================================================================= 96 | Links 97 | ========================================================================== */ 98 | 99 | /* 100 | * Addresses outline displayed oddly in Chrome 101 | */ 102 | 103 | a:focus { 104 | outline: thin dotted; 105 | } 106 | 107 | /* 108 | * Improves readability when focused and also mouse hovered in all browsers 109 | * people.opera.com/patrickl/experiments/keyboard/test 110 | */ 111 | 112 | a:hover, 113 | a:active { 114 | outline: 0; 115 | } 116 | 117 | 118 | /* ============================================================================= 119 | Typography 120 | ========================================================================== */ 121 | 122 | /* 123 | * Addresses font sizes and margins set differently in IE6/7 124 | * Addresses font sizes within 'section' and 'article' in FF4+, Chrome, S5 125 | */ 126 | 127 | h1 { 128 | font-size: 2em; 129 | margin: 0.67em 0; 130 | } 131 | 132 | h2 { 133 | font-size: 1.5em; 134 | margin: 0.83em 0; 135 | } 136 | 137 | h3 { 138 | font-size: 1.17em; 139 | margin: 1em 0; 140 | } 141 | 142 | h4 { 143 | font-size: 1em; 144 | margin: 1.33em 0; 145 | } 146 | 147 | h5 { 148 | font-size: 0.83em; 149 | margin: 1.67em 0; 150 | } 151 | 152 | h6 { 153 | font-size: 0.75em; 154 | margin: 2.33em 0; 155 | } 156 | 157 | /* 158 | * Addresses styling not present in IE7/8/9, S5, Chrome 159 | */ 160 | 161 | abbr[title] { 162 | border-bottom: 1px dotted; 163 | } 164 | 165 | /* 166 | * Addresses style set to 'bolder' in FF3+, S4/5, Chrome 167 | */ 168 | 169 | b, 170 | strong { 171 | font-weight: bold; 172 | } 173 | 174 | blockquote { 175 | margin: 1em 40px; 176 | } 177 | 178 | /* 179 | * Addresses styling not present in S5, Chrome 180 | */ 181 | 182 | dfn { 183 | font-style: italic; 184 | } 185 | 186 | /* 187 | * Addresses styling not present in IE6/7/8/9 188 | */ 189 | 190 | mark { 191 | background: #ff0; 192 | color: #000; 193 | } 194 | 195 | /* 196 | * Addresses margins set differently in IE6/7 197 | */ 198 | 199 | p, 200 | pre { 201 | margin: 1em 0; 202 | } 203 | 204 | /* 205 | * Corrects font family set oddly in IE6, S4/5, Chrome 206 | * en.wikipedia.org/wiki/User:Davidgothberg/Test59 207 | */ 208 | 209 | pre, 210 | code, 211 | kbd, 212 | samp { 213 | font-family: monospace, serif; 214 | _font-family: 'courier new', monospace; 215 | font-size: 1em; 216 | } 217 | 218 | /* 219 | * Improves readability of pre-formatted text in all browsers 220 | */ 221 | 222 | pre { 223 | white-space: pre; 224 | white-space: pre-wrap; 225 | word-wrap: break-word; 226 | } 227 | 228 | /* 229 | * 1. Addresses CSS quotes not supported in IE6/7 230 | * 2. Addresses quote property not supported in S4 231 | */ 232 | 233 | /* 1 */ 234 | 235 | q { 236 | quotes: none; 237 | } 238 | 239 | /* 2 */ 240 | 241 | q:before, 242 | q:after { 243 | content: ''; 244 | content: none; 245 | } 246 | 247 | small { 248 | font-size: 75%; 249 | } 250 | 251 | /* 252 | * Prevents sub and sup affecting line-height in all browsers 253 | * gist.github.com/413930 254 | */ 255 | 256 | sub, 257 | sup { 258 | font-size: 75%; 259 | line-height: 0; 260 | position: relative; 261 | vertical-align: baseline; 262 | } 263 | 264 | sup { 265 | top: -0.5em; 266 | } 267 | 268 | sub { 269 | bottom: -0.25em; 270 | } 271 | 272 | 273 | /* ============================================================================= 274 | Lists 275 | ========================================================================== */ 276 | 277 | /* 278 | * Addresses margins set differently in IE6/7 279 | */ 280 | 281 | dl, 282 | menu, 283 | ol, 284 | ul { 285 | margin: 1em 0; 286 | } 287 | 288 | dd { 289 | margin: 0 0 0 40px; 290 | } 291 | 292 | /* 293 | * Addresses paddings set differently in IE6/7 294 | */ 295 | 296 | menu, 297 | ol, 298 | ul { 299 | padding: 0 0 0 40px; 300 | } 301 | 302 | /* 303 | * Corrects list images handled incorrectly in IE7 304 | */ 305 | 306 | nav ul, 307 | nav ol { 308 | list-style: none; 309 | list-style-image: none; 310 | } 311 | 312 | 313 | /* ============================================================================= 314 | Embedded content 315 | ========================================================================== */ 316 | 317 | /* 318 | * 1. Removes border when inside 'a' element in IE6/7/8/9, FF3 319 | * 2. Improves image quality when scaled in IE7 320 | * code.flickr.com/blog/2008/11/12/on-ui-quality-the-little-things-client-side-image-resizing/ 321 | */ 322 | 323 | img { 324 | border: 0; /* 1 */ 325 | -ms-interpolation-mode: bicubic; /* 2 */ 326 | } 327 | 328 | /* 329 | * Corrects overflow displayed oddly in IE9 330 | */ 331 | 332 | svg:not(:root) { 333 | overflow: hidden; 334 | } 335 | 336 | 337 | /* ============================================================================= 338 | Figures 339 | ========================================================================== */ 340 | 341 | /* 342 | * Addresses margin not present in IE6/7/8/9, S5, O11 343 | */ 344 | 345 | figure { 346 | margin: 0; 347 | } 348 | 349 | 350 | /* ============================================================================= 351 | Forms 352 | ========================================================================== */ 353 | 354 | /* 355 | * Corrects margin displayed oddly in IE6/7 356 | */ 357 | 358 | form { 359 | margin: 0; 360 | } 361 | 362 | /* 363 | * Define consistent border, margin, and padding 364 | */ 365 | 366 | fieldset { 367 | border: 1px solid #c0c0c0; 368 | margin: 0 2px; 369 | padding: 0.35em 0.625em 0.75em; 370 | } 371 | 372 | /* 373 | * 1. Corrects color not being inherited in IE6/7/8/9 374 | * 2. Corrects text not wrapping in FF3 375 | * 3. Corrects alignment displayed oddly in IE6/7 376 | */ 377 | 378 | legend { 379 | border: 0; /* 1 */ 380 | padding: 0; 381 | white-space: normal; /* 2 */ 382 | *margin-left: -7px; /* 3 */ 383 | } 384 | 385 | /* 386 | * 1. Corrects font size not being inherited in all browsers 387 | * 2. Addresses margins set differently in IE6/7, FF3+, S5, Chrome 388 | * 3. Improves appearance and consistency in all browsers 389 | */ 390 | 391 | button, 392 | input, 393 | select, 394 | textarea { 395 | font-size: 100%; /* 1 */ 396 | margin: 0; /* 2 */ 397 | vertical-align: baseline; /* 3 */ 398 | *vertical-align: middle; /* 3 */ 399 | } 400 | 401 | /* 402 | * Addresses FF3/4 setting line-height on 'input' using !important in the UA stylesheet 403 | */ 404 | 405 | button, 406 | input { 407 | line-height: normal; /* 1 */ 408 | } 409 | 410 | /* 411 | * 1. Improves usability and consistency of cursor style between image-type 'input' and others 412 | * 2. Corrects inability to style clickable 'input' types in iOS 413 | * 3. Removes inner spacing in IE7 without affecting normal text inputs 414 | * Known issue: inner spacing remains in IE6 415 | */ 416 | 417 | button, 418 | input[type="button"], 419 | input[type="reset"], 420 | input[type="submit"] { 421 | cursor: pointer; /* 1 */ 422 | -webkit-appearance: button; /* 2 */ 423 | *overflow: visible; /* 3 */ 424 | } 425 | 426 | /* 427 | * Re-set default cursor for disabled elements 428 | */ 429 | 430 | button[disabled], 431 | input[disabled] { 432 | cursor: default; 433 | } 434 | 435 | /* 436 | * 1. Addresses box sizing set to content-box in IE8/9 437 | * 2. Removes excess padding in IE8/9 438 | * 3. Removes excess padding in IE7 439 | Known issue: excess padding remains in IE6 440 | */ 441 | 442 | input[type="checkbox"], 443 | input[type="radio"] { 444 | box-sizing: border-box; /* 1 */ 445 | padding: 0; /* 2 */ 446 | *height: 13px; /* 3 */ 447 | *width: 13px; /* 3 */ 448 | } 449 | 450 | /* 451 | * 1. Addresses appearance set to searchfield in S5, Chrome 452 | * 2. Addresses box-sizing set to border-box in S5, Chrome (include -moz to future-proof) 453 | */ 454 | 455 | input[type="search"] { 456 | -webkit-appearance: textfield; /* 1 */ 457 | -moz-box-sizing: content-box; 458 | -webkit-box-sizing: content-box; /* 2 */ 459 | box-sizing: content-box; 460 | } 461 | 462 | /* 463 | * Removes inner padding and search cancel button in S5, Chrome on OS X 464 | */ 465 | 466 | input[type="search"]::-webkit-search-decoration, 467 | input[type="search"]::-webkit-search-cancel-button { 468 | -webkit-appearance: none; 469 | } 470 | 471 | /* 472 | * Removes inner padding and border in FF3+ 473 | * www.sitepen.com/blog/2008/05/14/the-devils-in-the-details-fixing-dojos-toolbar-buttons/ 474 | */ 475 | 476 | button::-moz-focus-inner, 477 | input::-moz-focus-inner { 478 | border: 0; 479 | padding: 0; 480 | } 481 | 482 | /* 483 | * 1. Removes default vertical scrollbar in IE6/7/8/9 484 | * 2. Improves readability and alignment in all browsers 485 | */ 486 | 487 | textarea { 488 | overflow: auto; /* 1 */ 489 | vertical-align: top; /* 2 */ 490 | } 491 | 492 | 493 | /* ============================================================================= 494 | Tables 495 | ========================================================================== */ 496 | 497 | /* 498 | * Remove most spacing between table cells 499 | */ 500 | 501 | table { 502 | border-collapse: collapse; 503 | border-spacing: 0; 504 | } 505 | -------------------------------------------------------------------------------- /flask_dashed/static/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: #333; 3 | font-size: 14px; 4 | background: #343434; 5 | } 6 | a, 7 | a:visited { 8 | color: #00f; 9 | } 10 | header > h1, 11 | body > nav > ul, 12 | body > section > div, 13 | #main-navigation, 14 | footer { 15 | display: block; 16 | width: auto; 17 | padding-left: 30px; 18 | padding-right: 30px; 19 | } 20 | header { 21 | background-color: #474747; 22 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #555), color-stop(1, #333)); 23 | background-image: -webkit-linear-gradient(top, #555 0%, #333 100%); 24 | background-image: -moz-linear-gradient(top, #555 0%, #333 100%); 25 | background-image: -o-linear-gradient(top, #555 0%, #333 100%); 26 | background-image: -ms-linear-gradient(top, #555 0%, #333 100%); 27 | background-image: linear-gradient(top, #555 0%, #333 100%); 28 | } 29 | header > h1 { 30 | margin: 0; 31 | font-size: 120%; 32 | line-height: 3em; 33 | font-weight: normal; 34 | text-shadow: 1px 1px #333; 35 | } 36 | header > h1 > a, 37 | header > h1 > a:visited { 38 | color: #fff; 39 | text-decoration: none; 40 | } 41 | body > nav { 42 | background: #f1f1f1; 43 | border-bottom: 1px #ccc solid; 44 | line-height: 2em; 45 | } 46 | body > nav > ul { 47 | margin: 0; 48 | } 49 | body > nav > ul > li { 50 | display: inline-block; 51 | } 52 | body > nav > ul > li > a, 53 | body > nav > ul > li > a:visited { 54 | color: inherit; 55 | } 56 | body > nav > ul > li:not(:last-child):after { 57 | content: ">"; 58 | margin: 0 0 0 0.5em; 59 | } 60 | body > section { 61 | padding-top: 1em; 62 | background: url("../images/background.png") #ececec; 63 | } 64 | body > section h1, 65 | body > section h2 { 66 | text-shadow: 1px 1px #fff; 67 | } 68 | body > section h1 { 69 | font-size: 120%; 70 | } 71 | body > section h2 { 72 | margin: 1em 0; 73 | font-size: 100%; 74 | } 75 | body > section > div > *first-child { 76 | margin-top: 0; 77 | } 78 | body > section .flashes { 79 | padding: 0; 80 | } 81 | body > section .flashes > li { 82 | margin-top: 1em; 83 | margin-bottom: 1em; 84 | padding: 1em; 85 | -webkit-border-radius: 0.5em; 86 | border-radius: 0.5em; 87 | margin-top: 0; 88 | list-style: none; 89 | } 90 | body > section .flashes > li > *first-child { 91 | margin-top: 0; 92 | } 93 | body > section .flashes > li > *last-child { 94 | margin-bottom: 0; 95 | } 96 | body > section .flashes > li.success { 97 | color: #119e00; 98 | border: 1px #119e00 solid; 99 | background: #bcffbc; 100 | } 101 | body > section .flashes > li.error { 102 | color: #f00; 103 | border: 1px #f00 solid; 104 | background: #ffc6c6; 105 | } 106 | body > section p.actions a, 107 | body > section p.actions input { 108 | display: inline-block; 109 | padding: 0 1em; 110 | line-height: 2em; 111 | height: 2em; 112 | color: inherit; 113 | border: 1px #ccc solid; 114 | -webkit-border-radius: 0.3em; 115 | border-radius: 0.3em; 116 | -webkit-box-shadow: 0 1px 1px rgba(255,255,255,0.75); 117 | box-shadow: 0 1px 1px rgba(255,255,255,0.75); 118 | text-decoration: none; 119 | cursor: pointer; 120 | background-color: #f5f5f5; 121 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #fff), color-stop(1, #e6e6e6)); 122 | background-image: -webkit-linear-gradient(top, #fff 0%, #e6e6e6 100%); 123 | background-image: -moz-linear-gradient(top, #fff 0%, #e6e6e6 100%); 124 | background-image: -o-linear-gradient(top, #fff 0%, #e6e6e6 100%); 125 | background-image: -ms-linear-gradient(top, #fff 0%, #e6e6e6 100%); 126 | background-image: linear-gradient(top, #fff 0%, #e6e6e6 100%); 127 | } 128 | body > section p.actions a:hover, 129 | body > section p.actions a:focus, 130 | body > section p.actions a:active, 131 | body > section p.actions input:hover, 132 | body > section p.actions input:focus, 133 | body > section p.actions input:active { 134 | color: inherit; 135 | background-color: #efefef; 136 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #fafafa), color-stop(1, #e0e0e0)); 137 | background-image: -webkit-linear-gradient(top, #fafafa 0%, #e0e0e0 100%); 138 | background-image: -moz-linear-gradient(top, #fafafa 0%, #e0e0e0 100%); 139 | background-image: -o-linear-gradient(top, #fafafa 0%, #e0e0e0 100%); 140 | background-image: -ms-linear-gradient(top, #fafafa 0%, #e0e0e0 100%); 141 | background-image: linear-gradient(top, #fafafa 0%, #e0e0e0 100%); 142 | } 143 | body > section .widget { 144 | margin-top: 0; 145 | margin-bottom: 1em; 146 | padding: 1em; 147 | -webkit-border-radius: 0.5em; 148 | border-radius: 0.5em; 149 | color: inherit; 150 | border: 1px #ccc solid; 151 | background: #f5f5f5; 152 | } 153 | body > section .widget > *:first-child { 154 | margin-top: 0; 155 | } 156 | body > section .widget > *:last-child { 157 | margin-bottom: 0; 158 | } 159 | body > section #search-form * { 160 | display: inline-block; 161 | } 162 | body > section #search-form fieldset { 163 | padding: 0; 164 | border: 0; 165 | } 166 | body > section #main-navigation { 167 | border-top: 1px #ccc solid; 168 | padding-top: 1em; 169 | padding-bottom: 1em; 170 | background: #343434; 171 | } 172 | body > section #main-navigation ul { 173 | margin: 0; 174 | list-style: none; 175 | } 176 | body > section #main-navigation .active > a, 177 | body > section #main-navigation .active > span { 178 | text-decoration: none; 179 | color: #06dde0; 180 | } 181 | body > section #main-navigation .adminnode > span { 182 | position: relative; 183 | margin-left: 1em; 184 | } 185 | body > section #main-navigation .adminnode > span:before { 186 | content: ""; 187 | display: block; 188 | position: absolute; 189 | top: 0.7em; 190 | margin-left: -1em; 191 | border-left: 5px solid transparent; 192 | border-right: 5px solid transparent; 193 | border-top: 10px solid #555; 194 | } 195 | body > section #main-navigation > ul { 196 | padding: 0; 197 | } 198 | body > section #main-navigation > ul > li { 199 | margin: 0 0 1em 0; 200 | line-height: 2em; 201 | } 202 | body > section #main-navigation > ul a, 203 | body > section #main-navigation > ul span { 204 | display: block; 205 | } 206 | body > section form > fieldset { 207 | margin-top: 1em; 208 | margin-bottom: 1em; 209 | padding: 1em; 210 | -webkit-border-radius: 0.5em; 211 | border-radius: 0.5em; 212 | color: inherit; 213 | border: 1px #ccc solid; 214 | background: transparent; 215 | } 216 | body > section form > fieldset > *:first-child { 217 | margin-top: 0; 218 | } 219 | body > section form > fieldset > *:last-child { 220 | margin-bottom: 0; 221 | } 222 | body > section form > fieldset p { 223 | margin-top: 0; 224 | } 225 | body > section #counter, 226 | body > section #pager, 227 | body > section #pager li { 228 | display: inline; 229 | } 230 | #main-navigation, 231 | footer { 232 | color: #fff; 233 | } 234 | #main-navigation a, 235 | footer a { 236 | color: inherit; 237 | } 238 | #main-navigation a:hover, 239 | footer a:hover, 240 | #main-navigation a:active, 241 | footer a:active, 242 | #main-navigation a:focus, 243 | footer a:focus { 244 | background: #444; 245 | } 246 | @media screen and (min-width: 1000px) { 247 | body > section > div > div { 248 | padding: 0; 249 | margin-top: 1em; 250 | margin-bottom: 0; 251 | } 252 | body > section > div > div .widget { 253 | display: block; 254 | float: left; 255 | -webkit-box-sizing: border-box; 256 | -moz-box-sizing: border-box; 257 | box-sizing: border-box; 258 | margin-right: 2%; 259 | margin-top: 0; 260 | width: 49%; 261 | margin-bottom: 1em; 262 | } 263 | body > section > div > div .widget:last-child { 264 | margin-right: 0; 265 | } 266 | body > section > div > div:after { 267 | clear: both; 268 | display: table; 269 | content: ""; 270 | } 271 | } 272 | form { 273 | line-height: 2em; 274 | } 275 | form fieldset { 276 | padding: 0; 277 | border: 0; 278 | } 279 | form fieldset input, 280 | form fieldset textarea, 281 | form fieldset select { 282 | display: inline-block; 283 | -webkit-box-sizing: border-box; 284 | -moz-box-sizing: border-box; 285 | box-sizing: border-box; 286 | width: 250px; 287 | padding: 0.3em; 288 | border: 1px #ccc solid; 289 | -webkit-border-radius: 3px; 290 | border-radius: 3px; 291 | background: #fff; 292 | } 293 | form fieldset input[type=checkbox] { 294 | width: auto; 295 | } 296 | form input[type=submit] { 297 | display: inline-block; 298 | padding: 0 1em; 299 | line-height: 2em; 300 | height: 2em; 301 | color: inherit; 302 | border: 1px #ccc solid; 303 | -webkit-border-radius: 0.3em; 304 | border-radius: 0.3em; 305 | -webkit-box-shadow: 0 1px 1px rgba(255,255,255,0.75); 306 | box-shadow: 0 1px 1px rgba(255,255,255,0.75); 307 | text-decoration: none; 308 | cursor: pointer; 309 | background-color: #f5f5f5; 310 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #fff), color-stop(1, #e6e6e6)); 311 | background-image: -webkit-linear-gradient(top, #fff 0%, #e6e6e6 100%); 312 | background-image: -moz-linear-gradient(top, #fff 0%, #e6e6e6 100%); 313 | background-image: -o-linear-gradient(top, #fff 0%, #e6e6e6 100%); 314 | background-image: -ms-linear-gradient(top, #fff 0%, #e6e6e6 100%); 315 | background-image: linear-gradient(top, #fff 0%, #e6e6e6 100%); 316 | } 317 | form input[type=submit]:hover, 318 | form input[type=submit]:focus, 319 | form input[type=submit]:active { 320 | color: inherit; 321 | background-color: #efefef; 322 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #fafafa), color-stop(1, #e0e0e0)); 323 | background-image: -webkit-linear-gradient(top, #fafafa 0%, #e0e0e0 100%); 324 | background-image: -moz-linear-gradient(top, #fafafa 0%, #e0e0e0 100%); 325 | background-image: -o-linear-gradient(top, #fafafa 0%, #e0e0e0 100%); 326 | background-image: -ms-linear-gradient(top, #fafafa 0%, #e0e0e0 100%); 327 | background-image: linear-gradient(top, #fafafa 0%, #e0e0e0 100%); 328 | } 329 | form label { 330 | display: block; 331 | } 332 | form .errors { 333 | margin-top: 0; 334 | padding: 0; 335 | } 336 | form .errors > li { 337 | list-style-position: inside; 338 | color: #f00; 339 | } 340 | form .required:after { 341 | content: "*"; 342 | right: 0.5em; 343 | } 344 | @media screen and (min-width: 610px) { 345 | form label { 346 | display: block; 347 | float: left; 348 | width: 250px; 349 | } 350 | } 351 | table { 352 | max-width: 100%; 353 | margin: 1em 0; 354 | border-collapse: separate; 355 | border-spacing: 0; 356 | } 357 | table tbody > tr:nth-child(odd) { 358 | background: #fafafa; 359 | } 360 | table thead > tr:first-child > th { 361 | border-top: 0; 362 | } 363 | table th { 364 | font-weight: normal; 365 | } 366 | table th, 367 | table td { 368 | margin-right: 1em; 369 | padding: 0.5em 1em; 370 | text-align: left; 371 | border-top: #ddd 1px solid; 372 | } 373 | table th a:visited, 374 | table th a { 375 | color: inherit; 376 | } 377 | table .asc, 378 | table .desc { 379 | position: relative; 380 | } 381 | table .asc > a, 382 | table .desc > a { 383 | padding: 0 0 0 1em; 384 | } 385 | table .asc > a:before, 386 | table .desc > a:before { 387 | content: ""; 388 | display: block; 389 | position: absolute; 390 | top: 0.8em; 391 | border-left: 5px solid transparent; 392 | border-right: 5px solid transparent; 393 | } 394 | table .desc > a:before { 395 | border-top: 10px solid #555; 396 | } 397 | table .asc > a:before { 398 | border-bottom: 10px solid #555; 399 | } 400 | -------------------------------------------------------------------------------- /flask_dashed/static/css/style.styl: -------------------------------------------------------------------------------- 1 | @import "vendor/nib" 2 | 3 | body 4 | color #333 5 | font-size 14px 6 | background #343434 7 | 8 | a 9 | a:visited 10 | color blue 11 | 12 | header > h1 13 | body > nav > ul 14 | body > section > div 15 | #main-navigation 16 | footer 17 | display block 18 | width auto 19 | padding-left 30px 20 | padding-right 30px 21 | 22 | header 23 | background-color #474747 24 | background-image linear-gradient(top, #555, #333) 25 | > h1 26 | margin 0 27 | font-size 120% 28 | line-height 3em 29 | font-weight normal 30 | text-shadow 1px 1px #333 31 | > a 32 | > a:visited 33 | color #fff 34 | text-decoration none 35 | 36 | body > nav 37 | background #f1f1f1 38 | border-bottom 1px #ccc solid 39 | line-height 2em 40 | > ul 41 | margin 0 42 | > li 43 | display inline-block 44 | > a 45 | > a:visited 46 | color inherit 47 | > li:not(:last-child):after 48 | content ">" 49 | margin 0 0 0 0.5em 50 | 51 | body > section 52 | padding-top 1em 53 | background url(../images/background.png) #ececec 54 | h1 55 | h2 56 | text-shadow 1px 1px #fff 57 | h1 58 | font-size 120% 59 | h2 60 | margin 1em 0 61 | font-size 100% 62 | > div > *first-child 63 | margin-top 0 64 | .flashes 65 | padding 0 66 | > li 67 | margin-top 1em 68 | margin-bottom 1em 69 | padding 1em 70 | border-radius(0.5em) 71 | margin-top 0 72 | list-style none 73 | > *first-child 74 | margin-top 0 75 | > *last-child 76 | margin-bottom 0 77 | > li.success 78 | color #119e00 79 | border 1px #119e00 solid 80 | background #bcffbc 81 | > li.error 82 | color red 83 | border 1px red solid 84 | background #ffc6c6 85 | p.actions 86 | a 87 | input 88 | display inline-block 89 | padding 0 1em 90 | line-height 2em 91 | height 2em 92 | color inherit 93 | border 1px #cccccc solid 94 | border-radius(0.3em) 95 | box-shadow(0 1px 1px rgba(255, 255, 255, 0.75)) 96 | text-decoration none 97 | cursor pointer 98 | background-color #f5f5f5 99 | background-image linear-gradient(top, #fff, #e6e6e6) 100 | a:hover, 101 | a:focus, 102 | a:active, 103 | input:hover, 104 | input:focus, 105 | input:active 106 | color inherit 107 | background-color #efefef 108 | background-image linear-gradient(top, #fafafa, #e0e0e0) 109 | .widget 110 | margin-top 0 111 | margin-bottom 1em 112 | padding 1em 113 | border-radius(0.5em) 114 | color inherit 115 | border 1px #cccccc solid 116 | background #f5f5f5 117 | > *:first-child 118 | margin-top 0 119 | > *:last-child 120 | margin-bottom 0 121 | #search-form * 122 | display inline-block 123 | #search-form fieldset 124 | padding 0 125 | border 0 126 | #main-navigation 127 | border-top 1px #ccc solid 128 | padding-top 1em 129 | padding-bottom 1em 130 | background #343434 131 | ul 132 | margin 0 133 | list-style none 134 | .active > a 135 | .active > span 136 | text-decoration none 137 | color #06dde0 138 | .adminnode > span 139 | position relative 140 | margin-left 1em 141 | .adminnode > span:before 142 | content "" 143 | display block 144 | position absolute 145 | top 0.7em 146 | margin-left -1em 147 | border-left 5px solid transparent 148 | border-right 5px solid transparent 149 | border-top 10px solid #555 150 | > ul 151 | padding 0 152 | > li 153 | margin 0 0 1em 0 154 | line-height 2em 155 | a, 156 | span 157 | display block 158 | 159 | form > fieldset 160 | margin-top 1em 161 | margin-bottom 1em 162 | padding 1em 163 | border-radius(0.5em) 164 | color inherit 165 | border 1px #cccccc solid 166 | background transparent 167 | > *:first-child 168 | margin-top 0 169 | > *:last-child 170 | margin-bottom 0 171 | p 172 | margin-top 0 173 | #counter 174 | #pager 175 | #pager li 176 | display inline 177 | 178 | #main-navigation 179 | footer 180 | color #fff 181 | a 182 | color inherit 183 | a:hover, 184 | a:active, 185 | a:focus 186 | background #444 187 | 188 | @media screen and (min-width: 1000px) 189 | body > section > div 190 | > div 191 | padding 0 192 | margin-top 1em 193 | margin-bottom 0 194 | .widget 195 | display block 196 | float left 197 | box-sizing(border-box) 198 | margin-right 2% 199 | margin-top 0 200 | width 49% 201 | margin-bottom 1em 202 | .widget:last-child 203 | margin-right 0 204 | > div:after 205 | clear both 206 | display table 207 | content "" 208 | 209 | form 210 | line-height 2em 211 | fieldset 212 | padding 0 213 | border 0 214 | input 215 | textarea 216 | select 217 | display inline-block 218 | box-sizing(border-box) 219 | width 250px 220 | padding 0.3em 221 | border 1px #cccccc solid 222 | border-radius(3px) 223 | background white 224 | input[type=checkbox] 225 | width auto 226 | input[type=submit] 227 | display inline-block 228 | padding 0 1em 229 | line-height 2em 230 | height 2em 231 | color inherit 232 | border 1px #cccccc solid 233 | border-radius(0.3em) 234 | box-shadow(0 1px 1px rgba(255, 255, 255, 0.75)) 235 | text-decoration none 236 | cursor pointer 237 | background-color #f5f5f5 238 | background-image linear-gradient(top, #fff, #e6e6e6) 239 | input[type=submit]:hover 240 | input[type=submit]:focus 241 | input[type=submit]:active 242 | color inherit 243 | background-color #efefef 244 | background-image linear-gradient(top, #fafafa, #e0e0e0) 245 | label 246 | display block 247 | .errors 248 | margin-top 0 249 | padding 0 250 | > li 251 | list-style-position inside 252 | color #f00 253 | .required:after 254 | content "*" 255 | right 0.5em 256 | 257 | @media screen and (min-width: 610px) 258 | form label 259 | display block 260 | float left 261 | width 250px 262 | 263 | table 264 | max-width 100% 265 | margin 1em 0 266 | border-collapse separate 267 | border-spacing 0 268 | tbody > tr:nth-child(odd) 269 | background #fafafa 270 | thead > tr:first-child > th 271 | border-top 0 272 | th 273 | font-weight normal 274 | th 275 | td 276 | margin-right 1em 277 | padding 0.5em 1em 278 | text-align left 279 | border-top #dddddd 1px solid 280 | th a:visited 281 | th a 282 | color inherit 283 | .asc 284 | .desc 285 | position relative 286 | > a 287 | padding 0 0 0 1em 288 | > a:before 289 | content "" 290 | display block 291 | position absolute 292 | top 0.8em 293 | border-left 5px solid transparent 294 | border-right 5px solid transparent 295 | .desc > a:before 296 | border-top 10px solid #555 297 | .asc > a:before 298 | border-bottom 10px solid #555 299 | -------------------------------------------------------------------------------- /flask_dashed/static/images/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeanphix/Flask-Dashed/40e13e8557ca1a4a0515ff96c2356167b9ac71b4/flask_dashed/static/images/background.png -------------------------------------------------------------------------------- /flask_dashed/templates/flask_dashed/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {% block title %}Welcome to flask dashed{% endblock %} 7 | 8 | 9 | 10 | 11 |
12 | {% include 'flask_dashed/header.html' %} 13 |
14 | {% if module %} 15 | {% include 'flask_dashed/breadcrumbs.html' %} 16 | {% endif %} 17 |
18 |
19 | {% if get_flashed_messages(with_categories=true) %} 20 |
    21 | {% for category, message in get_flashed_messages(with_categories=true) %} 22 |
  • {{ message }}
  • 23 | {% endfor %} 24 |
25 | {% endif %} 26 | {% block content %}

Welcome to flask admin

{% endblock %} 27 |
28 | {% include 'flask_dashed/navigation.html' %} 29 |
30 |
31 | {% include 'flask_dashed/footer.html' %} 32 |
33 | 34 | 35 | -------------------------------------------------------------------------------- /flask_dashed/templates/flask_dashed/breadcrumbs.html: -------------------------------------------------------------------------------- 1 | 19 | -------------------------------------------------------------------------------- /flask_dashed/templates/flask_dashed/dashboard.html: -------------------------------------------------------------------------------- 1 | {% extends 'flask_dashed/base.html' %} 2 | 3 | {% block content %} 4 |
5 | {% for widget in module.widgets %} 6 |
7 |

{{ widget.title }}

8 |
{{ widget.render()|safe }}
9 |
10 | {% endfor %} 11 |
12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /flask_dashed/templates/flask_dashed/edit.html: -------------------------------------------------------------------------------- 1 | {% extends 'flask_dashed/base.html' %} 2 | 3 | {% from "flask_dashed/form.html" import render_form %} 4 | 5 | {% block title %} 6 | {% if is_new %} 7 | {{ module.new_title }} 8 | {% else %} 9 | {{ module.edit_title }} 10 | {% endif %} 11 | {% endblock %} 12 | 13 | {% block content %} 14 |

15 | {% if is_new %} 16 | {{ module.new_title }} 17 | {% else %} 18 | {{ module.edit_title }} 19 | {% endif %} 20 |

21 |
22 | {{ render_form(form) }} 23 |

24 | 25 |

26 |
27 | {% endblock %} -------------------------------------------------------------------------------- /flask_dashed/templates/flask_dashed/footer.html: -------------------------------------------------------------------------------- 1 | Generated with Flask-Dashed -------------------------------------------------------------------------------- /flask_dashed/templates/flask_dashed/form.html: -------------------------------------------------------------------------------- 1 | {% macro render_form(form, legend=None, child=False) %} 2 | {% if not child %} 3 | {{ form.csrf_token }} 4 | {% if form.csrf_token.errors %} 5 |
    6 | {% for error in form.csrf.errors %}
  • {{ error }}{% endfor %} 7 |
8 | {% endif %} 9 | {% endif %} 10 |
11 | {% if legend %}

{{ legend }}

{% endif %} 12 | {% for field in form %} 13 | {% if field.widget.__class__.__name__ == "HiddenInput" %} 14 | {{ field }} 15 | {% elif field.type == "FormField" %} 16 |
17 | {{ render_form(field, field.label.text, True) }} 18 | {% if not loop.last %}
{% endif %} 19 | {% else %} 20 | {% if field.errors %} 21 |
    22 | {% for error in field.errors %}
  • {{ error }}{% endfor %} 23 |
24 | {% endif %} 25 |

26 | {{ field.label }} 27 | {{ field }} 28 |

29 | {% endif %} 30 | {% endfor %} 31 | {% if not child %}
{% endif %} 32 | {% endmacro %} 33 | -------------------------------------------------------------------------------- /flask_dashed/templates/flask_dashed/header.html: -------------------------------------------------------------------------------- 1 |

{% block admin_title %}{{ admin.title }}{% endblock %}

2 | -------------------------------------------------------------------------------- /flask_dashed/templates/flask_dashed/list.html: -------------------------------------------------------------------------------- 1 | {% extends 'flask_dashed/base.html' %} 2 | 3 | {% block title %}{{ module.list_title }}{% endblock %} 4 | 5 | {% block help %}{{ module.user_doc }}{% endblock %} 6 | 7 | {% block content %} 8 |

{{ module.list_title }}

9 | {% if module.searchable_fields %} 10 |
11 |
12 | 13 |
14 | 15 |
16 | {% endif %} 17 | {% if objects %} 18 | 19 | 20 | 21 | {% for field in module.list_fields %} 22 | {% if module.list_fields[field].column %} 23 | {% if 'orderby' in request.args and request.args.orderby==field %} 24 | {% set current_dir=request.args.orderdir %} 25 | {% if current_dir == 'asc' %} 26 | {% set target_dir='desc' %} 27 | {% else %} 28 | {% set target_dir='asc' %} 29 | {% endif %} 30 | {% else %} 31 | {% set current_dir='' %} 32 | {% set target_dir='asc' %} 33 | {% endif %} 34 | {% endif %} 35 | 36 | {% endfor %} 37 | 38 | 39 | 40 | 41 | {% for object in objects %} 42 | 43 | {% for field in module.list_fields %} 44 | 58 | {% endfor %} 59 | 60 | 61 | {% endfor %} 62 | 63 |
{% if module.list_fields[field].column %}{% endif %}{{ module.list_fields[field].label }}{% if module.list_fields[field].column %}{% endif %}actions
45 | {% if object|recursive_getattr(field) %} 46 | {% with %} 47 | {% set title, url = module.get_action_for_field(field, object) %} 48 | {% if url %} 49 | 50 | {% endif %} 51 | {{ object|recursive_getattr(field) }} 52 | {% if url %} 53 | 54 | {% endif %} 55 | {% endwith %} 56 | {% endif %} 57 | {% for class, link, title, url in module.get_actions_for_object(object) %}{{ link }} {% endfor %}
64 |

{{ objects|length }} / {{ count }}

65 |
    66 | {% for page in pages %} 67 |
  • 68 | {% if page==current_page %} 69 | {{ page }} 70 | {% else %} 71 | {{ page }} 72 | {% endif %} 73 |
  • 74 | {% endfor %} 75 |
76 | {% else %} 77 |

no results

78 | {% endif %} 79 |

new

80 | {% endblock %} 81 | -------------------------------------------------------------------------------- /flask_dashed/templates/flask_dashed/navigation.html: -------------------------------------------------------------------------------- 1 | 25 | -------------------------------------------------------------------------------- /flask_dashed/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import 3 | 4 | from functools import wraps 5 | from math import ceil 6 | from flask import render_template, request, flash, redirect, url_for 7 | from flask import abort 8 | from flask.views import MethodView 9 | 10 | 11 | def get_next_or(url): 12 | """Returns next request args or url. 13 | """ 14 | return request.args['next'] if 'next' in request.args else url 15 | 16 | 17 | def secure(endpoint, function, http_code): 18 | """Secures view function. 19 | """ 20 | def decorator(view_func): 21 | @wraps(view_func) 22 | def _wrapped_view(self, *args, **kwargs): 23 | if not function(self, *args, **kwargs): 24 | return abort(http_code) 25 | return view_func(self, *args, **kwargs) 26 | return _wrapped_view 27 | return decorator 28 | 29 | 30 | class AdminModuleMixin(object): 31 | """Provides admin node. 32 | 33 | :param admin_module: The admin module 34 | """ 35 | def __init__(self, admin_module): 36 | self.admin_module = admin_module 37 | 38 | 39 | class DashboardView(MethodView, AdminModuleMixin): 40 | """Displays user dashboard. 41 | 42 | :param admin_module: The admin module 43 | """ 44 | def get(self): 45 | return render_template('flask_dashed/dashboard.html', 46 | admin=self.admin_module.admin, module=self.admin_module) 47 | 48 | 49 | def compute_args(request, update={}): 50 | """Merges all view_args and request args then update with 51 | user args. 52 | 53 | :param update: The user args 54 | """ 55 | args = request.view_args.copy() 56 | args = dict(dict(request.args.to_dict(flat=True)), **args) 57 | args = dict(args, **update) 58 | return args 59 | 60 | 61 | class ObjectListView(MethodView, AdminModuleMixin): 62 | """Lists objects. 63 | 64 | :param admin_module: the admin module 65 | """ 66 | def get(self, page=1): 67 | """Displays object list. 68 | 69 | :param page: The current page index 70 | """ 71 | page = int(page) 72 | search = request.args.get('search', None) 73 | order_by = request.args.get('orderby', None) 74 | order_direction = request.args.get('orderdir', None) 75 | count = self.admin_module.count_list(search=search) 76 | return render_template( 77 | self.admin_module.list_template, 78 | admin=self.admin_module.admin, 79 | module=self.admin_module, 80 | objects=self.admin_module.get_object_list( 81 | search=search, 82 | offset=self.admin_module.list_per_page * (page - 1), 83 | limit=self.admin_module.list_per_page, 84 | order_by_name=order_by, 85 | order_by_direction=order_direction, 86 | ), 87 | count=count, 88 | current_page=page, 89 | pages=self.iter_pages(count, page), 90 | compute_args=compute_args 91 | ) 92 | 93 | def iter_pages(self, count, current_page, left_edge=2, 94 | left_current=2, right_current=5, right_edge=2): 95 | per_page = self.admin_module.list_per_page 96 | pages = int(ceil(count / float(per_page))) 97 | last = 0 98 | for num in xrange(1, pages + 1): 99 | if num <= left_edge or \ 100 | (num > current_page - left_current - 1 and \ 101 | num < current_page + right_current) or \ 102 | num > pages - right_edge: 103 | if last + 1 != num: 104 | yield None 105 | yield num 106 | last = num 107 | 108 | 109 | class ObjectFormView(MethodView, AdminModuleMixin): 110 | """Creates or updates object. 111 | 112 | :param admin_module: The admin module 113 | """ 114 | def get(self, pk=None): 115 | """Displays form. 116 | 117 | :param pk: The object primary key 118 | """ 119 | obj = self.object 120 | if pk and obj is None: 121 | abort(404) 122 | is_new = pk is None 123 | form = self.admin_module.get_form(obj) 124 | return render_template( 125 | self.admin_module.edit_template, 126 | admin=self.admin_module.admin, 127 | module=self.admin_module, 128 | object=obj, 129 | form=form, 130 | is_new=is_new 131 | ) 132 | 133 | def post(self, pk=None): 134 | """Process form. 135 | 136 | :param pk: The object primary key 137 | """ 138 | obj = self.object 139 | if pk and obj is None: 140 | abort(404) 141 | is_new = pk is None 142 | form = self.admin_module.get_form(obj) 143 | form.process(request.form) 144 | if form.validate(): 145 | form.populate_obj(obj) 146 | self.admin_module.save_object(obj) 147 | if is_new: 148 | flash("Object successfully created", "success") 149 | else: 150 | flash("Object successfully updated", "success") 151 | return redirect(get_next_or(url_for(".%s_%s" % 152 | (self.admin_module.endpoint, 'list')))) 153 | else: 154 | flash("Can't save object due to errors", "error") 155 | return render_template( 156 | self.admin_module.edit_template, 157 | admin=self.admin_module.admin, 158 | module=self.admin_module, 159 | object=obj, 160 | form=form, 161 | is_new=is_new 162 | ) 163 | 164 | @property 165 | def object(self): 166 | """Gets object required by the form. 167 | 168 | :param pk: The object primary key 169 | """ 170 | if not hasattr(self, '_object'): 171 | if 'pk' in request.view_args: 172 | self._object = self.admin_module.get_object( 173 | request.view_args['pk']) 174 | else: 175 | self._object = self.admin_module.create_object() 176 | return self._object 177 | 178 | 179 | class ObjectDeleteView(MethodView, AdminModuleMixin): 180 | """Deletes object. 181 | 182 | :param admin_module: the admin module 183 | """ 184 | def get(self, pk): 185 | """Deletes object at given pk. 186 | 187 | :param pk: The primary key 188 | """ 189 | obj = self.admin_module.get_object(pk) 190 | self.admin_module.delete_object(obj) 191 | flash("Object successfully deleted", "success") 192 | return redirect(get_next_or(url_for(".%s_%s" % 193 | (self.admin_module.endpoint, 'list')))) 194 | -------------------------------------------------------------------------------- /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-Dashed 3 | ----------- 4 | 5 | Adds a way to easily build admin apps. 6 | 7 | """ 8 | from setuptools import setup, find_packages 9 | 10 | 11 | setup( 12 | name='Flask-Dashed', 13 | version='0.1b2', 14 | url='https://github.com/jeanphix/Flask-Dashed', 15 | license='mit', 16 | author='Jean-Philippe Serafin', 17 | author_email='serafinjp@gmail.com', 18 | description='Adds a way to easily build admin apps', 19 | long_description=__doc__, 20 | data_files=[('', ['README'])], 21 | packages=find_packages(), 22 | include_package_data=True, 23 | zip_safe=False, 24 | platforms='any', 25 | install_requires=[ 26 | 'Flask', 27 | 'WTForms== 1.0.2', 28 | 'Flask-WTF>=0.6', 29 | ], 30 | classifiers=[ 31 | 'Development Status :: 4 - Beta', 32 | 'Environment :: Web Environment', 33 | 'Intended Audience :: Developers', 34 | 'License :: OSI Approved :: MIT License', 35 | 'Operating System :: OS Independent', 36 | 'Programming Language :: Python', 37 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 38 | 'Topic :: Software Development :: Libraries :: Python Modules' 39 | ], 40 | ) 41 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeanphix/Flask-Dashed/40e13e8557ca1a4a0515ff96c2356167b9ac71b4/tests/__init__.py -------------------------------------------------------------------------------- /tests/admin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import unittest 3 | from flask import Flask 4 | from flask.ext.testing import TestCase 5 | from flask_dashed.admin import Admin, AdminModule 6 | 7 | 8 | class DashedTestCase(TestCase): 9 | 10 | def create_app(self): 11 | app = Flask(__name__) 12 | self.admin = Admin(app) 13 | return app 14 | 15 | 16 | class AdminTest(DashedTestCase): 17 | 18 | def test_main_dashboard_view(self): 19 | r = self.client.get(self.admin.root_nodes[0].url) 20 | self.assertEqual(r.status_code, 200) 21 | self.assertIn('Hello world', r.data) 22 | 23 | def test_register_admin_module(self): 24 | self.assertRaises( 25 | NotImplementedError, 26 | self.admin.register_module, 27 | AdminModule, '/my-module', 'my_module', 'my module title' 28 | ) 29 | 30 | def test_register_node(self): 31 | self.admin.register_node('/first-node', 'first_node', 'first node') 32 | self.assertEqual(len(self.admin.root_nodes), 2) 33 | 34 | def test_register_node_wrong_parent(self): 35 | self.assertRaises( 36 | Exception, 37 | self.admin.register_node, 38 | 'first_node', 'first node', parent='undifined' 39 | ) 40 | 41 | def test_register_node_with_parent(self): 42 | parent = self.admin.register_node('/parent', 'first_node', 43 | 'first node') 44 | child = self.admin.register_node('/child', 'child_node', 'child node', 45 | parent=parent) 46 | self.assertEqual(len(self.admin.root_nodes), 2) 47 | self.assertEqual(parent, child.parent) 48 | self.assertEqual(child.url_path, '/parent/child') 49 | self.assertEqual( 50 | child.parents, 51 | [parent] 52 | ) 53 | 54 | def test_children_two_levels(self): 55 | parent = self.admin.register_node('/root', 'first_root_node', 56 | 'first node') 57 | child = self.admin.register_node('/child', 'first_child_node', 58 | 'child node', parent=parent) 59 | second_child = self.admin.register_node('/child', 'second_child_node', 60 | 'child node', parent=child) 61 | self.assertEqual( 62 | parent.children, [child] 63 | ) 64 | self.assertEqual( 65 | child.children, [second_child] 66 | ) 67 | self.assertEqual( 68 | child.parent, parent 69 | ) 70 | self.assertEqual( 71 | second_child.parent, child 72 | ) 73 | 74 | 75 | if __name__ == '__main__': 76 | unittest.main() 77 | -------------------------------------------------------------------------------- /tests/all.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import unittest 3 | 4 | from admin import * 5 | from sqlalchemy_backend import * 6 | 7 | 8 | if __name__ == '__main__': 9 | unittest.main() 10 | 11 | -------------------------------------------------------------------------------- /tests/flask_dashed: -------------------------------------------------------------------------------- 1 | ../flask_dashed/ -------------------------------------------------------------------------------- /tests/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask 2 | hg+https://bitbucket.org/simplecodes/wtforms#egg=WTForms 3 | Flask-SQLAlchemy 4 | Flask-Testing 5 | Flask-WTF 6 | -------------------------------------------------------------------------------- /tests/sqlalchemy_backend.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import unittest 3 | import wtforms 4 | from werkzeug import OrderedMultiDict 5 | from flask import Flask, url_for 6 | from flask.ext.testing import TestCase 7 | from flask.ext.sqlalchemy import SQLAlchemy 8 | from flask_dashed.admin import Admin, ObjectAdminModule 9 | from flask_dashed.ext.sqlalchemy import ModelAdminModule 10 | from wtforms.ext.sqlalchemy.fields import QuerySelectField 11 | from sqlalchemy.orm import aliased, contains_eager 12 | 13 | 14 | app = Flask(__name__) 15 | app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db' 16 | app.config['SECRET_KEY'] = 'secret' 17 | db = SQLAlchemy(app) 18 | admin = Admin(app) 19 | 20 | 21 | class Author(db.Model): 22 | id = db.Column(db.Integer, primary_key=True) 23 | name = db.Column(db.String(255)) 24 | 25 | def __unicode__(self): 26 | return "Autor: %s" % self.name 27 | 28 | 29 | class Book(db.Model): 30 | id = db.Column(db.Integer, primary_key=True) 31 | title = db.Column(db.String(255)) 32 | year = db.Column(db.Integer) 33 | author_id = db.Column(db.Integer, db.ForeignKey('author.id')) 34 | author = db.relationship(Author, primaryjoin=author_id == Author.id, 35 | backref="books") 36 | 37 | 38 | class BaseTest(TestCase): 39 | def setUp(self): 40 | db.create_all() 41 | alain_fournier = Author(name=u"Alain Fournier") 42 | db.session.add(Book(title=u"Le grand Meaulnes", 43 | author=alain_fournier, year=1913)) 44 | db.session.add(Book(title=u"Miracles", 45 | author=alain_fournier, year=1924)) 46 | db.session.add(Book(title=u"Lettres à sa famille", 47 | author=alain_fournier, year=1929)) 48 | db.session.add(Book(title=u"Lettres au petit B.", 49 | author=alain_fournier, year=1930)) 50 | 51 | charles_baudelaire = Author(name=u"Charles Baudelaire") 52 | db.session.add(Book(title=u"La Fanfarlo", 53 | author=charles_baudelaire, year=1847)) 54 | db.session.add(Book(title=u"Du vin et du haschisch", 55 | author=charles_baudelaire, year=1851)) 56 | db.session.add(Book(title=u"Fusées", 57 | author=charles_baudelaire, year=1851)) 58 | db.session.add(Book(title=u"L'Art romantique", 59 | author=charles_baudelaire, year=1852)) 60 | db.session.add(Book(title=u"Morale du joujou", 61 | author=charles_baudelaire, year=1853)) 62 | db.session.add(Book(title=u"Exposition universelle", 63 | author=charles_baudelaire, year=1855)) 64 | db.session.add(Book(title=u"Les Fleurs du mal", 65 | author=charles_baudelaire, year=1857)) 66 | db.session.add(Book(title=u"Le Poème du haschisch", 67 | author=charles_baudelaire, year=1858)) 68 | db.session.add(Book(title=u"Les Paradis artificiels", 69 | author=charles_baudelaire, year=1860)) 70 | db.session.add(Book(title=u"La Chevelure", 71 | author=charles_baudelaire, year=1861)) 72 | db.session.add(Book(title=u"Réflexions sur quelques-uns de " 73 | + "mes contemporains", author=charles_baudelaire, year=1861)) 74 | 75 | albert_camus = Author(name=u"Albert Camus") 76 | db.session.add(Book(title=u"Révolte dans les Asturies", 77 | author=albert_camus, year=1936)) 78 | db.session.add(Book(title=u"L'Envers et l'Endroit", 79 | author=albert_camus, year=1937)) 80 | db.session.add(Book(title=u"Caligula", author=albert_camus, year=1938)) 81 | db.session.add(Book(title=u"Noces", author=albert_camus, year=1939)) 82 | db.session.add(Book(title=u"Le Mythe de Sisyphe", 83 | author=albert_camus, year=1942)) 84 | db.session.add(Book(title=u"L'Étranger", 85 | author=albert_camus, year=1942)) 86 | db.session.add(Book(title=u"Le Malentendu", 87 | author=albert_camus, year=1944)) 88 | db.session.add(Book(title=u"La Peste", author=albert_camus, year=1947)) 89 | db.session.add(Book(title=u"L'État de siège", 90 | author=albert_camus, year=1948)) 91 | db.session.add(Book(title=u"Les Justes", 92 | author=albert_camus, year=1949)) 93 | db.session.add(Book(title=u"L'Homme révolté", 94 | author=albert_camus, year=1951)) 95 | db.session.add(Book(title=u"L'Été", author=albert_camus, year=1954)) 96 | db.session.add(Book(title=u"La Chute", author=albert_camus, year=1956)) 97 | db.session.add(Book(title=u"L'Exil et le Royaume", 98 | author=albert_camus, year=1957)) 99 | 100 | db.session.commit() 101 | 102 | def tearDown(self): 103 | db.session.remove() 104 | db.drop_all() 105 | 106 | 107 | class AutoModelAdminModuleTest(BaseTest): 108 | 109 | class AutoBookModule(ModelAdminModule): 110 | db_session = db.session 111 | model = Book 112 | 113 | class AutoAuthorModule(ModelAdminModule): 114 | db_session = db.session 115 | model = Author 116 | 117 | def create_app(self): 118 | self.book_module = admin.register_module(self.AutoBookModule, 119 | '/book', 'book', 'auto generated book module') 120 | return app 121 | 122 | def test_created_rules(self): 123 | for endpoint in ('.book_list', '.book_edit', '.book_delete',): 124 | self.assertIn(endpoint, str(self.app.url_map)) 125 | 126 | def test_get_objects(self): 127 | objects = self.book_module.get_object_list() 128 | self.assertEqual(len(objects), ObjectAdminModule.list_per_page) 129 | 130 | def test_count_list(self): 131 | self.assertEqual(self.book_module.count_list(), Book.query.count()) 132 | 133 | def test_list_view(self): 134 | r = self.client.get(url_for('admin.book_list')) 135 | self.assertEqual(r.status_code, 200) 136 | 137 | def test_edit_view(self): 138 | r = self.client.get(url_for('admin.book_edit', 139 | pk=Book.query.first().id)) 140 | self.assertEqual(r.status_code, 200) 141 | 142 | def test_secure_node(self): 143 | 144 | @self.book_module.secure(403) 145 | def secure(): 146 | return False 147 | 148 | self.assertIn(self.book_module.url_path, 149 | admin.secure_functions.keys()) 150 | r = self.client.get(url_for('admin.book_list')) 151 | self.assertEqual(r.status_code, 403) 152 | r = self.client.get(url_for('admin.book_new')) 153 | self.assertEqual(r.status_code, 403) 154 | 155 | def test_secure_parent_node(self): 156 | 157 | @self.book_module.secure(403) 158 | def secure(): 159 | return True 160 | 161 | admin.register_module(self.AutoAuthorModule, '/author', 'author', 162 | 'auto generated author module', parent=self.book_module) 163 | self.assertIn(self.book_module.url_path, 164 | admin.secure_functions.keys()) 165 | r = self.client.get(url_for('admin.author_list')) 166 | self.assertEqual(r.status_code, 403) 167 | r = self.client.get(url_for('admin.author_new')) 168 | self.assertEqual(r.status_code, 403) 169 | 170 | def test_secure_module_endpoint(self): 171 | 172 | author_module = admin.register_module(self.AutoAuthorModule, 173 | '/author-again', 'author_again', 'auto generated author module') 174 | 175 | @author_module.secure_endpoint('list', 403) 176 | def secure(view): 177 | return False 178 | 179 | r = self.client.get(url_for('admin.author_again_list')) 180 | self.assertEqual(r.status_code, 403) 181 | r = self.client.get(url_for('admin.author_again_new')) 182 | self.assertEqual(r.status_code, 200) 183 | 184 | 185 | class BookForm(wtforms.Form): 186 | title = wtforms.TextField('Title', [wtforms.validators.required()]) 187 | author = QuerySelectField(query_factory=Author.query.all, 188 | allow_blank=True) 189 | 190 | 191 | class ExplicitModelAdminModuleTest(BaseTest): 192 | 193 | class BookModule(ModelAdminModule): 194 | """Sample module with explicit eager loaded query. 195 | """ 196 | model = Book 197 | db_session = db.session 198 | author_alias = aliased(Author) 199 | 200 | list_fields = OrderedMultiDict(( 201 | ('id', {'label': 'id', 'column': Book.id}), 202 | ('title', {'label': 'book title', 'column': Book.title}), 203 | ('year', {'label': 'year', 'column': Book.year}), 204 | ('author.name', {'label': 'author name', 205 | 'column': author_alias.name}), 206 | )) 207 | list_title = 'books list' 208 | 209 | searchable_fields = ['title', 'author.name'] 210 | 211 | order_by = ('id', 'asc') 212 | 213 | list_query_factory = model.query\ 214 | .outerjoin(author_alias, 'author')\ 215 | .options(contains_eager('author', alias=author_alias))\ 216 | 217 | form_class = BookForm 218 | 219 | def create_app(self): 220 | self.book_module = admin.register_module(self.BookModule, 221 | '/book', 'book', 'auto generated book module') 222 | return app 223 | 224 | def test_filtered_get_objects(self): 225 | objects = self.book_module.get_object_list(search='lettres') 226 | self.assertEqual(len(objects), 2) 227 | 228 | 229 | if __name__ == '__main__': 230 | unittest.main() 231 | --------------------------------------------------------------------------------