├── tests
├── themes
│ ├── plain
│ │ ├── license.txt
│ │ ├── templates
│ │ │ └── static.html
│ │ └── info.json
│ ├── cool
│ │ ├── templates
│ │ │ └── hello.html
│ │ └── info.json
│ ├── warm-theme
│ │ └── info.json
│ ├── notthis
│ │ └── info.json
│ └── identifier
│ │ └── info.json
├── templates
│ ├── hello.html
│ ├── static.html
│ ├── active.html
│ └── static_parent.html
├── morethemes
│ └── cool
│ │ ├── templates
│ │ ├── hello.html
│ │ ├── static.html
│ │ └── active.html
│ │ └── info.json
└── test-themes.py
├── base_theme
├── license.txt
├── static
│ └── style.css
├── templates
│ └── layout.html
└── info.json
├── docs
├── _themes
│ ├── flask
│ │ ├── theme.conf
│ │ └── static
│ │ │ └── flasky.css_t
│ ├── flask_small
│ │ ├── theme.conf
│ │ ├── layout.html
│ │ └── static
│ │ │ └── flasky.css_t
│ ├── README
│ └── flask_theme_support.py
├── Makefile
├── make.bat
├── conf.py
└── index.rst
├── example
├── templates
│ ├── _helpers.html
│ ├── about.html
│ ├── _post.html
│ ├── post.html
│ ├── index.html
│ ├── archive.html
│ ├── layout.html
│ └── themes.html
├── themes
│ ├── plain
│ │ ├── info.json
│ │ ├── templates
│ │ │ └── layout.html
│ │ └── static
│ │ │ └── static.css
│ └── calmblue
│ │ ├── info.json
│ │ ├── templates
│ │ └── layout.html
│ │ └── static
│ │ └── style.css
├── themesandbox.py
└── posts.yaml
├── MANIFEST.in
├── Pipfile
├── tox.ini
├── README
├── requirements.txt
├── LICENSE
├── new-theme.py
├── setup.py
├── .gitignore
├── requirements-dev.txt
├── Pipfile.lock
└── flask_themes
└── __init__.py
/tests/themes/plain/license.txt:
--------------------------------------------------------------------------------
1 | The license.
2 |
--------------------------------------------------------------------------------
/tests/templates/hello.html:
--------------------------------------------------------------------------------
1 | Hello from the application
2 |
--------------------------------------------------------------------------------
/tests/themes/cool/templates/hello.html:
--------------------------------------------------------------------------------
1 | Hello from Cool Blue v1.
2 |
--------------------------------------------------------------------------------
/tests/morethemes/cool/templates/hello.html:
--------------------------------------------------------------------------------
1 | Hello from Cool Blue v2.
2 |
--------------------------------------------------------------------------------
/base_theme/license.txt:
--------------------------------------------------------------------------------
1 | The complete text of your license should go here.
2 |
--------------------------------------------------------------------------------
/tests/templates/static.html:
--------------------------------------------------------------------------------
1 | Application, {{ theme_static('style.css') }}
2 |
--------------------------------------------------------------------------------
/tests/themes/plain/templates/static.html:
--------------------------------------------------------------------------------
1 | Plain, {{ theme_static('style.css') }}
2 |
--------------------------------------------------------------------------------
/tests/templates/active.html:
--------------------------------------------------------------------------------
1 | Application, Active theme: {{ _theme|default("none") }}
2 |
--------------------------------------------------------------------------------
/tests/morethemes/cool/templates/static.html:
--------------------------------------------------------------------------------
1 | Cool Blue v2, {{ theme_static('style.css') }}
2 |
--------------------------------------------------------------------------------
/tests/templates/static_parent.html:
--------------------------------------------------------------------------------
1 | Application, {% include '_themes/plain/static.html' %}
2 |
--------------------------------------------------------------------------------
/tests/morethemes/cool/templates/active.html:
--------------------------------------------------------------------------------
1 | Cool Blue v2, Active theme: {{ _theme|default("none") }}
2 |
--------------------------------------------------------------------------------
/docs/_themes/flask/theme.conf:
--------------------------------------------------------------------------------
1 | [theme]
2 | inherit = basic
3 | stylesheet = flasky.css
4 | pygments_style = flask_theme_support.FlaskyStyle
5 |
--------------------------------------------------------------------------------
/example/templates/_helpers.html:
--------------------------------------------------------------------------------
1 | {% macro link_to(text, endpoint) -%}
2 | {{ text }}
3 | {%- endmacro %}
4 |
--------------------------------------------------------------------------------
/tests/themes/plain/info.json:
--------------------------------------------------------------------------------
1 | {
2 | "identifier": "plain",
3 | "name": "Plain",
4 | "application": "testing",
5 | "author": "LeafStorm"
6 | }
7 |
--------------------------------------------------------------------------------
/tests/themes/cool/info.json:
--------------------------------------------------------------------------------
1 | {
2 | "identifier": "cool",
3 | "name": "Cool Blue v1",
4 | "application": "testing",
5 | "author": "LeafStorm"
6 | }
7 |
--------------------------------------------------------------------------------
/tests/morethemes/cool/info.json:
--------------------------------------------------------------------------------
1 | {
2 | "identifier": "cool",
3 | "name": "Cool Blue v2",
4 | "application": "testing",
5 | "author": "LeafStorm"
6 | }
7 |
--------------------------------------------------------------------------------
/tests/themes/warm-theme/info.json:
--------------------------------------------------------------------------------
1 | {
2 | "identifier": "warm-theme",
3 | "name": "Warm",
4 | "application": "testing",
5 | "author": "LeafStorm"
6 | }
7 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | recursive-include tests *.py *.html *.txt *.json
2 | include base_theme/*
3 | include base_theme/static/*
4 | include base_theme/templates/*
5 | include new-theme.py
--------------------------------------------------------------------------------
/base_theme/static/style.css:
--------------------------------------------------------------------------------
1 | /*
2 | * This directory is for static files. Static files could include CSS
3 | * stylesheets, images, or even JavaScript scripts.
4 | */
5 |
--------------------------------------------------------------------------------
/tests/themes/notthis/info.json:
--------------------------------------------------------------------------------
1 | {
2 | "identifier": "notthis",
3 | "name": "Not This App",
4 | "application": "anotherapp",
5 | "author": "LeafStorm"
6 | }
7 |
--------------------------------------------------------------------------------
/tests/themes/identifier/info.json:
--------------------------------------------------------------------------------
1 | {
2 | "identifier": "mismatch",
3 | "name": "Identifier Mismatch",
4 | "application": "testing",
5 | "author": "LeafStorm"
6 | }
7 |
--------------------------------------------------------------------------------
/base_theme/templates/layout.html:
--------------------------------------------------------------------------------
1 | {# This folder is for Jinja2 templates. Most of them have names ending in
2 | '.html'. 'layout.html' is the common convention for "layout" themes.
3 | #}
4 |
--------------------------------------------------------------------------------
/example/templates/about.html:
--------------------------------------------------------------------------------
1 | {% extends theme('layout.html') %}
2 |
3 | {% set title = 'About' %}
4 |
5 | {% block body %}
6 |
7 |
About
8 |
9 | {{ text }}
10 |
11 | {% endblock body %}
12 |
--------------------------------------------------------------------------------
/example/templates/_post.html:
--------------------------------------------------------------------------------
1 | {% macro show_post(post) %}
2 |
3 | {{ post.title }}
4 |
5 | Created on {{ post.created.strftime('%x at %X') }}
6 |
7 | {{ post.content }}
8 |
9 | {% endmacro %}
10 |
--------------------------------------------------------------------------------
/example/templates/post.html:
--------------------------------------------------------------------------------
1 | {% extends theme('layout.html') %}
2 | {% from theme('_post.html') import show_post %}
3 |
4 | {% set title = post.title %}
5 |
6 | {% block body %}
7 |
8 | {{ show_post(post) }}
9 |
10 | {% endblock body %}
11 |
--------------------------------------------------------------------------------
/example/themes/plain/info.json:
--------------------------------------------------------------------------------
1 | {
2 | "application": "themesandbox",
3 | "identifier": "plain",
4 | "name": "Plain",
5 | "author": "LeafStorm",
6 | "license": "MIT/X11",
7 | "description": "An easy-to-read, green-based theme."
8 | }
9 |
--------------------------------------------------------------------------------
/docs/_themes/flask_small/theme.conf:
--------------------------------------------------------------------------------
1 | [theme]
2 | inherit = basic
3 | stylesheet = flasky.css
4 | nosidebar = true
5 | pygments_style = flask_theme_support.FlaskyStyle
6 |
7 | [options]
8 | index_logo = ''
9 | index_logo_height = 120px
10 | github_fork = ''
11 |
--------------------------------------------------------------------------------
/example/themes/calmblue/info.json:
--------------------------------------------------------------------------------
1 | {
2 | "application": "themesandbox",
3 | "identifier": "calmblue",
4 | "name": "Calm Blue",
5 | "author": "LeafStorm",
6 | "description": "A calm, blue theme based on Flaskr.",
7 | "license": "MIT/X11"
8 | }
9 |
--------------------------------------------------------------------------------
/Pipfile:
--------------------------------------------------------------------------------
1 | [[source]]
2 |
3 | url = "https://pypi.python.org/simple"
4 | verify_ssl = true
5 | name = "pypi"
6 |
7 |
8 | [packages]
9 |
10 | flask = "*"
11 | six = "*"
12 |
13 |
14 | [dev-packages]
15 |
16 | "flake8" = "*"
17 | nose = "*"
18 | pyyaml = "*"
19 | sphinx = "*"
20 | tox = "*"
21 | "e1839a8" = {path = ".", editable = true}
22 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist = py25,py27,py36,docs,lint
3 |
4 | [testenv]
5 | deps =
6 | nose
7 | commands =
8 | nosetests
9 |
10 | [testenv:docs]
11 | basepython = python
12 | deps =
13 | sphinx
14 | commands =
15 | make -C docs html
16 |
17 | [testenv:lint]
18 | basepython = python
19 | deps =
20 | flake8
21 | commands =
22 | flake8 flask_theme
23 |
24 |
--------------------------------------------------------------------------------
/example/templates/index.html:
--------------------------------------------------------------------------------
1 | {% extends theme('layout.html') %}
2 | {% from theme('_post.html') import show_post %}
3 |
4 | {% set title = 'Index' %}
5 |
6 | {% block body %}
7 |
8 | {% for post in posts %}
9 |
10 | {{ show_post(post) }}
11 |
12 | {{ link_to('Permalink', 'post', slug=post.slug) }}
13 |
14 | {% endfor %}
15 |
16 | {% endblock body %}
17 |
--------------------------------------------------------------------------------
/example/templates/archive.html:
--------------------------------------------------------------------------------
1 | {% extends theme('layout.html') %}
2 |
3 | {% set title = 'Archive' %}
4 |
5 | {% block body %}
6 | Archive
7 |
8 |
9 | {%- for post in posts %}
10 | - {{ link_to(post.title, 'post', slug=post.slug) }}
11 | — created on {{ post.created.strftime('%x at %X') }}
12 | {%- endfor %}
13 |
14 | {% endblock body %}
15 |
--------------------------------------------------------------------------------
/README:
--------------------------------------------------------------------------------
1 | Flask-Themes
2 |
3 | This provides infrastructure for themes in Flask.
4 |
5 | Installation 'pip install Flask-Themes' or
6 | 'easy_install Flask-Themes'
7 | Documentation http://packages.python.org/Flask-Themes
8 | to build: cd to 'docs' and 'make html'
9 | Source code http://bitbucket.org/leafstorm/flask-themes/
10 | Tests run 'nosetests'
11 |
--------------------------------------------------------------------------------
/base_theme/info.json:
--------------------------------------------------------------------------------
1 | {
2 | "identifier": "base_theme",
3 | "application": "whatever_application",
4 | "name": "Base Theme",
5 | "description": "This isn't a real theme; it's an example of a theme's basic structure.",
6 | "author": "Matthew \"LeafStorm\" Frazier",
7 | "website": "http://leafstorm.us/",
8 | "license": "Public Domain",
9 | "preview": "preview.png",
10 | "doctype": "html5"
11 | }
12 |
--------------------------------------------------------------------------------
/example/templates/layout.html:
--------------------------------------------------------------------------------
1 |
2 | {% from "_helpers.html" import link_to %}
3 |
4 | Theme Sandbox: {{ title }}
5 | {% block head %}{% endblock head %}
6 |
7 |
8 |
9 | Theme Sandbox
10 |
11 |
12 | {{ link_to('Index', 'index') }} |
13 | {{ link_to('Archive', 'archive') }} |
14 | {{ link_to('About', 'about') }} |
15 | {{ link_to('Themes', 'themes') }}
16 |
17 |
18 | {% block body %}
19 | {% endblock body %}
20 |
21 |
--------------------------------------------------------------------------------
/example/templates/themes.html:
--------------------------------------------------------------------------------
1 | {% extends theme('layout.html') %}
2 | {% from "_helpers.html" import link_to %}
3 |
4 | {% set title = 'Themes' %}
5 |
6 | {% block body %}
7 |
8 | Theme Selection
9 |
10 | {% for theme in themes %}
11 |
12 | {{ theme.name }}
13 |
14 |
15 | By {{ theme.author }}{% if theme.license %}, license: {{ theme.license }}{% endif %}
16 |
17 |
18 | {{ theme.description }}
19 |
20 | {{ link_to('Select this theme', 'settheme', ident=theme.identifier) }}
21 |
22 | {% endfor %}
23 |
24 | {% endblock body %}
25 |
--------------------------------------------------------------------------------
/example/themes/calmblue/templates/layout.html:
--------------------------------------------------------------------------------
1 |
2 | {% from "_helpers.html" import link_to %}
3 |
4 | Theme Sandbox — {{ title }}
5 |
6 | {% block head %}{% endblock head %}
7 |
8 |
9 |
Theme Sandbox
10 |
11 | {{ link_to('Index', 'index') }} |
12 | {{ link_to('Archive', 'archive') }} |
13 | {{ link_to('About', 'about') }} |
14 | {{ link_to('Themes', 'themes') }}
15 |
16 |
17 | {% block body %}{% endblock %}
18 |
19 |
--------------------------------------------------------------------------------
/example/themes/plain/templates/layout.html:
--------------------------------------------------------------------------------
1 |
2 | {% from "_helpers.html" import link_to %}
3 |
4 | Theme Sandbox: {{ title }}
5 |
6 | {% block head %}{% endblock head %}
7 |
8 |
9 |
10 |
11 |
12 | {{ link_to('Index', 'index') }} |
13 | {{ link_to('Archive', 'archive') }} |
14 | {{ link_to('About', 'about') }} |
15 | {{ link_to('Themes', 'themes') }}
16 |
17 |
18 |
Theme Sandbox
19 |
20 |
21 |
22 | {% block body %}
23 | {% endblock body %}
24 |
25 |
26 |
--------------------------------------------------------------------------------
/docs/_themes/flask_small/layout.html:
--------------------------------------------------------------------------------
1 | {% extends "basic/layout.html" %}
2 | {% block header %}
3 | {{ super() }}
4 | {% if pagename == 'index' %}
5 |
6 | {% endif %}
7 | {% endblock %}
8 | {% block footer %}
9 | {% if pagename == 'index' %}
10 |
11 | {% endif %}
12 | {% endblock %}
13 | {# do not display relbars #}
14 | {% block relbar1 %}{% endblock %}
15 | {% block relbar2 %}
16 | {% if theme_github_fork %}
17 |
19 | {% endif %}
20 | {% endblock %}
21 | {% block sidebar1 %}{% endblock %}
22 | {% block sidebar2 %}{% endblock %}
23 |
--------------------------------------------------------------------------------
/example/themes/plain/static/static.css:
--------------------------------------------------------------------------------
1 | div#page {
2 | width: 40em;
3 | margin: 16px auto;
4 | font: serif;
5 | }
6 |
7 | h1, h2, h3 {
8 | color: #121;
9 | }
10 |
11 | a {
12 | color: #484;
13 | }
14 |
15 | h1 {
16 | font-size: 2em;
17 | }
18 |
19 | h2 {
20 | font-size: 1.5em;
21 | }
22 |
23 | h3 {
24 | font-size: 1.2em;
25 | }
26 |
27 | div.nav {
28 | text-align: right;
29 | font-size: 0.9em;
30 | float: right;
31 | }
32 |
33 | hr.top {
34 | border-color: #121;
35 | border-style: solid;
36 | border-width: 1px 0 0;
37 | clear: both;
38 | margin: 0 0 20px;
39 | height: 0;
40 | }
41 |
42 | p.meta {
43 | font-style: italic;
44 | font-size: 0.9em;
45 | }
46 |
47 | p.permalink, p.select {
48 | text-align: right;
49 | font-size: 0.9em;
50 | }
51 |
--------------------------------------------------------------------------------
/example/themes/calmblue/static/style.css:
--------------------------------------------------------------------------------
1 | body { font-family: sans-serif; background: #def; }
2 | a, h1, h2, h3 { color: #377BA8; }
3 | h1, h2, h3 { font-family: 'Georgia', serif; margin: 0; }
4 | h1 { border-bottom: 2px solid #eee; }
5 | h2 { font-size: 1.3em; }
6 | h3 { font-size: 1.1em; margin-top: 8px; }
7 |
8 | .page { margin: 2em auto; width: 35em; border: 5px solid #ccc;
9 | padding: 0.8em; background: white; }
10 | .nav { text-align: right; font-size: 0.8em; padding: 0.3em;
11 | margin-bottom: 1em; background: #fafafa; }
12 |
13 | p.permalink,
14 | p.select,
15 | p.meta { font-family: 'Georgia', serif; font-style: italic;
16 | font-size: 0.9em; }
17 | p.select,
18 | p.permalink { text-align: right; }
19 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | click==6.7 --hash=sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d --hash=sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b
2 | flask==0.12.2 --hash=sha256:0749df235e3ff61ac108f69ac178c9770caeaccad2509cb762ce1f65570a8856 --hash=sha256:49f44461237b69ecd901cc7ce66feea0319b9158743dd27a2899962ab214dac1
3 | itsdangerous==0.24 --hash=sha256:cbb3fcf8d3e33df861709ecaf89d9e6629cff0a217bc2848f1b41cd30d360519
4 | jinja2==2.10 --hash=sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd --hash=sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4
5 | markupsafe==1.0 --hash=sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665
6 | six==1.11.0 --hash=sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb --hash=sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9
7 | werkzeug==0.14.1 --hash=sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b --hash=sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2010 Matthew "LeafStorm" Frazier
2 |
3 | Permission is hereby granted, free of charge, to any person
4 | obtaining a copy of this software and associated documentation
5 | files (the "Software"), to deal in the Software without
6 | restriction, including without limitation the rights to use,
7 | copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software, and to permit persons to whom the
9 | Software is furnished to do so, subject to the following
10 | conditions:
11 |
12 | The above copyright notice and this permission notice shall be
13 | included in all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22 | OTHER DEALINGS IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/docs/_themes/README:
--------------------------------------------------------------------------------
1 | Flask Sphinx Styles
2 | ===================
3 |
4 | This repository contains sphinx styles for Flask and Flask related
5 | projects. To use this style in your Sphinx documentation, follow
6 | this guide:
7 |
8 | 1. put this folder as _themes into your docs folder. Alternatively
9 | you can also use git submodules to check out the contents there.
10 | 2. add this to your conf.py:
11 |
12 | sys.path.append(os.path.abspath('_themes'))
13 | html_theme_path = ['_themes']
14 | html_theme = 'flask'
15 |
16 | The following themes exist:
17 |
18 | - 'flask' - the standard flask documentation theme for large
19 | projects
20 | - 'flask_small' - small one-page theme. Intended to be used by
21 | very small addon libraries for flask.
22 |
23 | The following options exist for the flask_small theme:
24 |
25 | [options]
26 | index_logo = '' filename of a picture in _static
27 | to be used as replacement for the
28 | h1 in the index.rst file.
29 | index_logo_height = 120px height of the index logo
30 | github_fork = '' repository name on github for the
31 | "fork me" badge
32 |
--------------------------------------------------------------------------------
/new-theme.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | """
4 | new-theme.py
5 | ============
6 | This is a simple script that creates a new theme in the given directory.
7 | """
8 | import os
9 | import os.path
10 | import sys
11 | try:
12 | import simplejson as json
13 | except ImportError:
14 | import json
15 |
16 |
17 | def ident_to_title(ident):
18 | return ident.replace('_', ' ').title()
19 |
20 |
21 | def create_theme(appident, destination):
22 | destination = destination.rstrip(os.path.sep)
23 | identifier = os.path.basename(destination)
24 | data = dict(
25 | application=appident,
26 | identifier=identifier,
27 | name=ident_to_title(identifier),
28 | author='Your Name'
29 | )
30 | os.makedirs(destination)
31 |
32 | info_json = os.path.join(destination, 'info.json')
33 | templates_path = os.path.join(destination, 'templates')
34 | static_path = os.path.join(destination, 'static')
35 | with open(info_json, 'w') as fd:
36 | json.dump(data, fd, indent=4)
37 | os.makedirs(templates_path)
38 | os.makedirs(static_path)
39 |
40 |
41 | if __name__ == '__main__':
42 | args = sys.argv[1:]
43 | scriptname = os.path.basename(sys.argv[0])
44 | if len(args) < 2:
45 | print "Usage: %s APPIDENT PATH" % scriptname
46 | sys.exit(2)
47 | create_theme(args[0], args[1])
48 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | """
2 | Flask-Themes
3 | ------------
4 | Flask-Themes provides infrastructure for theming support in Flask
5 | applications. It takes care of:
6 |
7 | - Loading themes
8 | - Rendering templates from themes
9 | - Serving static files like CSS and images from themes
10 |
11 |
12 | Links
13 | `````
14 | * `documentation `_
15 | * `development version
16 | `_
17 |
18 |
19 | """
20 | from setuptools import setup
21 | import sys
22 |
23 | requires = ['Flask', 'six']
24 | if sys.version_info < (2, 6):
25 | requires.append('simplejson')
26 |
27 | test_requires = ['nose']
28 |
29 | setup(
30 | name='Flask-Themes',
31 | version='0.1.4',
32 | url='http://bitbucket.org/leafstorm/flask-themes/',
33 | license='MIT',
34 | author='Matthew \"LeafStorm\" Frazier',
35 | author_email='leafstormrush@gmail.com',
36 | description='Provides infrastructure for theming Flask applications',
37 | long_description=__doc__,
38 | packages=['flask_themes'],
39 | zip_safe=False,
40 | platforms='any',
41 | install_requires=requires,
42 | tests_require=test_requires,
43 | test_suite='nose.collector',
44 | classifiers=[
45 | 'Development Status :: 4 - Beta',
46 | 'Environment :: Web Environment',
47 | 'Intended Audience :: Developers',
48 | 'License :: OSI Approved :: MIT License',
49 | 'Operating System :: OS Independent',
50 | 'Programming Language :: Python',
51 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
52 | 'Topic :: Software Development :: Libraries :: Python Modules'
53 | ]
54 | )
55 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | env/
12 | build/
13 | develop-eggs/
14 | dist/
15 | downloads/
16 | eggs/
17 | .eggs/
18 | lib/
19 | lib64/
20 | parts/
21 | sdist/
22 | var/
23 | wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 |
28 | # PyInstaller
29 | # Usually these files are written by a python script from a template
30 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
31 | *.manifest
32 | *.spec
33 |
34 | # Installer logs
35 | pip-log.txt
36 | pip-delete-this-directory.txt
37 |
38 | # Unit test / coverage reports
39 | htmlcov/
40 | .tox/
41 | .coverage
42 | .coverage.*
43 | .cache
44 | nosetests.xml
45 | coverage.xml
46 | *.cover
47 | .hypothesis/
48 |
49 | # Translations
50 | *.mo
51 | *.pot
52 |
53 | # Django stuff:
54 | *.log
55 | local_settings.py
56 |
57 | # Flask stuff:
58 | instance/
59 | .webassets-cache
60 |
61 | # Scrapy stuff:
62 | .scrapy
63 |
64 | # Sphinx documentation
65 | docs/_build/
66 |
67 | # PyBuilder
68 | target/
69 |
70 | # Jupyter Notebook
71 | .ipynb_checkpoints
72 |
73 | # pyenv
74 | .python-version
75 |
76 | # celery beat schedule file
77 | celerybeat-schedule
78 |
79 | # SageMath parsed files
80 | *.sage.py
81 |
82 | # dotenv
83 | .env
84 |
85 | # virtualenv
86 | .venv
87 | venv/
88 | ENV/
89 |
90 | # Spyder project settings
91 | .spyderproject
92 | .spyproject
93 |
94 | # Rope project settings
95 | .ropeproject
96 |
97 | # mkdocs documentation
98 | /site
99 |
100 | # mypy
101 | .mypy_cache/
102 |
103 | # Sphinx documentation
104 | docs/_build/
105 |
106 | # OS generated files #
107 | .DS_Store
108 | .DS_Store?
109 | ._*
110 | .Spotlight-V100
111 | .Trashes
112 | ehthumbs.db
113 | Thumbs.db
114 |
115 | # IDEs and editors
116 | .idea/
117 | *.swp
118 |
--------------------------------------------------------------------------------
/example/themesandbox.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | """
4 | themesandbox.py
5 | ===============
6 | A sandbox to play around with themes in.
7 |
8 | :copyright: 2010 Matthew "LeafStorm" Frazier
9 | :license: MIT/X11, see LICENSE for details
10 | """
11 | import yaml
12 | from flask import (Flask, url_for, redirect, session, Markup, abort)
13 | from flask_themes import setup_themes, render_theme_template, get_themes_list
14 | from operator import attrgetter
15 |
16 | # default settings
17 |
18 | DEFAULT_THEME = 'calmblue'
19 | SECRET_KEY = 'not really secret'
20 |
21 |
22 | # application
23 |
24 | app = Flask(__name__)
25 | app.config.from_object(__name__)
26 | setup_themes(app, app_identifier='themesandbox')
27 |
28 |
29 | # data
30 |
31 | class Post(object):
32 | def __init__(self, data):
33 | self.slug = data['slug']
34 | self.body = data['body']
35 | self.title = data['title']
36 | self.created = data['created']
37 |
38 | @property
39 | def content(self):
40 | return Markup('\n\n'.join(
41 | '%s
' % line for line in self.body.splitlines()
42 | ))
43 |
44 |
45 | class PostStore(object):
46 | def __init__(self):
47 | self.by_date = []
48 | self.by_slug = {}
49 |
50 | def add_posts(self, post_data):
51 | posts = [Post(post) for post in post_data]
52 | for post in posts:
53 | if post.slug in self.by_slug:
54 | raise RuntimeError("slugs must be unique")
55 | self.by_slug[post.slug] = post
56 | self.by_date.extend(posts)
57 | self.by_date.sort(key=attrgetter('created'), reverse=True)
58 |
59 |
60 | store = PostStore()
61 |
62 | with app.open_resource('posts.yaml') as fd:
63 | post_data = yaml.load_all(fd)
64 | store.add_posts(post_data)
65 |
66 |
67 | ABOUT_TEXT = Markup('This is a demonstration of Flask-Themes.
')
68 |
69 |
70 | # themes
71 |
72 | def render(template, **context):
73 | theme = session.get('theme', app.config['DEFAULT_THEME'])
74 | return render_theme_template(theme, template, **context)
75 |
76 |
77 | # views
78 |
79 | @app.route('/')
80 | def index():
81 | posts = store.by_date[:3]
82 | return render('index.html', posts=posts)
83 |
84 |
85 | @app.route('/archive')
86 | def archive():
87 | posts = store.by_date[:]
88 | return render('archive.html', posts=posts)
89 |
90 |
91 | @app.route('/post/')
92 | def post(slug):
93 | post = store.by_slug.get(slug)
94 | if post is None:
95 | abort(404)
96 | return render('post.html', post=post)
97 |
98 |
99 | @app.route('/about')
100 | def about():
101 | return render('about.html', text=ABOUT_TEXT)
102 |
103 |
104 | @app.route('/themes/')
105 | def themes():
106 | themes = get_themes_list()
107 | return render('themes.html', themes=themes)
108 |
109 |
110 | @app.route('/themes/')
111 | def settheme(ident):
112 | if ident not in app.theme_manager.themes:
113 | abort(404)
114 | session['theme'] = ident
115 | return redirect(url_for('themes'))
116 |
117 |
118 | if __name__ == '__main__':
119 | app.run(debug=True)
120 |
--------------------------------------------------------------------------------
/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 pickle json htmlhelp qthelp latex 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 " pickle to make pickle files"
22 | @echo " json to make JSON files"
23 | @echo " htmlhelp to make HTML files and a HTML help project"
24 | @echo " qthelp to make HTML files and a qthelp project"
25 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
26 | @echo " changes to make an overview of all changed/added/deprecated items"
27 | @echo " linkcheck to check all external links for integrity"
28 | @echo " doctest to run all doctests embedded in the documentation (if enabled)"
29 |
30 | clean:
31 | -rm -rf $(BUILDDIR)/*
32 |
33 | html:
34 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
35 | @echo
36 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
37 |
38 | dirhtml:
39 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
40 | @echo
41 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
42 |
43 | pickle:
44 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
45 | @echo
46 | @echo "Build finished; now you can process the pickle files."
47 |
48 | json:
49 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
50 | @echo
51 | @echo "Build finished; now you can process the JSON files."
52 |
53 | htmlhelp:
54 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
55 | @echo
56 | @echo "Build finished; now you can run HTML Help Workshop with the" \
57 | ".hhp project file in $(BUILDDIR)/htmlhelp."
58 |
59 | qthelp:
60 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
61 | @echo
62 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \
63 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
64 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Flask-Themes.qhcp"
65 | @echo "To view the help file:"
66 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Flask-Themes.qhc"
67 |
68 | latex:
69 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
70 | @echo
71 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
72 | @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \
73 | "run these through (pdf)latex."
74 |
75 | changes:
76 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
77 | @echo
78 | @echo "The overview file is in $(BUILDDIR)/changes."
79 |
80 | linkcheck:
81 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
82 | @echo
83 | @echo "Link check complete; look for any errors in the above output " \
84 | "or in $(BUILDDIR)/linkcheck/output.txt."
85 |
86 | doctest:
87 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
88 | @echo "Testing of doctests in the sources finished, look at the " \
89 | "results in $(BUILDDIR)/doctest/output.txt."
90 |
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | REM Command file for Sphinx documentation
4 |
5 | set SPHINXBUILD=sphinx-build
6 | set BUILDDIR=_build
7 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
8 | if NOT "%PAPER%" == "" (
9 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
10 | )
11 |
12 | if "%1" == "" goto help
13 |
14 | if "%1" == "help" (
15 | :help
16 | echo.Please use `make ^` where ^ is one of
17 | echo. html to make standalone HTML files
18 | echo. dirhtml to make HTML files named index.html in directories
19 | echo. pickle to make pickle files
20 | echo. json to make JSON files
21 | echo. htmlhelp to make HTML files and a HTML help project
22 | echo. qthelp to make HTML files and a qthelp project
23 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
24 | echo. changes to make an overview over all changed/added/deprecated items
25 | echo. linkcheck to check all external links for integrity
26 | echo. doctest to run all doctests embedded in the documentation if enabled
27 | goto end
28 | )
29 |
30 | if "%1" == "clean" (
31 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
32 | del /q /s %BUILDDIR%\*
33 | goto end
34 | )
35 |
36 | if "%1" == "html" (
37 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
38 | echo.
39 | echo.Build finished. The HTML pages are in %BUILDDIR%/html.
40 | goto end
41 | )
42 |
43 | if "%1" == "dirhtml" (
44 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
45 | echo.
46 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
47 | goto end
48 | )
49 |
50 | if "%1" == "pickle" (
51 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
52 | echo.
53 | echo.Build finished; now you can process the pickle files.
54 | goto end
55 | )
56 |
57 | if "%1" == "json" (
58 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
59 | echo.
60 | echo.Build finished; now you can process the JSON files.
61 | goto end
62 | )
63 |
64 | if "%1" == "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 | goto end
70 | )
71 |
72 | if "%1" == "qthelp" (
73 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
74 | echo.
75 | echo.Build finished; now you can run "qcollectiongenerator" with the ^
76 | .qhcp project file in %BUILDDIR%/qthelp, like this:
77 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Flask-Themes.qhcp
78 | echo.To view the help file:
79 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Flask-Themes.ghc
80 | goto end
81 | )
82 |
83 | if "%1" == "latex" (
84 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
85 | echo.
86 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
87 | goto end
88 | )
89 |
90 | if "%1" == "changes" (
91 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
92 | echo.
93 | echo.The overview file is in %BUILDDIR%/changes.
94 | goto end
95 | )
96 |
97 | if "%1" == "linkcheck" (
98 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
99 | echo.
100 | echo.Link check complete; look for any errors in the above output ^
101 | or in %BUILDDIR%/linkcheck/output.txt.
102 | goto end
103 | )
104 |
105 | if "%1" == "doctest" (
106 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
107 | echo.
108 | echo.Testing of doctests in the sources finished, look at the ^
109 | results in %BUILDDIR%/doctest/output.txt.
110 | goto end
111 | )
112 |
113 | :end
114 |
--------------------------------------------------------------------------------
/example/posts.yaml:
--------------------------------------------------------------------------------
1 | # example posts for Theme Sandbox
2 | # part of Flask-Themes
3 | # copyright 2010 Matthew "LeafStorm" Frazier
4 | # licensed under the MIT/X11 license (see LICENSE for details)
5 |
6 | slug: introduction
7 | created: 2010-07-01 16:00:00
8 | title: Introduction
9 | body: >
10 | Welcome to the Theme Sandbox. This is a simple Web application intended
11 | to emulate a weblog. Of course, to save on time and effort, all the posts
12 | are pregenerated, and the application is just displaying them. But still,
13 | it gives people a chance to see how themes are implemented.
14 |
15 | There are some templates by default, but they aren't particularly shiny.
16 | You will want to set a theme if you want the blog to look good.
17 |
18 | ---
19 |
20 | slug: creating-themes
21 | created: 2010-07-01 14:00:00
22 | title: Creating Themes
23 | body: >
24 | With the Theme Sandbox, probably the easiest way to create a theme is just
25 | to drop it in the "themes/" folder. You can set THEME_PATHS in the
26 | configuration, but that is still the easiest.
27 |
28 | To actually create a theme, you will need to check the manual for details.
29 | But it essentially boils down to:
30 |
31 | An info.json file that contains metadata about the theme,
32 |
33 | A templates/ folder that contains the theme's Jinja2 templates.
34 |
35 | A static/ folder containing static files to be served.
36 |
37 | A license.txt file (optional) containing the theme's complete license.
38 |
39 | ---
40 |
41 | slug: inspirations
42 | created: 2010-07-01 13:00:00
43 | title: Theme System Inspirations
44 | body: >
45 | My primary inspiration for the theme system was the blogging system Zine.
46 | I didn't implement all its features, like options for themes, simply
47 | because those are tied in to Zine's particular mode of configuration, and
48 | applications will probably have their own.
49 |
50 | One person asked if it was inspired by Deliverance. I looked it up, and
51 | thought that it looked confusing. But basically, the differences are that
52 | Flask-Themes is for single applications, Deliverance is for multiple
53 | applications. Flask-Themes works at the template level, Deliverance works
54 | at the HTML level. Flask-Themes doesn't deal with HTML/XML, while
55 | Deliverance does.
56 |
57 | ---
58 |
59 | slug: templates
60 | created: 2010-07-01 11:00:00
61 | title: Templates
62 | body: >
63 | The templates used by this site are:
64 |
65 | layout.html, which should have a "body" block and a "head" block (where
66 | "head" is in the HTML head element), and should expect an exported
67 | variable named "title" that is the page's title.
68 |
69 | index.html, which accepts a "posts" variable that contains three posts.
70 |
71 | _helpers.html, which exports a "link_to" macro that takes a caption, an
72 | endpoint, and keyword arguments.
73 |
74 | archive.html, which accepts a "posts" variable that contains *all* of the
75 | posts.
76 |
77 | post.html, which accepts a single "post".
78 |
79 | about.html, which accepts "text", the about text as Markup.
80 |
81 | themes.html, which accepts a list of themes and should create links to
82 | "settheme" for each of them.
83 |
84 | ---
85 |
86 | slug: adding-posts
87 | created: 2010-07-01 9:00:00
88 | title: Adding Posts
89 | body: >
90 | The blog probably seems pretty small to you. And that's the idea - have a
91 | dataset just big enough to simulate real-world usage. But if you want, you
92 | can add more posts.
93 |
94 | All the posts are stored in the posts.yaml file. You can add them there.
95 | Slugs must be unique, but that's about the only restriction.
96 |
--------------------------------------------------------------------------------
/docs/_themes/flask_theme_support.py:
--------------------------------------------------------------------------------
1 | # flasky extensions. flasky pygments style based on tango style
2 | from pygments.style import Style
3 | from pygments.token import Keyword, Name, Comment, String, Error, \
4 | Number, Operator, Generic, Whitespace, Punctuation, Other, Literal
5 |
6 |
7 | class FlaskyStyle(Style):
8 | background_color = "#f8f8f8"
9 | default_style = ""
10 |
11 | styles = {
12 | # No corresponding class for the following:
13 | #Text: "", # class: ''
14 | Whitespace: "underline #f8f8f8", # class: 'w'
15 | Error: "#a40000 border:#ef2929", # class: 'err'
16 | Other: "#000000", # class 'x'
17 |
18 | Comment: "italic #8f5902", # class: 'c'
19 | Comment.Preproc: "noitalic", # class: 'cp'
20 |
21 | Keyword: "bold #004461", # class: 'k'
22 | Keyword.Constant: "bold #004461", # class: 'kc'
23 | Keyword.Declaration: "bold #004461", # class: 'kd'
24 | Keyword.Namespace: "bold #004461", # class: 'kn'
25 | Keyword.Pseudo: "bold #004461", # class: 'kp'
26 | Keyword.Reserved: "bold #004461", # class: 'kr'
27 | Keyword.Type: "bold #004461", # class: 'kt'
28 |
29 | Operator: "#582800", # class: 'o'
30 | Operator.Word: "bold #004461", # class: 'ow' - like keywords
31 |
32 | Punctuation: "bold #000000", # class: 'p'
33 |
34 | # because special names such as Name.Class, Name.Function, etc.
35 | # are not recognized as such later in the parsing, we choose them
36 | # to look the same as ordinary variables.
37 | Name: "#000000", # class: 'n'
38 | Name.Attribute: "#c4a000", # class: 'na' - to be revised
39 | Name.Builtin: "#004461", # class: 'nb'
40 | Name.Builtin.Pseudo: "#3465a4", # class: 'bp'
41 | Name.Class: "#000000", # class: 'nc' - to be revised
42 | Name.Constant: "#000000", # class: 'no' - to be revised
43 | Name.Decorator: "#888", # class: 'nd' - to be revised
44 | Name.Entity: "#ce5c00", # class: 'ni'
45 | Name.Exception: "bold #cc0000", # class: 'ne'
46 | Name.Function: "#000000", # class: 'nf'
47 | Name.Property: "#000000", # class: 'py'
48 | Name.Label: "#f57900", # class: 'nl'
49 | Name.Namespace: "#000000", # class: 'nn' - to be revised
50 | Name.Other: "#000000", # class: 'nx'
51 | Name.Tag: "bold #004461", # class: 'nt' - like a keyword
52 | Name.Variable: "#000000", # class: 'nv' - to be revised
53 | Name.Variable.Class: "#000000", # class: 'vc' - to be revised
54 | Name.Variable.Global: "#000000", # class: 'vg' - to be revised
55 | Name.Variable.Instance: "#000000", # class: 'vi' - to be revised
56 |
57 | Number: "#990000", # class: 'm'
58 |
59 | Literal: "#000000", # class: 'l'
60 | Literal.Date: "#000000", # class: 'ld'
61 |
62 | String: "#4e9a06", # class: 's'
63 | String.Backtick: "#4e9a06", # class: 'sb'
64 | String.Char: "#4e9a06", # class: 'sc'
65 | String.Doc: "italic #8f5902", # class: 'sd' - like a comment
66 | String.Double: "#4e9a06", # class: 's2'
67 | String.Escape: "#4e9a06", # class: 'se'
68 | String.Heredoc: "#4e9a06", # class: 'sh'
69 | String.Interpol: "#4e9a06", # class: 'si'
70 | String.Other: "#4e9a06", # class: 'sx'
71 | String.Regex: "#4e9a06", # class: 'sr'
72 | String.Single: "#4e9a06", # class: 's1'
73 | String.Symbol: "#4e9a06", # class: 'ss'
74 |
75 | Generic: "#000000", # class: 'g'
76 | Generic.Deleted: "#a40000", # class: 'gd'
77 | Generic.Emph: "italic #000000", # class: 'ge'
78 | Generic.Error: "#ef2929", # class: 'gr'
79 | Generic.Heading: "bold #000080", # class: 'gh'
80 | Generic.Inserted: "#00A000", # class: 'gi'
81 | Generic.Output: "#888", # class: 'go'
82 | Generic.Prompt: "#745334", # class: 'gp'
83 | Generic.Strong: "bold #000000", # class: 'gs'
84 | Generic.Subheading: "bold #800080", # class: 'gu'
85 | Generic.Traceback: "bold #a40000", # class: 'gt'
86 | }
87 |
--------------------------------------------------------------------------------
/docs/_themes/flask_small/static/flasky.css_t:
--------------------------------------------------------------------------------
1 | /*
2 | * flasky.css_t
3 | * ~~~~~~~~~~~~
4 | *
5 | * Sphinx stylesheet -- flasky theme based on nature theme.
6 | *
7 | * :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS.
8 | * :license: BSD, see LICENSE for details.
9 | *
10 | */
11 |
12 | @import url("basic.css");
13 |
14 | /* -- page layout ----------------------------------------------------------- */
15 |
16 | body {
17 | font-family: 'Georgia', serif;
18 | font-size: 17px;
19 | color: #000;
20 | background: white;
21 | margin: 0;
22 | padding: 0;
23 | }
24 |
25 | div.documentwrapper {
26 | float: left;
27 | width: 100%;
28 | }
29 |
30 | div.bodywrapper {
31 | margin: 40px auto 0 auto;
32 | width: 700px;
33 | }
34 |
35 | hr {
36 | border: 1px solid #B1B4B6;
37 | }
38 |
39 | div.body {
40 | background-color: #ffffff;
41 | color: #3E4349;
42 | padding: 0 30px 30px 30px;
43 | }
44 |
45 | img.floatingflask {
46 | padding: 0 0 10px 10px;
47 | float: right;
48 | }
49 |
50 | div.footer {
51 | text-align: right;
52 | color: #888;
53 | padding: 10px;
54 | font-size: 14px;
55 | width: 650px;
56 | margin: 0 auto 40px auto;
57 | }
58 |
59 | div.footer a {
60 | color: #888;
61 | text-decoration: underline;
62 | }
63 |
64 | div.related {
65 | line-height: 32px;
66 | color: #888;
67 | }
68 |
69 | div.related ul {
70 | padding: 0 0 0 10px;
71 | }
72 |
73 | div.related a {
74 | color: #444;
75 | }
76 |
77 | /* -- body styles ----------------------------------------------------------- */
78 |
79 | a {
80 | color: #004B6B;
81 | text-decoration: underline;
82 | }
83 |
84 | a:hover {
85 | color: #6D4100;
86 | text-decoration: underline;
87 | }
88 |
89 | div.body {
90 | padding-bottom: 40px; /* saved for footer */
91 | }
92 |
93 | div.body h1,
94 | div.body h2,
95 | div.body h3,
96 | div.body h4,
97 | div.body h5,
98 | div.body h6 {
99 | font-family: 'Garamond', 'Georgia', serif;
100 | font-weight: normal;
101 | margin: 30px 0px 10px 0px;
102 | padding: 0;
103 | }
104 |
105 | {% if theme_index_logo %}
106 | div.indexwrapper h1 {
107 | text-indent: -999999px;
108 | background: url({{ theme_index_logo }}) no-repeat center center;
109 | height: {{ theme_index_logo_height }};
110 | }
111 | {% endif %}
112 |
113 | div.body h2 { font-size: 180%; }
114 | div.body h3 { font-size: 150%; }
115 | div.body h4 { font-size: 130%; }
116 | div.body h5 { font-size: 100%; }
117 | div.body h6 { font-size: 100%; }
118 |
119 | a.headerlink {
120 | color: white;
121 | padding: 0 4px;
122 | text-decoration: none;
123 | }
124 |
125 | a.headerlink:hover {
126 | color: #444;
127 | background: #eaeaea;
128 | }
129 |
130 | div.body p, div.body dd, div.body li {
131 | line-height: 1.4em;
132 | }
133 |
134 | div.admonition {
135 | background: #fafafa;
136 | margin: 20px -30px;
137 | padding: 10px 30px;
138 | border-top: 1px solid #ccc;
139 | border-bottom: 1px solid #ccc;
140 | }
141 |
142 | div.admonition p.admonition-title {
143 | font-family: 'Garamond', 'Georgia', serif;
144 | font-weight: normal;
145 | font-size: 24px;
146 | margin: 0 0 10px 0;
147 | padding: 0;
148 | line-height: 1;
149 | }
150 |
151 | div.admonition p.last {
152 | margin-bottom: 0;
153 | }
154 |
155 | div.highlight{
156 | background-color: white;
157 | }
158 |
159 | dt:target, .highlight {
160 | background: #FAF3E8;
161 | }
162 |
163 | div.note {
164 | background-color: #eee;
165 | border: 1px solid #ccc;
166 | }
167 |
168 | div.seealso {
169 | background-color: #ffc;
170 | border: 1px solid #ff6;
171 | }
172 |
173 | div.topic {
174 | background-color: #eee;
175 | }
176 |
177 | div.warning {
178 | background-color: #ffe4e4;
179 | border: 1px solid #f66;
180 | }
181 |
182 | p.admonition-title {
183 | display: inline;
184 | }
185 |
186 | p.admonition-title:after {
187 | content: ":";
188 | }
189 |
190 | pre, tt {
191 | font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
192 | font-size: 0.85em;
193 | }
194 |
195 | img.screenshot {
196 | }
197 |
198 | tt.descname, tt.descclassname {
199 | font-size: 0.95em;
200 | }
201 |
202 | tt.descname {
203 | padding-right: 0.08em;
204 | }
205 |
206 | img.screenshot {
207 | -moz-box-shadow: 2px 2px 4px #eee;
208 | -webkit-box-shadow: 2px 2px 4px #eee;
209 | box-shadow: 2px 2px 4px #eee;
210 | }
211 |
212 | table.docutils {
213 | border: 1px solid #888;
214 | -moz-box-shadow: 2px 2px 4px #eee;
215 | -webkit-box-shadow: 2px 2px 4px #eee;
216 | box-shadow: 2px 2px 4px #eee;
217 | }
218 |
219 | table.docutils td, table.docutils th {
220 | border: 1px solid #888;
221 | padding: 0.25em 0.7em;
222 | }
223 |
224 | table.field-list, table.footnote {
225 | border: none;
226 | -moz-box-shadow: none;
227 | -webkit-box-shadow: none;
228 | box-shadow: none;
229 | }
230 |
231 | table.footnote {
232 | margin: 15px 0;
233 | width: 100%;
234 | border: 1px solid #eee;
235 | }
236 |
237 | table.field-list th {
238 | padding: 0 0.8em 0 0;
239 | }
240 |
241 | table.field-list td {
242 | padding: 0;
243 | }
244 |
245 | table.footnote td {
246 | padding: 0.5em;
247 | }
248 |
249 | dl {
250 | margin: 0;
251 | padding: 0;
252 | }
253 |
254 | dl dd {
255 | margin-left: 30px;
256 | }
257 |
258 | pre {
259 | padding: 0;
260 | margin: 15px -30px;
261 | padding: 8px;
262 | line-height: 1.3em;
263 | padding: 7px 30px;
264 | background: #eee;
265 | border-radius: 2px;
266 | -moz-border-radius: 2px;
267 | -webkit-border-radius: 2px;
268 | }
269 |
270 | dl pre {
271 | margin-left: -60px;
272 | padding-left: 60px;
273 | }
274 |
275 | tt {
276 | background-color: #ecf0f3;
277 | color: #222;
278 | /* padding: 1px 2px; */
279 | }
280 |
281 | tt.xref, a tt {
282 | background-color: #FBFBFB;
283 | }
284 |
285 | a:hover tt {
286 | background: #EEE;
287 | }
288 |
--------------------------------------------------------------------------------
/requirements-dev.txt:
--------------------------------------------------------------------------------
1 | alabaster==0.7.10 --hash=sha256:2eef172f44e8d301d25aff8068fddd65f767a3f04b5f15b0f4922f113aa1c732 --hash=sha256:37cdcb9e9954ed60912ebc1ca12a9d12178c26637abdf124e3cde2341c257fe0
2 | babel==2.5.1 --hash=sha256:f20b2acd44f587988ff185d8949c3e208b4b3d5d20fcab7d91fe481ffa435528 --hash=sha256:6007daf714d0cd5524bbe436e2d42b3c20e68da66289559341e48d2cd6d25811
3 | certifi==2017.11.5 --hash=sha256:244be0d93b71e93fc0a0a479862051414d0e00e16435707e5bf5000f92e04694 --hash=sha256:5ec74291ca1136b40f0379e1128ff80e866597e4e2c1e755739a913bbc3613c0
4 | chardet==3.0.4 --hash=sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691 --hash=sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae
5 | click==6.7 --hash=sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d --hash=sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b
6 | docutils==0.14 --hash=sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6 --hash=sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6 --hash=sha256:51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274
7 | flake8==3.5.0 --hash=sha256:c7841163e2b576d435799169b78703ad6ac1bbb0f199994fc05f700b2a90ea37 --hash=sha256:7253265f7abd8b313e3892944044a365e3f4ac3fcdcfb4298f55ee9ddf188ba0
8 | flask==0.12.2 --hash=sha256:0749df235e3ff61ac108f69ac178c9770caeaccad2509cb762ce1f65570a8856 --hash=sha256:49f44461237b69ecd901cc7ce66feea0319b9158743dd27a2899962ab214dac1
9 | idna==2.6 --hash=sha256:8c7309c718f94b3a625cb648ace320157ad16ff131ae0af362c9f21b80ef6ec4 --hash=sha256:2c6a5de3089009e3da7c5dde64a141dbc8551d5b7f6cf4ed7c2568d0cc520a8f
10 | imagesize==0.7.1 --hash=sha256:6ebdc9e0ad188f9d1b2cdd9bc59cbe42bf931875e829e7a595e6b3abdc05cdfb --hash=sha256:0ab2c62b87987e3252f89d30b7cedbec12a01af9274af9ffa48108f2c13c6062
11 | itsdangerous==0.24 --hash=sha256:cbb3fcf8d3e33df861709ecaf89d9e6629cff0a217bc2848f1b41cd30d360519
12 | jinja2==2.10 --hash=sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd --hash=sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4
13 | markupsafe==1.0 --hash=sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665
14 | mccabe==0.6.1 --hash=sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42 --hash=sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f
15 | nose==1.3.7 --hash=sha256:dadcddc0aefbf99eea214e0f1232b94f2fa9bd98fa8353711dacb112bfcbbb2a --hash=sha256:9ff7c6cc443f8c51994b34a667bbcf45afd6d945be7477b52e97516fd17c53ac --hash=sha256:f1bffef9cbc82628f6e7d7b40d7e255aefaa1adb6a1b1d26c69a8b79e6208a98
16 | pluggy==0.6.0 --hash=sha256:7f8ae7f5bdf75671a718d2daf0a64b7885f74510bcd98b1a0bb420eb9a9d0cff
17 | py==1.5.2 --hash=sha256:8cca5c229d225f8c1e3085be4fcf306090b00850fefad892f9d96c7b6e2f310f --hash=sha256:ca18943e28235417756316bfada6cd96b23ce60dd532642690dcfdaba988a76d
18 | pycodestyle==2.3.1 --hash=sha256:6c4245ade1edfad79c3446fadfc96b0de2759662dc29d07d80a6f27ad1ca6ba9 --hash=sha256:682256a5b318149ca0d2a9185d365d8864a768a28db66a84a2ea946bcc426766
19 | pyflakes==1.6.0 --hash=sha256:08bd6a50edf8cffa9fa09a463063c425ecaaf10d1eb0335a7e8b1401aef89e6f --hash=sha256:8d616a382f243dbf19b54743f280b80198be0bca3a5396f1d2e1fca6223e8805
20 | pygments==2.2.0 --hash=sha256:78f3f434bcc5d6ee09020f92ba487f95ba50f1e3ef83ae96b9d5ffa1bab25c5d --hash=sha256:dbae1046def0efb574852fab9e90209b23f556367b5a320c0bcb871c77c3e8cc
21 | pytz==2017.3 --hash=sha256:80af0f3008046b9975242012a985f04c5df1f01eed4ec1633d56cc47a75a6a48 --hash=sha256:feb2365914948b8620347784b6b6da356f31c9d03560259070b2f30cff3d469d --hash=sha256:59707844a9825589878236ff2f4e0dc9958511b7ffaae94dc615da07d4a68d33 --hash=sha256:d0ef5ef55ed3d37854320d4926b04a4cb42a2e88f71da9ddfdacfde8e364f027 --hash=sha256:c41c62827ce9cafacd6f2f7018e4f83a6f1986e87bfd000b8cfbd4ab5da95f1a --hash=sha256:8cc90340159b5d7ced6f2ba77694d946fc975b09f1a51d93f3ce3bb399396f94 --hash=sha256:dd2e4ca6ce3785c8dd342d1853dd9052b19290d5bf66060846e5dc6b8d6667f7 --hash=sha256:699d18a2a56f19ee5698ab1123bbcc1d269d061996aeb1eda6d89248d3542b82 --hash=sha256:fae4cffc040921b8a2d60c6cf0b5d662c1190fe54d718271db4eb17d44a185b7
22 | pyyaml==3.12 --hash=sha256:3262c96a1ca437e7e4763e2843746588a965426550f3797a79fca9c6199c431f --hash=sha256:16b20e970597e051997d90dc2cddc713a2876c47e3d92d59ee198700c5427736 --hash=sha256:e863072cdf4c72eebf179342c94e6989c67185842d9997960b3e69290b2fa269 --hash=sha256:bc6bced57f826ca7cb5125a10b23fd0f2fff3b7c4701d64c439a300ce665fff8 --hash=sha256:c01b880ec30b5a6e6aa67b09a2fe3fb30473008c85cd6a67359a1b15ed6d83a4 --hash=sha256:827dc04b8fa7d07c44de11fabbc888e627fa8293b695e0f99cb544fdfa1bf0d1 --hash=sha256:592766c6303207a20efc445587778322d7f73b161bd994f227adaa341ba212ab --hash=sha256:5f84523c076ad14ff5e6c037fe1c89a7f73a3e04cf0377cb4d017014976433f3 --hash=sha256:0c507b7f74b3d2dd4d1322ec8a94794927305ab4cebbe89cc47fe5e81541e6e8 --hash=sha256:b4c423ab23291d3945ac61346feeb9a0dc4184999ede5e7c43e1ffb975130ae6 --hash=sha256:ca233c64c6e40eaa6c66ef97058cdc80e8d0157a443655baa1b2966e812807ca --hash=sha256:4474f8ea030b5127225b8894d626bb66c01cda098d47a2b0d3429b6700af9fd8 --hash=sha256:326420cbb492172dec84b0f65c80942de6cedb5233c413dd824483989c000608 --hash=sha256:5ac82e411044fb129bae5cfbeb3ba626acb2af31a8d17d175004b70862a741a7
23 | requests==2.18.4 --hash=sha256:6a1b267aa90cac58ac3a765d067950e7dbbf75b1da07e895d1f594193a40a38b --hash=sha256:9c443e7324ba5b85070c4a818ade28bfabedf16ea10206da1132edaa6dda237e
24 | six==1.11.0 --hash=sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb --hash=sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9
25 | snowballstemmer==1.2.1 --hash=sha256:9f3bcd3c401c3e862ec0ebe6d2c069ebc012ce142cce209c098ccb5b09136e89 --hash=sha256:919f26a68b2c17a7634da993d91339e288964f93c274f1343e3bbbe2096e1128
26 | sphinx==1.6.6 --hash=sha256:b8baed19394af85b21755c68c7ec4eac57e8a482ed89cd01cd5d5ff72200fe0f --hash=sha256:c39a6fa41bd3ec6fc10064329a664ed3a3ca2e27640a823dc520c682e4433cdb
27 | sphinxcontrib-websupport==1.0.1 --hash=sha256:f4932e95869599b89bf4f80fc3989132d83c9faa5bf633e7b5e0c25dffb75da2 --hash=sha256:7a85961326aa3a400cd4ad3c816d70ed6f7c740acd7ce5d78cd0a67825072eb9
28 | tox==2.9.1 --hash=sha256:8af30fd835a11f3ff8e95176ccba5a4e60779df4d96a9dfefa1a1704af263225 --hash=sha256:752f5ec561c6c08c5ecb167d3b20f4f4ffc158c0ab78855701a75f5cef05f4b8
29 | urllib3==1.22 --hash=sha256:06330f386d6e4b195fbfc736b297f58c5a892e4440e54d294d7004e3a9bbea1b --hash=sha256:cc44da8e1145637334317feebd728bd869a35285b93cbb4cca2577da7e62db4f
30 | virtualenv==15.1.0; python_version != '3.2' --hash=sha256:39d88b533b422825d644087a21e78c45cf5af0ef7a99a1fc9fbb7b481e5c85b0 --hash=sha256:02f8102c2436bb03b3ee6dede1919d1dac8a427541652e5ec95171ec8adbc93a
31 | werkzeug==0.14.1 --hash=sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b --hash=sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c
32 | -e .
33 |
--------------------------------------------------------------------------------
/docs/_themes/flask/static/flasky.css_t:
--------------------------------------------------------------------------------
1 | /*
2 | * flasky.css_t
3 | * ~~~~~~~~~~~~
4 | *
5 | * Sphinx stylesheet -- flasky theme based on nature theme.
6 | *
7 | * :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS.
8 | * :license: BSD, see LICENSE for details.
9 | *
10 | */
11 |
12 | @import url("basic.css");
13 |
14 | /* -- page layout ----------------------------------------------------------- */
15 |
16 | body {
17 | font-family: 'Georgia', serif;
18 | font-size: 17px;
19 | background-color: #ddd;
20 | color: #000;
21 | margin: 0;
22 | padding: 0;
23 | }
24 |
25 | div.document {
26 | background: #fafafa;
27 | }
28 |
29 | div.documentwrapper {
30 | float: left;
31 | width: 100%;
32 | }
33 |
34 | div.bodywrapper {
35 | margin: 0 0 0 230px;
36 | }
37 |
38 | hr {
39 | border: 1px solid #B1B4B6;
40 | }
41 |
42 | div.body {
43 | background-color: #ffffff;
44 | color: #3E4349;
45 | padding: 0 30px 30px 30px;
46 | min-height: 34em;
47 | }
48 |
49 | img.floatingflask {
50 | padding: 0 0 10px 10px;
51 | float: right;
52 | }
53 |
54 | div.footer {
55 | position: absolute;
56 | right: 0;
57 | margin-top: -70px;
58 | text-align: right;
59 | color: #888;
60 | padding: 10px;
61 | font-size: 14px;
62 | }
63 |
64 | div.footer a {
65 | color: #888;
66 | text-decoration: underline;
67 | }
68 |
69 | div.related {
70 | line-height: 32px;
71 | color: #888;
72 | }
73 |
74 | div.related ul {
75 | padding: 0 0 0 10px;
76 | }
77 |
78 | div.related a {
79 | color: #444;
80 | }
81 |
82 | div.sphinxsidebar {
83 | font-size: 14px;
84 | line-height: 1.5;
85 | }
86 |
87 | div.sphinxsidebarwrapper {
88 | padding: 0 20px;
89 | }
90 |
91 | div.sphinxsidebarwrapper p.logo {
92 | padding: 20px 0 10px 0;
93 | margin: 0;
94 | text-align: center;
95 | }
96 |
97 | div.sphinxsidebar h3,
98 | div.sphinxsidebar h4 {
99 | font-family: 'Garamond', 'Georgia', serif;
100 | color: #222;
101 | font-size: 24px;
102 | font-weight: normal;
103 | margin: 20px 0 5px 0;
104 | padding: 0;
105 | }
106 |
107 | div.sphinxsidebar h4 {
108 | font-size: 20px;
109 | }
110 |
111 | div.sphinxsidebar h3 a {
112 | color: #444;
113 | }
114 |
115 | div.sphinxsidebar p {
116 | color: #555;
117 | margin: 10px 0;
118 | }
119 |
120 | div.sphinxsidebar ul {
121 | margin: 10px 0;
122 | padding: 0;
123 | color: #000;
124 | }
125 |
126 | div.sphinxsidebar a {
127 | color: #444;
128 | text-decoration: none;
129 | }
130 |
131 | div.sphinxsidebar a:hover {
132 | text-decoration: underline;
133 | }
134 |
135 | div.sphinxsidebar input {
136 | border: 1px solid #ccc;
137 | font-family: 'Georgia', serif;
138 | font-size: 1em;
139 | }
140 |
141 | /* -- body styles ----------------------------------------------------------- */
142 |
143 | a {
144 | color: #004B6B;
145 | text-decoration: underline;
146 | }
147 |
148 | a:hover {
149 | color: #6D4100;
150 | text-decoration: underline;
151 | }
152 |
153 | div.body {
154 | padding-bottom: 40px; /* saved for footer */
155 | }
156 |
157 | div.body h1,
158 | div.body h2,
159 | div.body h3,
160 | div.body h4,
161 | div.body h5,
162 | div.body h6 {
163 | font-family: 'Garamond', 'Georgia', serif;
164 | font-weight: normal;
165 | margin: 30px 0px 10px 0px;
166 | padding: 0;
167 | }
168 |
169 | div.body h1 { margin-top: 0; padding-top: 20px; font-size: 240%; }
170 | div.body h2 { font-size: 180%; }
171 | div.body h3 { font-size: 150%; }
172 | div.body h4 { font-size: 130%; }
173 | div.body h5 { font-size: 100%; }
174 | div.body h6 { font-size: 100%; }
175 |
176 | a.headerlink {
177 | color: white;
178 | padding: 0 4px;
179 | text-decoration: none;
180 | }
181 |
182 | a.headerlink:hover {
183 | color: #444;
184 | background: #eaeaea;
185 | }
186 |
187 | div.body p, div.body dd, div.body li {
188 | line-height: 1.4em;
189 | }
190 |
191 | div.admonition {
192 | background: #fafafa;
193 | margin: 20px -30px;
194 | padding: 10px 30px;
195 | border-top: 1px solid #ccc;
196 | border-bottom: 1px solid #ccc;
197 | }
198 |
199 | div.admonition p.admonition-title {
200 | font-family: 'Garamond', 'Georgia', serif;
201 | font-weight: normal;
202 | font-size: 24px;
203 | margin: 0 0 10px 0;
204 | padding: 0;
205 | line-height: 1;
206 | }
207 |
208 | div.admonition p.last {
209 | margin-bottom: 0;
210 | }
211 |
212 | div.highlight{
213 | background-color: white;
214 | }
215 |
216 | dt:target, .highlight {
217 | background: #FAF3E8;
218 | }
219 |
220 | div.note {
221 | background-color: #eee;
222 | border: 1px solid #ccc;
223 | }
224 |
225 | div.seealso {
226 | background-color: #ffc;
227 | border: 1px solid #ff6;
228 | }
229 |
230 | div.topic {
231 | background-color: #eee;
232 | }
233 |
234 | div.warning {
235 | background-color: #ffe4e4;
236 | border: 1px solid #f66;
237 | }
238 |
239 | p.admonition-title {
240 | display: inline;
241 | }
242 |
243 | p.admonition-title:after {
244 | content: ":";
245 | }
246 |
247 | pre, tt {
248 | font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
249 | font-size: 0.9em;
250 | }
251 |
252 | img.screenshot {
253 | }
254 |
255 | tt.descname, tt.descclassname {
256 | font-size: 0.95em;
257 | }
258 |
259 | tt.descname {
260 | padding-right: 0.08em;
261 | }
262 |
263 | img.screenshot {
264 | -moz-box-shadow: 2px 2px 4px #eee;
265 | -webkit-box-shadow: 2px 2px 4px #eee;
266 | box-shadow: 2px 2px 4px #eee;
267 | }
268 |
269 | table.docutils {
270 | border: 1px solid #888;
271 | -moz-box-shadow: 2px 2px 4px #eee;
272 | -webkit-box-shadow: 2px 2px 4px #eee;
273 | box-shadow: 2px 2px 4px #eee;
274 | }
275 |
276 | table.docutils td, table.docutils th {
277 | border: 1px solid #888;
278 | padding: 0.25em 0.7em;
279 | }
280 |
281 | table.field-list, table.footnote {
282 | border: none;
283 | -moz-box-shadow: none;
284 | -webkit-box-shadow: none;
285 | box-shadow: none;
286 | }
287 |
288 | table.footnote {
289 | margin: 15px 0;
290 | width: 100%;
291 | border: 1px solid #eee;
292 | }
293 |
294 | table.field-list th {
295 | padding: 0 0.8em 0 0;
296 | }
297 |
298 | table.field-list td {
299 | padding: 0;
300 | }
301 |
302 | table.footnote td {
303 | padding: 0.5em;
304 | }
305 |
306 | dl {
307 | margin: 0;
308 | padding: 0;
309 | }
310 |
311 | dl dd {
312 | margin-left: 30px;
313 | }
314 |
315 | pre {
316 | background: #eee;
317 | padding: 7px 30px;
318 | margin: 15px -30px;
319 | line-height: 1.3em;
320 | }
321 |
322 | dl pre {
323 | margin-left: -60px;
324 | padding-left: 60px;
325 | }
326 |
327 | dl dl pre {
328 | margin-left: -90px;
329 | padding-left: 90px;
330 | }
331 |
332 | tt {
333 | background-color: #ecf0f3;
334 | color: #222;
335 | /* padding: 1px 2px; */
336 | }
337 |
338 | tt.xref, a tt {
339 | background-color: #FBFBFB;
340 | }
341 |
342 | a:hover tt {
343 | background: #EEE;
344 | }
345 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Flask-Themes documentation build configuration file, created by
4 | # sphinx-quickstart on Tue Jul 6 10:48:18 2010.
5 | #
6 | # This file is execfile()d with the current directory set to its containing dir.
7 | #
8 | # Note that not all possible configuration values are present in this
9 | # autogenerated file.
10 | #
11 | # All configuration values have a default; values that are commented out
12 | # serve to show the default.
13 |
14 | import sys, os
15 |
16 | # If extensions (or modules to document with autodoc) are in another directory,
17 | # add these directories to sys.path here. If the directory is relative to the
18 | # documentation root, use os.path.abspath to make it absolute, like shown here.
19 | sys.path.append(os.path.abspath('_themes'))
20 |
21 | # -- General configuration -----------------------------------------------------
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.autodoc', 'sphinx.ext.intersphinx']
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'
35 |
36 | # The master toctree document.
37 | master_doc = 'index'
38 |
39 | # General information about the project.
40 | project = u'Flask-Themes'
41 | copyright = u'2010, Matthew "LeafStorm" Frazier'
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.1'
49 | # The full version, including alpha/beta/rc tags.
50 | release = '0.1'
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 documents that shouldn't be included in the build.
63 | #unused_docs = []
64 |
65 | # List of directories, relative to source directory, that shouldn't be searched
66 | # for source files.
67 | exclude_trees = ['_build']
68 |
69 | # The reST default role (used for this markup: `text`) to use for all documents.
70 | default_role = 'obj'
71 |
72 | # If true, '()' will be appended to :func: etc. cross-reference text.
73 | #add_function_parentheses = True
74 |
75 | # If true, the current module name will be prepended to all description
76 | # unit titles (such as .. function::).
77 | #add_module_names = True
78 |
79 | # If true, sectionauthor and moduleauthor directives will be shown in the
80 | # output. They are ignored by default.
81 | #show_authors = False
82 |
83 | # The name of the Pygments (syntax highlighting) style to use.
84 | #pygments_style = 'sphinx'
85 |
86 | # A list of ignored prefixes for module index sorting.
87 | #modindex_common_prefix = []
88 |
89 |
90 | # -- Options for HTML output ---------------------------------------------------
91 |
92 | # The theme to use for HTML and HTML Help pages. Major themes that come with
93 | # Sphinx are currently 'default' and 'sphinxdoc'.
94 | html_theme = 'flask_small'
95 |
96 | # Theme options are theme-specific and customize the look and feel of a theme
97 | # further. For a list of options available for each theme, see the
98 | # documentation.
99 | html_theme_options = {'github_fork': None,
100 | 'index_logo': None}
101 |
102 | # Add any paths that contain custom themes here, relative to this directory.
103 | html_theme_path = ['_themes']
104 |
105 | # The name for this set of Sphinx documents. If None, it defaults to
106 | # " v documentation".
107 | #html_title = None
108 |
109 | # A shorter title for the navigation bar. Default is the same as html_title.
110 | #html_short_title = None
111 |
112 | # The name of an image file (relative to this directory) to place at the top
113 | # of the sidebar.
114 | #html_logo = None
115 |
116 | # The name of an image file (within the static path) to use as favicon of the
117 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
118 | # pixels large.
119 | #html_favicon = None
120 |
121 | # Add any paths that contain custom static files (such as style sheets) here,
122 | # relative to this directory. They are copied after the builtin static files,
123 | # so a file named "default.css" will overwrite the builtin "default.css".
124 | html_static_path = ['_static']
125 |
126 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
127 | # using the given strftime format.
128 | #html_last_updated_fmt = '%b %d, %Y'
129 |
130 | # If true, SmartyPants will be used to convert quotes and dashes to
131 | # typographically correct entities.
132 | #html_use_smartypants = True
133 |
134 | # Custom sidebar templates, maps document names to template names.
135 | #html_sidebars = {}
136 |
137 | # Additional templates that should be rendered to pages, maps page names to
138 | # template names.
139 | #html_additional_pages = {}
140 |
141 | # If false, no module index is generated.
142 | #html_use_modindex = True
143 |
144 | # If false, no index is generated.
145 | #html_use_index = True
146 |
147 | # If true, the index is split into individual pages for each letter.
148 | #html_split_index = False
149 |
150 | # If true, links to the reST sources are added to the pages.
151 | #html_show_sourcelink = True
152 |
153 | # If true, an OpenSearch description file will be output, and all pages will
154 | # contain a tag referring to it. The value of this option must be the
155 | # base URL from which the finished HTML is served.
156 | #html_use_opensearch = ''
157 |
158 | # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
159 | #html_file_suffix = ''
160 |
161 | # Output file base name for HTML help builder.
162 | htmlhelp_basename = 'Flask-Themesdoc'
163 |
164 |
165 | # -- Options for LaTeX output --------------------------------------------------
166 |
167 | # The paper size ('letter' or 'a4').
168 | #latex_paper_size = 'letter'
169 |
170 | # The font size ('10pt', '11pt' or '12pt').
171 | #latex_font_size = '10pt'
172 |
173 | # Grouping the document tree into LaTeX files. List of tuples
174 | # (source start file, target name, title, author, documentclass [howto/manual]).
175 | latex_documents = [
176 | ('index', 'Flask-Themes.tex', u'Flask-Themes Documentation',
177 | u'Matthew "LeafStorm" Frazier', 'manual'),
178 | ]
179 |
180 | # The name of an image file (relative to this directory) to place at the top of
181 | # the title page.
182 | #latex_logo = None
183 |
184 | # For "manual" documents, if this is true, then toplevel headings are parts,
185 | # not chapters.
186 | #latex_use_parts = False
187 |
188 | # Additional stuff for the LaTeX preamble.
189 | #latex_preamble = ''
190 |
191 | # Documents to append as an appendix to all manuals.
192 | #latex_appendices = []
193 |
194 | # If false, no module index is generated.
195 | #latex_use_modindex = True
196 |
197 |
198 | # Example configuration for intersphinx: refer to the Python standard library.
199 | intersphinx_mapping = {'http://docs.python.org/': None,
200 | 'http://flask.pocoo.org/docs/': None}
201 |
--------------------------------------------------------------------------------
/tests/test-themes.py:
--------------------------------------------------------------------------------
1 | """
2 | test-themes.py
3 | ==============
4 | This tests the Flask-Themes extension.
5 | """
6 | from __future__ import with_statement
7 | import os.path
8 | from flask import Flask, url_for, render_template
9 | from flask_themes import (setup_themes, Theme, load_themes_from,
10 | packaged_themes_loader, theme_paths_loader, ThemeManager, static_file_url,
11 | template_exists, themes_blueprint, render_theme_template, get_theme,
12 | get_themes_list)
13 | from jinja2 import FileSystemLoader
14 | from operator import attrgetter
15 |
16 | TESTS = os.path.dirname(__file__)
17 | join = os.path.join
18 |
19 |
20 | class TestThemeObject(object):
21 | def test_theme(self):
22 | path = join(TESTS, 'themes', 'cool')
23 | cool = Theme(path)
24 | assert cool.name == 'Cool Blue v1'
25 | assert cool.identifier == 'cool'
26 | assert cool.path == os.path.abspath(path)
27 | assert cool.static_path == join(cool.path, 'static')
28 | assert cool.templates_path == join(cool.path, 'templates')
29 | assert cool.license_text is None
30 | assert isinstance(cool.jinja_loader, FileSystemLoader)
31 |
32 | def test_license_text(self):
33 | path = join(TESTS, 'themes', 'plain')
34 | plain = Theme(path)
35 | assert plain.license_text.strip() == 'The license.'
36 |
37 |
38 | class TestLoaders(object):
39 | def test_load_themes_from(self):
40 | path = join(TESTS, 'themes')
41 | themes_iter = load_themes_from(path)
42 | themes = list(sorted(themes_iter, key=attrgetter('identifier')))
43 | assert themes[0].identifier == 'cool'
44 | assert themes[1].identifier == 'notthis'
45 | assert themes[2].identifier == 'plain'
46 |
47 | def test_packaged_themes_loader(self):
48 | app = Flask(__name__)
49 | themes_iter = packaged_themes_loader(app)
50 | themes = list(sorted(themes_iter, key=attrgetter('identifier')))
51 | assert themes[0].identifier == 'cool'
52 | assert themes[1].identifier == 'notthis'
53 | assert themes[2].identifier == 'plain'
54 |
55 | def test_theme_paths_loader(self):
56 | app = Flask(__name__)
57 | app.config['THEME_PATHS'] = [join(TESTS, 'morethemes')]
58 | themes = list(theme_paths_loader(app))
59 | assert themes[0].identifier == 'cool'
60 |
61 |
62 | class TestSetup(object):
63 | def test_manager(self):
64 | app = Flask(__name__)
65 | manager = ThemeManager(app, 'testing')
66 | assert app.theme_manager is manager
67 | app.config['THEME_PATHS'] = [join(TESTS, 'morethemes')]
68 | manager.refresh()
69 | themeids = sorted(manager.themes)
70 | assert themeids == ['cool', 'plain']
71 | assert manager.themes['cool'].name == 'Cool Blue v2'
72 |
73 | def test_setup_themes(self):
74 | app = Flask(__name__)
75 | app.config['THEME_PATHS'] = [join(TESTS, 'morethemes')]
76 | setup_themes(app, app_identifier='testing')
77 |
78 | assert hasattr(app, 'theme_manager')
79 | assert '_themes' in app.blueprints
80 | assert 'theme' in app.jinja_env.globals
81 | assert 'theme_static' in app.jinja_env.globals
82 |
83 | def test_get_helpers(self):
84 | app = Flask(__name__)
85 | app.config['THEME_PATHS'] = [join(TESTS, 'morethemes')]
86 | setup_themes(app, app_identifier='testing')
87 |
88 | with app.test_request_context('/'):
89 | cool = app.theme_manager.themes['cool']
90 | plain = app.theme_manager.themes['plain']
91 | assert get_theme('cool') is cool
92 | assert get_theme('plain') is plain
93 | tl = get_themes_list()
94 | assert tl[0] is cool
95 | assert tl[1] is plain
96 | try:
97 | get_theme('notthis')
98 | except KeyError:
99 | pass
100 | else:
101 | raise AssertionError("Getting a nonexistent theme should "
102 | "raised KeyError")
103 |
104 |
105 | class TestStatic(object):
106 | def test_static_file_url(self):
107 | app = Flask(__name__)
108 | app.config['THEME_PATHS'] = [join(TESTS, 'morethemes')]
109 | setup_themes(app, app_identifier='testing')
110 |
111 | with app.test_request_context('/'):
112 | url = static_file_url('cool', 'style.css')
113 | genurl = url_for('_themes.static', themeid='cool',
114 | filename='style.css')
115 | assert url == genurl
116 |
117 |
118 | class TestTemplates(object):
119 | def test_template_exists(self):
120 | app = Flask(__name__)
121 | app.config['THEME_PATHS'] = [join(TESTS, 'morethemes')]
122 | setup_themes(app, app_identifier='testing')
123 |
124 | with app.test_request_context('/'):
125 | assert template_exists('hello.html')
126 | assert template_exists('_themes/cool/hello.html')
127 | assert not template_exists('_themes/plain/hello.html')
128 |
129 | def test_loader(self):
130 | app = Flask(__name__)
131 | app.config['THEME_PATHS'] = [join(TESTS, 'morethemes')]
132 | setup_themes(app, app_identifier='testing')
133 |
134 | with app.test_request_context('/'):
135 | src = themes_blueprint.jinja_loader.get_source(
136 | app.jinja_env, '_themes/cool/hello.html'
137 | )
138 | assert src[0].strip() == 'Hello from Cool Blue v2.'
139 |
140 | def test_render_theme_template(self):
141 | app = Flask(__name__)
142 | app.config['THEME_PATHS'] = [join(TESTS, 'morethemes')]
143 | setup_themes(app, app_identifier='testing')
144 |
145 | with app.test_request_context('/'):
146 | coolsrc = render_theme_template('cool', 'hello.html').strip()
147 | plainsrc = render_theme_template('plain', 'hello.html').strip()
148 | assert coolsrc == 'Hello from Cool Blue v2.'
149 | assert plainsrc == 'Hello from the application'
150 |
151 | def test_active_theme(self):
152 | app = Flask(__name__)
153 | app.config['THEME_PATHS'] = [join(TESTS, 'morethemes')]
154 | setup_themes(app, app_identifier='testing')
155 |
156 | with app.test_request_context('/'):
157 | appdata = render_template('active.html').strip()
158 | cooldata = render_theme_template('cool', 'active.html').strip()
159 | plaindata = render_theme_template('plain', 'active.html').strip()
160 | assert appdata == 'Application, Active theme: none'
161 | assert cooldata == 'Cool Blue v2, Active theme: cool'
162 | assert plaindata == 'Application, Active theme: plain'
163 |
164 | def test_theme_static(self):
165 | app = Flask(__name__)
166 | app.config['THEME_PATHS'] = [join(TESTS, 'morethemes')]
167 | setup_themes(app, app_identifier='testing')
168 |
169 | with app.test_request_context('/'):
170 | coolurl = static_file_url('cool', 'style.css')
171 | cooldata = render_theme_template('cool', 'static.html').strip()
172 | assert cooldata == 'Cool Blue v2, %s' % coolurl
173 |
174 | def test_theme_static_outside(self):
175 | app = Flask(__name__)
176 | app.config['THEME_PATHS'] = [join(TESTS, 'morethemes')]
177 | setup_themes(app, app_identifier='testing')
178 |
179 | with app.test_request_context('/'):
180 | try:
181 | render_template('static.html')
182 | except RuntimeError:
183 | pass
184 | else:
185 | raise AssertionError("Rendering static.html should have "
186 | "caused a RuntimeError")
187 |
188 | def test_theme_include_static(self):
189 | app = Flask(__name__)
190 | app.config['THEME_PATHS'] = [join(TESTS, 'morethemes')]
191 | setup_themes(app, app_identifier='testing')
192 |
193 | with app.test_request_context('/'):
194 | data = render_template('static_parent.html').strip()
195 | url = static_file_url('plain', 'style.css')
196 | assert data == 'Application, Plain, %s' % url
197 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | ============
2 | Flask-Themes
3 | ============
4 | .. currentmodule:: flask_themes
5 |
6 | Flask-Themes makes it easy for your application to support a wide range of
7 | appearances.
8 |
9 | .. contents::
10 | :local:
11 | :backlinks: none
12 |
13 |
14 | Writing Themes
15 | ==============
16 | A theme is simply a folder containing static media (like CSS files, images,
17 | and JavaScript) and Jinja2 templates, with some metadata. A theme folder
18 | should look something like this:
19 |
20 | .. sourcecode:: text
21 |
22 | my_theme/
23 | info.json
24 | license.txt
25 | templates/
26 | layout.html
27 | index.html
28 | static/
29 | style.css
30 |
31 | The ``info.json`` file contains the theme's metadata, so that the application
32 | can provide a nice switching interface if necessary. ``license.txt`` is
33 | optional and contains the full text of the theme's license. ``static`` is
34 | served directly to clients, and ``templates`` contains the Jinja2 template
35 | files.
36 |
37 | Note that exactly what templates you need to create will vary between
38 | applications. Check the application's docs (or source code) to see what you
39 | need.
40 |
41 |
42 | Writing Templates
43 | -----------------
44 | Flask uses the Jinja2 template engine, so you should read `its documentation`_
45 | to learn about the actual syntax of the templates.
46 |
47 | All templates loaded from a theme will have a global function named `theme`
48 | available to look up the theme's templates. For example, if you want to
49 | extend, import, or include another template from your theme, you can use
50 | ``theme(template_name)``, like this:
51 |
52 | .. sourcecode:: html+jinja
53 |
54 | {% extends theme('layout.html') %}
55 | {% from theme('_helpers.html') import form_field %}
56 |
57 | If the template you requested doesn't exist within the theme, it will fall
58 | back to using the application's template. If you pass `false` as the second
59 | parameter, it will only return the theme's template.
60 |
61 | .. sourcecode:: html+jinja
62 |
63 | {% include theme('header.html', false) %}
64 |
65 | You can still import/include templates from the application, though. Just use
66 | the tag without calling `theme`.
67 |
68 | .. sourcecode:: html+jinja
69 |
70 | {% from '_helpers.html' import link_to %}
71 | {% include '_jquery.html' %}
72 |
73 | You can also get the URL for the theme's media files with the `theme_static`
74 | function:
75 |
76 | .. sourcecode:: html+jinja
77 |
78 |
79 |
80 | .. _its documentation: http://jinja.pocoo.org/2/documentation/templates
81 |
82 |
83 | ``info.json`` Fields
84 | --------------------
85 | ``application`` : required
86 | This is the application's identifier. Exactly what identifier you need to
87 | use varies between applications.
88 |
89 | ``identifier`` : required
90 | The theme's identifier. It should be a Python identifier (starts with a
91 | letter or underscore, the rest can be letters, underscores, or numbers)
92 | and should match the name of the theme's folder.
93 |
94 | ``name`` : required
95 | A human-readable name for the theme.
96 |
97 | ``author`` : required
98 | The name of the theme's author, that is, you. It does not have to include
99 | an e-mail address, and should be displayed verbatim.
100 |
101 | ``description``
102 | A description of the theme in a few sentences. If you can write multiple
103 | languages, you can include additional fields in the form
104 | ``description_lc``, where ``lc`` is a two-letter language code like ``es``
105 | or ``de``. They should contain the description, but in the indicated
106 | language.
107 |
108 | ``website``
109 | The URL of the theme's Web site. This can be a Web site specifically for
110 | this theme, Web site for a collection of themes that includes this theme,
111 | or just the author's Web site.
112 |
113 | ``license``
114 | A simple phrase indicating your theme's license, like ``GPL``,
115 | ``MIT/X11``, ``Public Domain``, or ``Creative Commons BY-SA 3.0``. You
116 | can put the full license's text in the ``license.txt`` file.
117 |
118 | ``license_url``
119 | If you don't want to include the full text in the ``license.txt`` file,
120 | you can include a URL for a Web site where the text can be viewed. This is
121 | good for long licenses like the GPL or Creative Commons licenses.
122 |
123 | ``preview``
124 | A preview image for the theme. This should be the filename for an image
125 | within the ``static`` directory.
126 |
127 | ``doctype``
128 | The version of HTML used by the theme. It can be ``html4``, ``html5``, or
129 | ``xhtml``. The application can use this to do things like switch the
130 | output format of a markup generator. (The default if this is left out is
131 | ``html5`` to be safe. HTML5 is used by the majority of Flask users, so
132 | it's best to use it.)
133 |
134 | ``options``
135 | If this is given, it should be a dictionary (object in JSON parlance)
136 | containing application-specific options. You will need to check the
137 | application's docs to see what options it uses. (For example, an
138 | application like a pastebin or wiki that highlights source code may
139 | want the theme to specify a default `Pygments`_ style in the options.)
140 |
141 |
142 | .. _Pygments: http://pygments.org/
143 |
144 | Tips for Theme Writers
145 | ----------------------
146 | - Always specify a doctype.
147 | - Remember that you have to use double-quotes with strings in JSON.
148 | - Look at the non-theme templates provided with the application. See how they
149 | interact.
150 | - Remember that most of the time, you can alter the application's appearance
151 | completely just by changing the layout template and the style.
152 |
153 |
154 | Using Themes in Your Application
155 | ================================
156 | To set up your application to use themes, you need to use the
157 | `setup_themes` function. It doesn't rely on your application already being
158 | configured, so you can call it whenever is convenient. It does three things:
159 |
160 | * Adds a `ThemeManager` instance to your application as ``app.theme_manager``.
161 | * Registers the `theme` and `theme_static` globals with the Jinja2
162 | environment.
163 | * Registers the `_themes` module or blueprint (depending on the Flask version)
164 | to your application, by default with the URL prefix ``/_themes`` (you can
165 | change it).
166 |
167 | .. warning::
168 |
169 | Since the "Blueprints" mechanism of Flask 0.7 causes headaches in module
170 | compatibility mode, `setup_themes` will automatically register `_themes`
171 | as a blueprint and not as a module if possible. If this causes headaches
172 | with your application, then you need to either (a) upgrade to Flask 0.7 or
173 | (b) set ``Flask<0.7`` in your requirements.txt file.
174 |
175 |
176 | Theme Loaders
177 | -------------
178 | `setup_themes` takes a few arguments, but the one you will probably be using
179 | most is `loaders`, which is a list of theme loaders to use (in order) to find
180 | themes. The default theme loaders are:
181 |
182 | * `packaged_themes_loader`, which looks in your application's ``themes``
183 | directory for themes (you can use this to ship one or two default themes
184 | with your application)
185 | * `theme_paths_loader`, which looks at the `THEME_PATHS` configuration
186 | setting and loads themes from each folder therein
187 |
188 | It's easy to write your own loaders, though - a loader is just a callable that
189 | takes an application instance and returns an iterable of `Theme` instances.
190 | You can use the `load_themes_from` helper function to yield all the valid
191 | themes contained within a folder. For example, if your app uses an "instance
192 | folder" like `Zine`_ that can have a "themes" directory::
193 |
194 | def instance_loader(app):
195 | themes_dir = os.path.join(app.instance_root, 'themes')
196 | if os.path.isdir(themes_dir):
197 | return load_themes_from(themes_dir)
198 | else:
199 | return ()
200 |
201 | .. _Zine: http://zine.pocoo.org/
202 |
203 |
204 | Rendering Templates
205 | -------------------
206 | Once you have the themes set up, you can call in to the theme machinery with
207 | `render_theme_template`. It works like `render_template`, but takes a `theme`
208 | parameter before the template name. Also, `static_file_url` will generate a
209 | URL to the given static file.
210 |
211 | When you call `render_theme_template`, it sets the "active template" to the
212 | given theme, even if you have to fall back to rendering the application's
213 | template. That way, if you have a template like ``by_year.html`` that isn't
214 | defined by the current theme, you can still
215 |
216 | * extend (``{% extends theme('layout.html') %}``)
217 | * include (``{% include theme('archive_header.html') %}``)
218 | * import (``{% from theme('_helpers.html') import show_post %}``)
219 |
220 | templates defined by the theme. This way, the theme author doesn't have to
221 | implement every possible template - they can define templates like the layout,
222 | and showing posts, and things like that, and the application-provided
223 | templates can use those building blocks to form the more complicated pages.
224 |
225 |
226 | Selecting Themes
227 | ----------------
228 | How exactly you select the theme will vary between applications, so
229 | Flask-Themes doesn't make the decision for you. If your app is any larger than
230 | a few views, though, you will probably want to provide a helper function that
231 | selects the theme based on whatever (settings, logged-in user, page) and
232 | renders the template. For example::
233 |
234 | def get_current_theme():
235 | if g.user is not None:
236 | ident = g.user.theme
237 | else:
238 | ident = current_app.config.get('DEFAULT_THEME', 'plain')
239 | return get_theme(ident)
240 |
241 | def render(template, **context):
242 | return render_theme_template(get_current_theme(), template, **context)
243 |
244 |
245 | .. warning::
246 |
247 | Make sure that you *only* get `Theme` instances from the theme manager. If
248 | you need to create a `Theme` instance manually outside of a theme loader,
249 | that's a sign that you're doing it wrong. Instead, write a loader that can
250 | load that theme and pass it to `setup_themes`, because if the theme is not
251 | loaded by the manager, then its templates and static files won't be
252 | available, which will usually lead to your application breaking.
253 |
254 |
255 | Tips for Application Programmers
256 | --------------------------------
257 | - Provide default templates, preferably for everything. Use simple, unstyled
258 | HTML.
259 | - If you find yourself repeating design elements, put them in a macro in a
260 | separate template. That way, theme authors can override them more easily.
261 | - Put class names or IDs on any elements that the theme author may want to
262 | style. (And by that I mean all of them.) That way they won't have to
263 | override the template unnecessarily if all they want to do is right-align
264 | the meta information.
265 |
266 |
267 | API Documentation
268 | =================
269 | This API documentation is automatically generated from the source code.
270 |
271 | .. autoclass:: Theme
272 | :members:
273 |
274 | .. autofunction:: setup_themes
275 |
276 | .. autofunction:: render_theme_template
277 |
278 | .. autofunction:: static_file_url
279 |
280 | .. autofunction:: get_theme
281 |
282 | .. autofunction:: get_themes_list
283 |
284 |
285 | Loading Themes
286 | --------------
287 | .. autoclass:: ThemeManager
288 | :members:
289 |
290 | .. autofunction:: packaged_themes_loader
291 |
292 | .. autofunction:: theme_paths_loader
293 |
294 | .. autofunction:: load_themes_from
295 |
--------------------------------------------------------------------------------
/Pipfile.lock:
--------------------------------------------------------------------------------
1 | {
2 | "_meta": {
3 | "hash": {
4 | "sha256": "d378f0e3cfc363af764ca7aeb7343695fbae0ac287b30bee76952d638716fdc2"
5 | },
6 | "host-environment-markers": {
7 | "implementation_name": "cpython",
8 | "implementation_version": "3.6.4",
9 | "os_name": "posix",
10 | "platform_machine": "x86_64",
11 | "platform_python_implementation": "CPython",
12 | "platform_release": "16.7.0",
13 | "platform_system": "Darwin",
14 | "platform_version": "Darwin Kernel Version 16.7.0: Mon Nov 13 21:56:25 PST 2017; root:xnu-3789.72.11~1/RELEASE_X86_64",
15 | "python_full_version": "3.6.4",
16 | "python_version": "3.6",
17 | "sys_platform": "darwin"
18 | },
19 | "pipfile-spec": 6,
20 | "requires": {},
21 | "sources": [
22 | {
23 | "name": "pypi",
24 | "url": "https://pypi.python.org/simple",
25 | "verify_ssl": true
26 | }
27 | ]
28 | },
29 | "default": {
30 | "click": {
31 | "hashes": [
32 | "sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d",
33 | "sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b"
34 | ],
35 | "version": "==6.7"
36 | },
37 | "flask": {
38 | "hashes": [
39 | "sha256:0749df235e3ff61ac108f69ac178c9770caeaccad2509cb762ce1f65570a8856",
40 | "sha256:49f44461237b69ecd901cc7ce66feea0319b9158743dd27a2899962ab214dac1"
41 | ],
42 | "version": "==0.12.2"
43 | },
44 | "itsdangerous": {
45 | "hashes": [
46 | "sha256:cbb3fcf8d3e33df861709ecaf89d9e6629cff0a217bc2848f1b41cd30d360519"
47 | ],
48 | "version": "==0.24"
49 | },
50 | "jinja2": {
51 | "hashes": [
52 | "sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd",
53 | "sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4"
54 | ],
55 | "version": "==2.10"
56 | },
57 | "markupsafe": {
58 | "hashes": [
59 | "sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665"
60 | ],
61 | "version": "==1.0"
62 | },
63 | "six": {
64 | "hashes": [
65 | "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb",
66 | "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9"
67 | ],
68 | "version": "==1.11.0"
69 | },
70 | "werkzeug": {
71 | "hashes": [
72 | "sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b",
73 | "sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c"
74 | ],
75 | "version": "==0.14.1"
76 | }
77 | },
78 | "develop": {
79 | "alabaster": {
80 | "hashes": [
81 | "sha256:2eef172f44e8d301d25aff8068fddd65f767a3f04b5f15b0f4922f113aa1c732",
82 | "sha256:37cdcb9e9954ed60912ebc1ca12a9d12178c26637abdf124e3cde2341c257fe0"
83 | ],
84 | "version": "==0.7.10"
85 | },
86 | "babel": {
87 | "hashes": [
88 | "sha256:f20b2acd44f587988ff185d8949c3e208b4b3d5d20fcab7d91fe481ffa435528",
89 | "sha256:6007daf714d0cd5524bbe436e2d42b3c20e68da66289559341e48d2cd6d25811"
90 | ],
91 | "version": "==2.5.1"
92 | },
93 | "certifi": {
94 | "hashes": [
95 | "sha256:244be0d93b71e93fc0a0a479862051414d0e00e16435707e5bf5000f92e04694",
96 | "sha256:5ec74291ca1136b40f0379e1128ff80e866597e4e2c1e755739a913bbc3613c0"
97 | ],
98 | "version": "==2017.11.5"
99 | },
100 | "chardet": {
101 | "hashes": [
102 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691",
103 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"
104 | ],
105 | "version": "==3.0.4"
106 | },
107 | "click": {
108 | "hashes": [
109 | "sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d",
110 | "sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b"
111 | ],
112 | "version": "==6.7"
113 | },
114 | "docutils": {
115 | "hashes": [
116 | "sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6",
117 | "sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6",
118 | "sha256:51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274"
119 | ],
120 | "version": "==0.14"
121 | },
122 | "e1839a8": {
123 | "editable": true,
124 | "path": "."
125 | },
126 | "flake8": {
127 | "hashes": [
128 | "sha256:c7841163e2b576d435799169b78703ad6ac1bbb0f199994fc05f700b2a90ea37",
129 | "sha256:7253265f7abd8b313e3892944044a365e3f4ac3fcdcfb4298f55ee9ddf188ba0"
130 | ],
131 | "version": "==3.5.0"
132 | },
133 | "flask": {
134 | "hashes": [
135 | "sha256:0749df235e3ff61ac108f69ac178c9770caeaccad2509cb762ce1f65570a8856",
136 | "sha256:49f44461237b69ecd901cc7ce66feea0319b9158743dd27a2899962ab214dac1"
137 | ],
138 | "version": "==0.12.2"
139 | },
140 | "idna": {
141 | "hashes": [
142 | "sha256:8c7309c718f94b3a625cb648ace320157ad16ff131ae0af362c9f21b80ef6ec4",
143 | "sha256:2c6a5de3089009e3da7c5dde64a141dbc8551d5b7f6cf4ed7c2568d0cc520a8f"
144 | ],
145 | "version": "==2.6"
146 | },
147 | "imagesize": {
148 | "hashes": [
149 | "sha256:6ebdc9e0ad188f9d1b2cdd9bc59cbe42bf931875e829e7a595e6b3abdc05cdfb",
150 | "sha256:0ab2c62b87987e3252f89d30b7cedbec12a01af9274af9ffa48108f2c13c6062"
151 | ],
152 | "version": "==0.7.1"
153 | },
154 | "itsdangerous": {
155 | "hashes": [
156 | "sha256:cbb3fcf8d3e33df861709ecaf89d9e6629cff0a217bc2848f1b41cd30d360519"
157 | ],
158 | "version": "==0.24"
159 | },
160 | "jinja2": {
161 | "hashes": [
162 | "sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd",
163 | "sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4"
164 | ],
165 | "version": "==2.10"
166 | },
167 | "markupsafe": {
168 | "hashes": [
169 | "sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665"
170 | ],
171 | "version": "==1.0"
172 | },
173 | "mccabe": {
174 | "hashes": [
175 | "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
176 | "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
177 | ],
178 | "version": "==0.6.1"
179 | },
180 | "nose": {
181 | "hashes": [
182 | "sha256:dadcddc0aefbf99eea214e0f1232b94f2fa9bd98fa8353711dacb112bfcbbb2a",
183 | "sha256:9ff7c6cc443f8c51994b34a667bbcf45afd6d945be7477b52e97516fd17c53ac",
184 | "sha256:f1bffef9cbc82628f6e7d7b40d7e255aefaa1adb6a1b1d26c69a8b79e6208a98"
185 | ],
186 | "version": "==1.3.7"
187 | },
188 | "pluggy": {
189 | "hashes": [
190 | "sha256:7f8ae7f5bdf75671a718d2daf0a64b7885f74510bcd98b1a0bb420eb9a9d0cff"
191 | ],
192 | "version": "==0.6.0"
193 | },
194 | "py": {
195 | "hashes": [
196 | "sha256:8cca5c229d225f8c1e3085be4fcf306090b00850fefad892f9d96c7b6e2f310f",
197 | "sha256:ca18943e28235417756316bfada6cd96b23ce60dd532642690dcfdaba988a76d"
198 | ],
199 | "version": "==1.5.2"
200 | },
201 | "pycodestyle": {
202 | "hashes": [
203 | "sha256:6c4245ade1edfad79c3446fadfc96b0de2759662dc29d07d80a6f27ad1ca6ba9",
204 | "sha256:682256a5b318149ca0d2a9185d365d8864a768a28db66a84a2ea946bcc426766"
205 | ],
206 | "version": "==2.3.1"
207 | },
208 | "pyflakes": {
209 | "hashes": [
210 | "sha256:08bd6a50edf8cffa9fa09a463063c425ecaaf10d1eb0335a7e8b1401aef89e6f",
211 | "sha256:8d616a382f243dbf19b54743f280b80198be0bca3a5396f1d2e1fca6223e8805"
212 | ],
213 | "version": "==1.6.0"
214 | },
215 | "pygments": {
216 | "hashes": [
217 | "sha256:78f3f434bcc5d6ee09020f92ba487f95ba50f1e3ef83ae96b9d5ffa1bab25c5d",
218 | "sha256:dbae1046def0efb574852fab9e90209b23f556367b5a320c0bcb871c77c3e8cc"
219 | ],
220 | "version": "==2.2.0"
221 | },
222 | "pytz": {
223 | "hashes": [
224 | "sha256:80af0f3008046b9975242012a985f04c5df1f01eed4ec1633d56cc47a75a6a48",
225 | "sha256:feb2365914948b8620347784b6b6da356f31c9d03560259070b2f30cff3d469d",
226 | "sha256:59707844a9825589878236ff2f4e0dc9958511b7ffaae94dc615da07d4a68d33",
227 | "sha256:d0ef5ef55ed3d37854320d4926b04a4cb42a2e88f71da9ddfdacfde8e364f027",
228 | "sha256:c41c62827ce9cafacd6f2f7018e4f83a6f1986e87bfd000b8cfbd4ab5da95f1a",
229 | "sha256:8cc90340159b5d7ced6f2ba77694d946fc975b09f1a51d93f3ce3bb399396f94",
230 | "sha256:dd2e4ca6ce3785c8dd342d1853dd9052b19290d5bf66060846e5dc6b8d6667f7",
231 | "sha256:699d18a2a56f19ee5698ab1123bbcc1d269d061996aeb1eda6d89248d3542b82",
232 | "sha256:fae4cffc040921b8a2d60c6cf0b5d662c1190fe54d718271db4eb17d44a185b7"
233 | ],
234 | "version": "==2017.3"
235 | },
236 | "pyyaml": {
237 | "hashes": [
238 | "sha256:3262c96a1ca437e7e4763e2843746588a965426550f3797a79fca9c6199c431f",
239 | "sha256:16b20e970597e051997d90dc2cddc713a2876c47e3d92d59ee198700c5427736",
240 | "sha256:e863072cdf4c72eebf179342c94e6989c67185842d9997960b3e69290b2fa269",
241 | "sha256:bc6bced57f826ca7cb5125a10b23fd0f2fff3b7c4701d64c439a300ce665fff8",
242 | "sha256:c01b880ec30b5a6e6aa67b09a2fe3fb30473008c85cd6a67359a1b15ed6d83a4",
243 | "sha256:827dc04b8fa7d07c44de11fabbc888e627fa8293b695e0f99cb544fdfa1bf0d1",
244 | "sha256:592766c6303207a20efc445587778322d7f73b161bd994f227adaa341ba212ab",
245 | "sha256:5f84523c076ad14ff5e6c037fe1c89a7f73a3e04cf0377cb4d017014976433f3",
246 | "sha256:0c507b7f74b3d2dd4d1322ec8a94794927305ab4cebbe89cc47fe5e81541e6e8",
247 | "sha256:b4c423ab23291d3945ac61346feeb9a0dc4184999ede5e7c43e1ffb975130ae6",
248 | "sha256:ca233c64c6e40eaa6c66ef97058cdc80e8d0157a443655baa1b2966e812807ca",
249 | "sha256:4474f8ea030b5127225b8894d626bb66c01cda098d47a2b0d3429b6700af9fd8",
250 | "sha256:326420cbb492172dec84b0f65c80942de6cedb5233c413dd824483989c000608",
251 | "sha256:5ac82e411044fb129bae5cfbeb3ba626acb2af31a8d17d175004b70862a741a7"
252 | ],
253 | "version": "==3.12"
254 | },
255 | "requests": {
256 | "hashes": [
257 | "sha256:6a1b267aa90cac58ac3a765d067950e7dbbf75b1da07e895d1f594193a40a38b",
258 | "sha256:9c443e7324ba5b85070c4a818ade28bfabedf16ea10206da1132edaa6dda237e"
259 | ],
260 | "version": "==2.18.4"
261 | },
262 | "six": {
263 | "hashes": [
264 | "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb",
265 | "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9"
266 | ],
267 | "version": "==1.11.0"
268 | },
269 | "snowballstemmer": {
270 | "hashes": [
271 | "sha256:9f3bcd3c401c3e862ec0ebe6d2c069ebc012ce142cce209c098ccb5b09136e89",
272 | "sha256:919f26a68b2c17a7634da993d91339e288964f93c274f1343e3bbbe2096e1128"
273 | ],
274 | "version": "==1.2.1"
275 | },
276 | "sphinx": {
277 | "hashes": [
278 | "sha256:b8baed19394af85b21755c68c7ec4eac57e8a482ed89cd01cd5d5ff72200fe0f",
279 | "sha256:c39a6fa41bd3ec6fc10064329a664ed3a3ca2e27640a823dc520c682e4433cdb"
280 | ],
281 | "version": "==1.6.6"
282 | },
283 | "sphinxcontrib-websupport": {
284 | "hashes": [
285 | "sha256:f4932e95869599b89bf4f80fc3989132d83c9faa5bf633e7b5e0c25dffb75da2",
286 | "sha256:7a85961326aa3a400cd4ad3c816d70ed6f7c740acd7ce5d78cd0a67825072eb9"
287 | ],
288 | "version": "==1.0.1"
289 | },
290 | "tox": {
291 | "hashes": [
292 | "sha256:8af30fd835a11f3ff8e95176ccba5a4e60779df4d96a9dfefa1a1704af263225",
293 | "sha256:752f5ec561c6c08c5ecb167d3b20f4f4ffc158c0ab78855701a75f5cef05f4b8"
294 | ],
295 | "version": "==2.9.1"
296 | },
297 | "urllib3": {
298 | "hashes": [
299 | "sha256:06330f386d6e4b195fbfc736b297f58c5a892e4440e54d294d7004e3a9bbea1b",
300 | "sha256:cc44da8e1145637334317feebd728bd869a35285b93cbb4cca2577da7e62db4f"
301 | ],
302 | "version": "==1.22"
303 | },
304 | "virtualenv": {
305 | "hashes": [
306 | "sha256:39d88b533b422825d644087a21e78c45cf5af0ef7a99a1fc9fbb7b481e5c85b0",
307 | "sha256:02f8102c2436bb03b3ee6dede1919d1dac8a427541652e5ec95171ec8adbc93a"
308 | ],
309 | "markers": "python_version != '3.2'",
310 | "version": "==15.1.0"
311 | },
312 | "werkzeug": {
313 | "hashes": [
314 | "sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b",
315 | "sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c"
316 | ],
317 | "version": "==0.14.1"
318 | }
319 | }
320 | }
321 |
--------------------------------------------------------------------------------
/flask_themes/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | flaskext.themes
4 | ===============
5 | This provides infrastructure for theming support in your Flask applications.
6 | It takes care of:
7 |
8 | - Loading themes
9 | - Rendering their templates
10 | - Serving their static media
11 | - Letting themes reference their templates and static media
12 |
13 | :copyright: 2010 Matthew "LeafStorm" Frazier
14 | :license: MIT/X11, see LICENSE for details
15 | """
16 | from __future__ import with_statement
17 | import itertools
18 | import os
19 | import os.path
20 | import re
21 | from six import iteritems, itervalues, string_types
22 | from flask import (Blueprint, send_from_directory, render_template, json,
23 | _request_ctx_stack, abort, url_for)
24 | from jinja2 import contextfunction
25 | from jinja2.loaders import FileSystemLoader, BaseLoader, TemplateNotFound
26 | from operator import attrgetter
27 | from werkzeug import cached_property
28 |
29 | DOCTYPES = 'html4 html5 xhtml'.split()
30 | IDENTIFIER = re.compile(r'^[a-zA-Z_][a-zA-Z0-9_]*$')
31 |
32 | containable = lambda i: i if hasattr(i, '__contains__') else tuple(i)
33 |
34 |
35 | def starchain(i):
36 | return itertools.chain(*i)
37 |
38 |
39 | class Theme(object):
40 | """
41 | This contains a theme's metadata.
42 |
43 | :param path: The path to the theme directory.
44 | """
45 | def __init__(self, path):
46 | #: The theme's root path. All the files in the theme are under this
47 | #: path.
48 | self.path = os.path.abspath(path)
49 |
50 | with open(os.path.join(self.path, 'info.json')) as fd:
51 | self.info = i = json.load(fd)
52 |
53 | #: The theme's name, as given in info.json. This is the human
54 | #: readable name.
55 | self.name = i['name']
56 |
57 | #: The application identifier given in the theme's info.json. Your
58 | #: application will probably want to validate it.
59 | self.application = i['application']
60 |
61 | #: The theme's identifier. This is an actual Python identifier,
62 | #: and in most situations should match the name of the directory the
63 | #: theme is in.
64 | self.identifier = i['identifier']
65 |
66 | #: The human readable description. This is the default (English)
67 | #: version.
68 | self.description = i.get('description')
69 |
70 | #: This is a dictionary of localized versions of the description.
71 | #: The language codes are all lowercase, and the ``en`` key is
72 | #: preloaded with the base description.
73 | self.localized_desc = dict(
74 | (k.split('_', 1)[1].lower(), v) for k, v in i.items()
75 | if k.startswith('description_')
76 | )
77 | self.localized_desc.setdefault('en', self.description)
78 |
79 | #: The author's name, as given in info.json. This may or may not
80 | #: include their email, so it's best just to display it as-is.
81 | self.author = i['author']
82 |
83 | #: A short phrase describing the license, like "GPL", "BSD", "Public
84 | #: Domain", or "Creative Commons BY-SA 3.0".
85 | self.license = i.get('license')
86 |
87 | #: A URL pointing to the license text online.
88 | self.license_url = i.get('license_url')
89 |
90 | #: The URL to the theme's or author's Web site.
91 | self.website = i.get('website')
92 |
93 | #: The theme's preview image, within the static folder.
94 | self.preview = i.get('preview')
95 |
96 | #: The theme's doctype. This can be ``html4``, ``html5``, or ``xhtml``
97 | #: with html5 being the default if not specified.
98 | self.doctype = i.get('doctype', 'html5')
99 |
100 | #: Any additional options. These are entirely application-specific,
101 | #: and may determine other aspects of the application's behavior.
102 | self.options = i.get('options', {})
103 |
104 | @cached_property
105 | def static_path(self):
106 | """
107 | The absolute path to the theme's static files directory.
108 | """
109 | return os.path.join(self.path, 'static')
110 |
111 | @cached_property
112 | def templates_path(self):
113 | """
114 | The absolute path to the theme's templates directory.
115 | """
116 | return os.path.join(self.path, 'templates')
117 |
118 | @cached_property
119 | def license_text(self):
120 | """
121 | The contents of the theme's license.txt file, if it exists. This is
122 | used to display the full license text if necessary. (It is `None` if
123 | there was not a license.txt.)
124 | """
125 | lt_path = os.path.join(self.path, 'license.txt')
126 | if os.path.exists(lt_path):
127 | with open(lt_path) as fd:
128 | return fd.read()
129 | else:
130 | return None
131 |
132 | @cached_property
133 | def jinja_loader(self):
134 | """
135 | This is a Jinja2 template loader that loads templates from the theme's
136 | ``templates`` directory.
137 | """
138 | return FileSystemLoader(self.templates_path)
139 |
140 |
141 | ### theme loaders
142 |
143 | def list_folders(path):
144 | """
145 | This is a helper function that only returns the directories in a given
146 | folder.
147 |
148 | :param path: The path to list directories in.
149 | """
150 | return (name for name in os.listdir(path)
151 | if os.path.isdir(os.path.join(path, name)))
152 |
153 |
154 | def load_themes_from(path):
155 | """
156 | This is used by the default loaders. You give it a path, and it will find
157 | valid themes and yield them one by one.
158 |
159 | :param path: The path to search for themes in.
160 | """
161 | for basename in (b for b in list_folders(path) if IDENTIFIER.match(b)):
162 | try:
163 | t = Theme(os.path.join(path, basename))
164 | except:
165 | pass
166 | else:
167 | if t.identifier == basename:
168 | yield t
169 |
170 |
171 | def packaged_themes_loader(app):
172 | """
173 | This theme will find themes that are shipped with the application. It will
174 | look in the application's root path for a ``themes`` directory - for
175 | example, the ``someapp`` package can ship themes in the directory
176 | ``someapp/themes/``.
177 | """
178 | themes_path = os.path.join(app.root_path, 'themes')
179 | if os.path.exists(themes_path):
180 | return load_themes_from(themes_path)
181 | else:
182 | return ()
183 |
184 |
185 | def theme_paths_loader(app):
186 | """
187 | This checks the app's `THEME_PATHS` configuration variable to find
188 | directories that contain themes. The theme's identifier must match the
189 | name of its directory.
190 | """
191 | theme_paths = app.config.get('THEME_PATHS', ())
192 | if isinstance(theme_paths, string_types):
193 | theme_paths = [p.strip() for p in theme_paths.split(';')]
194 | return starchain(
195 | load_themes_from(path) for path in theme_paths
196 | )
197 |
198 |
199 | class ThemeManager(object):
200 | """
201 | This is responsible for loading and storing all the themes for an
202 | application. Calling `refresh` will cause it to invoke all of the theme
203 | loaders.
204 |
205 | A theme loader is simply a callable that takes an app and returns an
206 | iterable of `Theme` instances. You can implement your own loaders if your
207 | app has another way to load themes.
208 |
209 | :param app: The app to bind to. (Each instance is only usable for one
210 | app.)
211 | :param app_identifier: The value that the info.json's `application` key
212 | is required to have. If you require a more complex
213 | check, you can subclass and override the
214 | `valid_app_id` method.
215 | :param loaders: An iterable of loaders to use. The defaults are
216 | `packaged_themes_loader` and `theme_paths_loader`, in that
217 | order.
218 | """
219 | def __init__(self, app=None, app_identifier=None, loaders=None):
220 | self.app = app
221 | if app is not None:
222 | self.init_app(app)
223 |
224 | self.app_identifier = app_identifier
225 |
226 | self._themes = None
227 |
228 | #: This is a list of the loaders that will be used to load the themes.
229 | self.loaders = []
230 | if loaders:
231 | self.loaders.extend(loaders)
232 | else:
233 | self.loaders.extend((packaged_themes_loader, theme_paths_loader))
234 |
235 | def init_app(self, app):
236 | self.bind_app(app)
237 |
238 | @property
239 | def themes(self):
240 | """
241 | This is a dictionary of all the themes that have been loaded. The keys
242 | are the identifiers and the values are `Theme` objects.
243 | """
244 | if self._themes is None:
245 | self.refresh()
246 | return self._themes
247 |
248 | def list_themes(self):
249 | """
250 | This yields all the `Theme` objects, in sorted order.
251 | """
252 | return sorted(itervalues(self.themes), key=attrgetter('identifier'))
253 |
254 | def bind_app(self, app):
255 | """
256 | If an app wasn't bound when the manager was created, this will bind
257 | it. The app must be bound for the loaders to work.
258 |
259 | :param app: A `~flask.Flask` instance.
260 | """
261 | self.app = app
262 | app.theme_manager = self
263 |
264 | def valid_app_id(self, app_identifier):
265 | """
266 | This checks whether the application identifier given will work with
267 | this application. The default implementation checks whether the given
268 | identifier matches the one given at initialization.
269 |
270 | :param app_identifier: The application identifier to check.
271 | """
272 | return self.app_identifier == app_identifier
273 |
274 | def refresh(self):
275 | """
276 | This loads all of the themes into the `themes` dictionary. The loaders
277 | are invoked in the order they are given, so later themes will override
278 | earlier ones. Any invalid themes found (for example, if the
279 | application identifier is incorrect) will be skipped.
280 | """
281 | self._themes = {}
282 | for theme in starchain(ldr(self.app) for ldr in self.loaders):
283 | if self.valid_app_id(theme.application):
284 | self.themes[theme.identifier] = theme
285 |
286 |
287 | def get_theme(ident):
288 | """
289 | This gets the theme with the given identifier from the current app's
290 | theme manager.
291 |
292 | :param ident: The theme identifier.
293 | """
294 | ctx = _request_ctx_stack.top
295 | return ctx.app.theme_manager.themes[ident]
296 |
297 |
298 | def get_themes_list():
299 | """
300 | This returns a list of all the themes in the current app's theme manager,
301 | sorted by identifier.
302 | """
303 | ctx = _request_ctx_stack.top
304 | return list(ctx.app.theme_manager.list_themes())
305 |
306 |
307 | ### theme template loader
308 |
309 | class ThemeTemplateLoader(BaseLoader):
310 | """
311 | This is a template loader that loads templates from the current app's
312 | loaded themes.
313 | """
314 | def __init__(self, as_blueprint=False):
315 | self.as_blueprint = as_blueprint
316 | BaseLoader.__init__(self)
317 |
318 | def get_source(self, environment, template):
319 | if self.as_blueprint and template.startswith("_themes/"):
320 | template = template[8:]
321 | try:
322 | themename, templatename = template.split('/', 1)
323 | ctx = _request_ctx_stack.top
324 | theme = ctx.app.theme_manager.themes[themename]
325 | except (ValueError, KeyError):
326 | raise TemplateNotFound(template)
327 | try:
328 | return theme.jinja_loader.get_source(environment, templatename)
329 | except TemplateNotFound:
330 | raise TemplateNotFound(template)
331 |
332 | def list_templates(self):
333 | res = []
334 | ctx = _request_ctx_stack.top
335 | fmt = '_themes/%s/%s'
336 | for ident, theme in iteritems(ctx.app.theme_manager.themes):
337 | res.extend((fmt % (ident, t))
338 | for t in theme.jinja_loader.list_templates())
339 | return res
340 |
341 |
342 | def template_exists(templatename):
343 | ctx = _request_ctx_stack.top
344 | return templatename in containable(ctx.app.jinja_env.list_templates())
345 |
346 |
347 | ### theme functionality
348 |
349 |
350 | themes_blueprint = Blueprint('_themes', __name__, url_prefix='/_themes')
351 | themes_blueprint.jinja_loader = ThemeTemplateLoader(True)
352 |
353 |
354 | def static(themeid, filename):
355 | try:
356 | ctx = _request_ctx_stack.top
357 | theme = ctx.app.theme_manager.themes[themeid]
358 | except KeyError:
359 | abort(404)
360 | return send_from_directory(theme.static_path, filename)
361 |
362 |
363 | themes_blueprint.add_url_rule('//', 'static',
364 | view_func=static)
365 |
366 |
367 | def setup_themes(app, loaders=None, app_identifier=None,
368 | manager_cls=ThemeManager, theme_url_prefix='/_themes'):
369 | """
370 | This sets up the theme infrastructure by adding a `ThemeManager` to the
371 | given app and registering the module/blueprint containing the views and
372 | templates needed.
373 |
374 | :param app: The `~flask.Flask` instance to set up themes for.
375 | :param loaders: An iterable of loaders to use. It defaults to
376 | `packaged_themes_loader` and `theme_paths_loader`.
377 | :param app_identifier: The application identifier to use. If not given,
378 | it defaults to the app's import name.
379 | :param manager_cls: If you need a custom manager class, you can pass it
380 | in here.
381 | :param theme_url_prefix: The prefix to use for the URLs on the themes
382 | module. (Defaults to ``/_themes``.)
383 | """
384 | if app_identifier is None:
385 | app_identifier = app.import_name
386 | manager_cls(app, app_identifier, loaders=loaders)
387 | app.jinja_env.globals['theme'] = global_theme_template
388 | app.jinja_env.globals['theme_static'] = global_theme_static
389 | app.register_blueprint(themes_blueprint, url_prefix=theme_url_prefix)
390 |
391 |
392 | def active_theme(ctx):
393 | if '_theme' in ctx:
394 | return ctx['_theme']
395 | elif ctx.name.startswith('_themes/'):
396 | return ctx.name[8:].split('/', 1)[0]
397 | else:
398 | raise RuntimeError("Could not find the active theme")
399 |
400 |
401 | @contextfunction
402 | def global_theme_template(ctx, templatename, fallback=True):
403 | theme = active_theme(ctx)
404 | templatepath = '_themes/%s/%s' % (theme, templatename)
405 | if (not fallback) or template_exists(templatepath):
406 | return templatepath
407 | else:
408 | return templatename
409 |
410 |
411 | @contextfunction
412 | def global_theme_static(ctx, filename, external=False):
413 | theme = active_theme(ctx)
414 | return static_file_url(theme, filename, external)
415 |
416 |
417 | def static_file_url(theme, filename, external=False):
418 | """
419 | This is a shortcut for getting the URL of a static file in a theme.
420 |
421 | :param theme: A `Theme` instance or identifier.
422 | :param filename: The name of the file.
423 | :param external: Whether the link should be external or not. Defaults to
424 | `False`.
425 | """
426 | if isinstance(theme, Theme):
427 | theme = theme.identifier
428 | return url_for('_themes.static', themeid=theme, filename=filename,
429 | _external=external)
430 |
431 |
432 | def render_theme_template(theme, template_name, _fallback=True, **context):
433 | """
434 | This renders a template from the given theme. For example::
435 |
436 | return render_theme_template(g.user.theme, 'index.html', posts=posts)
437 |
438 | If `_fallback` is True and the themplate does not exist within the theme,
439 | it will fall back on trying to render the template using the application's
440 | normal templates. (The "active theme" will still be set, though, so you
441 | can try to extend or include other templates from the theme.)
442 |
443 | :param theme: Either the identifier of the theme to use, or an actual
444 | `Theme` instance.
445 | :param template_name: The name of the template to render.
446 | :param _fallback: Whether to fall back to the default
447 | """
448 | if isinstance(theme, Theme):
449 | theme = theme.identifier
450 | context['_theme'] = theme
451 | try:
452 | return render_template('_themes/%s/%s' % (theme, template_name),
453 | **context)
454 | except TemplateNotFound:
455 | if _fallback:
456 | return render_template(template_name, **context)
457 | else:
458 | raise
459 |
--------------------------------------------------------------------------------