├── .gitignore ├── LICENSE ├── MANIFEST.in ├── Pipfile ├── Pipfile.lock ├── README ├── base_theme ├── info.json ├── license.txt ├── static │ └── style.css └── templates │ └── layout.html ├── docs ├── Makefile ├── _themes │ ├── README │ ├── flask │ │ ├── static │ │ │ └── flasky.css_t │ │ └── theme.conf │ ├── flask_small │ │ ├── layout.html │ │ ├── static │ │ │ └── flasky.css_t │ │ └── theme.conf │ └── flask_theme_support.py ├── conf.py ├── index.rst └── make.bat ├── example ├── posts.yaml ├── templates │ ├── _helpers.html │ ├── _post.html │ ├── about.html │ ├── archive.html │ ├── index.html │ ├── layout.html │ ├── post.html │ └── themes.html ├── themes │ ├── calmblue │ │ ├── info.json │ │ ├── static │ │ │ └── style.css │ │ └── templates │ │ │ └── layout.html │ └── plain │ │ ├── info.json │ │ ├── static │ │ └── static.css │ │ └── templates │ │ └── layout.html └── themesandbox.py ├── flask_themes └── __init__.py ├── new-theme.py ├── requirements-dev.txt ├── requirements.txt ├── setup.py ├── tests ├── morethemes │ └── cool │ │ ├── info.json │ │ └── templates │ │ ├── active.html │ │ ├── hello.html │ │ └── static.html ├── templates │ ├── active.html │ ├── hello.html │ ├── static.html │ └── static_parent.html ├── test-themes.py └── themes │ ├── cool │ ├── info.json │ └── templates │ │ └── hello.html │ ├── identifier │ └── info.json │ ├── notthis │ └── info.json │ ├── plain │ ├── info.json │ ├── license.txt │ └── templates │ │ └── static.html │ └── warm-theme │ └── info.json └── tox.ini /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /base_theme/license.txt: -------------------------------------------------------------------------------- 1 | The complete text of your license should go here. 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/_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 | -------------------------------------------------------------------------------- /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/_themes/flask/theme.conf: -------------------------------------------------------------------------------- 1 | [theme] 2 | inherit = basic 3 | stylesheet = flasky.css 4 | pygments_style = flask_theme_support.FlaskyStyle 5 | -------------------------------------------------------------------------------- /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 | Fork me on GitHub 19 | {% endif %} 20 | {% endblock %} 21 | {% block sidebar1 %}{% endblock %} 22 | {% block sidebar2 %}{% endblock %} 23 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /example/templates/_helpers.html: -------------------------------------------------------------------------------- 1 | {% macro link_to(text, endpoint) -%} 2 | {{ text }} 3 | {%- endmacro %} 4 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | 13 | 14 | {% endfor %} 15 | 16 | {% endblock body %} 17 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | 16 | 17 | {% block body %}{% endblock %} 18 |
19 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | 17 | 18 |

Theme Sandbox

19 | 20 |
21 | 22 | {% block body %} 23 | {% endblock body %} 24 |
25 | 26 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tests/morethemes/cool/info.json: -------------------------------------------------------------------------------- 1 | { 2 | "identifier": "cool", 3 | "name": "Cool Blue v2", 4 | "application": "testing", 5 | "author": "LeafStorm" 6 | } 7 | -------------------------------------------------------------------------------- /tests/morethemes/cool/templates/active.html: -------------------------------------------------------------------------------- 1 | Cool Blue v2, Active theme: {{ _theme|default("none") }} 2 | -------------------------------------------------------------------------------- /tests/morethemes/cool/templates/hello.html: -------------------------------------------------------------------------------- 1 | Hello from Cool Blue v2. 2 | -------------------------------------------------------------------------------- /tests/morethemes/cool/templates/static.html: -------------------------------------------------------------------------------- 1 | Cool Blue v2, {{ theme_static('style.css') }} 2 | -------------------------------------------------------------------------------- /tests/templates/active.html: -------------------------------------------------------------------------------- 1 | Application, Active theme: {{ _theme|default("none") }} 2 | -------------------------------------------------------------------------------- /tests/templates/hello.html: -------------------------------------------------------------------------------- 1 | Hello from the application 2 | -------------------------------------------------------------------------------- /tests/templates/static.html: -------------------------------------------------------------------------------- 1 | Application, {{ theme_static('style.css') }} 2 | -------------------------------------------------------------------------------- /tests/templates/static_parent.html: -------------------------------------------------------------------------------- 1 | Application, {% include '_themes/plain/static.html' %} 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tests/themes/cool/info.json: -------------------------------------------------------------------------------- 1 | { 2 | "identifier": "cool", 3 | "name": "Cool Blue v1", 4 | "application": "testing", 5 | "author": "LeafStorm" 6 | } 7 | -------------------------------------------------------------------------------- /tests/themes/cool/templates/hello.html: -------------------------------------------------------------------------------- 1 | Hello from Cool Blue v1. 2 | -------------------------------------------------------------------------------- /tests/themes/identifier/info.json: -------------------------------------------------------------------------------- 1 | { 2 | "identifier": "mismatch", 3 | "name": "Identifier Mismatch", 4 | "application": "testing", 5 | "author": "LeafStorm" 6 | } 7 | -------------------------------------------------------------------------------- /tests/themes/notthis/info.json: -------------------------------------------------------------------------------- 1 | { 2 | "identifier": "notthis", 3 | "name": "Not This App", 4 | "application": "anotherapp", 5 | "author": "LeafStorm" 6 | } 7 | -------------------------------------------------------------------------------- /tests/themes/plain/info.json: -------------------------------------------------------------------------------- 1 | { 2 | "identifier": "plain", 3 | "name": "Plain", 4 | "application": "testing", 5 | "author": "LeafStorm" 6 | } 7 | -------------------------------------------------------------------------------- /tests/themes/plain/license.txt: -------------------------------------------------------------------------------- 1 | The license. 2 | -------------------------------------------------------------------------------- /tests/themes/plain/templates/static.html: -------------------------------------------------------------------------------- 1 | Plain, {{ theme_static('style.css') }} 2 | -------------------------------------------------------------------------------- /tests/themes/warm-theme/info.json: -------------------------------------------------------------------------------- 1 | { 2 | "identifier": "warm-theme", 3 | "name": "Warm", 4 | "application": "testing", 5 | "author": "LeafStorm" 6 | } 7 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------