├── .github └── workflows │ └── tests.yaml ├── .readthedocs.yaml ├── LICENSE ├── MANIFEST.in ├── README.rst ├── docs ├── Makefile ├── _static │ └── peewee.jpg ├── _themes │ └── flask │ │ ├── layout.html │ │ ├── relations.html │ │ ├── static │ │ ├── flasky.css_t │ │ └── small_flask.css │ │ └── theme.conf ├── admin.rst ├── api.rst ├── auth.rst ├── conf.py ├── database.rst ├── example.rst ├── fp-admin-btn-form.png ├── fp-admin-btn.png ├── fp-admin-filter.png ├── fp-admin-modal.png ├── fp-admin.jpg ├── fp-app.jpg ├── fp-getting-started.jpg ├── fp-message-admin-2.jpg ├── fp-message-admin.jpg ├── fp-model-admin.jpg ├── fp-note-admin-2.jpg ├── fp-note-admin.jpg ├── fp-note-panel-2.jpg ├── fp-note-panel.jpg ├── fp-panels.jpg ├── getting-started.rst ├── gevent.rst ├── index.rst ├── installation.rst ├── rest-api.rst └── utils.rst ├── example ├── __init__.py ├── admin.py ├── api.py ├── app.py ├── auth.py ├── config.py ├── example.db ├── main.py ├── models.py ├── requirements.txt ├── run_example.py ├── static │ └── style.css ├── templates │ ├── admin │ │ ├── notes.html │ │ └── user_stats.html │ ├── base.html │ ├── create.html │ ├── edit.html │ ├── homepage.html │ ├── includes │ │ ├── message.html │ │ └── pagination.html │ ├── join.html │ ├── private_messages.html │ ├── public_messages.html │ ├── user_detail.html │ ├── user_followers.html │ ├── user_following.html │ └── user_list.html └── views.py ├── flask_peewee ├── __init__.py ├── _compat.py ├── _wtforms_compat.py ├── admin.py ├── auth.py ├── db.py ├── exceptions.py ├── filters.py ├── forms.py ├── rest.py ├── serializer.py ├── static │ ├── css │ │ ├── admin.css │ │ ├── bootstrap-responsive.css │ │ ├── bootstrap-responsive.min.css │ │ ├── bootstrap.css │ │ ├── bootstrap.min.css │ │ ├── bootstrap.min.responsive.css │ │ ├── chosen-sprite.png │ │ ├── chosen.css │ │ └── datepicker.css │ ├── img │ │ ├── glyphicons-halflings-white.png │ │ └── glyphicons-halflings.png │ └── js │ │ ├── admin.js │ │ ├── ajax-chosen.min.js │ │ ├── bootstrap-datepicker.js │ │ ├── bootstrap.js │ │ ├── bootstrap.min.js │ │ ├── chosen.jquery.min.js │ │ ├── events.js │ │ ├── jquery.min.js │ │ └── modernizr-1.5.min.js ├── templates │ ├── admin │ │ ├── base.html │ │ ├── includes │ │ │ ├── filter_dropdown.html │ │ │ ├── filter_widgets.html │ │ │ ├── form_raw_id.html │ │ │ └── pagination.html │ │ ├── index.html │ │ ├── models │ │ │ ├── add.html │ │ │ ├── base.html │ │ │ ├── base_filters.html │ │ │ ├── base_forms.html │ │ │ ├── delete.html │ │ │ ├── edit.html │ │ │ ├── export.html │ │ │ └── index.html │ │ └── panels │ │ │ ├── base.html │ │ │ └── default.html │ ├── auth │ │ └── login.html │ ├── base.html │ └── macros │ │ └── forms.html ├── tests │ ├── __init__.py │ ├── admin.py │ ├── auth.py │ ├── base.py │ ├── rest.py │ ├── serializer.py │ ├── templates │ │ ├── admin │ │ │ └── notes.html │ │ └── base.html │ ├── test_app.py │ ├── test_config.py │ └── utils.py └── utils.py ├── runtests.py └── setup.py /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: [push] 3 | jobs: 4 | tests: 5 | name: ${{ matrix.python-version }} 6 | runs-on: ubuntu-latest 7 | strategy: 8 | fail-fast: false 9 | matrix: 10 | python-version: [3.7, "3.10"] 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-python@v2 14 | with: 15 | python-version: ${{ matrix.python-version }} 16 | - name: pip deps 17 | run: pip install peewee flask wtforms wtf-peewee 18 | - name: runtests 19 | run: python runtests.py 20 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | build: 3 | os: ubuntu-22.04 4 | tools: 5 | python: "3.11" 6 | sphinx: 7 | configuration: docs/conf.py 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Charles Leifer 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | recursive-include docs * 4 | recursive-include example * 5 | recursive-include flask_peewee * 6 | prune docs/_build 7 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | This package is in maintenance-only mode! 2 | ========================================= 3 | 4 | I'm sorry to announce that flask-peewee will now be in maintenance-only mode. This decision is motivated by a number of factors: 5 | 6 | * `Flask-Admin `_ provides a superior admin interface and has support for peewee models. 7 | * `Flask-Security `_ and `Flask-Login `_ both provide authentication functionality, and work well with Peewee. 8 | * Most importantly, though, I do not find myself wanting to work on flask-peewee. 9 | 10 | I plan on rewriting the ``Database`` and ``REST API`` portions of flask-peewee and repackaging them as a new library, but flask-peewee as it stands currently will be in maintenance-only mode. 11 | 12 | flask-peewee 13 | ============ 14 | 15 | provides a layer of integration between the `flask `_ 16 | web framework and the `peewee orm `_. 17 | 18 | batteries included: 19 | 20 | * admin interface 21 | * authentication 22 | * rest api 23 | 24 | requirements: 25 | 26 | * `flask `_ 27 | * `peewee `_ 28 | * `wtforms `_ 29 | * `wtf-peewee `_ 30 | * python 2.5 or greater 31 | 32 | 33 | check out the `documentation `_. 34 | 35 | 36 | admin interface 37 | --------------- 38 | 39 | influenced heavily by the `django `_ admin, provides easy 40 | create/edit/delete functionality for your project's models. 41 | 42 | .. image:: http://i.imgur.com/EtzdO.jpg 43 | 44 | 45 | rest api 46 | -------- 47 | 48 | influenced by `tastypie `_, provides 49 | a way to expose a RESTful interface for your project's models. 50 | 51 | :: 52 | 53 | curl localhost:5000/api/user/ 54 | { 55 | "meta": { 56 | "model": "user", 57 | "next": "", 58 | "page": 1, 59 | "previous": "" 60 | }, 61 | "objects": [ 62 | { 63 | "username": "admin", 64 | "admin": true, 65 | "email": "", 66 | "join_date": "2011-09-16 18:34:49", 67 | "active": true, 68 | "id": 1 69 | }, 70 | { 71 | "username": "coleifer", 72 | "admin": false, 73 | "email": "coleifer@gmail.com", 74 | "join_date": "2011-09-16 18:35:56", 75 | "active": true, 76 | "id": 2 77 | } 78 | ] 79 | } 80 | 81 | 82 | installing 83 | ---------- 84 | 85 | I recommend installing in a virtualenv. to get started:: 86 | 87 | # create a new virtualenv 88 | virtualenv --no-site-packages project 89 | cd project/ 90 | source bin/activate 91 | 92 | # install this project (will install dependencies as well) 93 | pip install flask-peewee 94 | 95 | 96 | example app 97 | ----------- 98 | 99 | the project ships with an example app, which is a silly twitter clone. to 100 | start the example app, ``cd`` into the "example" directory and execute 101 | the ``run_example.py`` script:: 102 | 103 | cd example/ 104 | python run_example.py 105 | 106 | if you would like to test out the admin area, log in as "admin/admin" and navigate to: 107 | 108 | http://127.0.0.1:5000/admin/ 109 | 110 | you can check out the REST api at the following url: 111 | 112 | http://127.0.0.1:5000/api/message/ 113 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | 15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " singlehtml to make a single large HTML file" 22 | @echo " pickle to make pickle files" 23 | @echo " json to make JSON files" 24 | @echo " htmlhelp to make HTML files and a HTML help project" 25 | @echo " qthelp to make HTML files and a qthelp project" 26 | @echo " devhelp to make HTML files and a Devhelp project" 27 | @echo " epub to make an epub" 28 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 29 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 30 | @echo " text to make text files" 31 | @echo " man to make manual pages" 32 | @echo " changes to make an overview of all changed/added/deprecated items" 33 | @echo " linkcheck to check all external links for integrity" 34 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 35 | 36 | clean: 37 | -rm -rf $(BUILDDIR)/* 38 | 39 | html: 40 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 41 | @echo 42 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 43 | 44 | dirhtml: 45 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 48 | 49 | singlehtml: 50 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 51 | @echo 52 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 53 | 54 | pickle: 55 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 56 | @echo 57 | @echo "Build finished; now you can process the pickle files." 58 | 59 | json: 60 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 61 | @echo 62 | @echo "Build finished; now you can process the JSON files." 63 | 64 | htmlhelp: 65 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 66 | @echo 67 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 68 | ".hhp project file in $(BUILDDIR)/htmlhelp." 69 | 70 | qthelp: 71 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 72 | @echo 73 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 74 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 75 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/flask-peewee.qhcp" 76 | @echo "To view the help file:" 77 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/flask-peewee.qhc" 78 | 79 | devhelp: 80 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 81 | @echo 82 | @echo "Build finished." 83 | @echo "To view the help file:" 84 | @echo "# mkdir -p $$HOME/.local/share/devhelp/flask-peewee" 85 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/flask-peewee" 86 | @echo "# devhelp" 87 | 88 | epub: 89 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 90 | @echo 91 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 92 | 93 | latex: 94 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 95 | @echo 96 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 97 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 98 | "(use \`make latexpdf' here to do that automatically)." 99 | 100 | latexpdf: 101 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 102 | @echo "Running LaTeX files through pdflatex..." 103 | make -C $(BUILDDIR)/latex all-pdf 104 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 105 | 106 | text: 107 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 108 | @echo 109 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 110 | 111 | man: 112 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 113 | @echo 114 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 115 | 116 | changes: 117 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 118 | @echo 119 | @echo "The overview file is in $(BUILDDIR)/changes." 120 | 121 | linkcheck: 122 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 123 | @echo 124 | @echo "Link check complete; look for any errors in the above output " \ 125 | "or in $(BUILDDIR)/linkcheck/output.txt." 126 | 127 | doctest: 128 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 129 | @echo "Testing of doctests in the sources finished, look at the " \ 130 | "results in $(BUILDDIR)/doctest/output.txt." 131 | -------------------------------------------------------------------------------- /docs/_static/peewee.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coleifer/flask-peewee/e4c8bd947b6524bb2fd93c3598e48818acf00307/docs/_static/peewee.jpg -------------------------------------------------------------------------------- /docs/_themes/flask/layout.html: -------------------------------------------------------------------------------- 1 | {%- extends "basic/layout.html" %} 2 | {%- block extrahead %} 3 | {{ super() }} 4 | {% if theme_touch_icon %} 5 | 6 | {% endif %} 7 | 9 | {% endblock %} 10 | {%- block relbar2 %}{% endblock %} 11 | {% block header %} 12 | {{ super() }} 13 | {% if pagename == 'index' %} 14 |
15 | {% endif %} 16 | {% endblock %} 17 | {%- block footer %} 18 | 22 | {% if pagename == 'index' %} 23 |
24 | {% endif %} 25 | {%- endblock %} 26 | -------------------------------------------------------------------------------- /docs/_themes/flask/relations.html: -------------------------------------------------------------------------------- 1 |

Related Topics

2 | 20 | -------------------------------------------------------------------------------- /docs/_themes/flask/static/flasky.css_t: -------------------------------------------------------------------------------- 1 | /* 2 | * flasky.css_t 3 | * ~~~~~~~~~~~~ 4 | * 5 | * :copyright: Copyright 2010 by Armin Ronacher. 6 | * :license: Flask Design License, see LICENSE for details. 7 | */ 8 | 9 | {% set page_width = '940px' %} 10 | {% set sidebar_width = '220px' %} 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: white; 20 | color: #000; 21 | margin: 0; 22 | padding: 0; 23 | } 24 | 25 | div.document { 26 | width: {{ page_width }}; 27 | margin: 30px auto 0 auto; 28 | } 29 | 30 | div.documentwrapper { 31 | float: left; 32 | width: 100%; 33 | } 34 | 35 | div.bodywrapper { 36 | margin: 0 0 0 {{ sidebar_width }}; 37 | } 38 | 39 | div.sphinxsidebar { 40 | width: {{ sidebar_width }}; 41 | } 42 | 43 | hr { 44 | border: 1px solid #B1B4B6; 45 | } 46 | 47 | div.body { 48 | background-color: #ffffff; 49 | color: #3E4349; 50 | padding: 0 30px 0 30px; 51 | } 52 | 53 | img.floatingflask { 54 | padding: 0 0 10px 10px; 55 | float: right; 56 | } 57 | 58 | div.footer { 59 | width: {{ page_width }}; 60 | margin: 20px auto 30px auto; 61 | font-size: 14px; 62 | color: #888; 63 | text-align: right; 64 | } 65 | 66 | div.footer a { 67 | color: #888; 68 | } 69 | 70 | div.related { 71 | display: none; 72 | } 73 | 74 | div.sphinxsidebar a { 75 | color: #444; 76 | text-decoration: none; 77 | border-bottom: 1px dotted #999; 78 | } 79 | 80 | div.sphinxsidebar a:hover { 81 | border-bottom: 1px solid #999; 82 | } 83 | 84 | div.sphinxsidebar { 85 | font-size: 14px; 86 | line-height: 1.5; 87 | } 88 | 89 | div.sphinxsidebarwrapper { 90 | padding: 18px 10px; 91 | } 92 | 93 | div.sphinxsidebarwrapper p.logo { 94 | padding: 0 0 20px 0; 95 | margin: 0; 96 | text-align: center; 97 | } 98 | 99 | div.sphinxsidebar h3, 100 | div.sphinxsidebar h4 { 101 | font-family: 'Garamond', 'Georgia', serif; 102 | color: #444; 103 | font-size: 24px; 104 | font-weight: normal; 105 | margin: 0 0 5px 0; 106 | padding: 0; 107 | } 108 | 109 | div.sphinxsidebar h4 { 110 | font-size: 20px; 111 | } 112 | 113 | div.sphinxsidebar h3 a { 114 | color: #444; 115 | } 116 | 117 | div.sphinxsidebar p.logo a, 118 | div.sphinxsidebar h3 a, 119 | div.sphinxsidebar p.logo a:hover, 120 | div.sphinxsidebar h3 a:hover { 121 | border: none; 122 | } 123 | 124 | div.sphinxsidebar p { 125 | color: #555; 126 | margin: 10px 0; 127 | } 128 | 129 | div.sphinxsidebar ul { 130 | margin: 10px 0; 131 | padding: 0; 132 | color: #000; 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 h1, 154 | div.body h2, 155 | div.body h3, 156 | div.body h4, 157 | div.body h5, 158 | div.body h6 { 159 | font-family: 'Garamond', 'Georgia', serif; 160 | font-weight: normal; 161 | margin: 30px 0px 10px 0px; 162 | padding: 0; 163 | } 164 | 165 | {% if theme_index_logo %} 166 | div.indexwrapper h1 { 167 | text-indent: -999999px; 168 | background: url({{ theme_index_logo }}) no-repeat center center; 169 | height: {{ theme_index_logo_height }}; 170 | } 171 | {% endif %} 172 | 173 | div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; } 174 | div.body h2 { font-size: 180%; } 175 | div.body h3 { font-size: 150%; } 176 | div.body h4 { font-size: 130%; } 177 | div.body h5 { font-size: 100%; } 178 | div.body h6 { font-size: 100%; } 179 | 180 | a.headerlink { 181 | color: #ddd; 182 | padding: 0 4px; 183 | text-decoration: none; 184 | } 185 | 186 | a.headerlink:hover { 187 | color: #444; 188 | background: #eaeaea; 189 | } 190 | 191 | div.body p, div.body dd, div.body li { 192 | line-height: 1.4em; 193 | } 194 | 195 | div.admonition { 196 | background: #fafafa; 197 | margin: 20px -30px; 198 | padding: 10px 30px; 199 | border-top: 1px solid #ccc; 200 | border-bottom: 1px solid #ccc; 201 | } 202 | 203 | div.admonition tt.xref, div.admonition a tt { 204 | border-bottom: 1px solid #fafafa; 205 | } 206 | 207 | dd div.admonition { 208 | margin-left: -60px; 209 | padding-left: 60px; 210 | } 211 | 212 | div.admonition p.admonition-title { 213 | font-family: 'Garamond', 'Georgia', serif; 214 | font-weight: normal; 215 | font-size: 24px; 216 | margin: 0 0 10px 0; 217 | padding: 0; 218 | line-height: 1; 219 | } 220 | 221 | div.admonition p.last { 222 | margin-bottom: 0; 223 | } 224 | 225 | div.highlight { 226 | background-color: white; 227 | } 228 | 229 | dt:target, .highlight { 230 | background: #FAF3E8; 231 | } 232 | 233 | div.note { 234 | background-color: #eee; 235 | border: 1px solid #ccc; 236 | } 237 | 238 | div.seealso { 239 | background-color: #ffc; 240 | border: 1px solid #ff6; 241 | } 242 | 243 | div.topic { 244 | background-color: #eee; 245 | } 246 | 247 | p.admonition-title { 248 | display: inline; 249 | } 250 | 251 | p.admonition-title:after { 252 | content: ":"; 253 | } 254 | 255 | pre, tt { 256 | font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; 257 | font-size: 0.9em; 258 | } 259 | 260 | img.screenshot { 261 | } 262 | 263 | tt.descname, tt.descclassname { 264 | font-size: 0.95em; 265 | } 266 | 267 | tt.descname { 268 | padding-right: 0.08em; 269 | } 270 | 271 | img.screenshot { 272 | -moz-box-shadow: 2px 2px 4px #eee; 273 | -webkit-box-shadow: 2px 2px 4px #eee; 274 | box-shadow: 2px 2px 4px #eee; 275 | } 276 | 277 | table.docutils { 278 | border: 1px solid #888; 279 | -moz-box-shadow: 2px 2px 4px #eee; 280 | -webkit-box-shadow: 2px 2px 4px #eee; 281 | box-shadow: 2px 2px 4px #eee; 282 | } 283 | 284 | table.docutils td, table.docutils th { 285 | border: 1px solid #888; 286 | padding: 0.25em 0.7em; 287 | } 288 | 289 | table.field-list, table.footnote { 290 | border: none; 291 | -moz-box-shadow: none; 292 | -webkit-box-shadow: none; 293 | box-shadow: none; 294 | } 295 | 296 | table.footnote { 297 | margin: 15px 0; 298 | width: 100%; 299 | border: 1px solid #eee; 300 | background: #fdfdfd; 301 | font-size: 0.9em; 302 | } 303 | 304 | table.footnote + table.footnote { 305 | margin-top: -15px; 306 | border-top: none; 307 | } 308 | 309 | table.field-list th { 310 | padding: 0 0.8em 0 0; 311 | } 312 | 313 | table.field-list td { 314 | padding: 0; 315 | } 316 | 317 | table.footnote td.label { 318 | width: 0px; 319 | padding: 0.3em 0 0.3em 0.5em; 320 | } 321 | 322 | table.footnote td { 323 | padding: 0.3em 0.5em; 324 | } 325 | 326 | dl { 327 | margin: 0; 328 | padding: 0; 329 | } 330 | 331 | dl dd { 332 | margin-left: 30px; 333 | } 334 | 335 | blockquote { 336 | margin: 0 0 0 30px; 337 | padding: 0; 338 | } 339 | 340 | ul, ol { 341 | margin: 10px 0 10px 30px; 342 | padding: 0; 343 | } 344 | 345 | pre { 346 | background: #eee; 347 | padding: 7px 30px; 348 | margin: 15px -30px; 349 | line-height: 1.3em; 350 | } 351 | 352 | dl pre, blockquote pre, li pre { 353 | margin-left: -60px; 354 | padding-left: 60px; 355 | } 356 | 357 | dl dl pre { 358 | margin-left: -90px; 359 | padding-left: 90px; 360 | } 361 | 362 | tt { 363 | background-color: #ecf0f3; 364 | color: #222; 365 | /* padding: 1px 2px; */ 366 | } 367 | 368 | tt.xref, a tt { 369 | background-color: #FBFBFB; 370 | border-bottom: 1px solid white; 371 | } 372 | 373 | a.reference { 374 | text-decoration: none; 375 | border-bottom: 1px dotted #004B6B; 376 | } 377 | 378 | a.reference:hover { 379 | border-bottom: 1px solid #6D4100; 380 | } 381 | 382 | a.footnote-reference { 383 | text-decoration: none; 384 | font-size: 0.7em; 385 | vertical-align: top; 386 | border-bottom: 1px dotted #004B6B; 387 | } 388 | 389 | a.footnote-reference:hover { 390 | border-bottom: 1px solid #6D4100; 391 | } 392 | 393 | a:hover tt { 394 | background: #EEE; 395 | } 396 | -------------------------------------------------------------------------------- /docs/_themes/flask/static/small_flask.css: -------------------------------------------------------------------------------- 1 | /* 2 | * small_flask.css_t 3 | * ~~~~~~~~~~~~~~~~~ 4 | * 5 | * :copyright: Copyright 2010 by Armin Ronacher. 6 | * :license: Flask Design License, see LICENSE for details. 7 | */ 8 | 9 | body { 10 | margin: 0; 11 | padding: 20px 30px; 12 | } 13 | 14 | div.documentwrapper { 15 | float: none; 16 | background: white; 17 | } 18 | 19 | div.sphinxsidebar { 20 | display: block; 21 | float: none; 22 | width: 102.5%; 23 | margin: 50px -30px -20px -30px; 24 | padding: 10px 20px; 25 | background: #333; 26 | color: white; 27 | } 28 | 29 | div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p, 30 | div.sphinxsidebar h3 a { 31 | color: white; 32 | } 33 | 34 | div.sphinxsidebar a { 35 | color: #aaa; 36 | } 37 | 38 | div.sphinxsidebar p.logo { 39 | display: none; 40 | } 41 | 42 | div.document { 43 | width: 100%; 44 | margin: 0; 45 | } 46 | 47 | div.related { 48 | display: block; 49 | margin: 0; 50 | padding: 10px 0 20px 0; 51 | } 52 | 53 | div.related ul, 54 | div.related ul li { 55 | margin: 0; 56 | padding: 0; 57 | } 58 | 59 | div.footer { 60 | display: none; 61 | } 62 | 63 | div.bodywrapper { 64 | margin: 0; 65 | } 66 | 67 | div.body { 68 | min-height: 0; 69 | padding: 0; 70 | } 71 | -------------------------------------------------------------------------------- /docs/_themes/flask/theme.conf: -------------------------------------------------------------------------------- 1 | [theme] 2 | inherit = basic 3 | stylesheet = flasky.css 4 | pygments_style = flask_theme_support.FlaskyStyle 5 | 6 | [options] 7 | index_logo = '' 8 | index_logo_height = 120px 9 | touch_icon = 10 | -------------------------------------------------------------------------------- /docs/admin.rst: -------------------------------------------------------------------------------- 1 | .. _admin-interface: 2 | 3 | Admin Interface 4 | =============== 5 | 6 | Many web applications ship with an "admin area", where priveleged users can 7 | view and modify content. By introspecting your application's models, flask-peewee 8 | can provide you with straightforward, easily-extensible forms for managing your 9 | application content. 10 | 11 | Here's a screen-shot of the admin dashboard: 12 | 13 | .. image:: fp-admin.jpg 14 | 15 | Getting started 16 | --------------- 17 | 18 | To get started with the admin, there are just a couple steps: 19 | 20 | 1. Instantiate an :py:class:`Auth` backend for your project -- this component is responsible for providing the security for the admin area 21 | 22 | .. code-block:: python 23 | 24 | from flask import Flask 25 | 26 | from flask_peewee.auth import Auth 27 | from flask_peewee.db import Database 28 | 29 | app = Flask(__name__) 30 | db = Database(app) 31 | 32 | # needed for authentication 33 | auth = Auth(app, db) 34 | 35 | 36 | 2. Instantiate an :py:class:`Admin` object 37 | 38 | .. code-block:: python 39 | 40 | # continued from above... 41 | from flask_peewee.admin import Admin 42 | 43 | admin = Admin(app, auth) 44 | 45 | 3. Register any :py:class:`ModelAdmin` or :py:class:`AdminPanel` objects you would like to expose via the admin 46 | 47 | .. code-block:: python 48 | 49 | # continuing... assuming "Blog" and "Entry" models 50 | admin.register(Blog) # register "Blog" with vanilla ModelAdmin 51 | admin.register(Entry, EntryAdmin) # register "Entry" with a custom ModelAdmin subclass 52 | 53 | # assume we have an "AdminPanel" called "NotePanel" 54 | admin.register_panel('Notes', NotePanel) 55 | 56 | 4. Call :py:meth:`Admin.setup()`, which registers the admin blueprint and configures the urls 57 | 58 | .. code-block:: python 59 | 60 | # after all models and panels are registered, configure the urls 61 | admin.setup() 62 | 63 | 64 | .. note:: 65 | 66 | For a complete example, check the :ref:`example` which ships with the project. 67 | 68 | 69 | Customizing how models are displayed 70 | ------------------------------------ 71 | 72 | We'll use the "Message" model taken from the `example app `_, 73 | which looks like this: 74 | 75 | .. code-block:: python 76 | 77 | class Message(db.Model): 78 | user = ForeignKeyField(User) 79 | content = TextField() 80 | pub_date = DateTimeField(default=datetime.datetime.now) 81 | 82 | def __unicode__(self): 83 | return '%s: %s' % (self.user, self.content) 84 | 85 | If we were to simply register this model with the admin, it would look something 86 | like this: 87 | 88 | .. code-block:: python 89 | 90 | admin = Admin(app, auth) 91 | admin.register(Message) 92 | 93 | admin.setup() 94 | 95 | .. image:: fp-message-admin.jpg 96 | 97 | A quick way to improve the appearance of this view is to specify which columns 98 | to display. To start customizing how the ``Message`` model is displayed in the 99 | admin, we'll subclass :py:class:`ModelAdmin`. 100 | 101 | .. code-block:: python 102 | 103 | from flask_peewee.admin import ModelAdmin 104 | 105 | class MessageAdmin(ModelAdmin): 106 | columns = ('user', 'content', 'pub_date',) 107 | 108 | admin.register(Message, MessageAdmin) 109 | 110 | admin.setup() 111 | 112 | Now the admin shows all the columns and they can be clicked to sort the data: 113 | 114 | .. image:: fp-message-admin-2.jpg 115 | 116 | Suppose privacy is a big concern, and under no circumstances should a user be 117 | able to see another user's messages -- even in the admin. This can be done by overriding 118 | the :py:meth:`~ModelAdmin.get_query` method: 119 | 120 | .. code-block:: python 121 | 122 | def get_query(self): 123 | return self.model.select().where(self.model.user == g.user) 124 | 125 | Now a user will only be able to see and edit their own messages. 126 | 127 | 128 | Overriding Admin Templates 129 | ^^^^^^^^^^^^^^^^^^^^^^^^^^ 130 | 131 | Use the :py:meth:`ModelAdmin.get_template_overrides` method to override templates 132 | for an individual ``Model``: 133 | 134 | .. code-block:: python 135 | 136 | class MessageAdmin(ModelAdmin): 137 | columns = ('user', 'content', 'pub_date',) 138 | 139 | def get_template_overrides(self): 140 | # override the edit template with a custom one 141 | return {'edit': 'messages/admin/edit.html'} 142 | 143 | admin.register(Message, MessageAdmin) 144 | 145 | This instructs the admin to use a custom template for the edit page in the Message 146 | admin. That template is stored in the application's templates. It might look 147 | something like this: 148 | 149 | .. code-block:: jinja 150 | 151 | {% extends "admin/models/edit.html" %} {# override the default edit template #} 152 | 153 | {# override any blocks here #} 154 | 155 | There are five templates that can be overridden: 156 | 157 | * index 158 | * add 159 | * edit 160 | * delete 161 | * export 162 | 163 | 164 | Nicer display for Foreign Key fields 165 | ------------------------------------ 166 | 167 | If you have a model that foreign keys to another, by default the related model 168 | instances are displayed in a 277 |

278 |

279 | 280 | {% endblock %} 281 | 282 | A panel can provide as many urls and views as you like. These views will all be 283 | protected by the same authentication as other parts of the admin area. 284 | 285 | 286 | Handling File Uploads 287 | --------------------- 288 | 289 | Flask and wtforms both provide support for handling file uploads (on the server 290 | and generating form fields). Peewee, however, does not have a "file field" -- 291 | generally I store a path to a file on disk and thus use a ``CharField`` for 292 | the storage. 293 | 294 | Here's a very simple example of a "photo" model and a ``ModelAdmin`` that enables 295 | file uploads. 296 | 297 | .. code-block:: models.py 298 | 299 | # models.py 300 | import datetime 301 | import os 302 | 303 | from flask import Markup 304 | from peewee import * 305 | from werkzeug import secure_filename 306 | 307 | from app import app, db 308 | 309 | 310 | class Photo(db.Model): 311 | image = CharField() 312 | 313 | def __unicode__(self): 314 | return self.image 315 | 316 | def save_image(self, file_obj): 317 | self.image = secure_filename(file_obj.filename) 318 | full_path = os.path.join(app.config['MEDIA_ROOT'], self.image) 319 | file_obj.save(full_path) 320 | self.save() 321 | 322 | def url(self): 323 | return os.path.join(app.config['MEDIA_URL'], self.image) 324 | 325 | def thumb(self): 326 | return Markup('' % self.url()) 327 | 328 | .. code-block:: python 329 | 330 | # admin.py 331 | from flask import request 332 | from flask_peewee.admin import Admin, ModelAdmin 333 | from wtforms.fields import FileField, HiddenField 334 | from wtforms.form import Form 335 | 336 | from app import app, db 337 | from auth import auth 338 | from models import Photo 339 | 340 | 341 | admin = Admin(app, auth) 342 | 343 | 344 | class PhotoAdmin(ModelAdmin): 345 | columns = ['image', 'thumb'] 346 | 347 | def get_form(self, adding=False): 348 | class PhotoForm(Form): 349 | image = HiddenField() 350 | image_file = FileField(u'Image file') 351 | 352 | return PhotoForm 353 | 354 | def save_model(self, instance, form, adding=False): 355 | instance = super(PhotoAdmin, self).save_model(instance, form, adding) 356 | if 'image_file' in request.files: 357 | file = request.files['image_file'] 358 | instance.save_image(file) 359 | return instance 360 | 361 | admin.register(Photo, PhotoAdmin) 362 | -------------------------------------------------------------------------------- /docs/auth.rst: -------------------------------------------------------------------------------- 1 | .. _authentication: 2 | 3 | Authentication 4 | ============== 5 | 6 | The :py:class:`Authentication` class provides a means of authenticating users 7 | of the site. It is designed to work out-of-the-box with a simple ``User`` model, 8 | but can be heavily customized. 9 | 10 | The :py:class:`Auth` system is comprised of a single class which is responsible 11 | for coordinating incoming requests to your project with known users. It provides 12 | the following: 13 | 14 | * views for login and logout 15 | * model to store user data (or you can provide your own) 16 | * mechanism for identifying users across requests (uses session storage) 17 | 18 | All of these pieces can be customized, but the default out-of-box implementation 19 | aims to provide a good starting place. 20 | 21 | The auth system is also designed to work closely with the :ref:`admin-interface`. 22 | 23 | 24 | Getting started 25 | --------------- 26 | 27 | In order to provide a method for users to authenticate with your site, instantiate 28 | an :py:class:`Auth` backend for your project: 29 | 30 | .. code-block:: python 31 | 32 | from flask import Flask 33 | 34 | from flask_peewee.auth import Auth 35 | from flask_peewee.db import Database 36 | 37 | app = Flask(__name__) 38 | db = Database(app) 39 | 40 | # needed for authentication 41 | auth = Auth(app, db) 42 | 43 | .. note:: 44 | ``user`` is reserverd keyword in Postgres. Pass db_table to Auth to override db table. 45 | 46 | Marking areas of the site as login required 47 | ------------------------------------------- 48 | 49 | If you want to mark specific areas of your site as requiring auth, you can 50 | decorate views using the :py:meth:`Auth.login_required` decorator: 51 | 52 | .. code-block:: python 53 | 54 | @app.route('/private/') 55 | @auth.login_required 56 | def private_timeline(): 57 | user = auth.get_logged_in_user() 58 | 59 | # ... display the private timeline for the logged-in user 60 | 61 | If the request comes from someone who has not logged-in with the site, they are 62 | redirected to the :py:meth:`Auth.login` view, which allows the user to authenticate. 63 | After successfully logging-in, they will be redirected to the page they requested 64 | initially. 65 | 66 | 67 | Retrieving the current user 68 | --------------------------- 69 | 70 | Whenever in a `request context `_, the 71 | currently logged-in user is available by calling :py:meth:`Auth.get_logged_in_user`, 72 | which will return ``None`` if the requesting user is not logged in. 73 | 74 | The auth system also registers a pre-request hook that stores the currently logged-in 75 | user in the special flask variable ``g``. 76 | 77 | 78 | Accessing the user in the templates 79 | ----------------------------------- 80 | 81 | The auth system registers a template context processor which makes the logged-in 82 | user available in any template: 83 | 84 | .. code-block:: html 85 | 86 | {% if user %} 87 |

Hello {{ user.username }}

88 | {% else %} 89 |

Please log in

90 | {% endif %} 91 | 92 | 93 | Using a custom "User" model 94 | --------------------------- 95 | 96 | It is easy to use your own model for the ``User``, though depending on the amount 97 | of changes it may be necessary to override methods in both the :py:class:`Auth` and 98 | :py:class:`Admin` classes. 99 | 100 | Unless you want to override the default behavior of the :py:class:`Auth` class' mechanism 101 | for actually authenticating users (which you may want to do if relying on a 3rd-party 102 | for auth) -- you will want to be sure your ``User`` model implements two methods: 103 | 104 | * ``set_password(password)`` -- takes a raw password and stores an encrypted version on model 105 | * ``check_password(password)`` -- returns whether or not the supplied password matches 106 | the one stored on the model instance 107 | 108 | .. note:: 109 | The :py:class:`BaseUser` mixin provides default implementations of these two methods. 110 | 111 | Here's a simple example of extending the auth system to use a custom user model: 112 | 113 | .. code-block:: python 114 | 115 | from flask_peewee.auth import BaseUser # <-- implements set_password and check_password 116 | 117 | app = Flask(__name__) 118 | db = Database(app) 119 | 120 | # create our custom user model. note that we're mixing in BaseUser in order to 121 | # use the default auth methods it implements, "set_password" and "check_password" 122 | class User(db.Model, BaseUser): 123 | username = CharField() 124 | password = CharField() 125 | email = CharField() 126 | 127 | # ... our custom fields ... 128 | is_superuser = BooleanField() 129 | 130 | 131 | # create a modeladmin for it 132 | class UserAdmin(ModelAdmin): 133 | columns = ('username', 'email', 'is_superuser',) 134 | 135 | # Make sure the user's password is hashed, after it's been changed in 136 | # the admin interface. If we don't do this, the password will be saved 137 | # in clear text inside the database and login will be impossible. 138 | def save_model(self, instance, form, adding=False): 139 | orig_password = instance.password 140 | 141 | user = super(UserAdmin, self).save_model(instance, form, adding) 142 | 143 | if orig_password != form.password.data: 144 | user.set_password(form.password.data) 145 | user.save() 146 | 147 | return user 148 | 149 | 150 | # subclass Auth so we can return our custom classes 151 | class CustomAuth(Auth): 152 | def get_user_model(self): 153 | return User 154 | 155 | def get_model_admin(self): 156 | return UserAdmin 157 | 158 | # instantiate the auth 159 | auth = CustomAuth(app, db) 160 | 161 | 162 | Here's how you might integrate the custom auth with the admin area of your site: 163 | 164 | .. code-block:: python 165 | 166 | # subclass Admin to check for whether the user is a superuser 167 | class CustomAdmin(Admin): 168 | def check_user_permission(self, user): 169 | return user.is_superuser 170 | 171 | # instantiate the admin 172 | admin = CustomAdmin(app, auth) 173 | 174 | admin.register(User, UserAdmin) 175 | admin.setup() 176 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # flask-peewee documentation build configuration file, created by 4 | # sphinx-quickstart on Tue Sep 20 13:19:30 2011. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # 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.insert(0, os.path.abspath('.')) 20 | 21 | # -- General configuration ----------------------------------------------------- 22 | 23 | # If your documentation needs a minimal Sphinx version, state it here. 24 | #needs_sphinx = '1.0' 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be extensions 27 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 28 | extensions = [] 29 | 30 | # Add any paths that contain templates here, relative to this directory. 31 | templates_path = ['_templates'] 32 | 33 | # The suffix of source filenames. 34 | source_suffix = '.rst' 35 | 36 | # The encoding of source files. 37 | #source_encoding = 'utf-8-sig' 38 | 39 | # The master toctree document. 40 | master_doc = 'index' 41 | 42 | # General information about the project. 43 | project = u'flask-peewee' 44 | copyright = u'2011, charles leifer' 45 | 46 | # The version info for the project you're documenting, acts as replacement for 47 | # |version| and |release|, also used in various other places throughout the 48 | # built documents. 49 | # 50 | # The short X.Y version. 51 | version = '0.6.7' 52 | # The full version, including alpha/beta/rc tags. 53 | release = '0.6.7' 54 | 55 | # The language for content autogenerated by Sphinx. Refer to documentation 56 | # for a list of supported languages. 57 | #language = None 58 | 59 | # There are two options for replacing |today|: either, you set today to some 60 | # non-false value, then it is used: 61 | #today = '' 62 | # Else, today_fmt is used as the format for a strftime call. 63 | #today_fmt = '%B %d, %Y' 64 | 65 | # List of patterns, relative to source directory, that match files and 66 | # directories to ignore when looking for source files. 67 | exclude_patterns = ['_build'] 68 | 69 | # The reST default role (used for this markup: `text`) to use for all documents. 70 | #default_role = None 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. See the documentation for 93 | # a list of builtin themes. 94 | html_theme = 'flask' 95 | 96 | html_theme_options = { 97 | 'index_logo': 'peewee.jpg', 98 | } 99 | 100 | # Add any paths that contain custom themes here, relative to this directory. 101 | html_theme_path = ['_themes'] 102 | 103 | # The name for this set of Sphinx documents. If None, it defaults to 104 | # " v documentation". 105 | #html_title = None 106 | 107 | # A shorter title for the navigation bar. Default is the same as html_title. 108 | #html_short_title = None 109 | 110 | # The name of an image file (relative to this directory) to place at the top 111 | # of the sidebar. 112 | #html_logo = None 113 | 114 | # The name of an image file (within the static path) to use as favicon of the 115 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 116 | # pixels large. 117 | #html_favicon = None 118 | 119 | # Add any paths that contain custom static files (such as style sheets) here, 120 | # relative to this directory. They are copied after the builtin static files, 121 | # so a file named "default.css" will overwrite the builtin "default.css". 122 | html_static_path = ['_static'] 123 | 124 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 125 | # using the given strftime format. 126 | #html_last_updated_fmt = '%b %d, %Y' 127 | 128 | # If true, SmartyPants will be used to convert quotes and dashes to 129 | # typographically correct entities. 130 | #html_use_smartypants = True 131 | 132 | # Custom sidebar templates, maps document names to template names. 133 | #html_sidebars = {} 134 | 135 | # Additional templates that should be rendered to pages, maps page names to 136 | # template names. 137 | #html_additional_pages = {} 138 | 139 | # If false, no module index is generated. 140 | #html_domain_indices = True 141 | 142 | # If false, no index is generated. 143 | #html_use_index = True 144 | 145 | # If true, the index is split into individual pages for each letter. 146 | #html_split_index = False 147 | 148 | # If true, links to the reST sources are added to the pages. 149 | #html_show_sourcelink = True 150 | 151 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 152 | #html_show_sphinx = True 153 | 154 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 155 | #html_show_copyright = True 156 | 157 | # If true, an OpenSearch description file will be output, and all pages will 158 | # contain a tag referring to it. The value of this option must be the 159 | # base URL from which the finished HTML is served. 160 | #html_use_opensearch = '' 161 | 162 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 163 | #html_file_suffix = None 164 | 165 | # Output file base name for HTML help builder. 166 | htmlhelp_basename = 'flask-peeweedoc' 167 | 168 | 169 | # -- Options for LaTeX output -------------------------------------------------- 170 | 171 | # The paper size ('letter' or 'a4'). 172 | #latex_paper_size = 'letter' 173 | 174 | # The font size ('10pt', '11pt' or '12pt'). 175 | #latex_font_size = '10pt' 176 | 177 | # Grouping the document tree into LaTeX files. List of tuples 178 | # (source start file, target name, title, author, documentclass [howto/manual]). 179 | latex_documents = [ 180 | ('index', 'flask-peewee.tex', u'flask-peewee Documentation', 181 | u'charles leifer', 'manual'), 182 | ] 183 | 184 | # The name of an image file (relative to this directory) to place at the top of 185 | # the title page. 186 | #latex_logo = None 187 | 188 | # For "manual" documents, if this is true, then toplevel headings are parts, 189 | # not chapters. 190 | #latex_use_parts = False 191 | 192 | # If true, show page references after internal links. 193 | #latex_show_pagerefs = False 194 | 195 | # If true, show URL addresses after external links. 196 | #latex_show_urls = False 197 | 198 | # Additional stuff for the LaTeX preamble. 199 | #latex_preamble = '' 200 | 201 | # Documents to append as an appendix to all manuals. 202 | #latex_appendices = [] 203 | 204 | # If false, no module index is generated. 205 | #latex_domain_indices = True 206 | 207 | 208 | # -- Options for manual page output -------------------------------------------- 209 | 210 | # One entry per manual page. List of tuples 211 | # (source start file, name, description, authors, manual section). 212 | man_pages = [ 213 | ('index', 'flask-peewee', u'flask-peewee Documentation', 214 | [u'charles leifer'], 1) 215 | ] 216 | -------------------------------------------------------------------------------- /docs/database.rst: -------------------------------------------------------------------------------- 1 | .. _database: 2 | 3 | Database Wrapper 4 | ================ 5 | 6 | The Peewee database wrapper provides a thin layer of integration between flask 7 | apps and the peewee orm. 8 | 9 | The database wrapper is important because it ensures that a database connection 10 | is created for every incoming request, and closed upon request completion. It 11 | also provides a subclass of ``Model`` which works with the database specified 12 | in your app's configuration. 13 | 14 | Most features of ``flask-peewee`` require a database wrapper, so you very likely 15 | always create one. 16 | 17 | The database wrapper reads its configuration from the Flask application. The 18 | configuration requires only two arguments, but any additional arguments will 19 | be passed to the database driver when connecting: 20 | 21 | `name` 22 | The name of the database to connect to (or filename if using sqlite3) 23 | 24 | `engine` 25 | The database driver to use, must be a subclass of ``peewee.Database``. 26 | 27 | .. code-block:: python 28 | 29 | from flask import Flask 30 | from peewee import * 31 | 32 | from flask_peewee.db import Database 33 | 34 | DATABASE = { 35 | 'name': 'example.db', 36 | 'engine': 'peewee.SqliteDatabase', 37 | } 38 | 39 | app = Flask(__name__) 40 | app.config.from_object(__name__) # load database configuration from this module 41 | 42 | # instantiate the db wrapper 43 | db = Database(app) 44 | 45 | # start creating models 46 | class Blog(db.Model): 47 | name = CharField() 48 | # .. etc 49 | 50 | Other examples 51 | -------------- 52 | 53 | To connect to MySQL using authentication: 54 | 55 | .. code-block:: python 56 | 57 | DATABASE = { 58 | 'name': 'my_database', 59 | 'engine': 'peewee.MySQLDatabase', 60 | 'user': 'db_user', 61 | 'passwd': 'secret password', 62 | } 63 | 64 | If using a multi-threaded WSGI server: 65 | 66 | .. code-block:: python 67 | 68 | DATABASE = { 69 | 'name': 'foo.db', 70 | 'engine': 'peewee.SqliteDatabase', 71 | 'threadlocals': True, 72 | } 73 | -------------------------------------------------------------------------------- /docs/example.rst: -------------------------------------------------------------------------------- 1 | .. _example: 2 | 3 | Example App 4 | =========== 5 | -------------------------------------------------------------------------------- /docs/fp-admin-btn-form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coleifer/flask-peewee/e4c8bd947b6524bb2fd93c3598e48818acf00307/docs/fp-admin-btn-form.png -------------------------------------------------------------------------------- /docs/fp-admin-btn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coleifer/flask-peewee/e4c8bd947b6524bb2fd93c3598e48818acf00307/docs/fp-admin-btn.png -------------------------------------------------------------------------------- /docs/fp-admin-filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coleifer/flask-peewee/e4c8bd947b6524bb2fd93c3598e48818acf00307/docs/fp-admin-filter.png -------------------------------------------------------------------------------- /docs/fp-admin-modal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coleifer/flask-peewee/e4c8bd947b6524bb2fd93c3598e48818acf00307/docs/fp-admin-modal.png -------------------------------------------------------------------------------- /docs/fp-admin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coleifer/flask-peewee/e4c8bd947b6524bb2fd93c3598e48818acf00307/docs/fp-admin.jpg -------------------------------------------------------------------------------- /docs/fp-app.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coleifer/flask-peewee/e4c8bd947b6524bb2fd93c3598e48818acf00307/docs/fp-app.jpg -------------------------------------------------------------------------------- /docs/fp-getting-started.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coleifer/flask-peewee/e4c8bd947b6524bb2fd93c3598e48818acf00307/docs/fp-getting-started.jpg -------------------------------------------------------------------------------- /docs/fp-message-admin-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coleifer/flask-peewee/e4c8bd947b6524bb2fd93c3598e48818acf00307/docs/fp-message-admin-2.jpg -------------------------------------------------------------------------------- /docs/fp-message-admin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coleifer/flask-peewee/e4c8bd947b6524bb2fd93c3598e48818acf00307/docs/fp-message-admin.jpg -------------------------------------------------------------------------------- /docs/fp-model-admin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coleifer/flask-peewee/e4c8bd947b6524bb2fd93c3598e48818acf00307/docs/fp-model-admin.jpg -------------------------------------------------------------------------------- /docs/fp-note-admin-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coleifer/flask-peewee/e4c8bd947b6524bb2fd93c3598e48818acf00307/docs/fp-note-admin-2.jpg -------------------------------------------------------------------------------- /docs/fp-note-admin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coleifer/flask-peewee/e4c8bd947b6524bb2fd93c3598e48818acf00307/docs/fp-note-admin.jpg -------------------------------------------------------------------------------- /docs/fp-note-panel-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coleifer/flask-peewee/e4c8bd947b6524bb2fd93c3598e48818acf00307/docs/fp-note-panel-2.jpg -------------------------------------------------------------------------------- /docs/fp-note-panel.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coleifer/flask-peewee/e4c8bd947b6524bb2fd93c3598e48818acf00307/docs/fp-note-panel.jpg -------------------------------------------------------------------------------- /docs/fp-panels.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coleifer/flask-peewee/e4c8bd947b6524bb2fd93c3598e48818acf00307/docs/fp-panels.jpg -------------------------------------------------------------------------------- /docs/getting-started.rst: -------------------------------------------------------------------------------- 1 | .. _getting-started: 2 | 3 | Getting Started 4 | =============== 5 | 6 | The goal of this document is to help get you up and running quickly. So without 7 | further ado, let's get started. 8 | 9 | .. note:: 10 | Hopefully you have some familiarity with the `flask framework `_ and 11 | the `peewee orm `_, but if not those links 12 | should help you get started. 13 | 14 | .. note:: 15 | For a complete example project, check the `example app `_ 16 | that ships with flask-peewee. 17 | 18 | 19 | Creating a flask app 20 | -------------------- 21 | 22 | First, be sure you have :ref:`installed flask-peewee and its dependencies `. 23 | You can verify by running the test suite: ``python setup.py test``. 24 | 25 | After ensuring things are installed, open a new file called "app.py" and enter the 26 | following code: 27 | 28 | .. code-block:: python 29 | 30 | from flask import Flask 31 | 32 | app = Flask(__name__) 33 | app.config.from_object(__name__) 34 | 35 | if __name__ == '__main__': 36 | app.run() 37 | 38 | This isn't very exciting, but we can check out our project by running the app: 39 | 40 | .. code-block:: console 41 | 42 | $ python app.py 43 | * Running on http://127.0.0.1:5000/ 44 | * Restarting with reloader 45 | 46 | 47 | Navigating to the url listed will show a simple 404 page, because we haven't 48 | configured any templates or views yet. 49 | 50 | 51 | Creating a simple model 52 | ----------------------- 53 | 54 | Let's add a simple model. Before we can do that, though, it is necessary to 55 | initialize the peewee database wrapper and configure the database: 56 | 57 | .. code-block:: python 58 | 59 | from flask import Flask 60 | 61 | # flask-peewee bindings 62 | from flask_peewee.db import Database 63 | 64 | # configure our database 65 | DATABASE = { 66 | 'name': 'example.db', 67 | 'engine': 'peewee.SqliteDatabase', 68 | } 69 | DEBUG = True 70 | SECRET_KEY = 'ssshhhh' 71 | 72 | app = Flask(__name__) 73 | app.config.from_object(__name__) 74 | 75 | # instantiate the db wrapper 76 | db = Database(app) 77 | 78 | if __name__ == '__main__': 79 | app.run() 80 | 81 | 82 | What this does is provides us with request handlers which connect to the database 83 | on each request and close it when the request is finished. It also provides a 84 | base model class which is configured to work with the database specified in the 85 | configuration. 86 | 87 | Now we can create a model: 88 | 89 | .. code-block:: python 90 | 91 | import datetime 92 | from peewee import * 93 | 94 | 95 | class Note(db.Model): 96 | message = TextField() 97 | created = DateTimeField(default=datetime.datetime.now) 98 | 99 | 100 | .. note:: 101 | The model we created, ``Note``, subclasses ``db.Model``, which in turn is a subclass 102 | of ``peewee.Model`` that is pre-configured to talk to our database. 103 | 104 | 105 | Setting up a simple base template 106 | --------------------------------- 107 | 108 | We'll need a simple template to serve as the base template for our app, so create 109 | a folder named ``templates``. In the ``templates`` folder create a file ``base.html`` 110 | and add the following: 111 | 112 | .. code-block:: html 113 | 114 | 115 | 116 | Test site 117 | 118 |

{% block content_title %}{% endblock %}

119 | {% block content %}{% endblock %} 120 | 121 | 122 | 123 | 124 | Adding users to the site 125 | ------------------------ 126 | 127 | Before we can edit these ``Note`` models in the admin, we'll need to have some 128 | way of authenticating users on the site. This is where :py:class:`Auth` comes in. 129 | :py:class:`Auth` provides a ``User`` model and views for logging in and logging out, 130 | among other things, and is required by the :py:class:`Admin`. 131 | 132 | .. code-block:: python 133 | 134 | from flask_peewee.auth import Auth 135 | 136 | # create an Auth object for use with our flask app and database wrapper 137 | auth = Auth(app, db) 138 | 139 | Let's also modify the code that runs our app to ensure our tables get created 140 | if need be: 141 | 142 | .. code-block:: python 143 | 144 | if __name__ == '__main__': 145 | auth.User.create_table(fail_silently=True) 146 | Note.create_table(fail_silently=True) 147 | 148 | app.run() 149 | 150 | After cleaning up the imports and declarations, we have something like the following: 151 | 152 | .. code-block:: python 153 | 154 | import datetime 155 | from flask import Flask 156 | 157 | from peewee import * 158 | from flask_peewee.auth import Auth 159 | from flask_peewee.db import Database 160 | 161 | # configure our database 162 | DATABASE = { 163 | 'name': 'example.db', 164 | 'engine': 'peewee.SqliteDatabase', 165 | } 166 | DEBUG = True 167 | SECRET_KEY = 'ssshhhh' 168 | 169 | app = Flask(__name__) 170 | app.config.from_object(__name__) 171 | 172 | # instantiate the db wrapper 173 | db = Database(app) 174 | 175 | 176 | class Note(db.Model): 177 | message = TextField() 178 | created = DateTimeField(default=datetime.datetime.now) 179 | 180 | 181 | # create an Auth object for use with our flask app and database wrapper 182 | auth = Auth(app, db) 183 | 184 | 185 | if __name__ == '__main__': 186 | auth.User.create_table(fail_silently=True) 187 | Note.create_table(fail_silently=True) 188 | 189 | app.run() 190 | 191 | 192 | Managing content using the admin area 193 | ------------------------------------- 194 | 195 | **Now** we're ready to add the admin. Place the following lines of code after 196 | the initialization of the ``Auth`` class: 197 | 198 | .. code-block:: python 199 | 200 | from flask_peewee.admin import Admin 201 | 202 | admin = Admin(app, auth) 203 | admin.register(Note) 204 | 205 | admin.setup() 206 | 207 | 208 | We now have a functioning admin site! Of course, we'll need a user log in with, 209 | so open up an interactive python shell in the directory alongside the app and run 210 | the following: 211 | 212 | .. code-block:: python 213 | 214 | from app import auth 215 | auth.User.create_table(fail_silently=True) # make sure table created. 216 | admin = auth.User(username='admin', email='', admin=True, active=True) 217 | admin.set_password('admin') 218 | admin.save() 219 | 220 | It should now be possible to: 221 | 222 | 1. navigate to http://127.0.0.1:5000/admin/ 223 | 2. enter in the username and password ("admin", "admin") 224 | 3. be redirected to the admin dashboard 225 | 226 | .. image:: fp-getting-started.jpg 227 | 228 | The dashboard is pretty empty right now. Go ahead and add a few notes (http://127.0.0.1:5000/admin/note/). If you navigate now to the note 229 | modeladmin you will see something like this: 230 | 231 | .. image:: fp-note-admin.jpg 232 | 233 | This is pretty lousy so let's clean it up to display the message and when it was 234 | published. We can do that by customizing the columns displayed. Edit the app with 235 | the following changes: 236 | 237 | .. code-block:: python 238 | 239 | from flask_peewee.admin import Admin, ModelAdmin 240 | 241 | class NoteAdmin(ModelAdmin): 242 | columns = ('message', 'created',) 243 | 244 | admin = Admin(app, auth) 245 | 246 | admin.register(Note, NoteAdmin) 247 | 248 | admin.setup() 249 | 250 | Now our modeladmin should look more like this: 251 | 252 | .. image:: fp-note-admin-2.jpg 253 | 254 | Let's go ahead and add the ``auth.User`` model to the admin as well: 255 | 256 | .. code-block:: python 257 | 258 | admin.register(Note, NoteAdmin) 259 | auth.register_admin(admin) 260 | 261 | admin.setup() 262 | 263 | 264 | Exposing content using a REST API 265 | --------------------------------- 266 | 267 | Adding a REST API is very similar to how we added the :py:class:`Admin` interface. 268 | We will create a :py:class:`RestAPI` object, and then register our project's models 269 | with it. If we want to customize things, we can subclass :py:class:`RestResource`. 270 | 271 | The first step, then, is to create the :py:class:`RestAPI` object: 272 | 273 | .. code-block:: python 274 | 275 | from flask_peewee.rest import RestAPI 276 | 277 | # create a RestAPI container 278 | api = RestAPI(app) 279 | 280 | api.setup() 281 | 282 | This doesn't do anything yet, we need to register models with it first. Let's 283 | register the ``Note`` model from earlier: 284 | 285 | .. code-block:: python 286 | 287 | # create a RestAPI container 288 | api = RestAPI(app) 289 | 290 | # register the Note model 291 | api.register(Note) 292 | 293 | api.setup() 294 | 295 | Assuming your project is still running, try executing the following command (or 296 | just browse to the url listed): 297 | 298 | .. code-block:: console 299 | 300 | $ curl http://127.0.0.1:5000/api/note/ 301 | 302 | You should see something like the following: 303 | 304 | .. code-block:: javascript 305 | 306 | { 307 | "meta": { 308 | "model": "note", 309 | "next": "", 310 | "page": 1, 311 | "previous": "" 312 | }, 313 | "objects": [ 314 | { 315 | "message": "blah blah blah this is a note", 316 | "id": 1, 317 | "created": "2011-09-23 09:07:39" 318 | }, 319 | { 320 | "message": "this is another note!", 321 | "id": 2, 322 | "created": "2011-09-23 09:07:54" 323 | } 324 | ] 325 | } 326 | 327 | Suppose we want it to also be possible for registered users to be able to POST 328 | messages using the API. If you try and make a POST right now, you will get a 329 | ``401`` response: 330 | 331 | .. code-block:: console 332 | 333 | $ curl -i -d '' http://127.0.0.1:5000/api/note/ 334 | 335 | HTTP/1.0 401 UNAUTHORIZED 336 | WWW-Authenticate: Basic realm="Login Required" 337 | Content-Type: text/html; charset=utf-8 338 | Content-Length: 21 339 | Server: Werkzeug/0.8-dev Python/2.6.6 340 | Date: Fri, 23 Sep 2011 14:45:38 GMT 341 | 342 | Authentication failed 343 | 344 | This is because we have not configured any :py:class:`Authentication` method for 345 | our :py:class:`RestAPI`. 346 | 347 | .. note:: 348 | The default authentication mechanism for the API only accepts GET requests. 349 | In order to handle POST/PUT/DELETE you will need to use a subclass of the 350 | :py:class:`Authentication` class. 351 | 352 | In order to allow users of the site to post notes, we will use the :py:class:`UserAuthentication` 353 | subclass, which requires that API requests be made with HTTP Basic auth and that the 354 | auth credentials match those of one of the ``auth.User`` models. 355 | 356 | .. code-block:: python 357 | 358 | from flask_peewee.rest import RestAPI, UserAuthentication 359 | 360 | # instantiate the user auth 361 | user_auth = UserAuthentication(auth) 362 | 363 | # create a RestAPI container 364 | api = RestAPI(app, default_auth=user_auth) 365 | 366 | Now we can post new notes using a command-line tool like curl: 367 | 368 | .. code-block:: console 369 | 370 | $ curl -u admin:admin -d data='{"message": "hello api"}' http://127.0.0.1:5000/api/note/ 371 | 372 | { 373 | "message": "hello api", 374 | "id": 3, 375 | "created": "2011-09-23 13:14:56" 376 | } 377 | 378 | You can see that it returns a serialized copy of the new ``Note`` object. 379 | 380 | .. note:: 381 | This is just a small example of what you can do with the Rest API -- refer to 382 | the :ref:`Rest API docs ` for more detailed information, including 383 | 384 | * limiting access on a per-model basis 385 | * customizing which fields are returned by the API 386 | * filtering and querying using GET parameters 387 | -------------------------------------------------------------------------------- /docs/gevent.rst: -------------------------------------------------------------------------------- 1 | .. _gevent: 2 | 3 | Using gevent 4 | ============ 5 | 6 | If you would like to serve your flask application using gevent, there are two small 7 | settings you will need to add. 8 | 9 | Database configuration 10 | ---------------------- 11 | 12 | Instruct peewee to store connection information in a thread local: 13 | 14 | .. code-block:: python 15 | 16 | # app configuration 17 | DATABASE = { 18 | 'name': 'my_db', 19 | 'engine': 'peewee.PostgresqlDatabase', 20 | 'user': 'postgres', 21 | 'threadlocals': True, # <-- this 22 | } 23 | 24 | 25 | Monkey-patch the thread module 26 | ------------------------------ 27 | 28 | Some time before instantiating a :py:class:`Database` object (and preferrably at 29 | the very "beginning" of your code) you will want to `monkey-patch `_ 30 | the standard library thread module: 31 | 32 | .. code-block:: python 33 | 34 | from gevent import monkey; monkey.patch_thread() 35 | 36 | If you want to patch everything (recommended): 37 | 38 | .. code-block:: python 39 | 40 | from gevent import monkey; monkey.patch_all() 41 | 42 | .. note:: Remember to monkey-patch before initializing your app 43 | 44 | 45 | Rationale 46 | --------- 47 | 48 | flask-peewee opens a connection-per-request. Flask stores things, like "per-request" 49 | information, in a special object called a `context local `_. 50 | Flask will ensure that this works even in a greened environment. Peewee does not 51 | automatically work in a "greened" environment, and stores connection state on the 52 | database instance in a local. Peewee can use a thread local instead, which ensures 53 | connections are not shared across threads. When using peewee with gevent, it is 54 | necessary to make this "threadlocal" a "greenlet local" by monkeypatching the thread module. 55 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. flask-peewee documentation master file, created by 2 | sphinx-quickstart on Tue Sep 20 13:19:30 2011. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | flask-peewee 7 | ============ 8 | 9 | .. warning:: 10 | 11 | This package is in maintenance-only mode! 12 | ----------------------------------------- 13 | 14 | I'm sorry to announce that flask-peewee will now be in maintenance-only mode. This decision is motivated by a number of factors: 15 | 16 | * `Flask-Admin `_ provides a superior admin interface and has support for peewee models. 17 | * `Flask-Security `_ and `Flask-Login `_ both provide authentication functionality, and work well with Peewee. 18 | * Most importantly, though, I do not find myself wanting to work on flask-peewee. 19 | 20 | I plan on rewriting the ``Database`` and ``REST API`` portions of flask-peewee and repackaging them as a new library, but flask-peewee as it stands currently will be in maintenance-only mode. 21 | 22 | ------------------------------ 23 | 24 | Welcome to the flask-peewee documentation! 25 | 26 | provides a layer of integration between the `flask `_ 27 | web framework and the `peewee orm `_. 28 | 29 | Contents: 30 | 31 | .. toctree:: 32 | :maxdepth: 2 33 | :glob: 34 | 35 | installation 36 | getting-started 37 | database 38 | admin 39 | auth 40 | rest-api 41 | utils 42 | gevent 43 | 44 | API in depth: 45 | 46 | .. toctree:: 47 | :maxdepth: 2 48 | :glob: 49 | 50 | api/ 51 | 52 | Indices and tables 53 | ================== 54 | 55 | * :ref:`genindex` 56 | * :ref:`modindex` 57 | * :ref:`search` 58 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | .. _installation: 2 | 3 | Installing 4 | ========== 5 | 6 | flask-peewee can be installed very easily using `pip `_. 7 | 8 | .. code-block:: shell 9 | 10 | pip install flask-peewee 11 | 12 | If you do not have the dependencies installed already, pip will install them 13 | for you, but for reference they are: 14 | 15 | * `flask `_ 16 | * `peewee `_ 17 | * `wtforms `_ 18 | * `wtf-peewee `_ 19 | * python 2.5 or greater 20 | 21 | 22 | Using git 23 | --------- 24 | 25 | If you want to run the very latest, feel free to pull down the repo from github 26 | and install by hand. 27 | 28 | .. code-block:: shell 29 | 30 | git clone https://github.com/coleifer/flask-peewee.git 31 | cd flask-peewee 32 | python setup.py install 33 | 34 | You can run the tests using the test-runner:: 35 | 36 | python setup.py test 37 | -------------------------------------------------------------------------------- /docs/utils.rst: -------------------------------------------------------------------------------- 1 | .. _utils: 2 | 3 | Utilities 4 | ========= 5 | 6 | flask-peewee ships with several useful utilities. If you're coming from the 7 | django world, some of these functions may look familiar to you. 8 | 9 | 10 | Getting objects 11 | --------------- 12 | 13 | :py:func:`get_object_or_404` 14 | 15 | Provides a handy way of getting an object or 404ing if not found, useful 16 | for urls that match based on ID. 17 | 18 | .. code-block:: python 19 | 20 | @app.route('/blog//') 21 | def blog_detail(title): 22 | blog = get_object_or_404(Blog.select().where(Blog.active==True), Blog.title==title) 23 | return render_template('blog/detail.html', blog=blog) 24 | 25 | :py:func:`object_list` 26 | 27 | Wraps the given query and handles pagination automatically. Pagination defaults to ``20`` 28 | but can be changed by passing in ``paginate_by=XX``. 29 | 30 | .. code-block:: python 31 | 32 | @app.route('/blog/') 33 | def blog_list(): 34 | active = Blog.select().where(Blog.active==True) 35 | return object_list('blog/index.html', active) 36 | 37 | .. code-block:: html 38 | 39 | <!-- template --> 40 | {% for blog in object_list %} 41 | {# render the blog here #} 42 | {% endfor %} 43 | 44 | {% if page > 1 %} 45 | <a href="./?page={{ page - 1 }}">Prev</a> 46 | {% endif %} 47 | {% if page < pagination.get_pages() %} 48 | <a href="./?page={{ page + 1 }}">Next</a> 49 | {% endif %} 50 | 51 | :py:class:`PaginatedQuery` 52 | 53 | A wrapper around a query (or model class) that handles pagination. 54 | 55 | Example: 56 | 57 | .. code-block:: python 58 | 59 | query = Blog.select().where(Blog.active==True) 60 | pq = PaginatedQuery(query) 61 | 62 | # assume url was /?page=3 63 | obj_list = pq.get_list() # returns 3rd page of results 64 | 65 | pq.get_page() # returns "3" 66 | 67 | pq.get_pages() # returns total objects / objects-per-page 68 | 69 | 70 | Misc 71 | ---- 72 | 73 | 74 | .. py:function:: slugify(string) 75 | 76 | Convert a string into something suitable for use as part of a URL, 77 | e.g. "This is a url" becomes "this-is-a-url" 78 | 79 | .. code-block:: python 80 | 81 | from flask_peewee.utils import slugify 82 | 83 | 84 | class Blog(db.Model): 85 | title = CharField() 86 | slug = CharField() 87 | 88 | def save(self, *args, **kwargs): 89 | self.slug = slugify(self.title) 90 | super(Blog, self).save(*args, **kwargs) 91 | 92 | .. py:function:: make_password(raw_password) 93 | 94 | Create a salted hash for the given plain-text password 95 | 96 | .. py:function:: check_password(raw_password, enc_password) 97 | 98 | Compare a plain-text password against a salted/hashed password 99 | -------------------------------------------------------------------------------- /example/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coleifer/flask-peewee/e4c8bd947b6524bb2fd93c3598e48818acf00307/example/__init__.py -------------------------------------------------------------------------------- /example/admin.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from flask import request, redirect 3 | 4 | from flask_peewee.admin import Admin, ModelAdmin, AdminPanel 5 | from flask_peewee.filters import QueryFilter 6 | 7 | from app import app, db 8 | from auth import auth 9 | from models import User, Message, Note, Relationship 10 | 11 | 12 | class NotePanel(AdminPanel): 13 | template_name = 'admin/notes.html' 14 | 15 | def get_urls(self): 16 | return ( 17 | ('/create/', self.create), 18 | ) 19 | 20 | def create(self): 21 | if request.method == 'POST': 22 | if request.form.get('message'): 23 | Note.create( 24 | user=auth.get_logged_in_user(), 25 | message=request.form['message'], 26 | ) 27 | next = request.form.get('next') or self.dashboard_url() 28 | return redirect(next) 29 | 30 | def get_context(self): 31 | return { 32 | 'note_list': Note.select().order_by(Note.created_date.desc()).paginate(1, 3) 33 | } 34 | 35 | class UserStatsPanel(AdminPanel): 36 | template_name = 'admin/user_stats.html' 37 | 38 | def get_context(self): 39 | last_week = datetime.datetime.now() - datetime.timedelta(days=7) 40 | signups_this_week = User.select().where(User.join_date > last_week).count() 41 | messages_this_week = Message.select().where(Message.pub_date > last_week).count() 42 | return { 43 | 'signups': signups_this_week, 44 | 'messages': messages_this_week, 45 | } 46 | 47 | 48 | admin = Admin(app, auth, branding='Example Site') 49 | 50 | 51 | class MessageAdmin(ModelAdmin): 52 | columns = ('user', 'content', 'pub_date',) 53 | foreign_key_lookups = {'user': 'username'} 54 | filter_fields = ('user', 'content', 'pub_date', 'user__username') 55 | 56 | class NoteAdmin(ModelAdmin): 57 | columns = ('user', 'message', 'created_date',) 58 | exclude = ('created_date',) 59 | 60 | 61 | auth.register_admin(admin) 62 | admin.register(Relationship) 63 | admin.register(Message, MessageAdmin) 64 | admin.register(Note, NoteAdmin) 65 | admin.register_panel('Notes', NotePanel) 66 | admin.register_panel('User stats', UserStatsPanel) 67 | -------------------------------------------------------------------------------- /example/api.py: -------------------------------------------------------------------------------- 1 | from flask_peewee.rest import RestAPI, RestResource, UserAuthentication, AdminAuthentication, RestrictOwnerResource 2 | 3 | from app import app 4 | from auth import auth 5 | from models import User, Message, Note, Relationship 6 | 7 | 8 | user_auth = UserAuthentication(auth) 9 | admin_auth = AdminAuthentication(auth) 10 | 11 | # instantiate our api wrapper 12 | api = RestAPI(app, default_auth=user_auth) 13 | 14 | 15 | class UserResource(RestResource): 16 | exclude = ('password', 'email',) 17 | 18 | 19 | class MessageResource(RestrictOwnerResource): 20 | owner_field = 'user' 21 | include_resources = {'user': UserResource} 22 | 23 | 24 | class RelationshipResource(RestrictOwnerResource): 25 | owner_field = 'from_user' 26 | include_resources = { 27 | 'from_user': UserResource, 28 | 'to_user': UserResource, 29 | } 30 | paginate_by = None 31 | 32 | 33 | class NoteResource(RestrictOwnerResource): 34 | owner_field = 'user' 35 | include_resources = { 36 | 'user': UserResource, 37 | } 38 | 39 | def get_query(self): 40 | query = super(NoteResource, self).get_query() 41 | return query.where(Note.status == 1) 42 | 43 | 44 | # register our models so they are exposed via /api/<model>/ 45 | api.register(User, UserResource, auth=admin_auth) 46 | api.register(Relationship, RelationshipResource) 47 | api.register(Message, MessageResource) 48 | api.register(Note, NoteResource) 49 | -------------------------------------------------------------------------------- /example/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | 3 | # flask-peewee bindings 4 | from flask_peewee.db import Database 5 | 6 | 7 | app = Flask(__name__) 8 | app.config.from_object('config.Configuration') 9 | 10 | db = Database(app) 11 | 12 | 13 | def create_tables(): 14 | User.create_table() 15 | Relationship.create_table() 16 | Message.create_table() 17 | Note.create_table() 18 | 19 | 20 | @app.template_filter('is_following') 21 | def is_following(from_user, to_user): 22 | return from_user.is_following(to_user) 23 | -------------------------------------------------------------------------------- /example/auth.py: -------------------------------------------------------------------------------- 1 | from flask_peewee.auth import Auth 2 | 3 | from app import app, db 4 | from models import User 5 | 6 | 7 | auth = Auth(app, db, user_model=User) 8 | -------------------------------------------------------------------------------- /example/config.py: -------------------------------------------------------------------------------- 1 | # config 2 | 3 | class Configuration(object): 4 | DATABASE = { 5 | 'name': 'example.db', 6 | 'engine': 'peewee.SqliteDatabase', 7 | 'check_same_thread': False, 8 | } 9 | DEBUG = True 10 | SECRET_KEY = 'shhhh' 11 | -------------------------------------------------------------------------------- /example/example.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coleifer/flask-peewee/e4c8bd947b6524bb2fd93c3598e48818acf00307/example/example.db -------------------------------------------------------------------------------- /example/main.py: -------------------------------------------------------------------------------- 1 | from app import app, db 2 | 3 | from auth import * 4 | from admin import admin 5 | from api import api 6 | from models import * 7 | from views import * 8 | 9 | admin.setup() 10 | api.setup() 11 | 12 | 13 | if __name__ == '__main__': 14 | app.run() 15 | -------------------------------------------------------------------------------- /example/models.py: -------------------------------------------------------------------------------- 1 | from hashlib import md5 2 | import datetime 3 | 4 | from flask_peewee.auth import BaseUser 5 | from peewee import * 6 | 7 | from app import db 8 | 9 | 10 | class User(db.Model, BaseUser): 11 | username = CharField() 12 | password = CharField() 13 | email = CharField() 14 | join_date = DateTimeField(default=datetime.datetime.now) 15 | active = BooleanField(default=True) 16 | admin = BooleanField(default=False) 17 | 18 | def __str__(self): 19 | return self.username 20 | 21 | def following(self): 22 | return User.select().join( 23 | Relationship, on=Relationship.to_user 24 | ).where(Relationship.from_user==self).order_by(User.username) 25 | 26 | def followers(self): 27 | return User.select().join( 28 | Relationship, on=Relationship.from_user 29 | ).where(Relationship.to_user==self).order_by(User.username) 30 | 31 | def is_following(self, user): 32 | return Relationship.select().where( 33 | Relationship.from_user==self, 34 | Relationship.to_user==user 35 | ).exists() 36 | 37 | def gravatar_url(self, size=80): 38 | return 'http://www.gravatar.com/avatar/%s?d=identicon&s=%d' % \ 39 | (md5(self.email.strip().lower().encode('utf-8')).hexdigest(), size) 40 | 41 | 42 | class Relationship(db.Model): 43 | from_user = ForeignKeyField(User, related_name='relationships') 44 | to_user = ForeignKeyField(User, related_name='related_to') 45 | 46 | def __str__(self): 47 | return 'Relationship from %s to %s' % (self.from_user, self.to_user) 48 | 49 | 50 | class Message(db.Model): 51 | user = ForeignKeyField(User) 52 | content = TextField() 53 | pub_date = DateTimeField(default=datetime.datetime.now) 54 | 55 | def __str__(self): 56 | return '%s: %s' % (self.user, self.content) 57 | 58 | 59 | class Note(db.Model): 60 | user = ForeignKeyField(User) 61 | message = TextField() 62 | status = IntegerField(choices=((1, 'live'), (2, 'deleted')), null=True) 63 | created_date = DateTimeField(default=datetime.datetime.now) 64 | -------------------------------------------------------------------------------- /example/requirements.txt: -------------------------------------------------------------------------------- 1 | flask 2 | werkzeug 3 | jinja2 4 | peewee 5 | wtforms 6 | wtf-peewee 7 | -------------------------------------------------------------------------------- /example/run_example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | sys.path.insert(0, '..') 5 | 6 | import main 7 | main.app.run() 8 | -------------------------------------------------------------------------------- /example/static/style.css: -------------------------------------------------------------------------------- 1 | body { font-family: sans-serif; background: #eee; } 2 | a, h1, h2 { color: #377BA8; } 3 | h1, h2 { font-family: 'Georgia', serif; margin: 0; } 4 | h1 { border-bottom: 2px solid #eee; } 5 | h2 { font-size: 1.2em; } 6 | 7 | .page { margin: 2em auto; width: 35em; border: 5px solid #ccc; 8 | padding: 0.8em; background: white; } 9 | .page ul { list-style-type: none; } 10 | .page li { clear: both; } 11 | .metanav { text-align: right; font-size: 0.8em; padding: 0.3em; 12 | margin-bottom: 1em; background: #fafafa; } 13 | .flash { background: #CEE5F5; padding: 0.5em; 14 | border: 1px solid #AACBE2; } 15 | .avatar { display: block; float: left; margin: 0 10px 0 0; } 16 | .message-content { min-height: 80px; } 17 | .message-edit { float: right; font-size: 0.6em; } 18 | -------------------------------------------------------------------------------- /example/templates/admin/notes.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/panels/default.html" %} 2 | 3 | {% block panel_content %} 4 | {% for note in note_list %} 5 | <p>{{ note.user.username }}: {{ note.message }}</p> 6 | {% endfor %} 7 | <form method="post" action="{{ url_for(panel.get_url_name('create')) }}"> 8 | <input type="hidden" value="{{ request.url }}" /> 9 | <p><textarea name="message"></textarea></p> 10 | <p><button type="submit" class="btn small">Save</button></p> 11 | </form> 12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /example/templates/admin/user_stats.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/panels/default.html" %} 2 | 3 | {% block panel_content %} 4 | <p>New users this week: {{ signups }}</p> 5 | <p>Messages this week: {{ messages }}</p> 6 | {% endblock %} 7 | -------------------------------------------------------------------------------- /example/templates/base.html: -------------------------------------------------------------------------------- 1 | <!doctype html> 2 | <title>Tweepee 3 | 4 |
5 |

Tweepee

6 |
7 | {% if not session.logged_in %} 8 | log in 9 | join 10 | {% else %} 11 | public timeline 12 | create 13 | log out 14 | {% endif %} 15 |
16 | {% for message in get_flashed_messages() %} 17 |
{{ message }}
18 | {% endfor %} 19 |

{% block content_title %}{% endblock %}

20 | {% block content %}{% endblock %} 21 |
22 | -------------------------------------------------------------------------------- /example/templates/create.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content_title %}Create{% endblock %} 4 | 5 | {% block content %} 6 |
7 |
8 |
Message:
9 |
10 |
11 |
12 |
13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /example/templates/edit.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content_title %}Edit message{% endblock %} 4 | 5 | {% block content %} 6 |
7 |
8 |
Message:
9 |
10 |
11 |
12 |
13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /example/templates/homepage.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content_title %}Home{% endblock %} 4 | 5 | {% block content %} 6 |

Welcome to the site!

7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /example/templates/includes/message.html: -------------------------------------------------------------------------------- 1 | {% if user and message.user == user %}edit{% endif %} 2 | 3 |

{{ message.content|urlize }}

4 | -------------------------------------------------------------------------------- /example/templates/includes/pagination.html: -------------------------------------------------------------------------------- 1 | {% if page > 1 %} 2 | 3 | {% endif %} 4 | {% if pagination.get_pages() > page %} 5 | 6 | {% endif %} 7 | -------------------------------------------------------------------------------- /example/templates/join.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content_title %}Join{% endblock %} 4 | 5 | {% block content %} 6 |
7 |
8 |
Username:
9 |
10 |
Password:
11 |
12 |
Email:
13 |
14 |

(used for gravatar)

15 |
16 |
17 |
18 |
19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /example/templates/private_messages.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content_title %}Private Timeline{% endblock %} 4 | 5 | {% block content %} 6 |
    7 | {% for message in message_list %} 8 |
  • {% include "includes/message.html" %}
  • 9 | {% endfor %} 10 |
11 | {% include "includes/pagination.html" %} 12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /example/templates/public_messages.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content_title %}Public Timeline{% endblock %} 4 | 5 | {% block content %} 6 |
    7 | {% for message in message_list %} 8 |
  • {% include "includes/message.html" %}
  • 9 | {% endfor %} 10 |
11 | {% include "includes/pagination.html" %} 12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /example/templates/user_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content_title %}Message from {{ person.username }}{% endblock %} 4 | 5 | {% block content %} 6 | {% if user %} 7 | {% if person.username != user.username %} 8 | {% if user|is_following(person) %} 9 |
10 | 11 |
12 | {% else %} 13 |
14 | 15 |
16 | {% endif %} 17 | {% endif %} 18 | {% else %} 19 |

Log-in to follow

20 | {% endif %} 21 |
    22 | {% for message in message_list %} 23 |
  • {% include "includes/message.html" %}
  • 24 | {% endfor %} 25 |
26 | {% include "includes/pagination.html" %} 27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /example/templates/user_followers.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content_title %}Followers{% endblock %} 4 | 5 | {% block content %} 6 | 11 | {% include "includes/pagination.html" %} 12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /example/templates/user_following.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content_title %}Following{% endblock %} 4 | 5 | {% block content %} 6 | 11 | {% include "includes/pagination.html" %} 12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /example/templates/user_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content_title %}Users{% endblock %} 4 | 5 | {% block content %} 6 | 11 | {% include "includes/pagination.html" %} 12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /example/views.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from flask import request, redirect, url_for, render_template, flash 4 | 5 | from flask_peewee.utils import get_object_or_404, object_list 6 | 7 | from app import app 8 | from auth import auth 9 | from models import User, Message, Relationship 10 | 11 | 12 | @app.route('/') 13 | def homepage(): 14 | if auth.get_logged_in_user(): 15 | return private_timeline() 16 | else: 17 | return public_timeline() 18 | 19 | @app.route('/private/') 20 | @auth.login_required 21 | def private_timeline(): 22 | user = auth.get_logged_in_user() 23 | 24 | messages = Message.select().where( 25 | Message.user << user.following() 26 | ).order_by(Message.pub_date.desc()) 27 | 28 | return object_list('private_messages.html', messages, 'message_list') 29 | 30 | @app.route('/public/') 31 | def public_timeline(): 32 | messages = Message.select().order_by(Message.pub_date.desc()) 33 | return object_list('public_messages.html', messages, 'message_list') 34 | 35 | @app.route('/join/', methods=['GET', 'POST']) 36 | def join(): 37 | if request.method == 'POST' and request.form['username']: 38 | try: 39 | user = User.select().where(User.username==request.form['username']).get() 40 | flash('That username is already taken') 41 | except User.DoesNotExist: 42 | user = User( 43 | username=request.form['username'], 44 | email=request.form['email'], 45 | join_date=datetime.datetime.now() 46 | ) 47 | user.set_password(request.form['password']) 48 | user.save() 49 | 50 | auth.login_user(user) 51 | return redirect(url_for('homepage')) 52 | 53 | return render_template('join.html') 54 | 55 | @app.route('/following/') 56 | @auth.login_required 57 | def following(): 58 | user = auth.get_logged_in_user() 59 | return object_list('user_following.html', user.following(), 'user_list') 60 | 61 | @app.route('/followers/') 62 | @auth.login_required 63 | def followers(): 64 | user = auth.get_logged_in_user() 65 | return object_list('user_followers.html', user.followers(), 'user_list') 66 | 67 | @app.route('/users/') 68 | def user_list(): 69 | users = User.select().order_by(User.username) 70 | return object_list('user_list.html', users, 'user_list') 71 | 72 | @app.route('/users//') 73 | def user_detail(username): 74 | user = get_object_or_404(User, User.username==username) 75 | messages = user.message_set.order_by(Message.pub_date.desc()) 76 | return object_list('user_detail.html', messages, 'message_list', person=user) 77 | 78 | @app.route('/users//follow/', methods=['POST']) 79 | @auth.login_required 80 | def user_follow(username): 81 | user = get_object_or_404(User, User.username==username) 82 | Relationship.get_or_create( 83 | from_user=auth.get_logged_in_user(), 84 | to_user=user, 85 | ) 86 | flash('You are now following %s' % user.username) 87 | return redirect(url_for('user_detail', username=user.username)) 88 | 89 | @app.route('/users//unfollow/', methods=['POST']) 90 | @auth.login_required 91 | def user_unfollow(username): 92 | user = get_object_or_404(User, User.username==username) 93 | Relationship.delete().where( 94 | Relationship.from_user==auth.get_logged_in_user(), 95 | Relationship.to_user==user, 96 | ).execute() 97 | flash('You are no longer following %s' % user.username) 98 | return redirect(url_for('user_detail', username=user.username)) 99 | 100 | @app.route('/create/', methods=['GET', 'POST']) 101 | @auth.login_required 102 | def create(): 103 | user = auth.get_logged_in_user() 104 | if request.method == 'POST' and request.form['content']: 105 | message = Message.create( 106 | user=user, 107 | content=request.form['content'], 108 | ) 109 | flash('Your message has been created') 110 | return redirect(url_for('user_detail', username=user.username)) 111 | 112 | return render_template('create.html') 113 | 114 | @app.route('/edit//', methods=['GET', 'POST']) 115 | @auth.login_required 116 | def edit(message_id): 117 | user = auth.get_logged_in_user() 118 | message = get_object_or_404(Message, Message.user==user, Message.id==message_id) 119 | if request.method == 'POST' and request.form['content']: 120 | message.content = request.form['content'] 121 | message.save() 122 | flash('Your changes were saved') 123 | return redirect(url_for('user_detail', username=user.username)) 124 | 125 | return render_template('edit.html', message=message) 126 | -------------------------------------------------------------------------------- /flask_peewee/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '3.0.5' 2 | -------------------------------------------------------------------------------- /flask_peewee/_compat.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | 4 | PY2 = sys.version_info[0] == 2 5 | 6 | if PY2: 7 | text_type = unicode 8 | string_types = (str, unicode) 9 | unichr = unichr 10 | reduce = reduce 11 | else: 12 | text_type = str 13 | string_types = (str,) 14 | unichr = chr 15 | from functools import reduce 16 | -------------------------------------------------------------------------------- /flask_peewee/_wtforms_compat.py: -------------------------------------------------------------------------------- 1 | # Helpers to work around wtforms 2/3 differences. 2 | try: 3 | from wtforms.validators import DataRequired 4 | except ImportError: 5 | from wtforms.validators import Required as DataRequired 6 | -------------------------------------------------------------------------------- /flask_peewee/auth.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import os 3 | 4 | from flask import Blueprint 5 | from flask import abort 6 | from flask import flash 7 | from flask import g 8 | from flask import redirect 9 | from flask import render_template 10 | from flask import request 11 | from flask import session 12 | from flask import url_for 13 | from peewee import * 14 | from wtforms import Form 15 | from wtforms import PasswordField 16 | from wtforms.fields import StringField 17 | 18 | from flask_peewee.utils import check_password 19 | from flask_peewee.utils import get_next 20 | from flask_peewee.utils import make_password 21 | from flask_peewee._wtforms_compat import DataRequired 22 | 23 | 24 | current_dir = os.path.dirname(__file__) 25 | 26 | 27 | 28 | class LoginForm(Form): 29 | username = StringField('Username', validators=[DataRequired()]) 30 | password = PasswordField('Password', validators=[DataRequired()]) 31 | 32 | 33 | class BaseUser(object): 34 | def set_password(self, password): 35 | self.password = make_password(password) 36 | 37 | def check_password(self, password): 38 | return check_password(password, self.password) 39 | 40 | 41 | class Auth(object): 42 | def __init__(self, app, db, user_model=None, prefix='/accounts', name='auth', 43 | clear_session=False, default_next_url='/', db_table='user'): 44 | self.app = app 45 | self.db = db 46 | 47 | self.db_table = db_table 48 | self.User = user_model or self.get_user_model() 49 | 50 | self.blueprint = self.get_blueprint(name) 51 | self.url_prefix = prefix 52 | 53 | self.clear_session = clear_session 54 | self.default_next_url = default_next_url 55 | 56 | self.setup() 57 | 58 | def get_context_user(self): 59 | return {'user': self.get_logged_in_user()} 60 | 61 | def get_user_model(self): 62 | class User(self.db.Model, BaseUser): 63 | username = CharField(unique=True) 64 | password = CharField() 65 | email = CharField(unique=True) 66 | active = BooleanField() 67 | admin = BooleanField(default=False) 68 | 69 | def __unicode__(self): 70 | return self.username 71 | 72 | class Meta: 73 | table_name = self.db_table 74 | 75 | return User 76 | 77 | def get_model_admin(self, model_admin=None): 78 | if model_admin is None: 79 | from flask_peewee.admin import ModelAdmin 80 | model_admin = ModelAdmin 81 | 82 | class UserAdmin(model_admin): 83 | columns = getattr(model_admin, 'columns') or ( 84 | ['username', 'email', 'active', 'admin']) 85 | 86 | def save_model(self, instance, form, adding=False): 87 | orig_password = instance.password 88 | 89 | user = super(UserAdmin, self).save_model(instance, form, adding) 90 | 91 | if orig_password != form.password.data: 92 | user.set_password(form.password.data) 93 | user.save() 94 | 95 | return user 96 | 97 | 98 | return UserAdmin 99 | 100 | def register_admin(self, admin_site, model_admin=None): 101 | admin_site.register(self.User, self.get_model_admin(model_admin)) 102 | 103 | def get_blueprint(self, blueprint_name): 104 | return Blueprint( 105 | blueprint_name, 106 | __name__, 107 | static_folder=os.path.join(current_dir, 'static'), 108 | template_folder=os.path.join(current_dir, 'templates'), 109 | ) 110 | 111 | def get_urls(self): 112 | return ( 113 | ('/logout/', self.logout), 114 | ('/login/', self.login), 115 | ) 116 | 117 | def get_login_form(self): 118 | return LoginForm 119 | 120 | def test_user(self, test_fn): 121 | def decorator(fn): 122 | @functools.wraps(fn) 123 | def inner(*args, **kwargs): 124 | user = self.get_logged_in_user() 125 | 126 | if not user or not test_fn(user): 127 | login_url = url_for('%s.login' % self.blueprint.name, next=get_next()) 128 | return redirect(login_url) 129 | return fn(*args, **kwargs) 130 | return inner 131 | return decorator 132 | 133 | def login_required(self, func): 134 | return self.test_user(lambda u: True)(func) 135 | 136 | def admin_required(self, func): 137 | return self.test_user(lambda u: u.admin)(func) 138 | 139 | def authenticate(self, username, password): 140 | active = self.User.select().where(self.User.active==True) 141 | try: 142 | user = active.where(self.User.username==username).get() 143 | except self.User.DoesNotExist: 144 | return False 145 | else: 146 | if not user.check_password(password): 147 | return False 148 | 149 | return user 150 | 151 | def login_user(self, user): 152 | session['logged_in'] = True 153 | session['user_pk'] = user._pk 154 | session.permanent = True 155 | g.user = user 156 | flash('You are logged in as %s' % user, 'success') 157 | 158 | def logout_user(self): 159 | if self.clear_session: 160 | session.clear() 161 | else: 162 | session.pop('logged_in', None) 163 | g.user = None 164 | flash('You are now logged out', 'success') 165 | 166 | def get_logged_in_user(self): 167 | if session.get('logged_in'): 168 | if getattr(g, 'user', None): 169 | return g.user 170 | 171 | try: 172 | return self.User.select().where( 173 | self.User.active==True, 174 | self.User.id==session.get('user_pk') 175 | ).get() 176 | except self.User.DoesNotExist: 177 | pass 178 | 179 | def login(self): 180 | error = None 181 | Form = self.get_login_form() 182 | 183 | if request.method == 'POST': 184 | form = Form(request.form) 185 | next_url = request.form.get('next') or self.default_next_url 186 | if form.validate(): 187 | authenticated_user = self.authenticate( 188 | form.username.data, 189 | form.password.data, 190 | ) 191 | if authenticated_user: 192 | self.login_user(authenticated_user) 193 | return redirect(next_url) 194 | else: 195 | flash('Incorrect username or password') 196 | else: 197 | form = Form() 198 | next_url = request.args.get('next') 199 | 200 | return render_template( 201 | 'auth/login.html', 202 | error=error, 203 | form=form, 204 | login_url=url_for('%s.login' % self.blueprint.name), 205 | next=next_url) 206 | 207 | def logout(self): 208 | self.logout_user() 209 | return redirect(request.args.get('next') or self.default_next_url) 210 | 211 | def configure_routes(self): 212 | for url, callback in self.get_urls(): 213 | self.blueprint.route(url, methods=['GET', 'POST'])(callback) 214 | 215 | def register_blueprint(self, **kwargs): 216 | self.app.register_blueprint(self.blueprint, url_prefix=self.url_prefix, **kwargs) 217 | 218 | def load_user(self): 219 | g.user = self.get_logged_in_user() 220 | 221 | def register_handlers(self): 222 | self.app.before_request_funcs.setdefault(None, []) 223 | self.app.before_request_funcs[None].append(self.load_user) 224 | 225 | def register_context_processors(self): 226 | self.app.template_context_processors[None].append(self.get_context_user) 227 | 228 | def setup(self): 229 | self.configure_routes() 230 | self.register_blueprint() 231 | self.register_handlers() 232 | self.register_context_processors() 233 | -------------------------------------------------------------------------------- /flask_peewee/db.py: -------------------------------------------------------------------------------- 1 | import peewee 2 | from peewee import * 3 | 4 | from flask_peewee.exceptions import ImproperlyConfigured 5 | from flask_peewee.utils import load_class 6 | 7 | 8 | class Database(object): 9 | def __init__(self, app, database=None): 10 | self.app = app 11 | self.database = database 12 | 13 | if self.database is None: 14 | self.load_database() 15 | 16 | self.register_handlers() 17 | 18 | self.Model = self.get_model_class() 19 | 20 | def load_database(self): 21 | self.database_config = dict(self.app.config['DATABASE']) 22 | try: 23 | self.database_name = self.database_config.pop('name') 24 | self.database_engine = self.database_config.pop('engine') 25 | except KeyError: 26 | raise ImproperlyConfigured('Please specify a "name" and "engine" for your database') 27 | 28 | try: 29 | self.database_class = load_class(self.database_engine) 30 | assert issubclass(self.database_class, peewee.Database) 31 | except ImportError: 32 | raise ImproperlyConfigured('Unable to import: "%s"' % self.database_engine) 33 | except AttributeError: 34 | raise ImproperlyConfigured('Database engine not found: "%s"' % self.database_engine) 35 | except AssertionError: 36 | raise ImproperlyConfigured('Database engine not a subclass of peewee.Database: "%s"' % self.database_engine) 37 | 38 | self.database = self.database_class(self.database_name, **self.database_config) 39 | 40 | def get_model_class(self): 41 | class BaseModel(Model): 42 | class Meta: 43 | database = self.database 44 | 45 | return BaseModel 46 | 47 | def connect_db(self): 48 | if self.database.is_closed(): 49 | self.database.connect() 50 | 51 | def close_db(self, exc): 52 | if not self.database.is_closed(): 53 | self.database.close() 54 | 55 | def register_handlers(self): 56 | self.app.before_request(self.connect_db) 57 | self.app.teardown_request(self.close_db) 58 | -------------------------------------------------------------------------------- /flask_peewee/exceptions.py: -------------------------------------------------------------------------------- 1 | class ImproperlyConfigured(Exception): 2 | pass 3 | -------------------------------------------------------------------------------- /flask_peewee/forms.py: -------------------------------------------------------------------------------- 1 | from peewee import BooleanField 2 | 3 | from wtforms import widgets 4 | from wtfpeewee.fields import BooleanSelectField 5 | from wtfpeewee.fields import ModelSelectField 6 | from wtfpeewee.orm import ModelConverter 7 | 8 | 9 | class BaseModelConverter(ModelConverter): 10 | def __init__(self, *args, **kwargs): 11 | super(BaseModelConverter, self).__init__(*args, **kwargs) 12 | self.converters[BooleanField] = self.handle_boolean 13 | 14 | def handle_boolean(self, model, field, **kwargs): 15 | return field.name, BooleanSelectField(**kwargs) 16 | 17 | 18 | class ChosenAjaxSelectWidget(widgets.Select): 19 | def __init__(self, data_source, data_param, *args, **kwargs): 20 | self.data_source = data_source 21 | self.data_param = data_param 22 | super(ChosenAjaxSelectWidget, self).__init__(*args, **kwargs) 23 | 24 | def __call__(self, field, **kwargs): 25 | if field.allow_blank and not self.multiple: 26 | kwargs['data-role'] = u'ajax-chosenblank' 27 | else: 28 | kwargs['data-role'] = u'ajax-chosen' 29 | kwargs['data-source'] = self.data_source 30 | kwargs['data-param'] = self.data_param 31 | kwargs['data-placeholder'] = 'Type to search...' 32 | 33 | return super(ChosenAjaxSelectWidget, self).__call__(field, **kwargs) 34 | 35 | 36 | class LimitedModelSelectField(ModelSelectField): 37 | def iter_choices(self): 38 | for obj in self.query.limit(20): 39 | yield (obj._pk, self.get_label(obj), obj == self.data) 40 | -------------------------------------------------------------------------------- /flask_peewee/serializer.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import sys 3 | import uuid 4 | 5 | from peewee import Model 6 | from flask_peewee.utils import get_dictionary_from_model 7 | from flask_peewee.utils import get_model_from_dictionary 8 | 9 | 10 | class Serializer(object): 11 | date_format = '%Y-%m-%d' 12 | time_format = '%H:%M:%S' 13 | datetime_format = ' '.join([date_format, time_format]) 14 | 15 | def convert_value(self, value): 16 | if isinstance(value, datetime.datetime): 17 | return value.strftime(self.datetime_format) 18 | elif isinstance(value, datetime.date): 19 | return value.strftime(self.date_format) 20 | elif isinstance(value, datetime.time): 21 | return value.strftime(self.time_format) 22 | elif isinstance(value, Model): 23 | return value._pk 24 | elif isinstance(value, uuid.UUID): 25 | return str(value) 26 | else: 27 | return value 28 | 29 | def clean_data(self, data): 30 | for key, value in data.items(): 31 | if isinstance(value, dict): 32 | self.clean_data(value) 33 | elif isinstance(value, (list, tuple)): 34 | data[key] = map(self.clean_data, value) 35 | else: 36 | data[key] = self.convert_value(value) 37 | return data 38 | 39 | def serialize_object(self, obj, fields=None, exclude=None): 40 | data = get_dictionary_from_model(obj, fields, exclude) 41 | return self.clean_data(data) 42 | 43 | 44 | class Deserializer(object): 45 | def deserialize_object(self, model, data): 46 | return get_model_from_dictionary(model, data) 47 | -------------------------------------------------------------------------------- /flask_peewee/static/css/admin.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | background-color: #fff; 3 | } 4 | body { 5 | padding-top: 60px; 6 | } 7 | .container > footer p { 8 | text-align: center; /* center align it with the container */ 9 | } 10 | 11 | /* The white background content wrapper */ 12 | .content { 13 | background-color: #fff; 14 | -webkit-border-radius: 0 0 6px 6px; 15 | -moz-border-radius: 0 0 6px 6px; 16 | border-radius: 0 0 6px 6px; 17 | } 18 | 19 | /* modeladmin filters */ 20 | form.modeladmin-filters { margin: 0; } 21 | form.modeladmin-filters ul { list-style-type: none; } 22 | form.modeladmin-filters select { margin-right: 10px; } 23 | 24 | div.form-wrapper { width: 100%; overflow: auto; } 25 | 26 | form textarea { height: 150px; } 27 | form input[type="text"].datetime-widget { width: 100px; margin-right: 10px; } 28 | 29 | div.sidebar li.active { font-weight: bold; } 30 | 31 | table.table-striped td.links .inline { margin: 0; } 32 | table.table-striped td.links .inline li { display: inline-block; } 33 | table.table-striped td.links .inline li a { 34 | border-right: 1px solid #DDDDDD; 35 | padding-right: 5px; 36 | } 37 | table.table-striped td.links .inline li:last-child a { 38 | border-right: medium none; 39 | } 40 | 41 | /* helpers */ 42 | .hidden { display: none; } 43 | -------------------------------------------------------------------------------- /flask_peewee/static/css/bootstrap-responsive.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Responsive v2.0.0 3 | * 4 | * Copyright 2012 Twitter, Inc 5 | * Licensed under the Apache License v2.0 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Designed and built with all the love in the world @twitter by @mdo and @fat. 9 | */ 10 | .hidden { 11 | display: none; 12 | visibility: hidden; 13 | } 14 | @media (max-width: 480px) { 15 | .nav-collapse { 16 | -webkit-transform: translate3d(0, 0, 0); 17 | } 18 | .page-header h1 small { 19 | display: block; 20 | line-height: 18px; 21 | } 22 | input[class*="span"], 23 | select[class*="span"], 24 | textarea[class*="span"], 25 | .uneditable-input { 26 | display: block; 27 | width: 100%; 28 | height: 28px; 29 | /* Make inputs at least the height of their button counterpart */ 30 | 31 | /* Makes inputs behave like true block-level elements */ 32 | 33 | -webkit-box-sizing: border-box; 34 | /* Older Webkit */ 35 | 36 | -moz-box-sizing: border-box; 37 | /* Older FF */ 38 | 39 | -ms-box-sizing: border-box; 40 | /* IE8 */ 41 | 42 | box-sizing: border-box; 43 | /* CSS3 spec*/ 44 | 45 | } 46 | .input-prepend input[class*="span"], .input-append input[class*="span"] { 47 | width: auto; 48 | } 49 | input[type="checkbox"], input[type="radio"] { 50 | border: 1px solid #ccc; 51 | } 52 | .form-horizontal .control-group > label { 53 | float: none; 54 | width: auto; 55 | padding-top: 0; 56 | text-align: left; 57 | } 58 | .form-horizontal .controls { 59 | margin-left: 0; 60 | } 61 | .form-horizontal .control-list { 62 | padding-top: 0; 63 | } 64 | .form-horizontal .form-actions { 65 | padding-left: 10px; 66 | padding-right: 10px; 67 | } 68 | .modal { 69 | position: absolute; 70 | top: 10px; 71 | left: 10px; 72 | right: 10px; 73 | width: auto; 74 | margin: 0; 75 | } 76 | .modal.fade.in { 77 | top: auto; 78 | } 79 | .modal-header .close { 80 | padding: 10px; 81 | margin: -10px; 82 | } 83 | .carousel-caption { 84 | position: static; 85 | } 86 | } 87 | @media (max-width: 768px) { 88 | .container { 89 | width: auto; 90 | padding: 0 20px; 91 | } 92 | .row-fluid { 93 | width: 100%; 94 | } 95 | .row { 96 | margin-left: 0; 97 | } 98 | .row > [class*="span"], .row-fluid > [class*="span"] { 99 | float: none; 100 | display: block; 101 | width: auto; 102 | margin: 0; 103 | } 104 | } 105 | @media (min-width: 768px) and (max-width: 980px) { 106 | .row { 107 | margin-left: -20px; 108 | *zoom: 1; 109 | } 110 | .row:before, .row:after { 111 | display: table; 112 | content: ""; 113 | } 114 | .row:after { 115 | clear: both; 116 | } 117 | [class*="span"] { 118 | float: left; 119 | margin-left: 20px; 120 | } 121 | .span1 { 122 | width: 42px; 123 | } 124 | .span2 { 125 | width: 104px; 126 | } 127 | .span3 { 128 | width: 166px; 129 | } 130 | .span4 { 131 | width: 228px; 132 | } 133 | .span5 { 134 | width: 290px; 135 | } 136 | .span6 { 137 | width: 352px; 138 | } 139 | .span7 { 140 | width: 414px; 141 | } 142 | .span8 { 143 | width: 476px; 144 | } 145 | .span9 { 146 | width: 538px; 147 | } 148 | .span10 { 149 | width: 600px; 150 | } 151 | .span11 { 152 | width: 662px; 153 | } 154 | .span12, .container { 155 | width: 724px; 156 | } 157 | .offset1 { 158 | margin-left: 82px; 159 | } 160 | .offset2 { 161 | margin-left: 144px; 162 | } 163 | .offset3 { 164 | margin-left: 206px; 165 | } 166 | .offset4 { 167 | margin-left: 268px; 168 | } 169 | .offset5 { 170 | margin-left: 330px; 171 | } 172 | .offset6 { 173 | margin-left: 392px; 174 | } 175 | .offset7 { 176 | margin-left: 454px; 177 | } 178 | .offset8 { 179 | margin-left: 516px; 180 | } 181 | .offset9 { 182 | margin-left: 578px; 183 | } 184 | .offset10 { 185 | margin-left: 640px; 186 | } 187 | .offset11 { 188 | margin-left: 702px; 189 | } 190 | .row-fluid { 191 | width: 100%; 192 | *zoom: 1; 193 | } 194 | .row-fluid:before, .row-fluid:after { 195 | display: table; 196 | content: ""; 197 | } 198 | .row-fluid:after { 199 | clear: both; 200 | } 201 | .row-fluid > [class*="span"] { 202 | float: left; 203 | margin-left: 2.762430939%; 204 | } 205 | .row-fluid > [class*="span"]:first-child { 206 | margin-left: 0; 207 | } 208 | .row-fluid .span1 { 209 | width: 5.801104972%; 210 | } 211 | .row-fluid .span2 { 212 | width: 14.364640883%; 213 | } 214 | .row-fluid .span3 { 215 | width: 22.928176794%; 216 | } 217 | .row-fluid .span4 { 218 | width: 31.491712705%; 219 | } 220 | .row-fluid .span5 { 221 | width: 40.055248616%; 222 | } 223 | .row-fluid .span6 { 224 | width: 48.618784527%; 225 | } 226 | .row-fluid .span7 { 227 | width: 57.182320438000005%; 228 | } 229 | .row-fluid .span8 { 230 | width: 65.74585634900001%; 231 | } 232 | .row-fluid .span9 { 233 | width: 74.30939226%; 234 | } 235 | .row-fluid .span10 { 236 | width: 82.87292817100001%; 237 | } 238 | .row-fluid .span11 { 239 | width: 91.436464082%; 240 | } 241 | .row-fluid .span12 { 242 | width: 99.999999993%; 243 | } 244 | input.span1, textarea.span1, .uneditable-input.span1 { 245 | width: 32px; 246 | } 247 | input.span2, textarea.span2, .uneditable-input.span2 { 248 | width: 94px; 249 | } 250 | input.span3, textarea.span3, .uneditable-input.span3 { 251 | width: 156px; 252 | } 253 | input.span4, textarea.span4, .uneditable-input.span4 { 254 | width: 218px; 255 | } 256 | input.span5, textarea.span5, .uneditable-input.span5 { 257 | width: 280px; 258 | } 259 | input.span6, textarea.span6, .uneditable-input.span6 { 260 | width: 342px; 261 | } 262 | input.span7, textarea.span7, .uneditable-input.span7 { 263 | width: 404px; 264 | } 265 | input.span8, textarea.span8, .uneditable-input.span8 { 266 | width: 466px; 267 | } 268 | input.span9, textarea.span9, .uneditable-input.span9 { 269 | width: 528px; 270 | } 271 | input.span10, textarea.span10, .uneditable-input.span10 { 272 | width: 590px; 273 | } 274 | input.span11, textarea.span11, .uneditable-input.span11 { 275 | width: 652px; 276 | } 277 | input.span12, textarea.span12, .uneditable-input.span12 { 278 | width: 714px; 279 | } 280 | } 281 | @media (max-width: 980px) { 282 | body { 283 | padding-top: 0; 284 | } 285 | .navbar-fixed-top { 286 | position: static; 287 | margin-bottom: 18px; 288 | } 289 | .navbar-fixed-top .navbar-inner { 290 | padding: 5px; 291 | } 292 | .navbar .container { 293 | width: auto; 294 | padding: 0; 295 | } 296 | .navbar .brand { 297 | padding-left: 10px; 298 | padding-right: 10px; 299 | margin: 0 0 0 -5px; 300 | } 301 | .navbar .nav-collapse { 302 | clear: left; 303 | } 304 | .navbar .nav { 305 | float: none; 306 | margin: 0 0 9px; 307 | } 308 | .navbar .nav > li { 309 | float: none; 310 | } 311 | .navbar .nav > li > a { 312 | margin-bottom: 2px; 313 | } 314 | .navbar .nav > .divider-vertical { 315 | display: none; 316 | } 317 | .navbar .nav > li > a, .navbar .dropdown-menu a { 318 | padding: 6px 15px; 319 | font-weight: bold; 320 | color: #999999; 321 | -webkit-border-radius: 3px; 322 | -moz-border-radius: 3px; 323 | border-radius: 3px; 324 | } 325 | .navbar .dropdown-menu li + li a { 326 | margin-bottom: 2px; 327 | } 328 | .navbar .nav > li > a:hover, .navbar .dropdown-menu a:hover { 329 | background-color: #222222; 330 | } 331 | .navbar .dropdown-menu { 332 | position: static; 333 | top: auto; 334 | left: auto; 335 | float: none; 336 | display: block; 337 | max-width: none; 338 | margin: 0 15px; 339 | padding: 0; 340 | background-color: transparent; 341 | border: none; 342 | -webkit-border-radius: 0; 343 | -moz-border-radius: 0; 344 | border-radius: 0; 345 | -webkit-box-shadow: none; 346 | -moz-box-shadow: none; 347 | box-shadow: none; 348 | } 349 | .navbar .dropdown-menu:before, .navbar .dropdown-menu:after { 350 | display: none; 351 | } 352 | .navbar .dropdown-menu .divider { 353 | display: none; 354 | } 355 | .navbar-form, .navbar-search { 356 | float: none; 357 | padding: 9px 15px; 358 | margin: 9px 0; 359 | border-top: 1px solid #222222; 360 | border-bottom: 1px solid #222222; 361 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); 362 | -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); 363 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); 364 | } 365 | .navbar .nav.pull-right { 366 | float: none; 367 | margin-left: 0; 368 | } 369 | .navbar-static .navbar-inner { 370 | padding-left: 10px; 371 | padding-right: 10px; 372 | } 373 | .btn-navbar { 374 | display: block; 375 | } 376 | .nav-collapse { 377 | overflow: hidden; 378 | height: 0; 379 | } 380 | } 381 | @media (min-width: 980px) { 382 | .nav-collapse.collapse { 383 | height: auto !important; 384 | } 385 | } 386 | @media (min-width: 1200px) { 387 | .row { 388 | margin-left: -30px; 389 | *zoom: 1; 390 | } 391 | .row:before, .row:after { 392 | display: table; 393 | content: ""; 394 | } 395 | .row:after { 396 | clear: both; 397 | } 398 | [class*="span"] { 399 | float: left; 400 | margin-left: 30px; 401 | } 402 | .span1 { 403 | width: 70px; 404 | } 405 | .span2 { 406 | width: 170px; 407 | } 408 | .span3 { 409 | width: 270px; 410 | } 411 | .span4 { 412 | width: 370px; 413 | } 414 | .span5 { 415 | width: 470px; 416 | } 417 | .span6 { 418 | width: 570px; 419 | } 420 | .span7 { 421 | width: 670px; 422 | } 423 | .span8 { 424 | width: 770px; 425 | } 426 | .span9 { 427 | width: 870px; 428 | } 429 | .span10 { 430 | width: 970px; 431 | } 432 | .span11 { 433 | width: 1070px; 434 | } 435 | .span12, .container { 436 | width: 1170px; 437 | } 438 | .offset1 { 439 | margin-left: 130px; 440 | } 441 | .offset2 { 442 | margin-left: 230px; 443 | } 444 | .offset3 { 445 | margin-left: 330px; 446 | } 447 | .offset4 { 448 | margin-left: 430px; 449 | } 450 | .offset5 { 451 | margin-left: 530px; 452 | } 453 | .offset6 { 454 | margin-left: 630px; 455 | } 456 | .offset7 { 457 | margin-left: 730px; 458 | } 459 | .offset8 { 460 | margin-left: 830px; 461 | } 462 | .offset9 { 463 | margin-left: 930px; 464 | } 465 | .offset10 { 466 | margin-left: 1030px; 467 | } 468 | .offset11 { 469 | margin-left: 1130px; 470 | } 471 | .row-fluid { 472 | width: 100%; 473 | *zoom: 1; 474 | } 475 | .row-fluid:before, .row-fluid:after { 476 | display: table; 477 | content: ""; 478 | } 479 | .row-fluid:after { 480 | clear: both; 481 | } 482 | .row-fluid > [class*="span"] { 483 | float: left; 484 | margin-left: 2.564102564%; 485 | } 486 | .row-fluid > [class*="span"]:first-child { 487 | margin-left: 0; 488 | } 489 | .row-fluid .span1 { 490 | width: 5.982905983%; 491 | } 492 | .row-fluid .span2 { 493 | width: 14.529914530000001%; 494 | } 495 | .row-fluid .span3 { 496 | width: 23.076923077%; 497 | } 498 | .row-fluid .span4 { 499 | width: 31.623931624%; 500 | } 501 | .row-fluid .span5 { 502 | width: 40.170940171000005%; 503 | } 504 | .row-fluid .span6 { 505 | width: 48.717948718%; 506 | } 507 | .row-fluid .span7 { 508 | width: 57.264957265%; 509 | } 510 | .row-fluid .span8 { 511 | width: 65.81196581200001%; 512 | } 513 | .row-fluid .span9 { 514 | width: 74.358974359%; 515 | } 516 | .row-fluid .span10 { 517 | width: 82.905982906%; 518 | } 519 | .row-fluid .span11 { 520 | width: 91.45299145300001%; 521 | } 522 | .row-fluid .span12 { 523 | width: 100%; 524 | } 525 | input.span1, textarea.span1, .uneditable-input.span1 { 526 | width: 60px; 527 | } 528 | input.span2, textarea.span2, .uneditable-input.span2 { 529 | width: 160px; 530 | } 531 | input.span3, textarea.span3, .uneditable-input.span3 { 532 | width: 260px; 533 | } 534 | input.span4, textarea.span4, .uneditable-input.span4 { 535 | width: 360px; 536 | } 537 | input.span5, textarea.span5, .uneditable-input.span5 { 538 | width: 460px; 539 | } 540 | input.span6, textarea.span6, .uneditable-input.span6 { 541 | width: 560px; 542 | } 543 | input.span7, textarea.span7, .uneditable-input.span7 { 544 | width: 660px; 545 | } 546 | input.span8, textarea.span8, .uneditable-input.span8 { 547 | width: 760px; 548 | } 549 | input.span9, textarea.span9, .uneditable-input.span9 { 550 | width: 860px; 551 | } 552 | input.span10, textarea.span10, .uneditable-input.span10 { 553 | width: 960px; 554 | } 555 | input.span11, textarea.span11, .uneditable-input.span11 { 556 | width: 1060px; 557 | } 558 | input.span12, textarea.span12, .uneditable-input.span12 { 559 | width: 1160px; 560 | } 561 | .thumbnails { 562 | margin-left: -30px; 563 | } 564 | .thumbnails > li { 565 | margin-left: 30px; 566 | } 567 | } 568 | -------------------------------------------------------------------------------- /flask_peewee/static/css/bootstrap.min.responsive.css: -------------------------------------------------------------------------------- 1 | 2 | .hidden{display:none;visibility:hidden;} 3 | @media (max-width:480px){.nav-collapse{-webkit-transform:translate3d(0, 0, 0);} .page-header h1 small{display:block;line-height:18px;} input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input{display:block;width:100%;height:28px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box;} .input-prepend input[class*="span"],.input-append input[class*="span"]{width:auto;} input[type="checkbox"],input[type="radio"]{border:1px solid #ccc;} .form-horizontal .control-group>label{float:none;width:auto;padding-top:0;text-align:left;} .form-horizontal .controls{margin-left:0;} .form-horizontal .control-list{padding-top:0;} .form-horizontal .form-actions{padding-left:10px;padding-right:10px;} .modal{position:absolute;top:10px;left:10px;right:10px;width:auto;margin:0;}.modal.fade.in{top:auto;} .modal-header .close{padding:10px;margin:-10px;} .carousel-caption{position:static;}}@media (max-width:768px){.container{width:auto;padding:0 20px;} .row-fluid{width:100%;} .row{margin-left:0;} .row>[class*="span"],.row-fluid>[class*="span"]{float:none;display:block;width:auto;margin:0;}}@media (min-width:768px) and (max-width:980px){.row{margin-left:-20px;*zoom:1;}.row:before,.row:after{display:table;content:"";} .row:after{clear:both;} [class*="span"]{float:left;margin-left:20px;} .span1{width:42px;} .span2{width:104px;} .span3{width:166px;} .span4{width:228px;} .span5{width:290px;} .span6{width:352px;} .span7{width:414px;} .span8{width:476px;} .span9{width:538px;} .span10{width:600px;} .span11{width:662px;} .span12,.container{width:724px;} .offset1{margin-left:82px;} .offset2{margin-left:144px;} .offset3{margin-left:206px;} .offset4{margin-left:268px;} .offset5{margin-left:330px;} .offset6{margin-left:392px;} .offset7{margin-left:454px;} .offset8{margin-left:516px;} .offset9{margin-left:578px;} .offset10{margin-left:640px;} .offset11{margin-left:702px;} .row-fluid{width:100%;*zoom:1;}.row-fluid:before,.row-fluid:after{display:table;content:"";} .row-fluid:after{clear:both;} .row-fluid>[class*="span"]{float:left;margin-left:2.762430939%;} .row-fluid>[class*="span"]:first-child{margin-left:0;} .row-fluid .span1{width:5.801104972%;} .row-fluid .span2{width:14.364640883%;} .row-fluid .span3{width:22.928176794%;} .row-fluid .span4{width:31.491712705%;} .row-fluid .span5{width:40.055248616%;} .row-fluid .span6{width:48.618784527%;} .row-fluid .span7{width:57.182320438000005%;} .row-fluid .span8{width:65.74585634900001%;} .row-fluid .span9{width:74.30939226%;} .row-fluid .span10{width:82.87292817100001%;} .row-fluid .span11{width:91.436464082%;} .row-fluid .span12{width:99.999999993%;} input.span1,textarea.span1,.uneditable-input.span1{width:32px;} input.span2,textarea.span2,.uneditable-input.span2{width:94px;} input.span3,textarea.span3,.uneditable-input.span3{width:156px;} input.span4,textarea.span4,.uneditable-input.span4{width:218px;} input.span5,textarea.span5,.uneditable-input.span5{width:280px;} input.span6,textarea.span6,.uneditable-input.span6{width:342px;} input.span7,textarea.span7,.uneditable-input.span7{width:404px;} input.span8,textarea.span8,.uneditable-input.span8{width:466px;} input.span9,textarea.span9,.uneditable-input.span9{width:528px;} input.span10,textarea.span10,.uneditable-input.span10{width:590px;} input.span11,textarea.span11,.uneditable-input.span11{width:652px;} input.span12,textarea.span12,.uneditable-input.span12{width:714px;}}@media (max-width:980px){body{padding-top:0;} .navbar-fixed-top{position:static;margin-bottom:18px;} .navbar-fixed-top .navbar-inner{padding:5px;} .navbar .container{width:auto;padding:0;} .navbar .brand{padding-left:10px;padding-right:10px;margin:0 0 0 -5px;} .navbar .nav-collapse{clear:left;} .navbar .nav{float:none;margin:0 0 9px;} .navbar .nav>li{float:none;} .navbar .nav>li>a{margin-bottom:2px;} .navbar .nav>.divider-vertical{display:none;} .navbar .nav>li>a,.navbar .dropdown-menu a{padding:6px 15px;font-weight:bold;color:#999999;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;} .navbar .dropdown-menu li+li a{margin-bottom:2px;} .navbar .nav>li>a:hover,.navbar .dropdown-menu a:hover{background-color:#222222;} .navbar .dropdown-menu{position:static;top:auto;left:auto;float:none;display:block;max-width:none;margin:0 15px;padding:0;background-color:transparent;border:none;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;} .navbar .dropdown-menu:before,.navbar .dropdown-menu:after{display:none;} .navbar .dropdown-menu .divider{display:none;} .navbar-form,.navbar-search{float:none;padding:9px 15px;margin:9px 0;border-top:1px solid #222222;border-bottom:1px solid #222222;-webkit-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1),0 1px 0 rgba(255, 255, 255, 0.1);-moz-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1),0 1px 0 rgba(255, 255, 255, 0.1);box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1),0 1px 0 rgba(255, 255, 255, 0.1);} .navbar .nav.pull-right{float:none;margin-left:0;} .navbar-static .navbar-inner{padding-left:10px;padding-right:10px;} .btn-navbar{display:block;} .nav-collapse{overflow:hidden;height:0;}}@media (min-width:980px){.nav-collapse.collapse{height:auto !important;}}@media (min-width:1200px){.row{margin-left:-30px;*zoom:1;}.row:before,.row:after{display:table;content:"";} .row:after{clear:both;} [class*="span"]{float:left;margin-left:30px;} .span1{width:70px;} .span2{width:170px;} .span3{width:270px;} .span4{width:370px;} .span5{width:470px;} .span6{width:570px;} .span7{width:670px;} .span8{width:770px;} .span9{width:870px;} .span10{width:970px;} .span11{width:1070px;} .span12,.container{width:1170px;} .offset1{margin-left:130px;} .offset2{margin-left:230px;} .offset3{margin-left:330px;} .offset4{margin-left:430px;} .offset5{margin-left:530px;} .offset6{margin-left:630px;} .offset7{margin-left:730px;} .offset8{margin-left:830px;} .offset9{margin-left:930px;} .offset10{margin-left:1030px;} .offset11{margin-left:1130px;} .row-fluid{width:100%;*zoom:1;}.row-fluid:before,.row-fluid:after{display:table;content:"";} .row-fluid:after{clear:both;} .row-fluid>[class*="span"]{float:left;margin-left:2.564102564%;} .row-fluid>[class*="span"]:first-child{margin-left:0;} .row-fluid .span1{width:5.982905983%;} .row-fluid .span2{width:14.529914530000001%;} .row-fluid .span3{width:23.076923077%;} .row-fluid .span4{width:31.623931624%;} .row-fluid .span5{width:40.170940171000005%;} .row-fluid .span6{width:48.717948718%;} .row-fluid .span7{width:57.264957265%;} .row-fluid .span8{width:65.81196581200001%;} .row-fluid .span9{width:74.358974359%;} .row-fluid .span10{width:82.905982906%;} .row-fluid .span11{width:91.45299145300001%;} .row-fluid .span12{width:100%;} input.span1,textarea.span1,.uneditable-input.span1{width:60px;} input.span2,textarea.span2,.uneditable-input.span2{width:160px;} input.span3,textarea.span3,.uneditable-input.span3{width:260px;} input.span4,textarea.span4,.uneditable-input.span4{width:360px;} input.span5,textarea.span5,.uneditable-input.span5{width:460px;} input.span6,textarea.span6,.uneditable-input.span6{width:560px;} input.span7,textarea.span7,.uneditable-input.span7{width:660px;} input.span8,textarea.span8,.uneditable-input.span8{width:760px;} input.span9,textarea.span9,.uneditable-input.span9{width:860px;} input.span10,textarea.span10,.uneditable-input.span10{width:960px;} input.span11,textarea.span11,.uneditable-input.span11{width:1060px;} input.span12,textarea.span12,.uneditable-input.span12{width:1160px;} .thumbnails{margin-left:-30px;} .thumbnails>li{margin-left:30px;}} 4 | -------------------------------------------------------------------------------- /flask_peewee/static/css/chosen-sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coleifer/flask-peewee/e4c8bd947b6524bb2fd93c3598e48818acf00307/flask_peewee/static/css/chosen-sprite.png -------------------------------------------------------------------------------- /flask_peewee/static/css/datepicker.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Datepicker for Bootstrap 3 | * 4 | * Copyright 2012 Stefan Petre 5 | * Licensed under the Apache License v2.0 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | */ 9 | .datepicker { 10 | top: 0; 11 | left: 0; 12 | padding: 4px; 13 | margin-top: 1px; 14 | -webkit-border-radius: 4px; 15 | -moz-border-radius: 4px; 16 | border-radius: 4px; 17 | /*.dow { 18 | border-top: 1px solid #ddd !important; 19 | }*/ 20 | } 21 | .datepicker:before { 22 | content: ''; 23 | display: inline-block; 24 | border-left: 7px solid transparent; 25 | border-right: 7px solid transparent; 26 | border-bottom: 7px solid #ccc; 27 | border-bottom-color: rgba(0, 0, 0, 0.2); 28 | position: absolute; 29 | top: -7px; 30 | left: 6px; 31 | } 32 | .datepicker:after { 33 | content: ''; 34 | display: inline-block; 35 | border-left: 6px solid transparent; 36 | border-right: 6px solid transparent; 37 | border-bottom: 6px solid #ffffff; 38 | position: absolute; 39 | top: -6px; 40 | left: 7px; 41 | } 42 | .datepicker > div { 43 | display: none; 44 | } 45 | .datepicker table { 46 | width: 100%; 47 | margin: 0; 48 | } 49 | .datepicker td, .datepicker th { 50 | text-align: center; 51 | width: 20px; 52 | height: 20px; 53 | -webkit-border-radius: 4px; 54 | -moz-border-radius: 4px; 55 | border-radius: 4px; 56 | } 57 | .datepicker td.day:hover { 58 | background: #eeeeee; 59 | cursor: pointer; 60 | } 61 | .datepicker td.old, .datepicker td.new { 62 | color: #999999; 63 | } 64 | .datepicker td.active, .datepicker td.active:hover { 65 | background-color: #006dcc; 66 | background-image: -moz-linear-gradient(top, #0088cc, #0044cc); 67 | background-image: -ms-linear-gradient(top, #0088cc, #0044cc); 68 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc)); 69 | background-image: -webkit-linear-gradient(top, #0088cc, #0044cc); 70 | background-image: -o-linear-gradient(top, #0088cc, #0044cc); 71 | background-image: linear-gradient(top, #0088cc, #0044cc); 72 | background-repeat: repeat-x; 73 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0088cc', endColorstr='#0044cc', GradientType=0); 74 | border-color: #0044cc #0044cc #002a80; 75 | border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); 76 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 77 | color: #fff; 78 | text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); 79 | } 80 | .datepicker td.active:hover, 81 | .datepicker td.active:hover:hover, 82 | .datepicker td.active:active, 83 | .datepicker td.active:hover:active, 84 | .datepicker td.active.active, 85 | .datepicker td.active:hover.active, 86 | .datepicker td.active.disabled, 87 | .datepicker td.active:hover.disabled, 88 | .datepicker td.active[disabled], 89 | .datepicker td.active:hover[disabled] { 90 | background-color: #0044cc; 91 | } 92 | .datepicker td.active:active, 93 | .datepicker td.active:hover:active, 94 | .datepicker td.active.active, 95 | .datepicker td.active:hover.active { 96 | background-color: #003399 \9; 97 | } 98 | .datepicker td span { 99 | display: block; 100 | width: 47px; 101 | height: 54px; 102 | line-height: 54px; 103 | float: left; 104 | margin: 2px; 105 | cursor: pointer; 106 | -webkit-border-radius: 4px; 107 | -moz-border-radius: 4px; 108 | border-radius: 4px; 109 | } 110 | .datepicker td span:hover { 111 | background: #eeeeee; 112 | } 113 | .datepicker td span.active { 114 | background-color: #006dcc; 115 | background-image: -moz-linear-gradient(top, #0088cc, #0044cc); 116 | background-image: -ms-linear-gradient(top, #0088cc, #0044cc); 117 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc)); 118 | background-image: -webkit-linear-gradient(top, #0088cc, #0044cc); 119 | background-image: -o-linear-gradient(top, #0088cc, #0044cc); 120 | background-image: linear-gradient(top, #0088cc, #0044cc); 121 | background-repeat: repeat-x; 122 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0088cc', endColorstr='#0044cc', GradientType=0); 123 | border-color: #0044cc #0044cc #002a80; 124 | border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); 125 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 126 | color: #fff; 127 | text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); 128 | } 129 | .datepicker td span.active:hover, 130 | .datepicker td span.active:active, 131 | .datepicker td span.active.active, 132 | .datepicker td span.active.disabled, 133 | .datepicker td span.active[disabled] { 134 | background-color: #0044cc; 135 | } 136 | .datepicker td span.active:active, .datepicker td span.active.active { 137 | background-color: #003399 \9; 138 | } 139 | .datepicker td span.old { 140 | color: #999999; 141 | } 142 | .datepicker th.switch { 143 | width: 145px; 144 | } 145 | .datepicker thead tr:first-child th { 146 | cursor: pointer; 147 | } 148 | .datepicker thead tr:first-child th:hover { 149 | background: #eeeeee; 150 | } 151 | .input-append.date .add-on i, .input-prepend.date .add-on i { 152 | display: block; 153 | cursor: pointer; 154 | width: 16px; 155 | height: 16px; 156 | } -------------------------------------------------------------------------------- /flask_peewee/static/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coleifer/flask-peewee/e4c8bd947b6524bb2fd93c3598e48818acf00307/flask_peewee/static/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /flask_peewee/static/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coleifer/flask-peewee/e4c8bd947b6524bb2fd93c3598e48818acf00307/flask_peewee/static/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /flask_peewee/static/js/admin.js: -------------------------------------------------------------------------------- 1 | var Admin = window.Admin || {}; 2 | 3 | (function(A, $) { 4 | 5 | /* paginated list of models displayed in a modal window */ 6 | function ModelAdminAjaxList() { 7 | this.autocomplete_selector = '.fk-lookup-input'; 8 | } 9 | 10 | ModelAdminAjaxList.prototype.init = function(click_cb_factory) { 11 | var self = this; 12 | 13 | /* bind keyboard handler for input */ 14 | $(this.autocomplete_selector).keyup(function(e) { 15 | var elem = $(this) 16 | , target = elem.siblings('ul.result-list') 17 | , modal = elem.parents('.modal'); 18 | 19 | self.show(elem.data('ajax-url'), elem.val(), target, click_cb_factory(modal)); 20 | }); 21 | 22 | /* bind next/prev buttons */ 23 | $('.modal a.next, .modal a.previous').click(function(e) { 24 | var elem = $(this) 25 | , modal = elem.parents('.modal') 26 | , input_elem = modal.find(self.autocomplete_selector) 27 | , target = input_elem.siblings('ul.result-list') 28 | , page = elem.data('page'); 29 | 30 | if (!elem.hasClass('disabled')) { 31 | self.show(input_elem.data('ajax-url')+'&page='+page, input_elem.val(), target, click_cb_factory(modal)); 32 | } 33 | }); 34 | } 35 | 36 | ModelAdminAjaxList.prototype.show = function(url, query, target, click_cb) { 37 | var modal = target.parents('.modal') 38 | , next_btn = modal.find('a.next') 39 | , prev_btn = modal.find('a.previous') 40 | , self = this; 41 | 42 | $.get(url+'&query='+query, function(data) { 43 | target.empty(); 44 | for (var i=0, l=data.object_list.length; i < l; i++) { 45 | var o = data.object_list[i]; 46 | target.append('
  • '+o.repr+'
  • '); 47 | } 48 | 49 | if (data.prev_page) { 50 | prev_btn.removeClass('disabled'); 51 | prev_btn.data('page', data.prev_page); 52 | } else { 53 | prev_btn.addClass('disabled'); 54 | } 55 | if (data.next_page) { 56 | next_btn.removeClass('disabled'); 57 | next_btn.data('page', data.next_page); 58 | } else { 59 | next_btn.addClass('disabled'); 60 | } 61 | 62 | target.find('a').click(function(e) { 63 | var data = $(this).data('object-id') 64 | , repr = $(this).text() 65 | , html = $(this).html() 66 | , sender = modal.data('sender'); 67 | 68 | click_cb(sender, repr, data, html); 69 | target.parents('.modal').modal('hide'); 70 | }); 71 | }); 72 | } 73 | 74 | var ModelAdminRawIDField = function(field_name) { 75 | this.field_name = field_name; 76 | this.selector = 'input#'+this.field_name; 77 | } 78 | 79 | ModelAdminRawIDField.prototype.init = function(repr) { 80 | var self = this 81 | , repr = repr || 'Select...' 82 | , hidden_elem = $(this.selector) 83 | , new_elem = $(''+repr+''); 84 | 85 | /* bind the ajax list */ 86 | this.ajax_list = new ModelAdminAjaxList(); 87 | this.ajax_list.init(function(modal) {return self.on_click}); 88 | 89 | new_elem.click(function(e) { 90 | e.preventDefault(); 91 | var modal = $('#modal-' + self.field_name) 92 | , modal_input = modal.find('.fk-lookup-input') 93 | , target = modal.find('ul.result-list'); 94 | 95 | self.ajax_list.show(modal_input.data('ajax-url'), '', target, self.on_click); 96 | modal.data('sender', $(this)); 97 | modal.modal('show'); 98 | }); 99 | hidden_elem.after(new_elem); 100 | } 101 | 102 | ModelAdminRawIDField.prototype.on_click = function(sender, repr, data, html) { 103 | if (repr) { 104 | sender.text(repr); 105 | } else { 106 | sender.html(html); 107 | } 108 | sender.parent().find('input[type="hidden"]').val(data); 109 | } 110 | 111 | /* filter class */ 112 | var ModelAdminFilter = function() { 113 | this.wrapper = '#filter-wrapper'; /* wrapper around the form that submits filters */ 114 | this.add_selector = 'a.field-filter'; /* links to add filters (in the navbar) */ 115 | this.lookups_wrapper = '#filter-fields'; /* wrapper around the filter fields */ 116 | } 117 | 118 | ModelAdminFilter.prototype.init = function() { 119 | var self = this; 120 | 121 | this.filter_list = $(this.wrapper + ' form div.filter-list'); 122 | this.lookups_elem = $(this.lookups_wrapper); 123 | 124 | /* bind the "add filter" click behavior */ 125 | $(this.add_selector).click(function(e) { 126 | e.preventDefault(); 127 | self.add_filter($(this)); 128 | }); 129 | } 130 | 131 | ModelAdminFilter.prototype.chosen_handler = function(data) { 132 | var results = {} 133 | , object_list = data.object_list; 134 | for (var i = 0, l = object_list.length; i < l; i++) { 135 | var item = object_list[i]; 136 | results[item['id']] = item['repr'] 137 | } 138 | return results; 139 | } 140 | 141 | ModelAdminFilter.prototype.add_row = function(qf_v, qf_s, ival, sval) { 142 | var select_elem = this.lookups_elem.find('#'+qf_s), 143 | input_elem = this.lookups_elem.find('#'+qf_v), 144 | field_label = $('#filter-'+qf_s).text(); 145 | 146 | var self = this, 147 | select_clone = select_elem.clone(), 148 | input_clone = input_elem.clone(), 149 | row = [ 150 | , '' 154 | ].join('\n'), 155 | row_elem = $(row).append(select_clone).append(input_clone); 156 | 157 | if (ival && sval) { 158 | select_clone.val(sval); 159 | input_clone.val(ival); 160 | } 161 | 162 | row_elem.find('a.btn-close').click(function(e) { 163 | row_elem.remove(); 164 | }); 165 | 166 | this.filter_list.prepend(row_elem); 167 | 168 | /* reload our jquery plugin stuff */ 169 | if (input_clone.hasClass('datetime-widget')) { 170 | $(input_clone[0]).datepicker({format: 'yyyy-mm-dd'}); 171 | } else if (input_clone.hasClass('date-widget')) { 172 | input_clone.datepicker({format: 'yyyy-mm-dd'}); 173 | } else if (input_clone.data('role') === 'chosen') { 174 | input_clone.chosen(); 175 | } else if (input_clone.data('role') === 'ajax-chosen') { 176 | input_clone.ajaxChosen({ 177 | type: 'GET', 178 | url: input_clone.data('source'), 179 | jsonTermKey: 'query', 180 | dataType: 'json', 181 | data: {'field': input_clone.data('param')}, 182 | minTermLength: 2 183 | }, this.chosen_handler); 184 | } 185 | 186 | $(this.wrapper).show(); 187 | 188 | /* twitter bootfap doesn't wanna close the dropdown */ 189 | $('.dropdown.open .dropdown-toggle').dropdown('toggle'); 190 | 191 | return row_elem; 192 | } 193 | 194 | /* add a filter of a given type */ 195 | ModelAdminFilter.prototype.add_filter = function(elem) { 196 | return this.add_row(elem.data('field'), elem.data('select')); 197 | } 198 | 199 | /* pull request data and simulate adding a filter */ 200 | ModelAdminFilter.prototype.add_filter_request = function(qf_s, filter_idx, qf_v, filter_val) { 201 | return this.add_row(qf_v, qf_s, filter_val, filter_idx); 202 | } 203 | 204 | /* export */ 205 | A.ModelAdminRawIDField = ModelAdminRawIDField; 206 | A.ModelAdminFilter = ModelAdminFilter; 207 | 208 | /* bind a simple listener */ 209 | A.index_submit = function(action) { 210 | $('form#model-list input[name=action]').val(action); 211 | $('form#model-list').submit(); 212 | } 213 | })(Admin, jQuery); 214 | 215 | jQuery(function() { 216 | jQuery(".alert").alert() 217 | }); 218 | -------------------------------------------------------------------------------- /flask_peewee/static/js/ajax-chosen.min.js: -------------------------------------------------------------------------------- 1 | 2 | (function($){return $.fn.ajaxChosen=function(settings,callback,chosenOptions){var chosenXhr,defaultOptions,options,select;if(settings==null){settings={};} 3 | if(callback==null){callback={};} 4 | if(chosenOptions==null){chosenOptions=function(){};} 5 | defaultOptions={minTermLength:3,afterTypeDelay:500,jsonTermKey:"term"};select=this;chosenXhr=null;options=$.extend({},defaultOptions,$(select).data(),settings);this.chosen(chosenOptions?chosenOptions:{});return this.each(function(){return $(this).next('.chzn-container').find(".search-field > input, .chzn-search > input").bind('keyup',function(){var field,msg,success,untrimmed_val,val;untrimmed_val=$(this).attr('value');val=$.trim($(this).attr('value'));msg=val.length").attr('label',element.text).appendTo(select);return $.each(element.items,function(value,text){if($.inArray(value+"-"+text,selected_values)===-1){return $("