├── tests ├── __init__.py ├── integration │ ├── __init__.py │ ├── templates │ │ ├── index.html │ │ ├── news.html │ │ └── _navbar.html │ ├── ext.py │ ├── app.py │ ├── views.py │ └── test_app.py ├── conftest.py ├── test_navbar.py ├── test_api.py ├── test_item_collection.py └── test_item.py ├── docs ├── .gitignore ├── _static │ └── index-logo@2x.png ├── requirements.txt ├── index.rst ├── Makefile └── conf.py ├── flask_navigation ├── __init__.py ├── signals.py ├── navbar.py ├── api.py ├── utils.py └── item.py ├── examples ├── templates │ ├── simple │ │ ├── home.html │ │ ├── article.html │ │ └── layout.html │ └── nested │ │ ├── pocket-video.html │ │ ├── pocket-article.html │ │ ├── pocket.html │ │ └── home.html ├── simple.py └── nested.py ├── .bumpversion.cfg ├── setup.cfg ├── tox.ini ├── .editorconfig ├── .travis.yml ├── CHANGES ├── .gitignore ├── LICENSE ├── setup.py └── README.rst /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | venv 2 | -------------------------------------------------------------------------------- /tests/integration/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/templates/index.html: -------------------------------------------------------------------------------- 1 | {% include "_navbar.html" %} 2 | 3 | Welcome 4 | -------------------------------------------------------------------------------- /tests/integration/ext.py: -------------------------------------------------------------------------------- 1 | from flask.ext.navigation import Navigation 2 | 3 | 4 | nav = Navigation() 5 | -------------------------------------------------------------------------------- /docs/_static/index-logo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonyseek/flask-navigation/HEAD/docs/_static/index-logo@2x.png -------------------------------------------------------------------------------- /flask_navigation/__init__.py: -------------------------------------------------------------------------------- 1 | from .api import Navigation 2 | 3 | 4 | __all__ = ['Navigation'] 5 | __version__ = '0.2.0' 6 | -------------------------------------------------------------------------------- /examples/templates/simple/home.html: -------------------------------------------------------------------------------- 1 | {% extends 'simple/layout.html' %} 2 | 3 | {% block content %} 4 | Welcome 5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /examples/templates/nested/pocket-video.html: -------------------------------------------------------------------------------- 1 | {% extends 'nested/pocket.html' %} 2 | 3 | {% block content %} 4 | Video 5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | docutils==0.12 2 | Jinja2==2.7.3 3 | MarkupSafe==0.23 4 | Pygments==2.0.2 5 | Sphinx==1.2.3 6 | sphinx-kr-theme==0.2.1 7 | -------------------------------------------------------------------------------- /examples/templates/nested/pocket-article.html: -------------------------------------------------------------------------------- 1 | {% extends 'nested/pocket.html' %} 2 | 3 | {% block content %} 4 | Article 5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /examples/templates/simple/article.html: -------------------------------------------------------------------------------- 1 | {% extends 'simple/layout.html' %} 2 | 3 | {% block content %} 4 | Article NO.{{ aid }} 5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.2.0 3 | files = setup.py flask_navigation/__init__.py docs/conf.py 4 | commit = True 5 | tag = False 6 | 7 | -------------------------------------------------------------------------------- /flask_navigation/signals.py: -------------------------------------------------------------------------------- 1 | from flask.signals import Namespace 2 | 3 | 4 | signals = Namespace() 5 | 6 | 7 | navbar_created = signals.signal('navbar-created') 8 | -------------------------------------------------------------------------------- /tests/integration/templates/news.html: -------------------------------------------------------------------------------- 1 | {% include "_navbar.html" %} 2 | 3 | News :: Page - {{ page }} 4 | 5 | Back 6 | -------------------------------------------------------------------------------- /tests/integration/templates/_navbar.html: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = --cov flask_navigation --pep8 --doctest-modules flask_navigation tests 3 | pep8ignore = 4 | docs/conf.py ALL 5 | docs/_themes/* ALL 6 | [bdist_wheel] 7 | universal = 1 8 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27,py33,py34,pypy 3 | [testenv] 4 | deps = 5 | pytest 6 | pytest-cov 7 | pytest-pep8 8 | mock 9 | WebTest 10 | pyquery 11 | commands = 12 | py.test 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | 8 | [*.py] 9 | indent_style = space 10 | indent_size = 4 11 | 12 | [*.html] 13 | indent_style = space 14 | indent_size = 2 15 | language = jinja 16 | -------------------------------------------------------------------------------- /tests/integration/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | 3 | from .ext import nav 4 | from .views import bp 5 | 6 | 7 | def create_app(): 8 | app = Flask(__name__) 9 | app.config.from_object(Config) 10 | nav.init_app(app) 11 | app.register_blueprint(bp) 12 | return app 13 | 14 | 15 | class Config(object): 16 | DEBUG = True 17 | NEWS_SPECIAL_PAGE = 42 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.3" 5 | - "3.4" 6 | - "pypy" 7 | install: 8 | - "pip install . --use-mirrors" 9 | - "pip install pytest>=2.4.2 -U" 10 | - "pip install pytest-cov pytest-pep8 mock coveralls WebTest pyquery" 11 | script: "py.test" 12 | after_success: 13 | coveralls 14 | branches: 15 | only: 16 | - master 17 | - develop 18 | -------------------------------------------------------------------------------- /examples/templates/nested/pocket.html: -------------------------------------------------------------------------------- 1 | {% extends 'nested/home.html' %} 2 | 3 | {% block navbar %} 4 | {% with bar = nav['pocket'] %} 5 | {{ bar.parent.label }} 6 | 13 | {% endwith %} 14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /examples/templates/simple/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Simple Example 6 | 7 | 8 | {% block navbar %} 9 | 16 | {% endblock %} 17 | {% block content %} 18 | {% endblock %} 19 | 20 | 21 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | Release 0.2.0 (Sep 24, 2014) 2 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 3 | 4 | - Implemented the HTML representation protocol of MarkupSafe. We can render 5 | items in Jinja 2 templates directly now. 6 | - Refactored the ``is_active`` implementation. We can use the extracted APIs 7 | such as ``is_internal`` now. 8 | 9 | Thanks for `Axel Haustant `_ who contributed 10 | those features: 11 | 12 | - Added support to create nested items. 13 | - Added support to access current item from navigation bar. 14 | 15 | 16 | Release 0.1.0 (Apr 22, 2014) 17 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 18 | 19 | - First public release. 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | bin/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # Installer logs 26 | pip-log.txt 27 | pip-delete-this-directory.txt 28 | 29 | # Unit test / coverage reports 30 | htmlcov/ 31 | .tox/ 32 | .coverage 33 | .cache 34 | coverage.xml 35 | 36 | # Translations 37 | *.mo 38 | 39 | # Sphinx documentation 40 | docs/_build/ 41 | 42 | # Editor 43 | *.sw[po] 44 | 45 | # Mac OS X 46 | .DS_Store 47 | -------------------------------------------------------------------------------- /examples/templates/nested/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Nested Example 6 | 7 | 8 | {% block navbar %} 9 | 23 | {% endblock %} 24 | {% block content %} 25 | {% endblock %} 26 | 27 | 28 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from pytest import fixture 2 | from flask import Flask, Blueprint 3 | 4 | 5 | class FlaskConfig(object): 6 | SERVER_NAME = 'example.dev' 7 | 8 | 9 | @fixture(scope='session') 10 | def app(request, biu_bp): 11 | app = Flask(__name__) 12 | app.config.from_object(FlaskConfig) 13 | 14 | app.register_blueprint(biu_bp) 15 | 16 | ctx = app.app_context() 17 | ctx.push() 18 | request.addfinalizer(ctx.pop) 19 | 20 | return app 21 | 22 | 23 | @fixture(scope='session') 24 | def biu_bp(): 25 | bp = Blueprint('biu', __name__, url_prefix='/biu') 26 | 27 | @bp.route('/biu') 28 | def biu(): 29 | return 'here is biu' 30 | 31 | @bp.route('/boom/') 32 | def boom(num): 33 | return 'boom:%d' % num 34 | 35 | return bp 36 | -------------------------------------------------------------------------------- /tests/integration/views.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, current_app, render_template 2 | 3 | from .ext import nav 4 | 5 | 6 | bp = Blueprint('main', __name__) 7 | 8 | navbar_top = nav.Bar('top', [ 9 | nav.Item('Home', 'main.index'), 10 | nav.Item('Latest News', 'main.news', {'page': 1}), 11 | ], alias={'index': nav.ItemReference('main.index')}) 12 | 13 | 14 | @navbar_top.initializer 15 | def initialize_navbar_top(nav): 16 | top = nav['top'] 17 | if len(top.items) > 2: 18 | return 19 | args = {'page': current_app.config['NEWS_SPECIAL_PAGE']} 20 | item = nav.Item('Special News', 'main.news', args) 21 | top.items.append(item) 22 | 23 | 24 | @bp.route('/') 25 | def index(): 26 | return render_template('index.html') 27 | 28 | 29 | @bp.route('/news/') 30 | def news(page): 31 | return render_template('news.html', page=page) 32 | -------------------------------------------------------------------------------- /examples/simple.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template 2 | from flask.ext.navigation import Navigation 3 | 4 | 5 | app = Flask() 6 | nav = Navigation(app) 7 | 8 | navbar = nav.Bar('top', [ 9 | nav.Item(u'Home', endpoint='home'), 10 | nav.Item(u'Latest Article', endpoint='article', args=lambda: {'aid': 1}), 11 | nav.Item(u'More', endpoint='more', url='//example.com/more'), 12 | ]) 13 | 14 | 15 | @navbar.initializer 16 | def init_navbar(nav): 17 | # yield items from database here 18 | def yield_items(): 19 | for i in range(2): 20 | external_url = '//example.com/x/%d' % i 21 | yield nav.Item('X:%d' % i, endpoint='x-serial', url=external_url) 22 | # extend your bar 23 | nav['top'].extend(yield_items(), after_endpoint='article') 24 | 25 | 26 | @app.route('/') 27 | def home(): 28 | return render_template('simple/home.html') 29 | 30 | 31 | @app.route('/article/') 32 | def article(aid): 33 | return render_template('simple/article.html', aid=aid) 34 | -------------------------------------------------------------------------------- /examples/nested.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, Blueprint, render_template 2 | from flask.ext.navigation import Navigation 3 | 4 | 5 | app = Flask(__name__) 6 | nav = Navigation(app) 7 | 8 | pocket = Blueprint(__name__, 'pocket', url_prefix='/pocket') 9 | app.register_blueprint(pocket) 10 | 11 | navbar = nav.Bar('top', [ 12 | nav.Item(u'Home', endpoint='home'), 13 | nav.Item(u'Pocket', endpoint='pocket') 14 | ]) 15 | 16 | pocket_navbar = nav.Bar('pocket', [ 17 | nav.Item(u'Back', endpoint='pocket'), 18 | nav.Item(u'Article', endpoint='pocket.article'), 19 | nav.Item(u'Video', endpoint='pocket.video'), 20 | ], alias={'back': nav.ItemReference('pocket')}) 21 | 22 | 23 | @app.route('/') 24 | def home(): 25 | return render_template('nested/home.html') 26 | 27 | 28 | @app.route('/pocket') 29 | def pocket(): 30 | return render_template('nested/pocket.html') 31 | 32 | 33 | @pocket.route('/article') 34 | def article(): 35 | return render_template('nested/pocket-article.html') 36 | 37 | 38 | @pocket.route('/video') 39 | def video(): 40 | return render_template('nested/pocket-video.html') 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Jiangge Zhang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | from os.path import dirname, realpath, join 3 | 4 | 5 | def read_long_description(): 6 | current_dir = dirname(realpath(__file__)) 7 | with open(join(current_dir, "README.rst")) as long_description_file: 8 | return long_description_file.read() 9 | 10 | 11 | setup( 12 | name="Flask-Navigation", 13 | packages=find_packages(exclude=["tests", "docs"]), 14 | version='0.2.0', 15 | description="The navigation of Flask application.", 16 | long_description=read_long_description(), 17 | author="Jiangge Zhang", 18 | author_email="tonyseek@gmail.com", 19 | url="https://github.com/tonyseek/flask-navigation", 20 | license="MIT", 21 | keywords=["navigation", "flask", "navbar", "nav"], 22 | classifiers=[ 23 | "Programming Language :: Python", 24 | "Programming Language :: Python :: 2.7", 25 | "Programming Language :: Python :: 3.3", 26 | "Programming Language :: Python :: 3.4", 27 | "Programming Language :: Python :: Implementation :: PyPy", 28 | "Development Status :: 3 - Alpha", 29 | "License :: OSI Approved :: MIT License", 30 | "Intended Audience :: Developers", 31 | "Operating System :: OS Independent", 32 | "Environment :: Web Environment", 33 | "Framework :: Flask", 34 | "Topic :: Software Development :: Libraries :: Python Modules", 35 | ], 36 | install_requires=["Flask", "blinker"], 37 | ) 38 | -------------------------------------------------------------------------------- /tests/integration/test_app.py: -------------------------------------------------------------------------------- 1 | from pytest import fixture 2 | from webtest import TestApp 3 | 4 | from .app import create_app 5 | 6 | 7 | @fixture 8 | def app(): 9 | app = create_app() 10 | return TestApp(app) 11 | 12 | 13 | def test_app(app): 14 | r = app.get('/') 15 | assert r.status == '200 OK' 16 | assert 'Welcome' in r 17 | assert_navbar_exists(r) 18 | assert_active(r, 'Home') 19 | 20 | r = r.click('Latest News') 21 | assert r.status == '200 OK' 22 | assert 'News :: Page - 1' in r 23 | assert_navbar_exists(r) 24 | assert_active(r, 'Latest News') 25 | 26 | r = r.click('Special News') 27 | assert r.status == '200 OK' 28 | assert 'News :: Page - 42' in r 29 | assert_navbar_exists(r) 30 | assert_active(r, 'Special News') 31 | 32 | r = r.click('Home') 33 | assert r.status == '200 OK' 34 | assert 'Welcome' in r 35 | assert_navbar_exists(r) 36 | assert_active(r, 'Home') 37 | 38 | 39 | def test_alias(app): 40 | r = app.get('/news/1024') 41 | assert r.status == '200 OK' 42 | assert 'News :: Page - 1024' in r 43 | assert_navbar_exists(r) 44 | assert_active(r, '') 45 | 46 | r = r.click('Back') 47 | assert r.status == '200 OK' 48 | assert 'Welcome' in r 49 | assert_navbar_exists(r) 50 | assert_active(r, 'Home') 51 | 52 | 53 | def assert_navbar_exists(r): 54 | assert 'Home' in r 55 | assert 'Latest News' in r 56 | assert 'Special News' in r 57 | 58 | 59 | def assert_active(r, text): 60 | assert r.pyquery('li.active > a').text().strip() == text 61 | -------------------------------------------------------------------------------- /flask_navigation/navbar.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | from .item import ItemCollection 4 | from .signals import navbar_created 5 | 6 | 7 | class NavigationBar(collections.Iterable): 8 | """The navigation bar object.""" 9 | 10 | def __init__(self, name, items=None, alias=None): 11 | self.name = name 12 | self.items = ItemCollection(items or []) 13 | self.initializers = [] 14 | self.alias = alias or {} 15 | 16 | # sends signal 17 | navbar_created.send(self.__class__, bar=self) 18 | 19 | def __iter__(self): 20 | return iter(self.items) 21 | 22 | def initializer(self, fn): 23 | """Adds a initializer function. 24 | 25 | If you want to initialize the navigation bar within a Flask app 26 | context, you can use this decorator. 27 | 28 | The decorated function should nave one paramater ``nav`` which is the 29 | bound navigation extension instance. 30 | """ 31 | self.initializers.append(fn) 32 | return fn 33 | 34 | def alias_item(self, alias): 35 | """Gets an item by its alias.""" 36 | ident = self.alias[alias] 37 | return self.items[ident] 38 | 39 | @property 40 | def current_item(self): 41 | """Get the current active navigation Item if any. 42 | 43 | .. versionadded:: 0.2.0 44 | """ 45 | return self._get_current_item(self) 46 | 47 | def _get_current_item(self, items): 48 | for item in items: 49 | if item.is_active: 50 | return item 51 | else: 52 | nested = self._get_current_item(item.items) 53 | if nested: 54 | return nested 55 | -------------------------------------------------------------------------------- /tests/test_navbar.py: -------------------------------------------------------------------------------- 1 | from pytest import fixture, raises 2 | 3 | from flask.ext.navigation.navbar import NavigationBar 4 | from flask.ext.navigation.item import Item, ItemReference 5 | 6 | 7 | @fixture 8 | def navbar(): 9 | navbar = NavigationBar('mybar', [ 10 | Item(u'Home', 'home'), 11 | Item(u'News', 'news'), 12 | ]) 13 | return navbar 14 | 15 | 16 | def test_attrs(navbar): 17 | assert navbar.name == 'mybar' 18 | assert len(navbar.items) == 2 19 | 20 | 21 | def test_iterable(navbar): 22 | iterable = iter(navbar) 23 | 24 | item_1st = next(iterable) 25 | assert item_1st.label == u'Home' 26 | assert item_1st.endpoint == 'home' 27 | 28 | item_2nd = next(iterable) 29 | assert item_2nd.label == u'News' 30 | assert item_2nd.endpoint == 'news' 31 | 32 | with raises(StopIteration): 33 | next(iterable) 34 | 35 | item_reentry = next(iter(navbar)) # test for reentry iterable 36 | assert item_reentry.label == u'Home' 37 | assert item_reentry.endpoint == 'home' 38 | 39 | 40 | def test_initializer(navbar): 41 | @navbar.initializer 42 | def initialize_more_items(nav): 43 | return nav 44 | assert navbar.initializers[0] is initialize_more_items 45 | 46 | 47 | def test_alias_item(): 48 | navbar = NavigationBar('mybar', [ 49 | Item(u'Home', 'home'), 50 | Item(u'News', 'news', args={'page': 1}), 51 | ], alias={ 52 | 'foo': ItemReference('home'), 53 | 'bar': ItemReference('news', {'page': 1}), 54 | 'egg': ItemReference('news', {'page': 2}), 55 | }) 56 | 57 | assert navbar.alias_item('foo').label == u'Home' 58 | assert navbar.alias_item('bar').label == u'News' 59 | 60 | with raises(KeyError): 61 | navbar.alias_item('egg') 62 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | |Build Status| |Coverage Status| |PyPI Version| |PyPI Downloads| |Wheel Status| 2 | 3 | Flask-Navigation 4 | ================ 5 | 6 | Build navigation bars in your Flask applications. :: 7 | 8 | nav.Bar('top', [ 9 | nav.Item('Home', 'index'), 10 | nav.Item('Latest News', 'news', {'page': 1}), 11 | ]) 12 | 13 | 14 | Installation 15 | ------------ 16 | 17 | :: 18 | 19 | $ pip install Flask-Navigation 20 | 21 | 22 | Links 23 | ----- 24 | 25 | - `Document `_ 26 | - `Issue Track `_ 27 | 28 | 29 | Issues 30 | ------ 31 | 32 | If you want to report bugs or request features, please create issues on 33 | `GitHub Issues `_. 34 | 35 | 36 | Contributes 37 | ----------- 38 | 39 | You can send a pull request on 40 | `GitHub `_. 41 | 42 | 43 | .. |Build Status| image:: https://travis-ci.org/tonyseek/flask-navigation.svg?branch=master,develop 44 | :target: https://travis-ci.org/tonyseek/flask-navigation 45 | :alt: Build Status 46 | .. |Coverage Status| image:: https://img.shields.io/coveralls/tonyseek/flask-navigation/develop.svg?style=flat 47 | :target: https://coveralls.io/r/tonyseek/flask-navigation 48 | :alt: Coverage Status 49 | .. |Wheel Status| image:: https://img.shields.io/pypi/wheel/Flask-Navigation.svg?style=flat 50 | :target: https://pypi.python.org/pypi/Flask-Navigation 51 | :alt: Wheel Status 52 | .. |PyPI Version| image:: https://img.shields.io/pypi/v/Flask-Navigation.svg?style=flat 53 | :target: https://pypi.python.org/pypi/Flask-Navigation 54 | :alt: PyPI Version 55 | .. |PyPI Downloads| image:: https://img.shields.io/pypi/dm/Flask-Navigation.svg?style=flat 56 | :target: https://pypi.python.org/pypi/Flask-Navigation 57 | :alt: Downloads 58 | -------------------------------------------------------------------------------- /flask_navigation/api.py: -------------------------------------------------------------------------------- 1 | from flask.signals import appcontext_pushed 2 | 3 | from .navbar import NavigationBar 4 | from .item import Item, ItemReference 5 | from .utils import BoundTypeProperty 6 | from .signals import navbar_created 7 | 8 | 9 | class Navigation(object): 10 | """The navigation extension API.""" 11 | 12 | #: The subclass of :class:`~flask.ext.navigation.navbar.NavigationBar`. It 13 | #: bound with the the current instance. 14 | #: 15 | #: :type: :class:`~flask.ext.navigation.utils.BoundTypeProperty` 16 | Bar = BoundTypeProperty('Bar', NavigationBar) 17 | 18 | #: The subclass of :class:`~flask.ext.navigation.item.Item`. It bound with 19 | #: the the current instance. 20 | #: 21 | #: :type: :class:`~flask.ext.navigation.utils.BoundTypeProperty` 22 | Item = BoundTypeProperty('Item', Item) 23 | 24 | ItemReference = ItemReference 25 | 26 | def __init__(self, app=None): 27 | self.bars = {} 28 | if app is not None: 29 | self.init_app(app) 30 | # connects ext-level signals 31 | navbar_created.connect(self.bind_bar, self.Bar) 32 | 33 | def __getitem__(self, name): 34 | """Gets a bound navigation bar by its name.""" 35 | return self.bars[name] 36 | 37 | def init_app(self, app): 38 | """Initializes an app to work with this extension. 39 | 40 | The app-context signals will be subscribed and the template context 41 | will be initialized. 42 | 43 | :param app: the :class:`flask.Flask` app instance. 44 | """ 45 | # connects app-level signals 46 | appcontext_pushed.connect(self.initialize_bars, app) 47 | # integrate with jinja template 48 | app.add_template_global(self, 'nav') 49 | 50 | def initialize_bars(self, sender=None, **kwargs): 51 | """Calls the initializers of all bound navigation bars.""" 52 | for bar in self.bars.values(): 53 | for initializer in bar.initializers: 54 | initializer(self) 55 | 56 | def bind_bar(self, sender=None, **kwargs): 57 | """Binds a navigation bar into this extension instance.""" 58 | bar = kwargs.pop('bar') 59 | self.bars[bar.name] = bar 60 | -------------------------------------------------------------------------------- /flask_navigation/utils.py: -------------------------------------------------------------------------------- 1 | import operator 2 | import collections 3 | 4 | 5 | def freeze_dict(dict_): 6 | """Freezes ``dict`` into ``tuple``. 7 | 8 | A typical usage is packing ``dict`` into hashable. 9 | 10 | e.g.:: 11 | 12 | >>> freeze_dict({'a': 1, 'b': 2}) 13 | (('a', 1), ('b', 2)) 14 | """ 15 | pairs = dict_.items() 16 | key_getter = operator.itemgetter(0) 17 | return tuple(sorted(pairs, key=key_getter)) 18 | 19 | 20 | def join_html_attrs(attrs): 21 | """Joins the map structure into HTML attributes. 22 | 23 | The return value is a 2-tuple ``(template, ordered_values)``. It should be 24 | passed into :class:`markupsafe.Markup` to prevent XSS attacked. 25 | 26 | e.g.:: 27 | 28 | >>> join_html_attrs({'href': '/', 'data-active': 'true'}) 29 | ('data-active="{0}" href="{1}"', ['true', '/']) 30 | """ 31 | attrs = collections.OrderedDict(freeze_dict(attrs or {})) 32 | template = ' '.join('%s="{%d}"' % (k, i) for i, k in enumerate(attrs)) 33 | return template, list(attrs.values()) 34 | 35 | 36 | class BoundTypeProperty(object): 37 | """This kind of property creates subclasses of given class for each 38 | instance. 39 | 40 | Those subclasses means "bound type" which be used for identifying 41 | themselves with blinker/Flask signals. 42 | 43 | e.g.:: 44 | 45 | >>> class Foo(object): 46 | ... pass 47 | >>> class Bar(object): 48 | ... Foo = BoundTypeProperty('Foo', Foo) 49 | >>> 50 | >>> Bar.Foo 51 | BoundTypeProperty('Foo', Foo) 52 | >>> bar = Bar() 53 | >>> bar.Foo is Foo 54 | False 55 | >>> issubclass(bar.Foo, Foo) 56 | True 57 | >>> egg = Bar() 58 | >>> egg.Foo is bar.Foo 59 | False 60 | >>> egg.Foo.__bases__ == bar.Foo.__bases__ == (Foo,) 61 | True 62 | 63 | :param name: the name of this property. 64 | :param cls: the base class of all bound classes. 65 | """ 66 | 67 | def __init__(self, name, cls): 68 | self.name = name 69 | self.cls = cls 70 | 71 | def __repr__(self): 72 | return 'BoundTypeProperty(%r, %s)' % (self.name, self.cls.__name__) 73 | 74 | def __get__(self, instance, owner): 75 | if instance is None: 76 | return self 77 | ns = vars(instance) # the instance namespace 78 | if self.name not in ns: 79 | ns[self.name] = type(self.name, (self.cls,), {}) 80 | return ns[self.name] 81 | -------------------------------------------------------------------------------- /tests/test_api.py: -------------------------------------------------------------------------------- 1 | from pytest import fixture 2 | 3 | from flask import Flask, current_app 4 | from flask.ext.navigation.api import Navigation 5 | from flask.ext.navigation.navbar import NavigationBar 6 | from flask.ext.navigation.item import Item 7 | 8 | 9 | @fixture 10 | def nav(app): 11 | nav = Navigation(app) 12 | nav.Bar('top', [ 13 | nav.Item('Biu', endpoint='biu.biu'), 14 | nav.Item('Boom', endpoint='biu.boom', args={'num': 1}), 15 | ]) 16 | return nav 17 | 18 | 19 | def test_bound_bar(app, nav): 20 | # ensure the navbar has been bound 21 | top = nav['top'] 22 | 23 | assert nav.Bar.__bases__ == (NavigationBar,) 24 | assert isinstance(top, nav.Bar) 25 | 26 | assert nav.Item.__bases__ == (Item,) 27 | assert isinstance(top.items['biu.biu'], nav.Item) 28 | 29 | 30 | def test_initializer(): 31 | app = Flask(__name__) 32 | app.config.setdefault('BIU_NUM', 42) 33 | 34 | nav = Navigation() 35 | nav.init_app(app) 36 | 37 | woo = nav.Bar('woo') 38 | 39 | @woo.initializer 40 | def initialize_woo(nav): 41 | boom_num = current_app.config['BIU_NUM'] 42 | nav['woo'].items.extend([ 43 | nav.Item('Boom', endpoint='biu', args={'num': boom_num}), 44 | ]) 45 | 46 | @app.route('/') 47 | def index(): 48 | return woo.items[0].url 49 | 50 | @app.route('/biu/') 51 | def biu(): 52 | return '' 53 | 54 | with app.test_client() as c: 55 | url = c.get('/') 56 | assert url.data == b'/biu/42' 57 | 58 | 59 | def test_current_item(): 60 | app = Flask(__name__) 61 | 62 | nav = Navigation() 63 | nav.init_app(app) 64 | news_item = Item(u'News', 'news') 65 | navbar = nav.Bar('test_current', [ 66 | Item(u'Home', 'home'), 67 | news_item, 68 | ]) 69 | 70 | @app.route('/') 71 | def home(): 72 | pass 73 | 74 | @app.route('/news') 75 | def news(): 76 | pass 77 | 78 | assert navbar.current_item is None 79 | 80 | with app.test_request_context('/news'): 81 | assert navbar.current_item == news_item 82 | 83 | 84 | def test_current_item_nested(): 85 | app = Flask(__name__) 86 | 87 | nav = Navigation() 88 | nav.init_app(app) 89 | item = Item('Nested item', 'nested') 90 | navbar = nav.Bar('test_current', [ 91 | Item(u'Home', 'home'), 92 | Item(u'News', 'news'), 93 | Item(u'With Children', 'with_children', items=[item]) 94 | ]) 95 | 96 | @app.route('/') 97 | def home(): 98 | pass 99 | 100 | @app.route('/nested') 101 | def nested(): 102 | pass 103 | 104 | assert navbar.current_item is None 105 | 106 | with app.test_request_context('/nested'): 107 | assert navbar.current_item == item 108 | -------------------------------------------------------------------------------- /tests/test_item_collection.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | from pytest import fixture, raises 4 | from mock import Mock 5 | 6 | from flask.ext.navigation.item import ItemCollection, ItemReference 7 | 8 | 9 | class Item(collections.namedtuple('Item', ['endpoint'])): 10 | @property 11 | def ident(self): 12 | return self.endpoint, () 13 | 14 | 15 | @fixture 16 | def items(): 17 | items = {'lumpy': Item('lumpy'), 18 | 'nutty': Item('nutty'), 19 | 'cuddles': Item('cuddles')} 20 | return items 21 | 22 | 23 | def test_creation(items): 24 | c1 = ItemCollection() 25 | assert repr(c1) == 'ItemCollection([])' 26 | assert len(c1) == 0 27 | 28 | c2 = ItemCollection([items['cuddles']]) 29 | assert repr(c2) == "ItemCollection([Item(endpoint='cuddles')])" 30 | assert len(c2) == 1 31 | 32 | 33 | def test_sequence(items): 34 | c = ItemCollection() 35 | raises(KeyError, lambda: c['cuddles']) 36 | raises(IndexError, lambda: c[0]) 37 | assert len(c) == 0 38 | 39 | c.append(items['cuddles']) 40 | assert len(c) == 1 41 | assert c['cuddles'] == Item('cuddles') 42 | assert c[0] == Item('cuddles') 43 | 44 | c.extend([items['nutty'], items['lumpy']]) 45 | assert len(c) == 3 46 | assert c['cuddles'] == Item('cuddles') 47 | assert c['nutty'] == Item('nutty') 48 | assert c['lumpy'] == Item('lumpy') 49 | assert c[0] == Item('cuddles') 50 | assert c[1] == Item('nutty') 51 | assert c[2] == Item('lumpy') 52 | 53 | del c[1] 54 | raises(KeyError, lambda: c['nutty']) 55 | raises(IndexError, lambda: c[2]) 56 | assert len(c) == 2 57 | assert c['cuddles'] == Item('cuddles') 58 | assert c['lumpy'] == Item('lumpy') 59 | assert c[0] == Item('cuddles') 60 | assert c[1] == Item('lumpy') 61 | 62 | c.insert(0, items['nutty']) 63 | assert len(c) == 3 64 | assert c[0] == Item('nutty') 65 | assert c[1] == Item('cuddles') 66 | assert c[2] == Item('lumpy') 67 | 68 | c[2] = Item('happy-tree') 69 | assert len(c) == 3 70 | assert c[2] == Item('happy-tree') 71 | assert c['happy-tree'] == Item('happy-tree') 72 | raises(KeyError, lambda: c['lumpy']) 73 | 74 | with raises(TypeError): 75 | c['pu'] = Item('pu') 76 | 77 | with raises(IndexError): 78 | c[3] = Item('pu') 79 | 80 | 81 | def test_getitem_with_args(): 82 | item_with_args = Mock(endpoint='nutty', args={'i': 12}, 83 | ident=ItemReference('nutty', {'i': 12})) 84 | c = ItemCollection([item_with_args]) 85 | 86 | raises(KeyError, lambda: c['nutty']) 87 | raises(KeyError, lambda: c['nutty', {'i': 1}]) 88 | 89 | assert c['nutty', {'i': 12}] == item_with_args 90 | 91 | 92 | def test_iterable(items): 93 | c = ItemCollection([items['cuddles'], items['lumpy']]) 94 | iterable = iter(c) 95 | 96 | item = next(iterable) 97 | assert item.endpoint == 'cuddles' 98 | 99 | item = next(iterable) 100 | assert item.endpoint == 'lumpy' 101 | 102 | with raises(StopIteration): 103 | next(iterable) 104 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Flask-Navigation 2 | ================ 3 | 4 | Build navigation bars in your Flask applications. 5 | 6 | 7 | Installation 8 | ------------ 9 | 10 | :: 11 | 12 | $ pip install Flask-Navigation 13 | 14 | 15 | Configuration 16 | ------------- 17 | 18 | Just like the most of Flask extension:: 19 | 20 | from flask import Flask 21 | from flask.ext.navigation import Navigation 22 | 23 | app = Flask(__name__) 24 | nav = Navigation(app) 25 | 26 | Or use the app factory pattern:: 27 | 28 | nav = Navigation() 29 | nav.init_app(app) 30 | 31 | 32 | Create Navigation Bar 33 | --------------------- 34 | 35 | :: 36 | 37 | nav.Bar('top', [ 38 | nav.Item('Home', 'index'), 39 | nav.Item('Latest News', 'news', {'page': 1}), 40 | ]) 41 | 42 | @app.route('/') 43 | def index(): 44 | return render_template('index.html') 45 | 46 | @app.route('/news/') 47 | def news(page): 48 | return render_template('news.html', page=page) 49 | 50 | The created navigation bars are accessible in any template with app context 51 | 52 | .. code-block:: html+jinja 53 | 54 |
    55 | {% for item in nav.top %} 56 |
  • 57 | {{ item.label }} 58 |
  • 59 | {% endfor %} 60 |
61 | 62 | The pre-defined html attributes is available too:: 63 | 64 | nav.Bar('top', [ 65 | nav.Item('Home', 'index', html_attrs={'class': ['home']}), 66 | nav.Item('Latest News', 'news', {'page': 1}, 67 | html_attrs={'class': ['news']}), 68 | ]) 69 | 70 | .. code-block:: html+jinja 71 | 72 |
    73 | {% for item in nav.top %} 74 |
  • 75 | {{ item }} 76 |
  • 77 | {% endfor %} 78 |
79 | 80 | 81 | You can also have direct access to the current active item: 82 | 83 | .. code-block:: html+jinja 84 | 85 |

{{ nav.top.current_item.label }}

86 | 87 | 88 | Nested Items 89 | ------------ 90 | 91 | Items are nestables: 92 | 93 | .. code-block:: python 94 | 95 | nav.Bar('top', [ 96 | nav.Item('Home', 'index'), 97 | nav.Item('Latest News', 'news', {'page': 1}), 98 | nav.Item('Nestable', 'nestable', items=[ 99 | nav.Item('Nested 1', 'nested-1'), 100 | nav.Item('Nested 2', 'nested-2'), 101 | ]), 102 | ]) 103 | 104 | 105 | .. code-block:: html+jinja 106 | 107 |
    108 | {% for item in nav.top %} 109 |
  • 110 | {{ item }} 111 | {% if item.items %} 112 |
      113 | {% for child in item.items %} 114 |
    • 115 | {{ child }} 116 |
    • 117 | {% endfor %} 118 |
    119 | {% endif %} 120 |
  • 121 | {% endfor %} 122 |
123 | 124 | 125 | API 126 | --- 127 | 128 | Extension Class 129 | ~~~~~~~~~~~~~~~ 130 | 131 | .. autoclass:: flask.ext.navigation.Navigation 132 | :members: init_app, Bar, Item, ItemReference 133 | 134 | Internal Classes 135 | ~~~~~~~~~~~~~~~~ 136 | 137 | .. autoclass:: flask.ext.navigation.navbar.NavigationBar 138 | :members: 139 | 140 | .. autoclass:: flask.ext.navigation.item.Item 141 | :members: 142 | 143 | .. autoclass:: flask.ext.navigation.item.ItemCollection 144 | :members: 145 | :inherited-members: 146 | 147 | Utilities 148 | ~~~~~~~~~ 149 | 150 | .. autofunction:: flask.ext.navigation.utils.freeze_dict 151 | 152 | .. autoclass:: flask.ext.navigation.utils.BoundTypeProperty 153 | :members: 154 | 155 | 156 | Release Changes 157 | --------------- 158 | 159 | .. include:: ../CHANGES 160 | -------------------------------------------------------------------------------- /tests/test_item.py: -------------------------------------------------------------------------------- 1 | from pytest import fixture, raises 2 | from flask import Markup 3 | 4 | from flask.ext.navigation.item import Item, ItemReference, ItemCollection 5 | 6 | 7 | @fixture 8 | def items(): 9 | items = {'biu': Item(u'Biu', endpoint='biu.biu'), 10 | 'boom1': Item(u'Boom', endpoint='biu.boom', args={'num': 1}), 11 | 'boom2': Item(u'Boom', endpoint='biu.boom', 12 | args=lambda: {'num': 2}), 13 | 'example': Item(u'Example', endpoint='external.example', 14 | url='//example.com')} 15 | return items 16 | 17 | 18 | def test_basic(app, items): 19 | # without args 20 | assert items['biu'].label == u'Biu' 21 | assert items['biu'].args == {} 22 | assert items['biu'].url == 'http://example.dev/biu/biu' 23 | 24 | # with static args 25 | assert items['boom1'].label == u'Boom' 26 | assert items['boom1'].args == {'num': 1} 27 | assert items['boom1'].url == 'http://example.dev/biu/boom/1' 28 | 29 | # with dynamic args 30 | assert items['boom2'].label == u'Boom' # non-conflic label 31 | assert items['boom2'].args == {'num': 2} 32 | assert items['boom2'].url == 'http://example.dev/biu/boom/2' 33 | 34 | # hard-coding url 35 | assert items['example'].label == u'Example' 36 | assert items['example'].args == {} 37 | assert items['example'].url == '//example.com' 38 | 39 | 40 | def test_nested(app): 41 | item_without = Item('Without Children', 'without_children') 42 | assert isinstance(item_without.items, ItemCollection) 43 | assert len(item_without.items) == 0 44 | 45 | item_with = Item('With Children', 'with_children', items=[ 46 | Item('Nested item', 'nested') 47 | ]) 48 | assert isinstance(item_with.items, ItemCollection) 49 | assert len(item_with.items) == 1 50 | 51 | 52 | def test_is_active(app, items): 53 | with app.test_client() as client: 54 | client.get('/biu/boom/2') 55 | assert not items['biu'].is_active 56 | assert not items['boom1'].is_active 57 | assert items['boom2'].is_active 58 | assert not items['example'].is_active 59 | 60 | with app.test_client() as client: 61 | client.get('/biu/boom/1') 62 | assert not items['biu'].is_active 63 | assert items['boom1'].is_active 64 | assert not items['boom2'].is_active 65 | assert not items['example'].is_active 66 | 67 | with app.test_client() as client: 68 | client.get('/biu/biu') 69 | assert items['biu'].is_active 70 | assert not items['boom1'].is_active 71 | assert not items['boom2'].is_active 72 | assert not items['example'].is_active 73 | 74 | 75 | def test_ident(items): 76 | assert items['biu'].endpoint != items['boom1'].endpoint 77 | assert items['boom1'].endpoint == items['boom2'].endpoint 78 | 79 | assert items['biu'].ident != items['boom1'].ident 80 | assert items['boom1'].ident != items['boom2'].ident 81 | 82 | 83 | def test_item_reference(): 84 | assert ItemReference('foo').endpoint == 'foo' 85 | assert ItemReference('foo').args == () 86 | assert ItemReference('foo') == ItemReference('foo', {}) 87 | 88 | assert ItemReference('bar', {'a': 1}).endpoint == 'bar' 89 | assert ItemReference('bar', {'b': 2, 'a': 1}).args == (('a', 1), ('b', 2)) 90 | 91 | 92 | def test_html_representation(app, items): 93 | with app.test_client() as client: 94 | client.get('/biu/biu') 95 | 96 | # without format_spec 97 | assert str(Markup(items['biu'])) == \ 98 | 'Biu' 99 | assert str(Markup(items['boom1'])) == \ 100 | 'Boom' 101 | 102 | # "li" as format_spec 103 | assert str(Markup('{0:li}').format(items['biu'])) == \ 104 | '
  • Biu
  • ' 105 | assert str(Markup('{0:li}').format(items['boom1'])) == \ 106 | '
  • Boom
  • ' 107 | 108 | # default format_spec 109 | assert str(Markup('{0:}').format(items['biu'])) == \ 110 | 'Biu' 111 | assert str(Markup('{0:}').format(items['boom1'])) == \ 112 | 'Boom' 113 | 114 | # invalid format_spec 115 | with raises(ValueError): 116 | str(Markup('{0:foo}').format(items['biu'])) 117 | 118 | 119 | def test_html_representation_with_class(app): 120 | biu_with_class = Item( 121 | u'Biu', endpoint='biu.biu', 122 | html_attrs={'class': ['icon', 'icon-biu'], 'data-icon': 'biu'}) 123 | boom_with_class = Item( 124 | u'Boom', endpoint='biu.boom', args={'num': 1}, 125 | html_attrs={'class': ['icon', 'icon-boom'], 'data-icon': 'boom'}) 126 | 127 | with app.test_client() as client: 128 | client.get('/biu/biu') 129 | 130 | assert str(Markup(biu_with_class)) == ( 131 | 'Biu') 133 | assert str(Markup(boom_with_class)) == ( 134 | 'Boom') 136 | -------------------------------------------------------------------------------- /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 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/FlaskNavigation.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/FlaskNavigation.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/FlaskNavigation" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/FlaskNavigation" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /flask_navigation/item.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | from flask import url_for, request, Markup 4 | 5 | from .utils import freeze_dict, join_html_attrs 6 | 7 | 8 | class Item(object): 9 | """The navigation item object. 10 | 11 | :param label: the display label of this navigation item. 12 | :param endpoint: the unique name of this navigation item. 13 | If this item point to a internal url, this parameter 14 | should be acceptable for ``url_for`` which will generate 15 | the target url. 16 | :param args: optional. If this parameter be provided, it will be passed to 17 | the ``url_for`` with ``endpoint`` together. 18 | Maybe this arguments need to be decided in the Flask app 19 | context, then this parameter could be a function to delay the 20 | execution. 21 | :param url: optional. If this parameter be provided, the target url of 22 | this navigation will be it. The ``endpoint`` and ``args`` will 23 | not been used to generate url. 24 | :param html_attrs: optional. This :class:`dict` will be used for 25 | representing html. 26 | 27 | The ``endpoint`` is the identity name of this navigation item. It will be 28 | unique in whole application. In mostly situation, it should be a endpoint 29 | name of a Flask view function. 30 | """ 31 | 32 | def __init__(self, label, endpoint, args=None, url=None, html_attrs=None, 33 | items=None): 34 | self.label = label 35 | self.endpoint = endpoint 36 | self._args = args 37 | self._url = url 38 | self.html_attrs = {} if html_attrs is None else html_attrs 39 | self.items = ItemCollection(items or None) 40 | 41 | def __html__(self): 42 | attrs = dict(self.html_attrs) 43 | 44 | # adds ``active`` to class list 45 | html_class = attrs.get('class', []) 46 | if self.is_active: 47 | html_class.append('active') 48 | 49 | # joins class list 50 | attrs['class'] = ' '.join(html_class) 51 | if not attrs['class']: 52 | del attrs['class'] 53 | attrs['href'] = self.url 54 | attrs_template, attrs_values = join_html_attrs(attrs) 55 | 56 | return Markup('{label}' % attrs_template).format( 57 | *attrs_values, label=self.label) 58 | 59 | def __html_format__(self, format_spec): 60 | if format_spec == 'li': 61 | li_attrs = Markup(' class="active"') if self.is_active else '' 62 | return Markup('{1}').format(li_attrs, self.__html__()) 63 | elif format_spec: 64 | raise ValueError('Invalid format spec') 65 | return self.__html__() 66 | 67 | @property 68 | def args(self): 69 | """The arguments which will be passed to ``url_for``. 70 | 71 | :type: :class:`dict` 72 | """ 73 | if self._args is None: 74 | return {} 75 | if callable(self._args): 76 | return dict(self._args()) 77 | return dict(self._args) 78 | 79 | @property 80 | def url(self): 81 | """The final url of this navigation item. 82 | 83 | By default, the value is generated by the :attr:`self.endpoint` and 84 | :attr:`self.args`. 85 | 86 | .. note:: 87 | 88 | The :attr:`url` property require the app context without a provided 89 | config value :const:`SERVER_NAME`, because of :func:`flask.url_for`. 90 | 91 | :type: :class:`str` 92 | """ 93 | if self.is_internal: 94 | return url_for(self.endpoint, **self.args) 95 | return self._url 96 | 97 | @property 98 | def is_active(self): 99 | """``True`` if the item should be presented as active, and ``False`` 100 | always if the request context is not bound. 101 | """ 102 | return bool(request and self.is_current) 103 | 104 | @property 105 | def is_internal(self): 106 | """``True`` if the target url is internal of current app.""" 107 | return self._url is None 108 | 109 | @property 110 | def is_current(self): 111 | """``True`` if current request has same endpoint with the item. 112 | 113 | The property should be used in a bound request context, or the 114 | :class:`RuntimeError` may be raised. 115 | """ 116 | if not self.is_internal: 117 | return False # always false for external url 118 | has_same_endpoint = (request.endpoint == self.endpoint) 119 | has_same_args = (request.view_args == self.args) 120 | return has_same_endpoint and has_same_args # matches the endpoint 121 | 122 | @property 123 | def ident(self): 124 | """The identity of this item. 125 | 126 | :type: :class:`~flask.ext.navigation.Navigation.ItemReference` 127 | """ 128 | return ItemReference(self.endpoint, self.args) 129 | 130 | 131 | class ItemCollection(collections.MutableSequence, 132 | collections.Iterable): 133 | """The collection of navigation items. 134 | 135 | This collection is a mutable sequence. All items have order index, and 136 | could be found by its endpoint name. e.g.:: 137 | 138 | c = ItemCollection() 139 | c.append(Item(endpoint='doge')) 140 | 141 | print(c['doge']) # output: Item(endpoint='doge') 142 | print(c[0]) # output: Item(endpoint='doge') 143 | print(c) # output: ItemCollection([Item(endpoint='doge')]) 144 | print(len(c)) # output: 1 145 | 146 | c.append(Item(endpoint='lumpy', args={'num': 4})) 147 | 148 | print(c[1]) # output: Item(endpoint='lumpy', args={'num': 4}) 149 | assert c['lumpy', {'num': 4}] is c[1] 150 | """ 151 | 152 | def __init__(self, iterable=None): 153 | #: the item collection 154 | self._items = [] 155 | #: the mapping collection of endpoint -> item 156 | self._items_mapping = {} 157 | #: initial extending 158 | self.extend(iterable or []) 159 | 160 | def __repr__(self): 161 | return 'ItemCollection(%r)' % self._items 162 | 163 | def __getitem__(self, index): 164 | if isinstance(index, int): 165 | return self._items[index] 166 | 167 | if isinstance(index, tuple): 168 | endpoint, args = index 169 | else: 170 | endpoint, args = index, {} 171 | ident = ItemReference(endpoint, args) 172 | return self._items_mapping[ident] 173 | 174 | def __setitem__(self, index, item): 175 | # remove the old reference 176 | old_item = self._items[index] 177 | del self._items_mapping[old_item.ident] 178 | 179 | self._items[index] = item 180 | self._items_mapping[item.ident] = item 181 | 182 | def __delitem__(self, index): 183 | item = self[index] 184 | del self._items[index] 185 | del self._items_mapping[item.ident] 186 | 187 | def __len__(self): 188 | return len(self._items) 189 | 190 | def __iter__(self): 191 | return iter(self._items) 192 | 193 | def insert(self, index, item): 194 | self._items.insert(index, item) 195 | self._items_mapping[item.ident] = item 196 | 197 | 198 | class ItemReference(collections.namedtuple('ItemReference', 'endpoint args')): 199 | """The identity tuple of navigation item. 200 | 201 | :param endpoint: the endpoint of view function. 202 | :type endpoint: ``str`` 203 | :param args: the arguments of view function. 204 | :type args: ``dict`` 205 | """ 206 | 207 | def __new__(cls, endpoint, args=()): 208 | if isinstance(args, dict): 209 | args = freeze_dict(args) 210 | return super(cls, ItemReference).__new__(cls, endpoint, args) 211 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Flask Navigation documentation build configuration file, created by 4 | # sphinx-quickstart on Mon Apr 14 01:10:02 2014. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | # -- General configuration ------------------------------------------------ 16 | 17 | # If your documentation needs a minimal Sphinx version, state it here. 18 | #needs_sphinx = '1.0' 19 | 20 | # Add any Sphinx extension module names here, as strings. They can be 21 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 22 | # ones. 23 | extensions = [ 24 | 'sphinx.ext.autodoc', 25 | 'sphinx.ext.intersphinx', 26 | ] 27 | 28 | # Add any paths that contain templates here, relative to this directory. 29 | templates_path = ['_templates'] 30 | 31 | # The suffix of source filenames. 32 | source_suffix = '.rst' 33 | 34 | # The encoding of source files. 35 | #source_encoding = 'utf-8-sig' 36 | 37 | # The master toctree document. 38 | master_doc = 'index' 39 | 40 | # General information about the project. 41 | project = u'Flask Navigation' 42 | copyright = u'2014, Jiangge Zhang' 43 | 44 | # The version info for the project you're documenting, acts as replacement for 45 | # |version| and |release|, also used in various other places throughout the 46 | # built documents. 47 | # 48 | # The short X.Y version. 49 | version = '0.2.0' 50 | # The full version, including alpha/beta/rc tags. 51 | release = '0.2.0' 52 | 53 | # The language for content autogenerated by Sphinx. Refer to documentation 54 | # for a list of supported languages. 55 | #language = None 56 | 57 | # There are two options for replacing |today|: either, you set today to some 58 | # non-false value, then it is used: 59 | #today = '' 60 | # Else, today_fmt is used as the format for a strftime call. 61 | #today_fmt = '%B %d, %Y' 62 | 63 | # List of patterns, relative to source directory, that match files and 64 | # directories to ignore when looking for source files. 65 | exclude_patterns = ['_build'] 66 | 67 | # The reST default role (used for this markup: `text`) to use for all 68 | # documents. 69 | #default_role = None 70 | 71 | # If true, '()' will be appended to :func: etc. cross-reference text. 72 | #add_function_parentheses = True 73 | 74 | # If true, the current module name will be prepended to all description 75 | # unit titles (such as .. function::). 76 | #add_module_names = True 77 | 78 | # If true, sectionauthor and moduleauthor directives will be shown in the 79 | # output. They are ignored by default. 80 | #show_authors = False 81 | 82 | # The name of the Pygments (syntax highlighting) style to use. 83 | #pygments_style = 'sphinx' 84 | 85 | # A list of ignored prefixes for module index sorting. 86 | #modindex_common_prefix = [] 87 | 88 | # If true, keep warnings as "system message" paragraphs in the built documents. 89 | #keep_warnings = False 90 | 91 | 92 | # -- Options for HTML output ---------------------------------------------- 93 | 94 | # The theme to use for HTML and HTML Help pages. See the documentation for 95 | # a list of builtin themes. 96 | html_theme = 'kr_small' 97 | 98 | # Theme options are theme-specific and customize the look and feel of a theme 99 | # further. For a list of options available for each theme, see the 100 | # documentation. 101 | html_theme_options = { 102 | 'github_fork': 'tonyseek/flask-navigation', 103 | 'github_fork_ribbon': 'http://aral.github.com/fork-me-on-github-retina-ribbons/right-grey@2x.png', 104 | 'index_logo': 'index-logo@2x.png', 105 | 'index_logo_width': '340px', 106 | 'index_logo_height': '50px', 107 | } 108 | 109 | # Add any paths that contain custom themes here, relative to this directory. 110 | #html_theme_path = [] 111 | 112 | # The name for this set of Sphinx documents. If None, it defaults to 113 | # " v documentation". 114 | #html_title = None 115 | 116 | # A shorter title for the navigation bar. Default is the same as html_title. 117 | #html_short_title = None 118 | 119 | # The name of an image file (relative to this directory) to place at the top 120 | # of the sidebar. 121 | #html_logo = None 122 | 123 | # The name of an image file (within the static path) to use as favicon of the 124 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 125 | # pixels large. 126 | #html_favicon = None 127 | 128 | # Add any paths that contain custom static files (such as style sheets) here, 129 | # relative to this directory. They are copied after the builtin static files, 130 | # so a file named "default.css" will overwrite the builtin "default.css". 131 | html_static_path = ['_static'] 132 | 133 | # Add any extra paths that contain custom files (such as robots.txt or 134 | # .htaccess) here, relative to this directory. These files are copied 135 | # directly to the root of the documentation. 136 | #html_extra_path = [] 137 | 138 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 139 | # using the given strftime format. 140 | #html_last_updated_fmt = '%b %d, %Y' 141 | 142 | # If true, SmartyPants will be used to convert quotes and dashes to 143 | # typographically correct entities. 144 | #html_use_smartypants = True 145 | 146 | # Custom sidebar templates, maps document names to template names. 147 | #html_sidebars = {} 148 | 149 | # Additional templates that should be rendered to pages, maps page names to 150 | # template names. 151 | #html_additional_pages = {} 152 | 153 | # If false, no module index is generated. 154 | #html_domain_indices = True 155 | 156 | # If false, no index is generated. 157 | #html_use_index = True 158 | 159 | # If true, the index is split into individual pages for each letter. 160 | #html_split_index = False 161 | 162 | # If true, links to the reST sources are added to the pages. 163 | #html_show_sourcelink = True 164 | 165 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 166 | #html_show_sphinx = True 167 | 168 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 169 | #html_show_copyright = True 170 | 171 | # If true, an OpenSearch description file will be output, and all pages will 172 | # contain a tag referring to it. The value of this option must be the 173 | # base URL from which the finished HTML is served. 174 | #html_use_opensearch = '' 175 | 176 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 177 | #html_file_suffix = None 178 | 179 | # Output file base name for HTML help builder. 180 | htmlhelp_basename = 'FlaskNavigationdoc' 181 | 182 | 183 | # -- Options for LaTeX output --------------------------------------------- 184 | 185 | latex_elements = { 186 | # The paper size ('letterpaper' or 'a4paper'). 187 | #'papersize': 'letterpaper', 188 | 189 | # The font size ('10pt', '11pt' or '12pt'). 190 | #'pointsize': '10pt', 191 | 192 | # Additional stuff for the LaTeX preamble. 193 | #'preamble': '', 194 | } 195 | 196 | # Grouping the document tree into LaTeX files. List of tuples 197 | # (source start file, target name, title, 198 | # author, documentclass [howto, manual, or own class]). 199 | latex_documents = [ 200 | ('index', 'FlaskNavigation.tex', u'Flask Navigation Documentation', 201 | u'Jiangge Zhang', 'manual'), 202 | ] 203 | 204 | # The name of an image file (relative to this directory) to place at the top of 205 | # the title page. 206 | #latex_logo = None 207 | 208 | # For "manual" documents, if this is true, then toplevel headings are parts, 209 | # not chapters. 210 | #latex_use_parts = False 211 | 212 | # If true, show page references after internal links. 213 | #latex_show_pagerefs = False 214 | 215 | # If true, show URL addresses after external links. 216 | #latex_show_urls = False 217 | 218 | # Documents to append as an appendix to all manuals. 219 | #latex_appendices = [] 220 | 221 | # If false, no module index is generated. 222 | #latex_domain_indices = True 223 | 224 | 225 | # -- Options for manual page output --------------------------------------- 226 | 227 | # One entry per manual page. List of tuples 228 | # (source start file, name, description, authors, manual section). 229 | man_pages = [ 230 | ('index', 'flasknavigation', u'Flask Navigation Documentation', 231 | [u'Jiangge Zhang'], 1) 232 | ] 233 | 234 | # If true, show URL addresses after external links. 235 | #man_show_urls = False 236 | 237 | 238 | # -- Options for Texinfo output ------------------------------------------- 239 | 240 | # Grouping the document tree into Texinfo files. List of tuples 241 | # (source start file, target name, title, author, 242 | # dir menu entry, description, category) 243 | texinfo_documents = [ 244 | ('index', 'FlaskNavigation', u'Flask Navigation Documentation', 245 | u'Jiangge Zhang', 'FlaskNavigation', 'One line description of project.', 246 | 'Miscellaneous'), 247 | ] 248 | 249 | # Documents to append as an appendix to all manuals. 250 | #texinfo_appendices = [] 251 | 252 | # If false, no module index is generated. 253 | #texinfo_domain_indices = True 254 | 255 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 256 | #texinfo_show_urls = 'footnote' 257 | 258 | # If true, do not generate a @detailmenu in the "Top" node's menu. 259 | #texinfo_no_detailmenu = False 260 | 261 | 262 | # -- Options for Epub output ---------------------------------------------- 263 | 264 | # Bibliographic Dublin Core info. 265 | epub_title = u'Flask Navigation' 266 | epub_author = u'Jiangge Zhang' 267 | epub_publisher = u'Jiangge Zhang' 268 | epub_copyright = u'2014, Jiangge Zhang' 269 | 270 | # The basename for the epub file. It defaults to the project name. 271 | #epub_basename = u'Flask Navigation' 272 | 273 | # The HTML theme for the epub output. Since the default themes are not optimized 274 | # for small screen space, using the same theme for HTML and epub output is 275 | # usually not wise. This defaults to 'epub', a theme designed to save visual 276 | # space. 277 | #epub_theme = 'epub' 278 | 279 | # The language of the text. It defaults to the language option 280 | # or en if the language is not set. 281 | #epub_language = '' 282 | 283 | # The scheme of the identifier. Typical schemes are ISBN or URL. 284 | #epub_scheme = '' 285 | 286 | # The unique identifier of the text. This can be a ISBN number 287 | # or the project homepage. 288 | #epub_identifier = '' 289 | 290 | # A unique identification for the text. 291 | #epub_uid = '' 292 | 293 | # A tuple containing the cover image and cover page html template filenames. 294 | #epub_cover = () 295 | 296 | # A sequence of (type, uri, title) tuples for the guide element of content.opf. 297 | #epub_guide = () 298 | 299 | # HTML files that should be inserted before the pages created by sphinx. 300 | # The format is a list of tuples containing the path and title. 301 | #epub_pre_files = [] 302 | 303 | # HTML files shat should be inserted after the pages created by sphinx. 304 | # The format is a list of tuples containing the path and title. 305 | #epub_post_files = [] 306 | 307 | # A list of files that should not be packed into the epub file. 308 | epub_exclude_files = ['search.html'] 309 | 310 | # The depth of the table of contents in toc.ncx. 311 | #epub_tocdepth = 3 312 | 313 | # Allow duplicate toc entries. 314 | #epub_tocdup = True 315 | 316 | # Choose between 'default' and 'includehidden'. 317 | #epub_tocscope = 'default' 318 | 319 | # Fix unsupported image types using the PIL. 320 | #epub_fix_images = False 321 | 322 | # Scale large images. 323 | #epub_max_image_width = 0 324 | 325 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 326 | #epub_show_urls = 'inline' 327 | 328 | # If false, no index is generated. 329 | #epub_use_index = True 330 | 331 | 332 | # Example configuration for intersphinx: refer to the Python standard library. 333 | intersphinx_mapping = { 334 | 'http://docs.python.org/': None, 335 | 'http://flask.pocoo.org/docs/': None, 336 | } 337 | --------------------------------------------------------------------------------