├── README
├── tests
├── __init__.py
├── flask_dashed
├── requirements.txt
├── all.py
├── admin.py
└── sqlalchemy_backend.py
├── flask_dashed
├── __init__.py
├── ext
│ ├── __init__.py
│ └── sqlalchemy.py
├── templates
│ └── flask_dashed
│ │ ├── footer.html
│ │ ├── header.html
│ │ ├── dashboard.html
│ │ ├── breadcrumbs.html
│ │ ├── edit.html
│ │ ├── navigation.html
│ │ ├── form.html
│ │ ├── base.html
│ │ └── list.html
├── static
│ ├── images
│ │ └── background.png
│ └── css
│ │ ├── style.styl
│ │ ├── style.css
│ │ └── normalize.css
├── dashboard.py
├── views.py
└── admin.py
├── docs
├── _static
│ ├── screen.png
│ ├── favicon.ico
│ └── screen-edit.png
├── build
│ └── doctrees
│ │ ├── api.doctree
│ │ ├── index.doctree
│ │ └── environment.pickle
├── api.rst
├── index.rst
├── Makefile
├── make.bat
└── conf.py
├── Makefile
├── .gitignore
├── setup.cfg
├── .travis.yml
├── MANIFEST.in
├── .gitmodules
├── setup.py
├── README.rst
└── examples
└── sqlalchemy_backend.py
/README:
--------------------------------------------------------------------------------
1 | README.rst
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/flask_dashed/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/flask_dashed/ext/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/flask_dashed:
--------------------------------------------------------------------------------
1 | ../flask_dashed/
--------------------------------------------------------------------------------
/flask_dashed/templates/flask_dashed/footer.html:
--------------------------------------------------------------------------------
1 | Generated with Flask-Dashed
--------------------------------------------------------------------------------
/docs/_static/screen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeanphix/Flask-Dashed/HEAD/docs/_static/screen.png
--------------------------------------------------------------------------------
/docs/_static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeanphix/Flask-Dashed/HEAD/docs/_static/favicon.ico
--------------------------------------------------------------------------------
/docs/_static/screen-edit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeanphix/Flask-Dashed/HEAD/docs/_static/screen-edit.png
--------------------------------------------------------------------------------
/docs/build/doctrees/api.doctree:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeanphix/Flask-Dashed/HEAD/docs/build/doctrees/api.doctree
--------------------------------------------------------------------------------
/docs/build/doctrees/index.doctree:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeanphix/Flask-Dashed/HEAD/docs/build/doctrees/index.doctree
--------------------------------------------------------------------------------
/docs/build/doctrees/environment.pickle:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeanphix/Flask-Dashed/HEAD/docs/build/doctrees/environment.pickle
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/flask_dashed/static/images/background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeanphix/Flask-Dashed/HEAD/flask_dashed/static/images/background.png
--------------------------------------------------------------------------------
/tests/requirements.txt:
--------------------------------------------------------------------------------
1 | Flask
2 | hg+https://bitbucket.org/simplecodes/wtforms#egg=WTForms
3 | Flask-SQLAlchemy
4 | Flask-Testing
5 | Flask-WTF
6 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/flask_dashed/templates/flask_dashed/header.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
10 | {% endfor %}
11 |
12 | {% endblock %}
13 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/flask_dashed/templates/flask_dashed/breadcrumbs.html:
--------------------------------------------------------------------------------
1 |
19 |
--------------------------------------------------------------------------------
/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 |
27 | {% endblock %}
--------------------------------------------------------------------------------
/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/templates/flask_dashed/navigation.html:
--------------------------------------------------------------------------------
1 |
25 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
17 | {{ render_form(field, field.label.text, True) }}
18 | {% if not loop.last %}{% endif %}
32 | {% endmacro %}
33 |
--------------------------------------------------------------------------------
/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 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
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 | | {% if module.list_fields[field].column %}{% endif %}{{ module.list_fields[field].label }}{% if module.list_fields[field].column %}{% endif %} |
36 | {% endfor %}
37 | actions |
38 |
39 |
40 |
41 | {% for object in objects %}
42 |
43 | {% for field in module.list_fields %}
44 | |
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 | |
58 | {% endfor %}
59 | {% for class, link, title, url in module.get_actions_for_object(object) %}{{ link }} {% endfor %} |
60 |
61 | {% endfor %}
62 |
63 |
64 | {{ objects|length }} / {{ count }}
65 |
76 | {% else %}
77 | no results
78 | {% endif %}
79 | new
80 | {% endblock %}
81 |
--------------------------------------------------------------------------------
/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------