pax_global_header00006660000000000000000000000064137561111560014520gustar00rootroot0000000000000052 comment=e0909096a5e0aa474b3b789915e75b91520ce094 flask-mongoengine-1.0.0/000077500000000000000000000000001375611115600151215ustar00rootroot00000000000000flask-mongoengine-1.0.0/.github/000077500000000000000000000000001375611115600164615ustar00rootroot00000000000000flask-mongoengine-1.0.0/.github/labels.yml000066400000000000000000000060061375611115600204500ustar00rootroot00000000000000- name: "topic: CI/CD" color: "54f449" description: "This issue/PR relates to CI/CD pipeline change" - name: "topic: code style" color: "54f449" description: "This issue/PR relates to code style." from_name: "code quality" - name: "topic: tests" color: "54f449" description: "This issue/PR relates to tests, QA, CI." - name: "topic: deprecated" color: "999999" description: "Feature or component marked as deprecated and will be removed" - name: "topic: documentation" color: "54f449" description: "This issue/PR relates to or includes documentation." from_name: "Documentation" - name: "type: breaking-change" color: "310372" description: "Marks an important and likely breaking old interface change." - name: "type: feature" color: "310372" description: "New feature implementation" - name: "type: enhancement" color: "310372" description: "Enhancement update for old feature" from_name: "enhancement" - name: "type: good first issue" color: "310372" description: "Good for newcomers" from_name: "Good first contrib" - name: "type: bug" color: "b71914" description: "Something isn't working" from_name: "bug" - name: "log:added" color: "ff935e" description: "Changelog mark label. Marks new added features." - name: "log:breaking-change" color: "ff935e" description: "Changelog mark label. Marks breaking changes." - name: "log:changed" color: "ff935e" description: "Changelog mark label. Marks old, but changed features." - name: "log:deprecated" color: "ff935e" description: "Changelog mark label. Marks deprecated features, that will be removed in next major release." - name: "log:fixed" color: "ff935e" description: "Changelog mark label. Marks fixed bug issues." - name: "log:removed" color: "ff935e" description: "Changelog mark label. Marks removed features." - name: "log:skip-changelog" color: "ff935e" description: "Should be excluded from the changelog." - name: "os: linux" color: "fbca04" description: "This issue/PR related to linux systems" - name: "os: mac" color: "fbca04" description: "This issue/PR related to mac systems" - name: "os: windows" color: "fbca04" description: "This issue/PR related to windows systems" - name: "question" color: "d876e3" description: "User questions, not related to issues" from_name: "question" - name: "decision: duplicate" color: "eeeeee" description: "This issue or pull request already exists" from_name: "duplicate" - name: "decision: wontfix" color: "eeeeee" description: "This will not be worked on" from_name: "wontfix" - name: "decision: wontmerge" color: "eeeeee" description: "PR Only: This pull request will not be merged (problem described in comments)" - name: "stage: waiting-for-contributor" color: "efc1ae" from_name: "awaiting response" description: "Waiting for answer from original contributor." - name: "stage: WIP" color: "efc1ae" description: "Work In Progress" - name: "stage: help wanted" color: "efc1ae" description: "Extra attention is needed" from_name: "Help wanted" flask-mongoengine-1.0.0/.github/release-drafter.yml000066400000000000000000000010131375611115600222440ustar00rootroot00000000000000categories: - title: 'Breaking Changes' labels: - 'log:breaking-change' - title: 'Added' labels: - 'log:added' - title: 'Changed' labels: - 'log:changed' - title: 'Fixed' labels: - 'log:fixed' - title: 'Deprecated' labels: - 'log:deprecated' - title: 'Removed' labels: - 'log:removed' exclude-labels: - 'log:skip-changelog' sort-by: 'title' template: | ## Changes $CHANGES ## This release is made by wonderful contributors: $CONTRIBUTORS flask-mongoengine-1.0.0/.github/workflows/000077500000000000000000000000001375611115600205165ustar00rootroot00000000000000flask-mongoengine-1.0.0/.github/workflows/coverage.yml000066400000000000000000000026011375611115600230330ustar00rootroot00000000000000name: Send Coverage to different analitic engines # Only for last versions of python and mongo on: push: branches: - master tags: - "*" pull_request: branches: - "*" jobs: coverage: runs-on: ubuntu-latest strategy: fail-fast: false matrix: mongodb-version: [4.2] include: - name: "coverage" python: "3.8" tox_env: "py38" steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python }} uses: actions/setup-python@v1 with: python-version: ${{ matrix.python }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install tox virtualenv - name: Start MongoDB uses: supercharge/mongodb-github-action@1.3.0 with: mongodb-version: ${{ matrix.mongodb-version }} - name: Test build run: "tox -e ${{ matrix.tox_env }} -- --cov-report=xml --cov-report=html" - name: Send coverage report to codecov uses: codecov/codecov-action@v1 with: file: ./coverage.xml - name: Send coverage report to codeclimate uses: paambaati/codeclimate-action@v2.6.0 with: coverageCommand: echo "Ignore rerun" coverageLocations: ${{github.workspace}}/coverage.xml:coverage.py env: CC_TEST_REPORTER_ID: ${{secrets.CC_TEST_REPORTER_ID}} flask-mongoengine-1.0.0/.github/workflows/labeler-check.yml000066400000000000000000000005741375611115600237300ustar00rootroot00000000000000name: Labels verification on: pull_request: jobs: labeler: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v2 - name: Run Labeler uses: crazy-max/ghaction-github-labeler@v2 with: yaml_file: .github/labels.yml dry_run: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} flask-mongoengine-1.0.0/.github/workflows/labeler.yml000066400000000000000000000005631375611115600226530ustar00rootroot00000000000000name: Labels verification on: push: branches: [master] jobs: labeler: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v2 - name: Run Labeler uses: crazy-max/ghaction-github-labeler@v2 with: yaml_file: .github/labels.yml env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} flask-mongoengine-1.0.0/.github/workflows/linting.yml000066400000000000000000000013321375611115600227040ustar00rootroot00000000000000name: Linting Tests # Only for last versions of python on: push: branches: - master tags: - "*" pull_request: branches: - "*" jobs: linting: runs-on: ubuntu-latest strategy: fail-fast: false matrix: include: - name: "linting" python: "3.8" tox_env: "lint" steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python }} uses: actions/setup-python@v1 with: python-version: ${{ matrix.python }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install tox virtualenv - name: Test build run: "tox -e ${{ matrix.tox_env }}" flask-mongoengine-1.0.0/.github/workflows/main.yml000066400000000000000000000023171375611115600221700ustar00rootroot00000000000000name: CI Tests on: push: branches: - master tags: - "*" pull_request: branches: - "*" jobs: build: runs-on: ubuntu-latest strategy: fail-fast: false matrix: mongodb-version: [3.6, 4.0, 4.2, 4.4] include: - name: "ubuntu-py36" python: "3.6" tox_env: "py36" - name: "ubuntu-py37" python: "3.7" tox_env: "py37" - name: "ubuntu-py38" python: "3.8" tox_env: "py38" - name: "ubuntu-py39" python: "3.9" tox_env: "py39" - name: "ubuntu-pypy3" python: "pypy3" tox_env: "pypy3" steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python }} uses: actions/setup-python@v1 with: python-version: ${{ matrix.python }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install tox virtualenv - name: Start MongoDB uses: supercharge/mongodb-github-action@1.3.0 with: mongodb-version: ${{ matrix.mongodb-version }} - name: Test build run: "tox -e ${{ matrix.tox_env }}" flask-mongoengine-1.0.0/.github/workflows/release-drafter.yml000066400000000000000000000003671375611115600243140ustar00rootroot00000000000000name: Release Drafter on: push: branches: - master jobs: update_release_draft: runs-on: ubuntu-latest steps: - uses: release-drafter/release-drafter@v5 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} flask-mongoengine-1.0.0/.gitignore000066400000000000000000000002551375611115600171130ustar00rootroot00000000000000*.pyc .*.swp *.egg docs/.build docs/_build build/ dist/ *.egg-info/ env/ venv/ ._* .DS_Store .coverage .project .pydevproject .tox .eggs .idea .vscode htmlcov/ coverage.xml flask-mongoengine-1.0.0/.pre-commit-config.yaml000066400000000000000000000007361375611115600214100ustar00rootroot00000000000000repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v3.3.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: check-yaml - id: check-added-large-files - repo: https://github.com/psf/black rev: 20.8b1 hooks: - id: black language_version: python3.6 exclude: ^docs/ - repo: https://gitlab.com/pycqa/flake8 rev: 3.8.4 hooks: - id: flake8 exclude: ^docs/|^examples/ flask-mongoengine-1.0.0/.travis.yml000066400000000000000000000032751375611115600172410ustar00rootroot00000000000000dist: focal os: linux language: python jobs: include: - language: python python: 3.6 name: "linting" env: TOXENV=lint - language: python python: 3.6 name: "Test on python 3.6" env: TOXENV=py36 - language: python python: 3.7 name: "Test on python 3.7" env: TOXENV=py37 - language: python python: 3.8 name: "Test on python 3.8" env: TOXENV=py38 - language: python python: 3.9 name: "Test on python 3.9" env: TOXENV=py39 - language: python python: pypy3 name: "Test on python pypy3" env: TOXENV=pypy3 services: - mongodb install: - travis_retry pip install --upgrade pip - travis_retry pip install coveralls - travis_retry pip install flake8 - travis_retry pip install tox>=3.14 - travis_retry pip install virtualenv script: - tox after_success: - coveralls --verbose notifications: irc: irc.freenode.org#flask-mongoengine # Only run builds on the master branch and GitHub releases (tagged as vX.Y.Z) branches: only: - master - /^v.*$/ # Whenever a new release is created via GitHub, publish it on PyPI. deploy: provider: pypi user: wojcikstefan password: secure: ZShEfSeu1pUHPGWtO5JAERp3nkG1omFIDxz/N+qNEJGwIRxXH6S0E17p9jaRAwrBY11F6ecajOgz1E1ICovYf8IitOP8I/CyieF6EPV7Bv4PZyKJdWGzT/Edjrt+wAjY7Kwlr2Gkh9Tu1lChZcWvhyMVPYCgqyA2yP0W2b9kC4Y= # create a source distribution and a pure python wheel for faster installs distributions: "sdist bdist_wheel" # only deploy on tagged commits (aka GitHub releases) and only for the # parent repo's builds running Python 3.6 on: tags: true python: 3.6 repo: MongoEngine/flask-mongoengine flask-mongoengine-1.0.0/AUTHORS000066400000000000000000000026521375611115600161760ustar00rootroot00000000000000The PRIMARY AUTHORS are (and/or have been): Ross Lawley Bright Dadson Jorge Bastida Dan Jacob https://bitbucket.org/danjac Marat Khabibullin https://bitbucket.org/maratfm Streetlife.com atroche - https://github.com/atroche Rodrigue Cloutier Thomas Steinacher Anthony Nemitz Nauman Ahmad CURRENT MAINTAINER: Andrey Shpak - https://github.com/insspb CONTRIBUTORS Dervived from the git logs, inevitably incomplete but all of whom and others have submitted patches, reported bugs and generally helped make MongoEngine that much better: * Martin Hanzík - https://github.com/martinhanzik * Tony Narlock - https://github.com/tony * Dragos - https://github.com/cdragos * IamFive - https://github.com/IamFive * mickey06 - https://github.com/mickey06 * Serge S. Koval - https://github.com/mrjoes * Marcus Carlsson - https://github.com/xintron * RealJTG - https://github.com/RealJTG * Peter D. Gray * Massimo Santini * Len Buckens - https://github.com/buckensl * Garito - https://github.com/garito * Jérôme Lafréchoux - https://github.com/lafrech * Bruno Belarmino - https://github.com/brunobelarmino * Sibelius Seraphini - https://github.com/sibelius * Denny Huang - https://github.com/denny0223 * Stefan Wojcik - https://github.com/wojcikstefan * John Cass - https://github.com/jcass77 * Aly Sivji - https://github.com/alysivji * Buğra İşgüzar - https://github.com/bisguzar flask-mongoengine-1.0.0/CONTRIBUTING.rst000066400000000000000000000055631375611115600175730ustar00rootroot00000000000000Contributing to Flask-MongoEngine ================================= MongoEngine has a large `community `_ and contributions are always encouraged. Contributions can be as simple as minor tweaks to the documentation. Please read these guidelines before sending a pull request. Bugfixes and New Features ------------------------- Before starting to write code, look for existing `tickets `_ or `create one `_ for your specific issue or feature request. That way you avoid working on something that might not be of interest or that has already been addressed. If in doubt post to the `user group ` Supported Interpreters ---------------------- Flask-MongoEngine supports CPython 3.6 and newer. Language features not supported by all interpreters can not be used. Style Guide ----------- MongoEngine aims to follow `PEP8 `_ including 4 space indents and 79 character line limits. But as this project is less maintained as we wish several changes was introduced in version 1.0.0: 1. Code formatting now is completely done by `black `_ and following black's style implementation of `PEP8 `_. Target python version is python 3.6. 2. Code formatting should be done before passing any new merge requests (after 1st Jan 2020). 3. Code style should use f-strings python 3.6 feature if possible. 4. Docs formattings should be checked and formatted with `pre-commit `_ plugin before submit. Testing ------- All tests are run on `Travis `_ and any pull requests are automatically tested by Travis. Any pull requests without tests will take longer to be integrated and might be refused. General Guidelines ------------------ - Avoid backward breaking changes if at all possible. - Write inline documentation for new classes and methods. - Write tests and make sure they pass (make sure you have a mongod running on the default port, then execute ``python setup.py test`` from the cmd line to run the test suite). - Add yourself to AUTHORS :) Documentation ------------- To contribute to the `API documentation `_ just make your changes to the inline documentation of the appropriate `source code `_ or `rst file `_ in a branch and submit a `pull request `_. You might also use the github `Edit `_ button. flask-mongoengine-1.0.0/LICENSE000066400000000000000000000026601375611115600161320ustar00rootroot00000000000000Copyright (c) 2010-2016 See AUTHORS. Some rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * The names of the contributors may not be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. flask-mongoengine-1.0.0/MANIFEST.in000066400000000000000000000003711375611115600166600ustar00rootroot00000000000000include MANIFEST.in include README.rst include LICENSE include AUTHORS recursive-include flask_mongoengine/templates *.html recursive-include docs * recursive-exclude docs *.pyc recursive-exclude docs *.pyo prune docs/_build prune docs/_themes/.git flask-mongoengine-1.0.0/README.rst000077500000000000000000000033351375611115600166170ustar00rootroot00000000000000================= Flask-MongoEngine ================= :Info: MongoEngine for Flask web applications. :Repository: https://github.com/MongoEngine/flask-mongoengine .. image:: https://travis-ci.org/MongoEngine/flask-mongoengine.svg?branch=master :target: https://travis-ci.org/MongoEngine/flask-mongoengine .. image:: https://coveralls.io/repos/github/MongoEngine/flask-mongoengine/badge.svg?branch=master :target: https://coveralls.io/github/MongoEngine/flask-mongoengine?branch=master About ===== Flask-MongoEngine is a Flask extension that provides integration with MongoEngine. It handles connection management for your app. You can also use WTForms as model forms for your models. Documentation ============= You can find the documentation at https://flask-mongoengine.readthedocs.io Installation ============ You can install this package using pypi: ``pip install flask-mongoengine`` Tests ===== To run the test suite, ensure you are running a local copy of Flask-MongoEngine and simply run: ``pytest``. To run the test suite on every supported versions of Python, PyPy and MongoEngine you can use ``tox``. Ensure tox and each supported Python, PyPy versions are installed in your environment: .. code-block:: shell # Install tox $ pip install tox # Run the test suites $ tox To run a single or selected test suits, use pytest `-k` option. Contributing ============ We welcome contributions! see the `Contribution guidelines `_ Community ========= - `#flask-mongoengine IRC channel `_ License ======= Flask-MongoEngine is distributed under MIT license, see LICENSE for more details. flask-mongoengine-1.0.0/codecov.yml000066400000000000000000000000171375611115600172640ustar00rootroot00000000000000comment: false flask-mongoengine-1.0.0/docs/000077500000000000000000000000001375611115600160515ustar00rootroot00000000000000flask-mongoengine-1.0.0/docs/Makefile000066400000000000000000000110161375611115600175100ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/flask-unittest.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/flask-unittest.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/flask-unittest" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/flask-unittest" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." make -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." flask-mongoengine-1.0.0/docs/_static/000077500000000000000000000000001375611115600174775ustar00rootroot00000000000000flask-mongoengine-1.0.0/docs/_static/debugtoolbar.png000066400000000000000000002261371375611115600226710ustar00rootroot00000000000000PNG  IHDR\N! IDATxw|U[{iB/tTPA˪vWEQwgUkY{õ .!4-3ܹ{s <|;;=d;9!H$D"4jKg@"H$)$D"H)$D"H{KXQ-H$DPu{J1D"H$'qb"!WLI%H$Xi4}s&"wH$D"҄\\uMImD"H$Gh&Զu \u‚ BXhi^"H$$D% b1jXBK/D"H$MxS F׉\M[%NTI$D"4U\56/>1Wt [Md cPdIH$DLSiM8F59u<ɂ(c7 uD"H$h,,EUSC MR'2"S )~,H$̣ߏ ` uҤW#ULoXD"H$ϣFq\7ukD뺎-|VEAQt]oPִ}o6`[냏۠|H_/?_[[[{>zqPPu 樂K o)2mҮ]Gkf_hu?\ Q|h`3;~}B kt_/K˒%KeddбcǨv 0ZR- 0((to)KlB̫iZ"pCUn,MP+}ڤ*)V/K*ݻwg۶ml6^/iZ8>1逩i4&LKN}&B-7&uͪj+\Fj}SDzWpP{pcJ_/o~W\Idd$:^ ]b'!`ej].+JQDyŲ.MP"+WuT¡Ԫ( r` No)2ac_៬ X/K_4v?n'<!9 %5Z@e[YQVkp+pv{yZ&)+˟ ^ֻ~EXbw%/FBJs!/u3|獤Ķb/~{[*#tԂz\P|iU:g a<. K;U|+-Uo?t5*vzՃm[CEEEYNTU 13lP!4Om|WvCL['R3]7_/?B$&&6\d6W%9G.B5 ˖&:sZ|kR&w+vm#%1A4 t(  Vyj}>۷jw-j!i@:(W fcoݘABX4rFڏ?Ru2z48n7p8'71h1L1 (dd| 3fۂG4_LTT`<-ql~㴯 \P7Rbbc*+*3{58x 񄅅QUUETTTFv;iiigqݥ׼;e5o0cYۂ >YYq:!KgQQQDDDSlߖ)B.F9Ga:5MSqm!golmՉW={`5Q1أS:{jmĵ [wp6ZM"%2 nc\3;7h@fTUU"P ,-n(^^]+ S= |u 6ÍFkbboK'\`WﱬlÃ}Ȑnb|c}׳{qnTfp^?ߜɯFed\qNpjv݂{o%˚jOTd5[Ĺ̋{C: ySMƏOiNO)Jt_Utŷ2}p8(xno?))[CXXDFFܽ8^oȗO8`kNݝD`L/IJ/K FYeZl'i^ Z{-UQ7FEv8lلZ r:#P&d^"qF%zc6kV]C%3FoX9*Q<4sQm[\:< !4:3C͹/$܉$+r~MC^Lm8UxS_&aջr=̙m?~tXKwxO#!a,t վ/Y1ǁ%ٳOy8>gqd3 [<+9_Ϻ>`Op59߭[w֯_@޽mׄ >>4 < \qqq_ZwOV?MUUmO}X'lHOpWVbl۬W8jRhh .XxO<٬tdZ Á㡶6-"n Hx OfpqhGuu?"vii)ݻw'X %YE):vąG Ci, ܅kz\3f}\qϣ,2y/n6ʧSz pɁ.GB5<8ѿS*?P);Lށ lˋV;}/:}[ס*6* Y8v!.tB|t59CK#n94-իW5k87:mdu㉌VZذ[O&f}ZO*8Tטe3 (8AoR@p ΃5[_/B`Kqe±#\^bu]1Fu]z!ÊSQQQ04X5ѬJ"Xnbccq\FE!Ku~3` D|D4}hEer+yM+^munszΜoaGCթ=yT~9Qq\2F[#c;oBK/}M,0fmvGd;w"*:2~ll .rx< :ϛm?h/kPvl6>a-z]ծ55O8IM/K~aLAe~d`&Gk-6؀_;-;noK\V͗W:Ml>RUE |Za.Xڶm>| t+P|]Ʌ1$bYF |k+7טEZ *O Un7W1ǰ/\|k^GBQ͝*999iѺuk"""o1srrHJJ:j[~v{S'T tj_oy뭷x;cn߲uykd ߼!U;ߦ_s>92i*ckXÚ4+>mnTUEUU\6ۓyb1XO>f{sݠy~cLk4Z9+Mj*ZM-}!Cx E= eX=}ҥKHMMEUU"""چ( SǼkN̚*Dz–n_/;[ֱI(RxY9o)rAPU$$$TU4*4аnpz+.Tgy郦iDFFp%pa`+j'cMjߖn_؇+x&xQq烕`RT%""B ~,4?r~@֤4,&7:7d̿/KҾz }r\wy@%h:2EB9oR E tPaS#N3KҾ/KҾ/B7x"h9a!g(x}CB@}%`J_r3h8ۅ$dۯp WJ$DK@4߰91RbXt Bk`KWF'! 10?!7Bf5ZQo9gggcِH$Ν;ׁ4b7]l\HrBciױRE#Ev5f,D5VBSVVFvv6aaachbKRQQAn8CU"H$Au\&%70`+BfΜI^ԩS|A{yߵkגA֭l_{k=<]DsK6l.B0#\PjfϞM]]^x!YYYG?MX4- IjXS|T*B7>1#YaQ> /BۅOPըc޽9Rܨ}]]G"{1x ^z%N{3f‹/o}̘17w6 >Z| /?KO0-eGdސD"i94McǏsq<qk$~|\梪*+WlѮ{B M馛Xlq7Di4nmuq0뇰p:uĞ={x8sq<-+G 45å4a>EF.0~P\T?'\q۟Lccb"pCUU5.HLL 曤0uTX`'22ҢCJOΝ)--%55nݺ_͕W^O[UG(6܋39Gm},?r~+~2N6Zf}Ϗyd֙˝Î+D"9yݻLvTnZJҤsRF\wؓMΝCZ ˾}6l-b(B۶miӦ `Dt[;#[ UXXȬY`}S9ɓss:AQK)bQ<*)W$c"G*PvCh)J`ʗBjjjݻ7{e۶mlܴt_7ic4Uۜ>\PO͂Q55+˷|ʽ{ 8Du#F9@PUl6bbcәQ\\o~k#),(`Æ V?Ӱ µqF –-[HJJ"==!iiil߾=d!}S[_ɑm̻3WsE^;6X ϭߩz'mՙ?_oeh }7euftW,~5põcw3tn"ʄ,6y%.syv:=:eN,]H$dRUUEXe9֢et@ޏX: ҥK9r$dff2rHرck׮ .!v&_~xgfVn7v $''3}#8^{5{9-ZɓIyo)^_+Ar+-Sy՗Rx$wigRvk7<q]{f*ݯMІ;lWpṃPF6nE5_tZвV"HZG$RQ{zx{tOǸq;v,ʕ+z9ҟV B_gep~(r*~x7ޟ\'TGŴp}k.ϟπ4hP=ލ\@A$U-o&{6OkآE!..[oٳdҥh1cBꇖ .E2INN?&));3Hnl6UUUTVVbw8OL@yhΝ9w<T c##e_s.Hsw?c^w6oS{bQ%%lVȄQdQØ,H$Cux8 5 8bw5޾}{hk "b }}t2>̘M2\:m@szkcfذa?M1bDЬ{f*M:<}J؉D2(d}לsS#E#Bc欳zrٽ4+W2|/_NYy9::Bl=XhC}}=h׷_߾m9 Nbb")))X;wҽ{\n^&C4.ttMUGqq1N< IDATDnI|BQHMMoaժU[sҾ}{V00:N1޺g$''c… 48y:biچog~՟{'a%ml+X6,ac/&+3҉M mGƌ|AoG.QLju=ҦGO hFtT1q$MN\mr4]va9sڷn,H$#33ͺJ/H݇}6J%>OU,X@LL se˖-dddr:e$%3s:bp ZGX1#m۶t҅;ROUu5IIԿ:nmpCcʨq3(m2v%0* iǿ]EA6mp8QOyy9t+W:jժUBL7x!&,+%X)#{Y6=hrNoo:9& %RZZ(ƆJrT<7IiUU͛7SRRB.]СCTu={5Mc:t<ڵkE$D"hwަaBreM *BꫯҹsgJQQgϦ_~۟nǎ!ul 9zrVۦ|Z-"bccl?qy7N11~/[UUXz5ƍk0)1C3S~i_ڗ}i_?y].Qm-KQKe[Kf+CIt:ni CKC"tEڗ}i_ڗǾA_d'XZ0U0"1?)|[| gƾ_1%"KҾ/KҾ߸}N=-œ_K _ +"8̨?F9T/KҾ/K~cO%-2q"tsV4v_0 _b.~jt *Lf\ %&&o7}T{"> ֢mmP/$Ҿ/KҾ -A F r<)^FfMyn~:XǬUA1: *BM:ՔŴeKSZ9Ŵbw.%<:X£bAuU)7gs *ˢiZYYEǎIbb"TUUh n˂YPD%0|n!3ѬOyWs7)p!y,D 9"ŮKϫo^~ur7oF򧯏Z>˿/~jGS;}C>=|'V7Zmxʧ_ڗ}ido/yXYTuI< *f/?kr-H\^Ae n.t].mQ;n)MycB/ GOf `y箫'*F cx+(**b֒MXXiӆ[.ieB~wpʗCuY V_EwpUwޚr3ߤTi<0mӯGgI57ɠXqzR%[rxyu]9Vur2ӊw֊»'s΄QDf9܉=hE$,"ߕĘ׳"= pob14=L@oM}:l>.%u[1 txϴnۍ*egřI҉,u%qFTO5ww(_wX$~Cby?wfs/K'_ Be님=/ưۅ̌:;q,yuLb>\Vo0"Ptn^yYڜ݅[7L2}#;> ‹("\F'7pZJ]VQ^֣Dh,@hےNhU,)*#%Nn0B_9 J}G<>:]t#Gt:IOOG&GW'ߢnkZfՔyApڟua}D'uG Vz8~,}ȭ}`ƍn[xشkO*;դoK.} GAE$zT:8C7دBA(:!/eײe+;fTᆬxr$! 5c20#w^~sV[F#.Ym3 ֽ+kpO+f5WMثt\0wZ\6\x=+"5/"tW歡z?6awxф`섋W.0J@2NcUI&q6M1僈eоDؕYj߯ێZRFgF˗-#954]0,0J2N%w@kWP^q{ٱilo}E6N|լج1 3FgSg\#Ctڜ?o?͔d̄f(qLƏiYӧ`Wt/GQ #0s{h:"e04('bS۵0EӍ8|~ n^d~3Pߓ!|k}i_?zy~n?x/m.lyUkp#;a8>|'|yՋf3ge.u't B1\=OOyז2}<_ψ[y뭀yq n|9'p+ b\2;ϝYb1+UM˧%8R;&qC8-mLap:Kfw ]#O˟8ӆ{Qm6l;̋ TE 㭷"**A;n͎5Վ(<.?TN x<EaSхUQ ]xT /Ϻf|5p"<6W Q&@GAh^l6^ݡph_Ow/6 ' Daz༴/K믢b ݤihWCU}UcZ @uS6nnGAA<(6nݮ nw_ PF_,]3D v;cm;|^}EǫW:t!9 *(Gk[DlfC:jëi4h9}GqY}R#쓐OXL9)~ie%nϧUn"0p@bcc۾"OQUUռUKҾ/ 'K-WZ@pLy`~*<,_0 KYsC("Ϸ q9towʴ !*(;PIMMCդM'Ds{ :,^NFT *i_ڗad\H.|Ԭ](+W_D(,zQ#KҾ/KҾ4SeD"H$/ɜJN7;{+H$I3y饙- S~̖i^"H$ @ .D"H$f>#H$q ]?hDrH%H$0*+x<6cM@QFL8W PKD .D"H[ ` n#GTTѺuNRK"H$gBz5<yy1WJ%i2;v,k)%̿]3ōWr]^ /Ym-VnyaI\?ia,ygᣥ;l%?T˫_F$DAA_l%$$ֿW111SZZѰ6m*-O}ttR&MĒ%Kѣwf߾}7tovE9RwݔWyeWS/f}wg1kuze_}@]LW"Ys)t(U!nuy Н}I[!/>4?!B*sc4%ƷsĖ$$$clJOOM6ݻ͆ (((H۫WfˇpZGEya]H9K']]Ŗ9b5_Rщp({%*v ~嗑BT +(q.IE2z:#7N.}磿/#3k:z{䢎=_X79s[pknL2 &;Jū(^7vښd]{a#;3W .pUcbunS9y)%=;0t]gx4ԾR„)Ӓ8\FBFڥFQF$KEqbJAuu5QQQTWWSYY륢IL*v?p饗n:^/=dffpwӮ];xtvq8x<%8#\g}b~[p;p']OjǴms ?WҮM.Pv!Q)c=]}%]zCFСμԋ? <Y]&q5>C0G{+ Jc4Ghe|bnpDS:a/q9mGӽ5Iv4]OC֥u7+Y@Tt< Ɛ F\|+ɱ#Y|+Sv!g?!9r.q//aSrTeDrz#\ ϧBjj됪DG7.zY8\zL>ӧӻwo"##q8tML6{wjڵ+y0aNrr2< W^y%_~yeX(j׷;.H{_u1;' j{Tr|/h!yc#BVUO=˳4o>[IEM1Cxe!j\(Bc?^q+f={%OOy-cѳ#UueU7n92hҋzIq)-fk &cڴ3PϰYUXzw9__,-˟À)]Fi P~7`߶U6, <>yj-Ƅfڭy |d@P{˴S˴>t޽WrٟT"l7Gy:22Xjkk!&& Lj[|I|I֮]-99 t]Ddɒ%ǓRr8Ws٨|:Gh|&MW;TƣST3lڙcHɝםfcŃpy{&iӐpinf5?+PqdPΦg2fCSwӿc?}t\qqᙾ?6atgTeHgٱ~3=TA3o}F}M>#ط,x3~=|m1* v‰iIrG9tUG^'oaDbx2z!Cm ֬Ye]Fܹ3n3w\HLLu:҅J_u9rSGƂsvs'X9z5Nm(^Xv6aۦc;팻7Ov =s8!4x-}={4M@Ftonz6iUH~g{&ZKDK Ԟ~e>#߸+y)w :J$EQ <܈^UUUi``>PϟF׳&%OE)/CDLߝ0Y@sh^թ#F#-ѸZKyp ?y5k'9#WOt?h."b-&MRB"ESWgt$99?른bD&ř|stxQqq&e#c4ߤ[~W uZ BheC柯eᒫ^M?0Gi%}"}M4Χ"K$*UU>rա5HOfkc;)N/ZBp 5zI?N_D1G\K1Q%ŖD"9ӰERSD6HU&-T =#s huӁҹH$}l6Ajj5.WNuTUQQ.bc݄imt%iYˏ2S3qF .3cw8v8Z"H$gaaZՒ\BQzv +!iYZFgH$X( 1$/b.D"H$_2RpI$D"43RpI$D"43WUU,KD"9sm,HZS.l6fD"H ls!ɩC>RH$4&%%ſ|}x6$RpI$DrRPPc@>ȚpI$Drwswx?{-w@:X(Rv ,4EzAPADH MBMH%\q% 9q>ϓ';3;3|W\IVؾ};}xyy1fڶmKvرc͚5#66+2h >C _|AFF9:#puZ-Fb6 -"IOO'77///ڷoϴi%44!CP^=}Yٸq#۶m^`ʔ)&RJZ-'NQF}:999 6ݻsN=ʹsHKK`%+K ;ɓ6qqq^ںk|Q=j>s W^eժU111Ŏ)zbWI{1$X@7~I5@ ®p3RˇZZNZ4?>ۛCq<3@ [`ׄ!5o^=Cٻڽ @ʍ$ҳrPtR02pI""GQ9Y$%`J n s'C(8_EQdYFQ̜;y}$e(x]V ]c1-?A4BCB:M1R}FN`Ԩ3c$Rm;2}/eYoC<^z5ۗ+W ?LIHH 774|JEΝINNҥK\~Ih׮\xׯo`Wש9>֔L*g*U aPozbb~ MO}mSݹx9W\Jmr/K[cN#%c{|Fh%*Թ/%@VVqrrDZl޽{R X8m3ؽ7Lol֠kD %rrrAX`dY___ΦFTR~ dYFc6[Srss#==7n\zg 4e'?;vTl[UQ*۾UxԷ`rTeÿ2ݞ@ ARRYYYYnKINN&11Zja4v~~~$$$HF0$$$ၻ;ZΥoa@ eN>l]K AA\@ e@ p @ 1bҼ@ eNյq 6JOO'##pss+`U(( }ilnݺ@ wEttty C.@ 2썈 "Љ\9{cgu^px&^g1Qܵ-=3- ldǖȥl*(K0/9zz޾nS&~ߴ n(ѧ9{9/cR3L3.ߌ۵ϊp1Ƨ<2oE>CN`g9v˯Ud="Acd_(2" /"%>-K!vpŜӳys7k~_G8|r7 2,s=ߟK%_[xۯ s|>uj18Tl+Y;t( z*w&.Kjՙۖ5?,stt4Ŕ͖l_i|ܩ+fS-$ݷhSa+ l˦OpnSfvgm`v۔ms2}|ہ45cK+bNY0SM~m <8oryy+ƻo"c֩ђg឵Ѣ iYFIkw?}^yL:17ʢM7:Of㺵4 ]Ʒ_ "N'1~ȋNZxyy8~> OWg|@+jY<%IGP|Bhp~q$Wo UH[=!1 7wC;%rS$W"~ 00 c1٢a] l6 Ύ3 ō6o봭ZΑIjk /@w'i$rc$zs& ERYBBCqwqk#K{1fm۞},޹Ncr1[*IBBJMFۗS7-[Mwf/8GHsC^|/W)^ 4,GWob GnՆC;^ T X}{("1Ѽb%*[3 <\ܨ\&//xgprIҍ"7F{̦ ,m]*C6Y+i \#Y>a[JgԸ w\0o>B3OWōnb^_Ns# Q,d"8M~=WF^}8#1et/jvʺuyrc[CU9죻4:!CqcotGrx1ҵޑOSW׮xRk~E03vfk~ޣX9$䱭HOc@P}KNѩ$\\5d4@љ=Сc'J%sP5slOMt.f$\*&Cn@%L}"9Q4~i}>&gu LY/y~:d'7#sl4xjCޤF`-Tr'#żt4X~ek5+Ԥfg%Y熣˳JM`TKzmB]\}.8=#Yo?@C qYamYOR <ctxZ}bxhJȿ[O?720td%utvgG$$BCb}-/Cez?Uyg  kIAGUSS0+R8fEPZ!7;S[g2f1vڍԨL`'`Je٦_{~Sˉ<JlX;*ofϞM@@uܹs>|gϖg/eA-pWAJFR9V@ U<7q/X[7GPԀR^v:{$^ ޭ#j4tn^*y 쎇+}fs@]+Iׯt@]'ooitM,{1=I? ێgS}y> &Ms8NTx&L<]'qSz.ſ~vΊwViQ4USKv.e R.\J;ˉS #GM`ҧh1h:=o;aY4"o.Iqh{@M[K$%%{Kſ:oЍke^?b ݲ>Aw=H*P4(:5c&{&̔gU>>PL~jn|8;_AoKg탏G74&Ϟ*|DCN0]Z'`Dk~鞨Tjz C):6"J ٣Qy3b$Iۗdzm6.^Xޮ JTtR$Yg HyBՅ!+ty>6aÆc)))8iQQ$IBnl6"+*_xƬ+<Й ' v.\E1Lh:2.+kwAQٌdF! %^!f#&NC`{]Q/GP0-eUvG1e5?Ym$05ZԪ{sɈYN[l3ȨШU2( Tj-]]/˧~ *T`ȑ\zeyPIJJړ40"Bȟ9O._)_P4RX@q'Zhw8Wb-h%_EV,!=R$ Iz~oimh4: 5lY.(m&*#ޕx?VWaU/ZjMoh\]ߒA_'jn?Cnֆm%5"TR}j`LM^CJx}O]NlJ}WVɧcʢ? ?/-6 R)4'ܻtjZ-~-`ѢE|nj; ϰaèS=z`ƌAu/$ Jw|eĿRXy $IEX$I\PbOupf3yf///t:7`ӦM颠vp@ S.^;BjNKKv@  Æ ~^v-\r,F#'N`ܸq裏aΜ)ݲF{]\&?4iA.@P^\|999뾘-Rg)))/C`rEa烩_{ ӠAmBe5ߢ]AQhؠ>/}EdSAC]Kg+De?sXjBQ. aw\x4rN1~}oőSyq6y%mv{7Xum+EݚҴol9;Gnjϔ7_kK(bdYXO46F6$nݺL]u۱noߣ;vp+Ǝ=YkEr盈?ؿi>w%|5\46|1d>3MFL hElcIMH"Dңi0q Ցmojy a~6Su$CƭŘzDQle+nɎ dpd]FRjɘ6>]Q䍗s*F4{tsɼ=a&)0ؔ5%_QQ0\`5,q|Ǯ ֋ Ym6mʊ 5x`Hf%G/:&χfƺ֯kgѴiOƧ(z?L(I+ƴYHO)fKpo`LK=c?fwzZ>+ɲL?L\#ۯޡoWF0G˘2m}R{v(˹V;G/{6ày^=$,A销 IDAT4mڍWj, cAR L6*XYc?BZ{PN%ÉeF듯Iy4!ų3h`Nfq:Ov_Fz4R: FØ1|'S=ޣ3rzΪN]G'-5첩۠( >mo7q3I=_lfT.D+JϼE75L:q˚K$swDf4k0%|wgfҰu]gmy+4q4,qv㹗\!_Z-fF&,?]0㏘9n0ϫ\=&M.bOA]r;lB1c?CRө1KZ%߮=7Cğ (E J;iCs0MXjU2/1pV~4i>"[>:le%Bw4ryg,Y>O't4{Z?Eu˙- ǡls'pk0̢Uz*cpz|9>e>T;}ҺeB?RӐT:L1zx긒 Ղ| QA²Ƈ¾'^Bʩ$%)W<;{x5-A$IbslOG{`wJWG}&]2xwoZvȌQA ̓~1贖P<^+^_&IA$0TyR2i* d ERQlQ4ɭUخJHr2Tϒe!H*U]adT*\".( ]ċŒ_ dJ|̮d&JA>wcߴ$QPY%VdY.n(2jͯao&YFV[WJ+!$>2Mbr',ѵx(b3ޫ^`~>HA@qq*fT6o-֬Y;mMV<;Z<; U[ڽJ{I\\=$ 233o}F$T*fQhAVI\ٌWEQHJ'E6qvQ(̲UP`(fҒoWTxcgs#5WOo\ue6 YHqvА~d}"{pUV www\c=ƍ֭4k֌;vЦMnJ`` 8q|OOOٱc]v%>>WJNݝ~5jԈ .Fёby4mڔߋQjFq~@oy[7C$qvZ~18;XED@xZ%sr!0(W'2m:;*v'1ժUȑ#$%%e|ӱbŊ[ӧ =zի͡CPTаEf]vOkBZZǏŅ?&|^WWWN: AAA66۵k$I4h___ڴiC1xzziozcy AMQ<==IJJ-{A[T*L&@6ọ>ʥKedyfe~\nnE{LJP233"..t"IFF,yv^}Urss!,, ///khZF}Ǯ9$@ ׈nݺ9sƍ믿EllT*ժU#** WWWf3999$''[ߎiӦ(BBB^^^$''ӨQ#J*䄫+B  ++k׮QN=hVDxx8Ǐ,) GJXbZ"..f͚Ym:99@zz:DDDb]QA .߆yp+oN>ܹ}e*TcHOOGY={ deeqQ^mge4INNFYT9;;s)N8Ave)ݻw[~ MT*5rssܹsL*؂.EQTZߢ⊼c擷PZ5_Mgԑ{xfMU?+BvIW3,fTZ^G#y6cn| {2lIJW( wi^f:8{8!!!|H}nońO IOrr2111#?`0p5>`=d2q5iii$$$-FYmL&bccM&zz=)))6AЉ'puu%++˺pYYY 0 `20L 'O) `WJ07]xF"J8sb'=z[ҩyS8ߖpdǚr}y;uSzb{8SӠWR0m-jT&'.kbJ/H8ςE~/֩?lLcbdYf_{Σ( .`7/YC~9Έ[ԕ+މ81I"i4^ՙ`:7e]Yb/! DtӱR F]9~s ,D}|>k9g+LوUY}ҭmN}ne9jЅb[}EԸUڧfOcNFFF-WeKa>2sp50\^WFµb8LI7p<DzԾY{^rХ7|uqiA#&g~HId̯r.ʛoϘ^2a%Т|*?b~u[\QkU@9fH>sa#1f\bL\j4aOscz^ll Fk3 _JX IBy8[")a%*y-Ψ bά/J8F_Lre3&V"S/aZ54jޙFy?O2kV6(OjGD_,ٹBhZjШդʲ9OgҨ},r4=7~寋&{*TEOiJV$IhZ>)_ WaʨT>1-*rOᶺx q'pLp>Z}B>6@`}s`槓dH85"vR|=Yۡ}FnBg.Ws;P9J΅)];Sڃɴz#7"u>gX1&>:gw \IA#kT+ v 9M9MzڿTSu9`f>C>c=wy5OHLwҥf6xcTTv$3CK>9鸹W'744|( q*f.bϘ{V hS&|fuZ֭G/2hn\!)*x6ӠV5I>Q]מּĈ\Oxwi_Win$xB*עǨǯc!?܋֦WxGsQ5*= A4 DNo}dǎ P  ɏn |ڡ37o`^ԨJ.0sOIP&퓧*7svCL"R7p9Μ9Ù3gHY"i<߰6616<ѾW2y|7xm춾j =0*6k /!(TEϧG:R0aQz5K#o8Ա^j6}o=qM Wz;B2&iP S|*^&#"/Veɜ0EHF7>o ?bްwU%6 $0 s(cF>Sxem%,[eEP($X~Ng˷eA1i^̙35k>E')1 _?o2o$ñ4h-VF;m-_'F"w<;}f>Tup/.ҥKYlY9y$(JLL̞X!( YY88Q~P6dgtb˳܍&($%ᅣV" 'P7t<8(LFj*EfCiY8yX>t'̴4.SRUZ-EAP>b񲋫svW<g"$ o?m훛 Fw"2,OȲ(L۟c 9&SFuzWOyn">Y?}qzɤw%3铷y'v]Nq6+e*3XvG>ܡόI+WӁ(r9~!JbJ^܂V( 139Y,͞]&w'ᱚ>8UcŎ-k* /J|h9Wk[iimy>KZ}} X}Lꋎ[N0W{~1w;H}wO%I+ƌ9Ͱ:s:{'SпdYZ>?݂/fHyίʩJD[Ie˺Viw&sv(˹V;}|p>~hE_#5ᒷ1;qC9ڙWB-ϓS# ॱSȺe$ü)l=Zsϵg˽6iK0uF/t[\QiVq!r͐rO3hD^aa̺ƒ99i;`UX{2?ViFNe {~Ւ+ZZEm9#+L?a=1{[\9&M.bOC-D]>f$ VIFèO+pL=zcl$Hn?ϝñC8tMYc }䌋6ڃDڵu'44%K}v̙3K׼ys5kUM3gδ~f̘1wcz޽{ .sdgÈɌG hXbjfvEŎ=H6?_~=g$$ӯ v)" aQp6m%d'z1#MdBy>.( p:elO{а@:c` wG>q͔ZLEwEI#~z=k/Kزnmmzh"ժNV|w<޺5^.^oeoN纾eY-j<֬U:̷vhU5} -5007>~5m:oQN F*{W̵D^~e kXC+|O`˺uT\IeӁM|cJx}b ۑ/irB3nţիh܃КVL9w?>BGKjI|3i\ld1ֶ?8%}p$IpX>j?|"u\y2AVZz^Cnn.`Qζ9>HNNfX`$剕ƍCQUc9rF7|իWiݺ5=aaa̘1;v,֭CVvZBBBfڵl߾WԿ^i>000OFdVh~+ H*sۥoݶilImeJ"˘t[xd,O>U>(]2 q;YV,:ڙ|8TDeȓ e+K;8EQ8z F'59\#~x:;XdP};gط(Hy>dKZ dk?_n}<EA6S80cydkGBp̜9ٳge3Tˤfi]% Eɓ4Ԧ(ԬYN|;jg<;<bɒ%Jf٧*rnwabsh [4m VZ@J%O ,+1-''1rH\]]QTTZʕ+ڵ+UTadddly-Jb޼y ,oO>PƍGll,SN-MU#F$ptrF]BpUZ'<ܜ ʊ[a*$I{xZztZ$wW* OOOPG@uQ6>mxߟzE͚5 f޽4i҄N8A[nݻpf3 d.AAA,_7;{аGx@ _#EGGGGpdggc2Yƍ6F!+ Rpss#-- '''7n˗z*ZZjiiiT*֭KTTuXHHAAA>|^'hZ7n,=zxWPG*`@ ?9>tV}z^ +2iiiQ}iF'O;?پx"/^nZϏ@ 2F\@ e@ p @ 1v;i^ ݋! ʋ/ D%AK^P2@ p @ 1"@ (cD%@Pu -╡(BQ H@ {Ů.P1<'GzD5GQO(d|}Z"{w%15cv*{m 4~i=kDbYIN#!9rYO\\Y%ڑe3,@@  R kqaT ZU) 7:6Ѩ1̨h074:68K]1›"`;Ձi?Ert+|Cke2mޜ´EvtR6͉dnJOr cҤIeݨ ~2$IVI&1c ]3?~Ϫ?x]D*Fz8 $IAATRJH("! 9rnƉqʥH\e͑lf_M\~/.Dgpb=Ia_C珚.i)pE-ƳLL\Zx@vAA2FêUxwiӦ קhѢ9_ݺuiذa#GЬY3\eߛXr%[fpG˻(Ňpt^ +&?U)eֱZǔnЖ-]{6A M7Ο`lOhҦ=oAzDAxfF#ggg(^8۷ogذaL<ŋ3h lBǎꫯ(_<sয়~b„ _EҡCƌ hѢkצQFT?h;vQF4i҄۷h")^8ݻwg۶mtЁ5kлwoV^7U/)vjX-72?]?c?ƕHp,5'sB{biR +낼ߩ>lYݩҍgŢׁi7 Z7'oѲ{nAm_ЦMx" 0!Cp}ONug8;;T*Q(/(Jh4?%KҶm[dYh42h $IBPh0@baaaxxxдiSƏ@.\Rٳ9t{,_YKRv C"9 AЊ+W oooۛ3ghtipttY&&&+V0k,VZϐ!Cҍ۶mlVKxx8Vۛ+W$?0BBB-[hѢl۶ WWR>m|'Jшi4<K]{xCP?;XAa 5燭\:wLJ8%^~,Ԡ#~~Qk*@Vjҍ GjʖJ{JAxVfӇcDzzjtž={x1}%J͍;wMDDCeΝ/_˗/n:Zj^Osؾ}o޷!&&Ǐ3qD.\seСk8(Rwgxr2tgRHl|imһU/ >AJAKt,VQ*g$ ш^^^\xɄxxxŚ5k(PQQQL&h4TTJ|H|à tڢ鐤GÇll6FrtR(44Phh<R)S9%xSrV>% o+d]8|@@._Lݺue9;'F#NJn692[cbbINGJaaaU*j #KkncI-ZFVZeՒ?~Viggfq&/$%,\ë~PbjQFF(iETads: }oY5'dLVo.E:Ohj4T͘uwvv̈́WD GE& DRa]#qQ!WVúJ׆]&sy&6Qi|e2[L Xi(U*V0E J֪<uG GQz1ZjiS23gk#7ңEIFBAak9j$z !z⢣{UJʾl޴"ywq)(U̥L0JLڝk&-Tr/^5 }pPFQ'zD=)[F3=t迃O<;PUDw1M % IF`\ge:Nc6p'srx$o+Խ(=9{oqmWEx!}j(DIUhk56l}rE:l$VE+]5RtQLPŁm0LܹW76i8o[E %].g51/0lF׳۷sҥի_:9s; gg_kJI!P($̒dPcIIn*f*yY;'ZU7 IB yH3cI]N8(h_ W .;KmVl)Ʊ uƳnAilMֽ058=MWsJ:8jFzgt~ۚNM|q.Or]o_ּ/&M]@.-e cCJ^Rʠ@Rk1C^ԡMIKpr,=JB/?c̰QόKPrsYq7Mot={PW\B tTTNS 4xn!RZljQ(Uvܨ^ J2B>yUʰIMkv(ݓuT`ϐygmj|؃jYv'6zeyLͽb{bNZw36oj=#V_h>V4ٛ;;f*<= s;n6`Eo{IqTeQ%lsҟQ󮤼R+&N_g/_7?BG0jۜ JJ6^\^(`>Ϳ >ۯ/kp7qySGR=sG,rW*U(Uv]=0ˣon=KP @%_ٹs'oÜ>}jժ?/_ɉZjq1J*E>O,,[cǎѼysFEZ֭[̙3'9aÆm6Zhg}ʷÛB&CBORxWxWIڤw)_Ç>|3{,( &5=2 "):}LNg'~eUe(3+d5Od:Y!26Rl‡e]ӍGPX:ț|2!Is,ɱ(ez?X^hzFa3/MǧK.eҥ/ڵkС'Ofʕ<|~oyѭ[7̑#GsÇ'((pYf̘I4iGT̰aؽ{7ZFC͚5-;v,7}ƍHԾBCC$%O1RI/)Ż]SU<IzʗR|˲PEl3IΥq{onA'm,܇ y{2M|~BH.,Rxsw,JfNzf6Ym#fkkFٙ)S@\\\(ccct7nٙ3gX4zSNFb߾}gYxs㍻{-=WOrfXή cȲZ 7e|[VLRe=k&nVXΎ1O!V`_C^W?P.gOOX&6N;Pƃp)?Wbc}o:[ic'1cG>yN6FMӐ)TY*"!.ѣ\;?FeϚ_l$qiٕpbon} :YƶP >M/jx){*'GCGV*J~eҨ*R-ۓX{d9 _5Rϸ,C( ,S}}eI{(S(C&/DtLВJ͛s&EtlZzAxk`5|U{1EݤUe6u kudW-8mP_ WNdʸkb*iA+k݇\}|d8,qpT,!ˀBh#F`ȑٟ\]pҦM6l@GÕڙ5ޔ}^bx~zHPfO3Rde7{Cvm[5r GQG^Dxx8/_֤Iy8;;sM ,Ȏ;(Wxxxpq+{\/r .&eľj燧':,TR@hh(wjժؤRL^^9yuN  dsNݻp,/_>PAAAɃ$/ 9'6\\\\AI!uΟ?ٳg-_xb]ܹs./d/ќAAa% DKA!  B .AAgRA7NNiFrGn/\ z=QQQ !`xqK  9L4AAjq IDATrXmpɲLB|, N  Kɳ .0B5_, \JN3ʽ  Bv7ͫeY&*'\/\A8ڨ18qh/"TU(08cRԩ^JI}?<2DD#@tB<638xРnUl4j1  /-O720me5&ɭ.g݂쿯J$>AOk Bke٨\/gXl 4k҆=BRlw0f}bmcɠR7$[  <|I1" :؟i}AhoVvdޯ4[.`㬾pg@9 Xn-PiWNŎ4WmX/a2fnAYvh۶KӫW/*U )^kr+QuHY -5AT/W{{{\aƼFe$3`{ f/,ոG8% * F#zkkkf3FJFIXl$hjzL&SefsMZJ-^~YIwqI*[k}uߤ$0$dL&3Ӧũxcgi YA oXҥKS~}/^̌3زe ~k.ڵkǗ_~IŊѣ֭s,Z(ep)_28dHX25myC(U|Ycmo)Y-8>Mφ:ӿL 3^hQ-3' ϯdɒ~z.]D6mo߾ű`|}}r ӦMxzzZ?pIVe앇\ZVK&綣gvtmm  dg{.>#2NY0vfNBZˣ~rcP;P§@gQAqqqh4$IB"IRSe +6CmYST=nt ;'J yC:u`ԨQ///:uĺueԨQܹs"Ep{Μ9C>}Xn]ageY  6l ,oߞ%J0vX ׯ_gӦML&|}Uҥ z>8w\r8}EPYV9%  YqFjժ1bbbbXx1,3oZ`ǎ4h;;;ܹ$I-Zs93W9wUw[:XQfe),Yw ۍJS8gMei"2.b%dz~^`[dKzql^7PZS~pzʨ'Ψ)Qʹ?jyR(x:,,K\>}w$~:hV$ё88Vf絓1:|)JkƤf35L|9יV@c4&n0|}} ,,`L&Z/&RI8uJ;;; фEhh(ӷCi>6!Ϟ`\,X7MT]|qqq #AoD2 &<"$#27wwY*#::"c&<2$M@@q  W*Owa9B"Sy|*5Z<>M|- MtT: }B,AAvRc;weՑY2|ոgƠ' 8$IBD,#^gl2d6eX)!IfbbbL*[3LӁc1 ›e7]ǏnmjQ2l..P K~dɜ1/'ұ?tMFBCC dNlk= K &.>)D7eYt1N@tt Qa 4%$)HC4ш'aaaܽ{ׯɓ' _|ɟHHH@;z%JǥKh4rʖ-[}3\kbVf:'ӡkGa/fά+`0g';5ve@H/ہƘS0k@V.\FF0`]\ݝ }L$+F(U=OON~Y3Hșbıyl="m~Ƶ~aR)ιNcnjXs%[nF#4NXaM^CkW&'qH<&t:C0l;Ưk3&R[D;Dizqq8ظT՞b,T=~pk:4f{ˉt_2~3YK!hajf9ƅ5p+4 ;%Fq.if|X;Fy80?j!ͻl{59t666l߾=yڅ ]VӡV Be)/T*9yW^EpɌpG$RCs`PZ|?]!!A1,W{6%w]|J@Z=W~Y"#0بl`8P` Bd /K^~q EU2ZsֵSP*FDMƝfO,!)$.Nٺ~[.g2@B(Z@R~yٸ2R+At_e< +4r,׍;P&ph9t-9c3XuN޾MOA<8QnԶ•)H[E,Q;Զ zFIF! \_цkf{ [V'O/2Po~\RU},CӴxq)ᙄ9?̫ פ-_A58"9ʇ@ecC|nMa\]w'Һf%f:f,VFK|\ [QOT ռ,~V?7ʭ[p-1.:BGPYٖPX@:u7gB2/s,8ھ+[ G ejb`qoTtˣxpr>:Z;hU];kܼsD qIQxx3GŊ?Pɧl7KA4QIY?D:l}Y8>=5EF\g?ƩZ>/4?MgӅ@ilx;OJmM>hT)0?@iCyH}QQEK]]Eb-Khޭ>|io |||2<%P(Tlk=L g &vpq> Bgeeeџt**B  N,c2~6^^eYd2eq% ){˗^{N@eLfX$Y!u:$9{̦t"d4|^'ј!Ðd ^-lZ {꒐1р^ox:,o @!sAtIɒ%6z g_t̙<"lu\`&:(J(M:KɳBx(OE > D~西oc;1`,iXn'pm\θD mw6۠RFUKWF)dc'M*1oq"^TzYƦ` ^,9+ai_1s.+Vs1Dgg{RN8F̑S=YʾK{(WƄPݚ+SD!Y̓e9yg(NAJWAbh~'O YX1Rg/'ro N`!|0r=G,Ȝ|X}ӷ=G]g2jnV1"q6)mt.]0j>?ɘ Lr~$nu3Rt믰yMjN]`Q%NH7 }L{) QM'e"oZ߳dTϦ&9ZKVbchqşBȉ`Sc":Fˤ#,U~Rv GCbRyKY,_Ax .0Hf d/}< G6 Ce(S!#z5\w[W|Tc-;G$c$)y<4mpwRpU~,)ބdD2ZTX̓Sm!t[ϛEz)7c(^Ne} dX&#&7^!<ILk֠n=3&T2&YAݳ Z*XKk>]ed`ԧ[+72~p#lgof݉(֕$3$[Q@EO޴Ytvx2H3Vq/|Z`^S*ٜf"zogԥ3ɯ2G9+bC4jUjգq&*_2EI@sسʗ+X7:hkvʕI_ppʖ-h'V˟˙z1>2ԭ\NF ,6Rm8`ZUC2y^=&qٛ(c4JǴW@A|4徦"<T:5R\iJ]KŊ9KTP1?o/tdM:Z €"=:7M^Nh+e|l߼ k4zunNZհ+[5YK}^+ *쉎̹iQ w೶n`-TT=7"qp+̲=mSAUiT")ŧho,bآ+.`wof?iP3M,KJ y';>UrǠPٲy}ofuɢʖlQ~ԿeRճG-.t ӘiU*WyirgTs;Q1RcڣlmSp+EٹYr]-X[2w=eJ&zWj5&29ƷEEwg>/U4F*Ug_/OKUy_z-F]/?K}oy_z-N}Mɟ?-H9e"3"KgmKa=>渲 LxpKcn1xa˜l;+ìD'3f`EhE!0Sǟr~Fӟ5/O+ݱJmNG 2o2ꈊ+Qd%N(3IKr yG```NN/믗3DTp+oCoӨ1%fJ%2`R`6cdT 6jIUP+HLYs34ey7' o!z@B^#\ŧ۷oCFBf慃,7i˶fSXoՉgiߩh-挩ma^nCVh~V賌{YyBIBȴqٟ' Nd -q ( +aQusl)lύ|C>)c)ȩp&ʺ: 6"vl{dץ(:,( 4J%V"^Vn .Ax^DN&Ƚ yxW ^Wq vqZEɌVڊp+Q$(LaSw+t %0^VM97g< d֪wwU+xڠQ)ШTXk_3\A4le4Q5@Y2c0z!ShByVϝfUj5JшBF:˓3]J5QNoDV(qCP$S+ߋzPUJLzq: A<2QΨdYFLL2VZMrɈTD"IũxjLenEP)X&%>R*;6%MI&VI˪D%ul0 @Ɍ=,mo^+c4eGոFt|MgeG'(ns$l` ihFKI|ӽT_pe%UqǾDAdÿ,KںU[eϩr4f/Zb^FegXt7W 3(ݢ=wBqǍ9솙%򋼹)N?~q]rKV(\0dBzz{+2.< 0NG;7z{p+27[H4f ά3 YV:5K}zΕf)Gґ IDAT,qY?m&?ϚǢaW"F}֏;iѨ}>AɨR,Aϕx(СGS}2&/It:4Ğ#'ܯ ?NWXۇ6\9x3B.nbx]9H -(aط;K"S(vo\ʮxӏN!$$?^̸ɿҮae8.nr&3+ tR,=KA TOɤNvkvŭKxkıؠ*%t)9ISSBE C"*ef/筧 ښ3fЪU+Fc0W/[{3}$v< 0^lj0x'ެ~pFGq*(H%=Ns,0e'u]Ns"(&hp /L2sa u>z,X*~uԣ\ؿb:7d$!|X#ןc2SװkK33F 7F\iOΘ+`4cIciXZ䄦E:]TYyat:T3kηtQՇ )G炘c|5w:h-\z?n ZG~r5jS޷(T_X6w+Ǒ n]O)ׅ?tӡv4%cQ<*]΅'s(3OO3Grx u61kB[ {fĉjFcR?ewv6TǜAx2/ΎfxzDKxa dD͓ ~z/BF#t&LfFSQ O\:-xQr[avؖӁmJLz I-e=L7BA@6HTZ3]ҙ*f#qqq.a+7k!NVz%\cX!ׁA1}bu& #e?u#R.ZJ-G\mx7}R!sE>%:}qJl2a6IOI%N,g@lAf8>!UV+W$IrfGlM~b`WL4,Yv-Ň>`֭lݺ{axZZDPӞ/Q@Z֮_N4Z/*+R2n?s] ( s/ Ӥoє^gѣ(Kv>gOL\j5Z(֜*/i4ӾF5*VO>?֮/lҨ444izĪOե!\bW7;lZ6sϤ y%~\j5w&44UO%n|}I9e-ueaᤱtI [Ӑ(ۃ6`@83%~&f#) /BXUghuד)\YJ9Y]Z1=fl̜1mMNlP3X32O7xk^@؛뉮&Ϗ$`*ki,?ׯ_caa9H{ SqJ ߪ~1܂1Eǚ?I5\?fҤIT*LMM133!$R4/K=Ԩb_ZG=/EΟ?ʕ+E LMM111_.oBT_nQqj%roYQ\\*hDFcS*|eY/QDj ATR'~|"*:51AJςVu(RD EA@Vb@j/cZ_TQ41wS %AA5BIHcnn5"ْdX[[cUX"kkkLMc6fgEj$RU2z@G+g˹6ʃJ] EQ/\Oreȝ+f-ΒY,eGzmU9-n}Q#W3kcnD#"[& a ܶutPKSL?Nܔ-S3$J`Tȸ/\3~삠aTZ+rV*:жPJ_:UJRC+gRjUwJHp\EgK'rd"K a cwN[y'q4Yޝb٩[p8Ыj<^16?2Rw2J|aoyBɐ!5jԠrPfM*WLƌQUVʊE3gNgNݺuqrrhѢ:kQti찷ɉr-Z(UT‚"E=cTOmmmɕ+AA_ňMMM  2gLődTV SSSJ*E֬Yl.TE9`I6>lKf__€yb=zV/8p˽np}-C*.ڵݰJMׄ󦏣Ť=ۏ"Д%ײm6`}wF&'lJPX1jBFe?FCQ&CWaXF<;f=Ryfùp&Xgb(S 9 IؽNa&´ Iap:=Qh?SHܾk_"eWX6~s~]==uYEώt,M&fLOO4T jefA_O-UJZfKkYdݻh_/հʕ+cffFPPJenaTTAAAhZF&ajjJLܺu+ѵd2666 [RP$5o޼FZܹsܸqŋ@ܹMH5*pA|ܿ""`! Uvմ.(er"EA!נѪQk4jS4ZڒLy[J4.H^HayOoq(PXKO ڥ>5{:M7$?ZI综u-NnX5{ToZ%KSƖ^clFhH{Kr+ǘ 4*5 ZcZ\|Sعv jkį9+-O;/yDh?+oȢenD=KJS5XD052nCq-[D// 1cƸZ-666XXXh Q LFPP>| w:W|'gώ o߾FoYdIߠ 133CrIsXYYQlYrE@@Z;;;Ȑ!C6_JCQ!T%N'5l@PgӴtswIpV8CjPř5ڲw8rWS di.<3kcj2wclӧy1;fĖ.~{.g>j̇ ٜ+c%_^dqb%ȎɓRa֔6M^#c3/`ɘ^,9ۏ3d6i 55 *zNl'cܻ/[`EelwM˧a94B"_>ftqϯ9;1|4e fF TٞI.SۥKvr&\wU^ҕ ooxlO:Z< {h Y ?\eqܾ};/_bnnΓ'O?gs^xVMMM 4xׯߣGΝ;ヅVo/IK"|5A]?ϞfS`_'?YN3[ӘyYӔbhʹK3X۠ xst+!񫓘׾}طo߿DB$.5̲0`(fk\ vYӘyY39]5R;Cϖe{%$$$$t. 킄7#ݥ(!!!!!!!C(hQXZZ F1[2SLM@PXZ! "I-JyPA ٠R*01Թ0Mm J-,MKHP"ֶd25j?R!7*/VB41T+X{Fʑ3gWI)"QQXOsMU 9jӑnp(T17@&#I_t|D[Xaa'kcP)娵Iů\.'8BmV^mp*_Mgޭ ñ4*esBbe]-* wr2Ru>(>!=1 ŷv8uRdX%pxúhwgOtT:L2C{B xB4sE0kpE[)sC,@eNxmjg+i̊"j0{kVIz='ĉcx_Ad&8_W/2 DQ ٰl*n ڭ}pSѣV5d??/}PyӨ7`uУڨ;OB`hz#T%vco4mԋ5 RlCCkЀmgP"EϹ= zzw}ulX P]JMyanaӫ!5Ykc^ K օįp a[M29Hy1|MVNmܽHgDŽq_=1 slm0҅ːghpSSOmICX9?2q48bv_ߔTE ӸqSʸ6D=e)!oPxVz8_L{k&mroR+69,3cnfuL\X&k.|OI?=օs;wզ?q.sWE%p8Ǯ.fmے<ťxQ6¹lqyIO5Gu9e mgk$$fgzr[/{}~o|yџ^X/iMru3a\<~YALu!!7\֙rcQ`,w/ A$/~_?RŒ^uUi^fX#M0 @VRkQk4ޫѭ7*-Ki):;ݭ`tVѨP͙ډ2.0r"ymu#rP<^gˮh܋M1kd+n(C$j/>(@Zň@Fm,>P5J9vY20ɉcnk㧊s+ρ!ʸ$U˟Z5s.s Lnƒs.U)u62[7(ڳ={43˨\م˯C)զ'KW5 KޛЯZTuxtr-5gBş Q|˗[FyR}uXTv Ki7vs5.#+trMș+g90,ymkcn,FIK܊|'gZٱzVܮNqi3y(""r} 16cQ'5t:yI1Cm{ܾ\;BT͙k|p_Lg%ofBr91WK bbn˟FcF^5qqust¦2>X*o]aKܸqFrյKȓ;T]TJl% <-HlIzuHW(;:?/m?C17[?> Gl-lŸ-㺬,N5j[7w/1n@bY0q4]/$E}K,6Q@J t﷉aI"U~%&}%6:Oc&JӃv()mnRNI>klޓE|LMi avƗƜwc^HʕW# 6=fMɑC7s޼yz[lܸ7KI$$00b``wo$~> 5o#5M`{o7ӆspϟ"5\?>F_x\Ⱦ_% o凿KFjv9et4jM5 Sh1h5*/OVAL\EVI' ZcƗX>5*%jm.Nm>4DFFT'CBZYbQJEj$ ŧ8ERŹgyY^Ғ7S!e(rh;5|\k;>Q>pFJy {CP%;˕coˈ}HzeZO>4*Sr`Wp)Snm  gFT\W5b _!=R'o(>|h %-i(/E%m\Tɉɫ}AZ. IDATL[}j%9{bGGVP\)eNMS~[= c.o*Oq. kصm G"HrɁKY5y$=cװA.} @u:*0)q̕B^ocf*Fv=[dބn<MCg"c֝,?92i~6lIvw]KG10"?ܡd>4ϟ_#빆Nn L2fj (fo„Q'|*?0W#o@>(]ҍ#qrɭ%Ju\^z /I S_,ۏ фZ ZD]x! 9.%꺹~ AgIŮ `b-UISN^<'.x^fszr1/дnuZwAJCkhռ<4&<:b= oEKېebqh޼%L{KGsͤN|seaX MfT4?.nIKi)A#q/3~EPې޴|2^)ʘ>to74ȄOWY&W84fz?HƝ^,qD^T.;66QAyߵyXJ >c9{8vNggsA捸h:i8tܸw_rs%-6*awѿR~5DL5"5&sLIӸskI~`djl*i|{ 0s'":{v?0g>o 36~kcOBBB[>& FFPUZlIWŃ( P"+t5 |m Oq68&VN,G6SZIe9w.c2VQ~2Q{ކLz$c QJ"dxNw6:(OBBBK" ժr#$jf Nf_ff+GN=tҀ>wP?e̊]zۗ!I kѥV |Sݐd0nMF_ۙ.mqNUzysaa?_\:it>AFaq ݔ6(L3r8~“3^qkL]5+goaw`k, 58-c X c<Ƙu%u RcOg;> CH wgIAK"#"FIx\XϤ%ZtFct@/ID.W3EECPVf4VLj|VVQ@!WbrUP(TJ;+Ay VUⱄ(PiDQCHH!bD&)L1 cIIJHHH"5\߄ Jd달sQuBOCbE׏P֥&uvUX,Z>țKgxEhɝR* RѩDQԬ߉?EQI((SɃ K0ix{>+cT>Ȗ<{9'x TXo//R|U mGZHrMu??M ԩUAl߄U+#[4hʏV5RsUۍBZmM秷QPݕkCv]dpvԪ׍+K8(LҊ]7@s<(K=c%1QjWBBB"!R%MX)Pr ^M1w|)3Wcn"޼9~?@oE q56 ,g||vu*x%B_KQd67C2&x4Bٺ]kػn?qqcpbh҉w|of;kn^=Fa;s?Q 57F0Uk5ez{zXeQTmؖFjOQI'PzCwp b r6lې4=ѷJunz9w6ۖ#]?Q!IyW'WٶϛVYnNFI/jƒxp"@P2}PZ^Gn3YzKIEy-ʌ0~D&,&HKpeChBWJp~e;Oy/yy=Z_,> 3 [8ͭS/fĩmOyokWBBB">R%M$FFF+՘cm!ȱ#Rtpކȹ p^B>ETrP"~4tgXZGThj \AtVL#RQUئ?yċzcYgU4d`\9mɔð{̂eУu( &\$"*붲eƺdg__V'Bȣ1Ej8J&ZcN&k}VБ3f~ǖödjE#Y|?߫s1_g>FE1:jgޖѡ0r,GlJ* J:A~c :=/%("y1y":3dK =2"Y"2Qԩ;SS1|ZftJHHHGKQ"MEKs5l6;nݟg2o;<[0{3Z?: FLMnޒ]PEߔwXTlc8CVnF0moLY>L:Dv=ނ#+ sւU{S\3UnZ>&P,{x2JW hFd'E3ӵIG|]jhgS6R,u*NiZ.bvߎtlR Yg͉<5k9deqg>Ѹ1krfusԩEOcilej#`Gqpf 0n^{dRʼnWРuo|ddMz-37撁nZ𘰌 GM]ΔG8nf1[ewbB^^+-1[2˫;pS=^˹xj/|"gknƝFsӥy F lBDX4Ջ׬%ԌI!vl?-] H§iBrBɉx&&diH߹p\L8U" }=U3$\Y3KnԌ%/iYw$ HNLO1(l߅*{ 3Gt_3AGǵM֦ߪ㼼rJӰ}gBr祵 hظ Ek\t jUfLjǛbiToَ/پb.K DJQ󏥬c;ԕ x̤qycEs9s5GTXJd=#eW1W`C?s>u6+U;˼[g7L=7ҿ@NmdῨX%-}qkі3{%,5qcUj=QYhH5ڐ,3=S̙:3#|-_ 7ԩRϹ)߲3NcNN3VURUh2B"eoAosO݋`[l%G©f;ã#ϙr&du:cl5o onF4ĄeJRsP^3;4o6{9KGAk'2ad&.66_> K1km)gfϢ?C̽MFj!R>b[HxIg맑B8uֹѻ( }Qg k>f. gfHQrC!iWҞrq?'r@TX(/>ėѽ<8b*!| ϋ N%Te^șOO7"5\FVakx,u'&'*ZN&33\1k+JgZJ5R ZЛ'"~,:9frI9x{8'臆F(P(% ^l>OwLhjEl3ۡ4A\Lg^y&KKF"+ɪym޺FP)U*5GîdʀZ+ SDb ,>m#$DѠUiT) ";ςqI;VX{vyP ::=Q4O`t|2Ss rsJiػ<<}l:UC. /)S}/$&_FRl8ٗHkK"XؗxҾyIkEVmhќp9d6 ᳖gl+5B(\<6f`1'QPNgDyJU+lدڵdS7JVw'WAV]xA eҠ~#dhÜ4W;.E&^_X 2++ݲLO00&[8_!6oچwe 89o(<y3Z^!3Ec%38q zL.f QqI͋˅/u4ޙY꜅ZSq,㻱 :nܴ+G-7eںQ]8ףn㎌_:pV5\)^'/VõkQv"9dC g[BBB@>H5ɉ~omQQ2vX'/8AlJ77w!sF4( h Z^NU=牢⩃B;0clsTfǯ`@Ø5% B>7OKB;9gM~y?:R#)KHӢ?< "?to !!!!!!!HKHHHHHHH'm=#n{@ȣ?Vrs\|ewACBo:yTX}EydJ? U'|𞐈hS:a!f#?JOC/ɣ# $S[:-h< JԪ C4Bhʵ!Ï".Q]iӢk]m$ m؄v2d No^N{wBGBԯW K~`.Dwo^-vvXG}w(c殌OB {r9c;^aGN'N%]s]'*8q?cn e\"oI>} 4/m]T^O6|TUEsYqGoX{!s _zbعʓ/K[P0IԖ'틊=:[w=%a^bQKl'WrwڇSf+\zO?Er~zNuGq6x.]kI&V9RQSfMOvS(`_Sfaʠi\ r68ya3ݓr=hV>?o{:,uJ|ӫU}j8ףCVnu];׳{.cپK9sXp! fŭEG|^5Z5[{?ZТ$U W1nECsA_&P\9BU:d둓hdɒY;s$k=>L֖[se9(*رqNqLOt<sQi{~|=ٳ{3|Ӝ߶b\&,s\u~ŸrE1^v' K01;ON'kG6q/AVgsI\12K|_~4z5^ui# 7eK ڞ#Ъ .ZKc|d16">ߣKt1'U?2*EOmX7#wJyLsا LgED=hfpoG\c)-4cEt7`]HHj V4!0'# Ӳx6~){aAP§~Egs\/%i\!%&_F6%P# "f–çy}qoZz#(}vD![S#+jBR%Hz9`|%7.Yց]b Җ+7% gn;kܺlĭd]M0g>dh̅j-K)^' /^5:~~zF91,r/yVpͽu3 (𤎟c. ʻcL.k~YLe-_ڐg:yI}ivgW(rFwi&Y}/ʏu%ŒJZ:v_'%R^ JN$` IDATsNՙ:>/.fo+)P,^!B@z9OkÙ+=ɡm0Sh.$$~5~+C<y}V]#U f5hQ*9'Cʐ}c#M0 ٦EMV C"א?gAu4Ȕv#'cq/L!arT*r)ۺ\guޒ˗/ %q0j-Y7}<pW~ w/'<-Pn=[)BPF#7Ӑ.S^={RB-Ӝ;[T&) uTcOYR $/4ȨhrQ±ZOQc,G):(:_^R|Kk(^zg$.eD 2l>u[EB6[A-SKlc<(hd13ǥ^CF󢙣NY()R;wƑz:ƐT)r3Y6DDu2fPתIow7}쌤BBWoua tnߛf!.ދH4X(Bѣcokiִw3u}Q?#M0 ! "wBXf}cƤS0 zo">Yv-k׮echD JvAmS*sع3&X!MӮuk6̻kҥu(i"EAϷ"E32ookǣi[󾡭GL٠Ʉ\\I;.OEALs{dm'dF,B@O6EtU;VYVG\ D0s{N t|1ML8͍CՂī%0̳Kszr|V<:$H鱌w6USk%~o%CƎ֋1%=Ptuۻ'M[)Ia<1_Z`Ň-b㳑u!!+C+ȑ1)K."ƦSMqlПرwfƪvFMS.-̝WK(sml|iy7v(m6qyD0>5W>X L\!p}pI|$7ٷiC՟I!5\?>>p,f?{~dR? ďQ5T)!:"5KV2Ga1m'`b%p8#/5mKۅ_kvUK4 Nc󗈺-2c,qIhJ ň3o-}GNhBϊۙؿ9 ,Di3 ټOT1{[.߷L?4ɜ˱ki=]X?nudwعd S'vV\C~<^uV,_nIqZӴYE1$u|M[Eqg|vԊup(:q_foŎ_CRuغy%d%$Mim '|>]pj;nYlO!.qw ڳxj+ҩSB-g=5ş9b[W|ԨX5*Suu_Lz<{@dH|$}qLaJT`֍< 43qG̣pJ +nň^y[?bT$Nz M}|߫$]-g0G΢57\ C@a.KI+"?ŀSt#GNoLhR&?.{?gMB 3\}'s7b5lcbzlZC덋?~NȔ)S|o̚2^?ϑTc}j뵽@}xQ^?z]8t-,bca ,oeT)m?ٞB- \h ś̘]F}۱(r9up%&gƭS<1O>)u☀bdu`j FէCZM)\N|dFͦHx\(F!㍮Nb%yѫ-WsWu@Z/7ѯF!s)K1eT,ah0e|\$=9` d6nKrw:VS@' PX>vL׋?-_3fУmUL9=ܮewXp8ل'r8VIމ#{JȸcysqdLR=.Iɖ]6b\N܁d…}|yGQ'%lT+Sv:gJ!4)N֭t/l(6}ThޅcxM/tۏ^_|~gdpn݊cOHyI~\4={YU 92*Sƿi5y;gagYiϟ !/x¡?nףw>!ă!k]q9l)( Ndńz)݉ⴓlx(.bbb35_SJ!9)%>SbMLvS؛\EJrN=ײ[-NԹvuVvIJr vg췋m7|#EQi'%ŜmNɌ) XN͛|qyRX BIwuWLz ;wh_4i6?sq>~#/</B- \d{JG#1) @Ji"[0~<`Lw\j!t\[70GEo>'ּ>M 1G|2bԯגϿ9% #:qv|+W̧+y]O7T z=7ȵke*Ok%.ZAN,UxZyX6e(o5md yKWOvYqҠe{]BAwBoL1`JjBB`~N@gwS$b |:2_72fdZ7ALt 5z<=3L`&# }lloa´.ӟz՘h% 6oUfmȰVRR+~AjQTcNEg8&=}-zNfu""ʦqy3-=1jCfm"G`sB5 \rӹ[c{}|4{>6ҧ&ޝ7Ow!9reX)m˿l&O|4cyq'yQ:cS*47,aռ4/L3['X~KbpzoԯH\ՑƝ{㊾f]Z Pj@žs8r*\.p9܈-NW/ :L;7WKyx7oa K6W 3wMkmzL;EQ+5Mܟ'3|۹SC}v:.X/cv:=>k|zOQˢ>~Ï(rmJߣ~ !.qW)՜ f.gm 鏻|Wn/dcINa{)RE % QHqnȑ?'?ՔDՉLRR  lN, vǂ9N\'䙨tzG!h H|=G9z,Z|Nӥ*Ҡsܼ"11d3 ,|# ֭qB;E:ӹ=tRԨ"J?Ӟ ?dҰY$Bޢ}v& }H:-ޣPaT) 1㞩4 Š`frRݸ^Sg\7hMi?)Ȁ[p\7ou8{R/Fo/t/5߻-_Aq)AwF x +j)EqO(®/?_Vkj=k&y_z!iס;~/P$gYYrtۙb2Jt_70{{(=9K0Ev ҷhNEu*-@TO)|s%9SIVUyac&JPXAURUgQڽOB$Q@*2o8 Sbd¬{#+ P8kOx[ Ġaʢ*O\?Ke%iמuAD-{È7d|vk_6dD O8bgղ]JWʥVTyt;sLmڌgm #g,|8M*m ?_c֐넛<&-Xؾr2]UwdnF HXX{.{6s4\o?[R3մ+`!tl2ytE `eb^/(uJُBISqOdTH2"z,3Z|pF?{R$UQ1̘4#Q.4c2W9A}˻]1l8@hkb|Y"z4IL^M >lo43\&k0Ȗ cfT_^[30BYjfsEQjAQNz$h !!Kߒ?t_n= !=G+ q8(xfNjwem͔@}E!!& ͉ %ń+grZSq kF*ݗN!3%'cwdP|T縬~Fɜn_qq71Yɜ=}NY!$p,S3}ȳlNHm>_JNh(9EkرnS뿱y~?viDz L)Qm۫#}ow_$':w":%͑RYEHzˋs){RQ4([V-[29rk!:4͗ncoޜ-QF>ۿjqZ_.kw{X~S&mП74e˖Kz?G]w.؜Owo~Z<|dtQ(DݲyBK2(ʎSj/\1'!MÍFժH\b͚R ]#GAfNRնB|sN@m'/'QGqSɓGH'Ǝ?i|9.P؋.}{YOQ;ڷ#_g0pv:CyXvOk?Z%}ڵML,]quK'޲ ]t')mN$3h5k*^apإ@`(6l`H~`hߋj-Rh U?mz(-UiU7'?gV\Z1ԭK!<~&[cԫ4!MWN\vl}Z5DMt k3v!dKd >=fdѢT.L+V,%hRU܋&sSFMI:1=U߁1n0 yxr|:nռgϘIIޚ[7Z'xa^V>}Rzi;ز|ǶB3td8Wh˖NSo@_.>qoOPiݻ{WN'Lca!G`,1u\ O. >Z>}ϑqWZJqeؾ_#E՗ ~^R\,ٸի0X|M*z3xdڿV[,:4!cgܗgAelrct# wI2N /9oٝV(FXwzTUt\  +J:;fqhu8\̕ `8PqvQTG Cng֨$%)\@߻b*l"jly{ jEɜpKI sBo rB{IȲx!:_C65?"FСo -̡\cˢ Hm>sOR3T]^`OΆ'3G mR#g?3&ڿ^ҳGΘ 1uyzvʆozl;m" 7/Z*#cVukЕëpb!."D 8y>8 i3z\KpYDDDpR" ;*ffCS>#5O$>ٶ%Ҳ IDATBwۊ`KHkwDtia%~qSu8EQX:{gVY?nױ:Khkm^* H qHG(|*KLߜ?O;NGGT~'̕+Wv7BLɞ=x=+GȑawC!ɧB!3 \B!RLII͛Y!@޼y |dgB|8Çj2uTV^?~uv7nM z)mJQkJJ1tCMھgJQ=@iEtb{J1͔f?sPs$p !"} ri9tO U?Sv<kmhzJQ}qFQ;ϩgTu\/PIB!?[r7RvL/`y ZFH SwV9.!Bem>MFNd5pe DF[Q-ʔB!p98te4mKo.yg6].<һIB!yGJJ[^ЕO)zkHo*QoK=m͖8y %Bu&6n4cFYF]k^ \: ^.̠Ӏ?J;Em#ϰ$p !B|K=WT2ZǕQ躳Q[G!۔Bt?6f}>-uY7 \B! Q.o!:^JYwk\oA;h-כU,c.!Bܑ:DyR-Ǹ2+pG+7=ҏnɧB>ڑ.ꧯFԆ7umY.EkO\*D{wyy$p ![p^KMov]—å7 `AK.!ByhW4Br^w?vdU \!Ex \ڟO/*L!L7pY[rSp9tn^`[h^!Zz浡KvTˁ~VyYo0 \xި4.!BQgK; sK?h^[A/xȫjM#zRB!#tiGAWк/#]zRTsO'-W-?OY,/B_2bfQ.m>e!}):.Zz!Ksw;2%B=zk܃AzSBR?ӡkY8]ϥ^@{PjmIB!/7"浕-xmȖX!B5oa |&o [?}nA+ͻi;H17mp !B͟.m@wK^K^2d;Yޚ+okx[/AK!7x4|iGʼ?-ԢӊnzSc[xyM!BMoJoCa_~p(*_EL3 \<B!?O =p_>z-cF>-Hy~]! D`cxrUF:$ !Wpd #_ގxd2pA(dz5!B>c0_e*lA=j%B-_˙|H\ [ |)CcB!'|^|Y [p+/^gB!/= ZnwNF$x !_LAurgd>DIB!DF2hB} \ _^B!?EYj=pym8kAL!"w.!B B!ߝ.!BLB!}?0Fo?vrIENDB`flask-mongoengine-1.0.0/docs/_themes/000077500000000000000000000000001375611115600174755ustar00rootroot00000000000000flask-mongoengine-1.0.0/docs/_themes/README000066400000000000000000000021051375611115600203530ustar00rootroot00000000000000Flask Sphinx Styles =================== This repository contains sphinx styles for Flask and Flask related projects. To use this style in your Sphinx documentation, follow this guide: 1. put this folder as _themes into your docs folder. Alternatively you can also use git submodules to check out the contents there. 2. add this to your conf.py: sys.path.append(os.path.abspath('_themes')) html_theme_path = ['_themes'] html_theme = 'flask' The following themes exist: - 'flask' - the standard flask documentation theme for large projects - 'flask_small' - small one-page theme. Intended to be used by very small addon libraries for flask. The following options exist for the flask_small theme: [options] index_logo = '' filename of a picture in _static to be used as replacement for the h1 in the index.rst file. index_logo_height = 120px height of the index logo github_fork = '' repository name on github for the "fork me" badge flask-mongoengine-1.0.0/docs/_themes/flask/000077500000000000000000000000001375611115600205755ustar00rootroot00000000000000flask-mongoengine-1.0.0/docs/_themes/flask/static/000077500000000000000000000000001375611115600220645ustar00rootroot00000000000000flask-mongoengine-1.0.0/docs/_themes/flask/static/flasky.css_t000066400000000000000000000124151375611115600244150ustar00rootroot00000000000000/* * flasky.css_t * ~~~~~~~~~~~~ * * Sphinx stylesheet -- flasky theme based on nature theme. * * :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. * :license: BSD, see LICENSE for details. * */ @import url("basic.css"); /* -- page layout ----------------------------------------------------------- */ body { font-family: 'Georgia', serif; font-size: 17px; background-color: #ddd; color: #000; margin: 0; padding: 0; } div.document { background: #fafafa; } div.documentwrapper { float: left; width: 100%; } div.bodywrapper { margin: 0 0 0 230px; } hr { border: 1px solid #B1B4B6; } div.body { background-color: #ffffff; color: #3E4349; padding: 0 30px 30px 30px; min-height: 34em; } img.floatingflask { padding: 0 0 10px 10px; float: right; } div.footer { position: absolute; right: 0; margin-top: -70px; text-align: right; color: #888; padding: 10px; font-size: 14px; } div.footer a { color: #888; text-decoration: underline; } div.related { line-height: 32px; color: #888; } div.related ul { padding: 0 0 0 10px; } div.related a { color: #444; } div.sphinxsidebar { font-size: 14px; line-height: 1.5; } div.sphinxsidebarwrapper { padding: 0 20px; } div.sphinxsidebarwrapper p.logo { padding: 20px 0 10px 0; margin: 0; text-align: center; } div.sphinxsidebar h3, div.sphinxsidebar h4 { font-family: 'Garamond', 'Georgia', serif; color: #222; font-size: 24px; font-weight: normal; margin: 20px 0 5px 0; padding: 0; } div.sphinxsidebar h4 { font-size: 20px; } div.sphinxsidebar h3 a { color: #444; } div.sphinxsidebar p { color: #555; margin: 10px 0; } div.sphinxsidebar ul { margin: 10px 0; padding: 0; color: #000; } div.sphinxsidebar a { color: #444; text-decoration: none; } div.sphinxsidebar a:hover { text-decoration: underline; } div.sphinxsidebar input { border: 1px solid #ccc; font-family: 'Georgia', serif; font-size: 1em; } /* -- body styles ----------------------------------------------------------- */ a { color: #004B6B; text-decoration: underline; } a:hover { color: #6D4100; text-decoration: underline; } div.body { padding-bottom: 40px; /* saved for footer */ } div.body h1, div.body h2, div.body h3, div.body h4, div.body h5, div.body h6 { font-family: 'Garamond', 'Georgia', serif; font-weight: normal; margin: 30px 0px 10px 0px; padding: 0; } div.body h1 { margin-top: 0; padding-top: 20px; font-size: 240%; } div.body h2 { font-size: 180%; } div.body h3 { font-size: 150%; } div.body h4 { font-size: 130%; } div.body h5 { font-size: 100%; } div.body h6 { font-size: 100%; } a.headerlink { color: white; padding: 0 4px; text-decoration: none; } a.headerlink:hover { color: #444; background: #eaeaea; } div.body p, div.body dd, div.body li { line-height: 1.4em; } div.admonition { background: #fafafa; margin: 20px -30px; padding: 10px 30px; border-top: 1px solid #ccc; border-bottom: 1px solid #ccc; } div.admonition p.admonition-title { font-family: 'Garamond', 'Georgia', serif; font-weight: normal; font-size: 24px; margin: 0 0 10px 0; padding: 0; line-height: 1; } div.admonition p.last { margin-bottom: 0; } div.highlight{ background-color: white; } dt:target, .highlight { background: #FAF3E8; } div.note { background-color: #eee; border: 1px solid #ccc; } div.seealso { background-color: #ffc; border: 1px solid #ff6; } div.topic { background-color: #eee; } div.warning { background-color: #ffe4e4; border: 1px solid #f66; } p.admonition-title { display: inline; } p.admonition-title:after { content: ":"; } pre, tt { font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; font-size: 0.9em; } img.screenshot { } tt.descname, tt.descclassname { font-size: 0.95em; } tt.descname { padding-right: 0.08em; } img.screenshot { -moz-box-shadow: 2px 2px 4px #eee; -webkit-box-shadow: 2px 2px 4px #eee; box-shadow: 2px 2px 4px #eee; } table.docutils { border: 1px solid #888; -moz-box-shadow: 2px 2px 4px #eee; -webkit-box-shadow: 2px 2px 4px #eee; box-shadow: 2px 2px 4px #eee; } table.docutils td, table.docutils th { border: 1px solid #888; padding: 0.25em 0.7em; } table.field-list, table.footnote { border: none; -moz-box-shadow: none; -webkit-box-shadow: none; box-shadow: none; } table.footnote { margin: 15px 0; width: 100%; border: 1px solid #eee; } table.field-list th { padding: 0 0.8em 0 0; } table.field-list td { padding: 0; } table.footnote td { padding: 0.5em; } dl { margin: 0; padding: 0; } dl dd { margin-left: 30px; } pre { background: #eee; padding: 7px 30px; margin: 15px -30px; line-height: 1.3em; } dl pre { margin-left: -60px; padding-left: 60px; } dl dl pre { margin-left: -90px; padding-left: 90px; } tt { background-color: #ecf0f3; color: #222; /* padding: 1px 2px; */ } tt.xref, a tt { background-color: #FBFBFB; text-decoration: none!important; } a:hover tt { background: #EEE; } flask-mongoengine-1.0.0/docs/_themes/flask/theme.conf000066400000000000000000000001411375611115600225420ustar00rootroot00000000000000[theme] inherit = basic stylesheet = flasky.css pygments_style = flask_theme_support.FlaskyStyle flask-mongoengine-1.0.0/docs/_themes/flask_small/000077500000000000000000000000001375611115600217655ustar00rootroot00000000000000flask-mongoengine-1.0.0/docs/_themes/flask_small/layout.html000066400000000000000000000012531375611115600241710ustar00rootroot00000000000000{% extends "basic/layout.html" %} {% block header %} {{ super() }} {% if pagename == 'index' %}
{% endif %} {% endblock %} {% block footer %} {% if pagename == 'index' %}
{% endif %} {% endblock %} {# do not display relbars #} {% block relbar1 %}{% endblock %} {% block relbar2 %} {% if theme_github_fork %} Fork me on GitHub {% endif %} {% endblock %} {% block sidebar1 %}{% endblock %} {% block sidebar2 %}{% endblock %} flask-mongoengine-1.0.0/docs/_themes/flask_small/static/000077500000000000000000000000001375611115600232545ustar00rootroot00000000000000flask-mongoengine-1.0.0/docs/_themes/flask_small/static/flasky.css_t000066400000000000000000000107531375611115600256100ustar00rootroot00000000000000/* * flasky.css_t * ~~~~~~~~~~~~ * * Sphinx stylesheet -- flasky theme based on nature theme. * * :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. * :license: BSD, see LICENSE for details. * */ @import url("basic.css"); /* -- page layout ----------------------------------------------------------- */ body { font-family: 'Georgia', serif; font-size: 17px; color: #000; background: white; margin: 0; padding: 0; } div.documentwrapper { float: left; width: 100%; } div.bodywrapper { margin: 40px auto 0 auto; width: 700px; } hr { border: 1px solid #B1B4B6; } div.body { background-color: #ffffff; color: #3E4349; padding: 0 30px 30px 30px; } img.floatingflask { padding: 0 0 10px 10px; float: right; } div.footer { text-align: right; color: #888; padding: 10px; font-size: 14px; width: 650px; margin: 0 auto 40px auto; } div.footer a { color: #888; text-decoration: underline; } div.related { line-height: 32px; color: #888; } div.related ul { padding: 0 0 0 10px; } div.related a { color: #444; } /* -- body styles ----------------------------------------------------------- */ a { color: #004B6B; text-decoration: underline; } a:hover { color: #6D4100; text-decoration: underline; } div.body { padding-bottom: 40px; /* saved for footer */ } div.body h1, div.body h2, div.body h3, div.body h4, div.body h5, div.body h6 { font-family: 'Garamond', 'Georgia', serif; font-weight: normal; margin: 30px 0px 10px 0px; padding: 0; } {% if theme_index_logo %} div.indexwrapper h1 { text-indent: -999999px; background: url({{ theme_index_logo }}) no-repeat center center; height: {{ theme_index_logo_height }}; } {% endif %} div.body h2 { font-size: 180%; } div.body h3 { font-size: 150%; } div.body h4 { font-size: 130%; } div.body h5 { font-size: 100%; } div.body h6 { font-size: 100%; } a.headerlink { color: white; padding: 0 4px; text-decoration: none; } a.headerlink:hover { color: #444; background: #eaeaea; } div.body p, div.body dd, div.body li { line-height: 1.4em; } div.admonition { background: #fafafa; margin: 20px -30px; padding: 10px 30px; border-top: 1px solid #ccc; border-bottom: 1px solid #ccc; } div.admonition p.admonition-title { font-family: 'Garamond', 'Georgia', serif; font-weight: normal; font-size: 24px; margin: 0 0 10px 0; padding: 0; line-height: 1; } div.admonition p.last { margin-bottom: 0; } div.highlight{ background-color: white; } dt:target, .highlight { background: #FAF3E8; } div.note { background-color: #eee; border: 1px solid #ccc; } div.seealso { background-color: #ffc; border: 1px solid #ff6; } div.topic { background-color: #eee; } div.warning { background-color: #ffe4e4; border: 1px solid #f66; } p.admonition-title { display: inline; } p.admonition-title:after { content: ":"; } pre, tt { font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; font-size: 0.85em; } img.screenshot { } tt.descname, tt.descclassname { font-size: 0.95em; } tt.descname { padding-right: 0.08em; } img.screenshot { -moz-box-shadow: 2px 2px 4px #eee; -webkit-box-shadow: 2px 2px 4px #eee; box-shadow: 2px 2px 4px #eee; } table.docutils { border: 1px solid #888; -moz-box-shadow: 2px 2px 4px #eee; -webkit-box-shadow: 2px 2px 4px #eee; box-shadow: 2px 2px 4px #eee; } table.docutils td, table.docutils th { border: 1px solid #888; padding: 0.25em 0.7em; } table.field-list, table.footnote { border: none; -moz-box-shadow: none; -webkit-box-shadow: none; box-shadow: none; } table.footnote { margin: 15px 0; width: 100%; border: 1px solid #eee; } table.field-list th { padding: 0 0.8em 0 0; } table.field-list td { padding: 0; } table.footnote td { padding: 0.5em; } dl { margin: 0; padding: 0; } dl dd { margin-left: 30px; } pre { padding: 0; margin: 15px -30px; padding: 8px; line-height: 1.3em; padding: 7px 30px; background: #eee; border-radius: 2px; -moz-border-radius: 2px; -webkit-border-radius: 2px; } dl pre { margin-left: -60px; padding-left: 60px; } tt { background-color: #ecf0f3; color: #222; /* padding: 1px 2px; */ } tt.xref, a tt { background-color: #FBFBFB; } a:hover tt { background: #EEE; } flask-mongoengine-1.0.0/docs/_themes/flask_small/theme.conf000066400000000000000000000002701375611115600237350ustar00rootroot00000000000000[theme] inherit = basic stylesheet = flasky.css nosidebar = true pygments_style = flask_theme_support.FlaskyStyle [options] index_logo = '' index_logo_height = 120px github_fork = '' flask-mongoengine-1.0.0/docs/_themes/flask_theme_support.py000066400000000000000000000114131375611115600241250ustar00rootroot00000000000000# flasky extensions. flasky pygments style based on tango style from pygments.style import Style from pygments.token import Keyword, Name, Comment, String, Error, \ Number, Operator, Generic, Whitespace, Punctuation, Other, Literal class FlaskyStyle(Style): background_color = "#f8f8f8" default_style = "" styles = { # No corresponding class for the following: #Text: "", # class: '' Whitespace: "underline #f8f8f8", # class: 'w' Error: "#a40000 border:#ef2929", # class: 'err' Other: "#000000", # class 'x' Comment: "italic #8f5902", # class: 'c' Comment.Preproc: "noitalic", # class: 'cp' Keyword: "bold #004461", # class: 'k' Keyword.Constant: "bold #004461", # class: 'kc' Keyword.Declaration: "bold #004461", # class: 'kd' Keyword.Namespace: "bold #004461", # class: 'kn' Keyword.Pseudo: "bold #004461", # class: 'kp' Keyword.Reserved: "bold #004461", # class: 'kr' Keyword.Type: "bold #004461", # class: 'kt' Operator: "#582800", # class: 'o' Operator.Word: "bold #004461", # class: 'ow' - like keywords Punctuation: "bold #000000", # class: 'p' # because special names such as Name.Class, Name.Function, etc. # are not recognized as such later in the parsing, we choose them # to look the same as ordinary variables. Name: "#000000", # class: 'n' Name.Attribute: "#c4a000", # class: 'na' - to be revised Name.Builtin: "#004461", # class: 'nb' Name.Builtin.Pseudo: "#3465a4", # class: 'bp' Name.Class: "#000000", # class: 'nc' - to be revised Name.Constant: "#000000", # class: 'no' - to be revised Name.Decorator: "#888", # class: 'nd' - to be revised Name.Entity: "#ce5c00", # class: 'ni' Name.Exception: "bold #cc0000", # class: 'ne' Name.Function: "#000000", # class: 'nf' Name.Property: "#000000", # class: 'py' Name.Label: "#f57900", # class: 'nl' Name.Namespace: "#000000", # class: 'nn' - to be revised Name.Other: "#000000", # class: 'nx' Name.Tag: "bold #004461", # class: 'nt' - like a keyword Name.Variable: "#000000", # class: 'nv' - to be revised Name.Variable.Class: "#000000", # class: 'vc' - to be revised Name.Variable.Global: "#000000", # class: 'vg' - to be revised Name.Variable.Instance: "#000000", # class: 'vi' - to be revised Number: "#990000", # class: 'm' Literal: "#000000", # class: 'l' Literal.Date: "#000000", # class: 'ld' String: "#4e9a06", # class: 's' String.Backtick: "#4e9a06", # class: 'sb' String.Char: "#4e9a06", # class: 'sc' String.Doc: "italic #8f5902", # class: 'sd' - like a comment String.Double: "#4e9a06", # class: 's2' String.Escape: "#4e9a06", # class: 'se' String.Heredoc: "#4e9a06", # class: 'sh' String.Interpol: "#4e9a06", # class: 'si' String.Other: "#4e9a06", # class: 'sx' String.Regex: "#4e9a06", # class: 'sr' String.Single: "#4e9a06", # class: 's1' String.Symbol: "#4e9a06", # class: 'ss' Generic: "#000000", # class: 'g' Generic.Deleted: "#a40000", # class: 'gd' Generic.Emph: "italic #000000", # class: 'ge' Generic.Error: "#ef2929", # class: 'gr' Generic.Heading: "bold #000080", # class: 'gh' Generic.Inserted: "#00A000", # class: 'gi' Generic.Output: "#888", # class: 'go' Generic.Prompt: "#745334", # class: 'gp' Generic.Strong: "bold #000000", # class: 'gs' Generic.Subheading: "bold #800080", # class: 'gu' Generic.Traceback: "bold #a40000", # class: 'gt' } flask-mongoengine-1.0.0/docs/changelog.rst000066400000000000000000000101661375611115600205360ustar00rootroot00000000000000========= Changelog ========= Changes in 1.0.0 ================ Changelog maintenance automated and latest changelog available at `github release page `_. Use version 0.9.5 if old dependencies required. Changes in 0.9.5 ================ - Disable flake8 on travis. - Correct `Except` clauses in code. - Fix warning about undefined unicode variable in orm.py with python 3 Changes in 0.9.4 ================ - ADDED: Support for `MONGODB_CONNECT` mongodb parameter (#321) - ADDED: Support for `MONGODB_TZ_AWARE` mongodb parameter. Changes in 0.9.3 ================ - Fix test and mongomock (#304) - Run Travis builds in a container-based environment (#301) Changes in 0.9.2 ================ - Travis CI/CD pipeline update to automatically publish 0.9.1. Changes in 0.9.1 ================ - Fixed setup.py for various platforms (#298). - Added Flask-WTF v0.14 support (#294). - MongoEngine instance now holds a reference to a particular Flask app it was initialized with (#261). Changes in 0.9.0 ================ - BREAKING CHANGE: Dropped Python v2.6 support Changes in 0.8.2 ================ - Fixed relying on mongoengine.python_support. - Fixed cleaning up empty connection settings #285 Changes in 0.8.1 ================ - Fixed connection issues introduced in 0.8 - Removed support for MongoMock Changes in 0.8 ============== - Dropped MongoEngine 0.7 support - Added MongoEngine 0.10 support - Added PyMongo 3 support - Added Python3 support up to 3.5 - Allowed empying value list in SelectMultipleField - Fixed paginator issues - Use InputRequired validator to allow 0 in required field - Made help_text Field attribute optional - Added "radio" form_arg to convert field into RadioField - Added "textarea" form_arg to force conversion into TextAreaField - Added field parameters (validators, filters...) - Fixed 'False' connection settings ignored - Fixed bug to allow multiple instances of extension - Added MongoEngineSessionInterface support for PyMongo's tz_aware option - Support arbitrary primary key fields (not "id") - Configurable httponly flag for MongoEngineSessionInterface - Various bugfixes, code cleanup and documentation improvements - Move from deprecated flask.ext.* to flask_* syntax in imports - Added independent connection handler for FlaskMongoEngine - All MongoEngine connection calls are proxied via FlaskMongoEngine connection handler - Added backward compatibility for settings key names - Added support for MongoMock and temporary test DB - Fixed issue with multiple DB support - Various bugfixes Changes in 0.7 ============== - Fixed only / exclude in model forms (#49) - Added automatic choices coerce for simple types (#34) - Fixed EmailField and URLField rendering and validation (#44, #9) - Use help_text for field description (#43) - Fixed Pagination and added Document.paginate_field() helper - Keep model_forms fields in order of creation - Added MongoEngineSessionInterface (#5) - Added customisation hooks for FieldList sub fields (#19) - Handle non ascii chars in the MongoDebugPanel (#22) - Fixed toolbar stacktrace if a html directory is in the path (#31) - ModelForms no longer patch Document.update (#32) - No longer wipe field kwargs in ListField (#20, #19) - Passthrough ModelField.save-arguments (#26) - QuerySetSelectMultipleField now supports initial value (#27) - Clarified configuration documentation (#33) - Fixed forms when EmbeddedDocument has no default (#36) - Fixed multiselect restore bug (#37) - Split out the examples into a single file app and a cross file app Changes in 0.6 ============== - Support for JSON and DictFields - Speeding up QuerySetSelectField with big querysets Changes in 0.5 ============== - Added support for all connection settings - Fixed extended DynamicDocument Changes in 0.4 ============== - Added CSRF support and validate_on_save via flask.ext.WTF - Fixed DateTimeField not required Changes in 0.3 =============== - Reverted mongopanel - got knocked out by a merge - Updated imports paths Changes in 0.2 =============== - Added support for password StringField - Added ModelSelectMultiple Changes in 0.1 =============== - Released to PyPi flask-mongoengine-1.0.0/docs/conf.py000066400000000000000000000161461375611115600173600ustar00rootroot00000000000000# flask-script documentation build configuration file, created by # sphinx-quickstart on Wed Jun 23 08:26:41 2010. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys, os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.append(os.path.abspath('_themes')) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = 'Flask-MongoEngine' copyright = '2010-2020, Streetlife and others' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # import flask_mongoengine # The short X.Y version. version = flask_mongoengine.__version__ # The full version, including alpha/beta/rc tags. release = flask_mongoengine.__version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. #pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'flask_small' html_theme_options = { 'index_logo': '', 'github_fork': None } # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. html_theme_path = ['_themes'] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = '' # Output file base name for HTML help builder. htmlhelp_basename = 'flask-mongoenginedoc' # -- Options for LaTeX output -------------------------------------------------- # The paper size ('letter' or 'a4'). #latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'flask-mongoengine.tex', 'Flask-MongoEngine Documentation', 'Ross Lawley', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Additional stuff for the LaTeX preamble. #latex_preamble = '' # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'flask-mongoengine', 'Flask-MongoEngine Documentation', ['Ross Lawley', 'Dan Jacob', 'Marat Khabibullin'], 1) ] flask-mongoengine-1.0.0/docs/index.rst000066400000000000000000000207071375611115600177200ustar00rootroot00000000000000Flask-MongoEngine ================= A Flask extension that provides integration with `MongoEngine `_. For more information on MongoEngine please check out the `MongoEngine Documentation `_. It handles connection management for your app. You can also use `WTForms `_ as model forms for your models. Pre-requisite ============= Make sure you have `wheel` installed:: pip install wheel Installing Flask-MongoEngine ============================ Install with **pip**:: pip install flask-mongoengine Configuration ============= Basic setup is easy, just fetch the extension:: from flask import Flask from flask_mongoengine import MongoEngine app = Flask(__name__) app.config.from_pyfile('the-config.cfg') db = MongoEngine(app) Or, if you are setting up your database before your app is initialized, as is the case with application factories:: from flask import Flask from flask_mongoengine import MongoEngine db = MongoEngine() ... app = Flask(__name__) app.config.from_pyfile('the-config.cfg') db.init_app(app) By default, Flask-MongoEngine assumes that the `mongod` instance is running on **localhost** on port **27017**, and you wish to connect to the database named **test**. If MongoDB is running elsewhere, you should provide the `host` and `port` settings in the `'MONGODB_SETTINGS'` dictionary wih `app.config`.:: app.config['MONGODB_SETTINGS'] = { 'db': 'project1', 'host': '192.168.1.35', 'port': 12345 } If the database requires authentication, the `username` and `password` arguments should be provided `'MONGODB_SETTINGS'` dictionary wih `app.config`.:: app.config['MONGODB_SETTINGS'] = { 'db': 'project1', 'username':'webapp', 'password':'pwd123' } Uri style connections are also supported, just supply the uri as the `host` in the `'MONGODB_SETTINGS'` dictionary with `app.config`. **Note that database name from uri has priority over name.** If uri presents and doesn't contain database name db setting entirely ignore and db name set to 'test'. :: app.config['MONGODB_SETTINGS'] = { 'db': 'project1', 'host': 'mongodb://localhost/database_name' } Connection settings may also be provided individually by prefixing the setting with `'MONGODB_'` in the `app.config`.:: app.config['MONGODB_DB'] = 'project1' app.config['MONGODB_HOST'] = '192.168.1.35' app.config['MONGODB_PORT'] = 12345 app.config['MONGODB_USERNAME'] = 'webapp' app.config['MONGODB_PASSWORD'] = 'pwd123' By default flask-mongoengine open the connection when extension is instanciated but you can configure it to open connection only on first database access by setting the ``MONGODB_SETTINGS['connect']`` parameter or its ``MONGODB_CONNECT`` flat equivalent to ``False``:: app.config['MONGODB_SETTINGS'] = { 'host': 'mongodb://localhost/database_name', 'connect': False, } # or app.config['MONGODB_CONNECT'] = False Custom Queryset =============== flask-mongoengine attaches the following methods to Mongoengine's default QuerySet: * **get_or_404**: works like .get(), but calls abort(404) if the object DoesNotExist. Optional arguments: *message* - custom message to display. * **first_or_404**: same as above, except for .first(). Optional arguments: *message* - custom message to display. * **paginate**: paginates the QuerySet. Takes two arguments, *page* and *per_page*. * **paginate_field**: paginates a field from one document in the QuerySet. Arguments: *field_name*, *doc_id*, *page*, *per_page*. Examples:: # 404 if object doesn't exist def view_todo(todo_id): todo = Todo.objects.get_or_404(_id=todo_id) .. # Paginate through todo def view_todos(page=1): paginated_todos = Todo.objects.paginate(page=page, per_page=10) # Paginate through tags of todo def view_todo_tags(todo_id, page=1): todo = Todo.objects.get_or_404(_id=todo_id) paginated_tags = todo.paginate_field('tags', page, per_page=10) Properties of the pagination object include: iter_pages, next, prev, has_next, has_prev, next_num, prev_num. In the template:: {# Display a page of todos #}
    {% for todo in paginated_todos.items %}
  • {{ todo.title }}
  • {% endfor %}
{# Macro for creating navigation links #} {% macro render_navigation(pagination, endpoint) %} {% endmacro %} {{ render_navigation(paginated_todos, 'view_todos') }} MongoEngine and WTForms ======================= flask-mongoengine automatically generates WTForms from MongoEngine models:: from flask_mongoengine.wtf import model_form class User(db.Document): email = db.StringField(required=True) first_name = db.StringField(max_length=50) last_name = db.StringField(max_length=50) class Content(db.EmbeddedDocument): text = db.StringField() lang = db.StringField(max_length=3) class Post(db.Document): title = db.StringField(max_length=120, required=True, validators=[validators.InputRequired(message='Missing title.'),]) author = db.ReferenceField(User) tags = db.ListField(db.StringField(max_length=30)) content = db.EmbeddedDocumentField(Content) PostForm = model_form(Post) def add_post(request): form = PostForm(request.POST) if request.method == 'POST' and form.validate(): # do something redirect('done') return render_template('add_post.html', form=form) For each MongoEngine field, the most appropriate WTForm field is used. Parameters allow the user to provide hints if the conversion is not implicit:: PostForm = model_form(Post, field_args={'title': {'textarea': True}}) Supported parameters: For fields with `choices`: - `multiple` to use a SelectMultipleField - `radio` to use a RadioField For `StringField`: - `password` to use a PasswordField - `textarea` to use a TextAreaField For `ListField`: - `min_entries` to set the minimal number of entries (By default, a StringField is converted into a TextAreaField if and only if it has no max_length.) Supported fields ---------------- * StringField * BinaryField * URLField * EmailField * IntField * FloatField * DecimalField * BooleanField * DateTimeField * **ListField** (using wtforms.fields.FieldList ) * SortedListField (duplicate ListField) * **EmbeddedDocumentField** (using wtforms.fields.FormField and generating inline Form) * **ReferenceField** (using wtforms.fields.SelectFieldBase with options loaded from QuerySet or Document) * DictField Not currently supported field types: ------------------------------------ * ObjectIdField * GeoLocationField * GenericReferenceField Session Interface ================= To use MongoEngine as your session store simple configure the session interface:: from flask_mongoengine import MongoEngine, MongoEngineSessionInterface app = Flask(__name__) db = MongoEngine(app) app.session_interface = MongoEngineSessionInterface(db) Debug Toolbar Panel =================== .. image:: _static/debugtoolbar.png :target: #debug-toolbar-panel If you use the Flask-DebugToolbar you can add `'flask_mongoengine.panels.MongoDebugPanel'` to the `DEBUG_TB_PANELS` config list and then it will automatically track your queries:: from flask import Flask from flask_debugtoolbar import DebugToolbarExtension app = Flask(__name__) app.config['DEBUG_TB_PANELS'] = ['flask_mongoengine.panels.MongoDebugPanel'] db = MongoEngine(app) toolbar = DebugToolbarExtension(app) Upgrading ========= 0.6 to 0.7 ---------- `ListFieldPagination` order of arguments have been changed to be more logical:: # Old order ListFieldPagination(self, queryset, field_name, doc_id, page, per_page, total) # New order ListFieldPagination(self, queryset, doc_id, field_name, page, per_page, total) Credits ======= Inspired by two repos: `danjac `_ `maratfm `_ flask-mongoengine-1.0.0/docs/make.bat000066400000000000000000000100321375611115600174520ustar00rootroot00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. changes to make an overview over all changed/added/deprecated items echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\flask-unittest.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\flask-unittest.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) :end flask-mongoengine-1.0.0/examples/000077500000000000000000000000001375611115600167375ustar00rootroot00000000000000flask-mongoengine-1.0.0/examples/biggerapp/000077500000000000000000000000001375611115600206775ustar00rootroot00000000000000flask-mongoengine-1.0.0/examples/biggerapp/README.rst000066400000000000000000000005331375611115600223670ustar00rootroot00000000000000Sample app to test the toolbar ============================== A simple multi file app - to help get you started 1. Install all the requirements from: flask-mongoengine/requirements.txt:: pip install -r requirements.txt 2. From the root folder run the app eg:: python ./examples/biggerapp/app.py 3. Point your browser to localhost:4000/ flask-mongoengine-1.0.0/examples/biggerapp/__init__.py000066400000000000000000000000001375611115600227760ustar00rootroot00000000000000flask-mongoengine-1.0.0/examples/biggerapp/app.py000066400000000000000000000021701375611115600220310ustar00rootroot00000000000000import os import sys import flask sys.path.insert(0, os.path.realpath(os.path.join(os.path.dirname(__file__), "../../"))) from flask_debugtoolbar import DebugToolbarExtension app = flask.Flask(__name__) app.config.from_object(__name__) app.config["MONGODB_SETTINGS"] = {"DB": "testing"} app.config["TESTING"] = True app.config["SECRET_KEY"] = "flask+mongoengine=<3" app.debug = True app.config["DEBUG_TB_PANELS"] = ( "flask_debugtoolbar.panels.versions.VersionDebugPanel", "flask_debugtoolbar.panels.timer.TimerDebugPanel", "flask_debugtoolbar.panels.headers.HeaderDebugPanel", "flask_debugtoolbar.panels.request_vars.RequestVarsDebugPanel", "flask_debugtoolbar.panels.template.TemplateDebugPanel", "flask_debugtoolbar.panels.logger.LoggingPanel", "flask_mongoengine.panels.MongoDebugPanel", ) app.config["DEBUG_TB_INTERCEPT_REDIRECTS"] = False from models import db db.init_app(app) DebugToolbarExtension(app) from views import index, pagination app.add_url_rule("/", view_func=index) app.add_url_rule("/pagination", view_func=pagination) if __name__ == "__main__": app.run(host="0.0.0.0", port=4000) flask-mongoengine-1.0.0/examples/biggerapp/models.py000066400000000000000000000004311375611115600225320ustar00rootroot00000000000000import datetime from flask_mongoengine import MongoEngine db = MongoEngine() class Todo(db.Document): title = db.StringField(max_length=60) text = db.StringField() done = db.BooleanField(default=False) pub_date = db.DateTimeField(default=datetime.datetime.now) flask-mongoengine-1.0.0/examples/biggerapp/static/000077500000000000000000000000001375611115600221665ustar00rootroot00000000000000flask-mongoengine-1.0.0/examples/biggerapp/static/style.css000066400000000000000000000016261375611115600240450ustar00rootroot00000000000000body { font-family: sans-serif; background: #eee; } a, h1, h2 { color: #377BA8; } h1, h2 { font-family: 'Georgia', serif; margin: 0; } h1 { border-bottom: 2px solid #eee; } h2 { font-size: 1.2em; } .page { margin: 2em auto; width: 35em; border: 5px solid #ccc; padding: 0.8em; background: white; } .entries { list-style: none; margin: 0; padding: 0; } .entries li { margin: 0.8em 1.2em; } .entries li h2 { margin-left: -1em; } .add-entry { font-size: 0.9em; border-bottom: 1px solid #ccc; } .add-entry dl { font-weight: bold; } .metanav { text-align: right; font-size: 0.8em; padding: 0.3em; margin-bottom: 1em; background: #fafafa; } .flash { background: #CEE5F5; padding: 0.5em; border: 1px solid #AACBE2; } .error { background: #F0D6D6; padding: 0.5em; } flask-mongoengine-1.0.0/examples/biggerapp/templates/000077500000000000000000000000001375611115600226755ustar00rootroot00000000000000flask-mongoengine-1.0.0/examples/biggerapp/templates/index.html000066400000000000000000000005211375611115600246700ustar00rootroot00000000000000{% extends "layout.html" %} {% block body %} {% for todo in todos %}

{{ todo.title }}

{{ todo.text|safe }}
{% else %} Unbelievable. No todos here so far Add one {% endfor %}
See pagination {% endblock %} flask-mongoengine-1.0.0/examples/biggerapp/templates/layout.html000066400000000000000000000004341375611115600251010ustar00rootroot00000000000000 Flask MongoEngine

Toolbar example

{% block body %}{% endblock %}
flask-mongoengine-1.0.0/examples/biggerapp/templates/pagination.html000066400000000000000000000015471375611115600257230ustar00rootroot00000000000000{% extends "layout.html" %} {# Macro for creating navigation links #} {% macro render_navigation(pagination, endpoint) %} {% endmacro %} {% block body %}
    {% for todo in todos_page.items %}
  • {{ todo.title }}
  • {% endfor %}
{% endblock %} flask-mongoengine-1.0.0/examples/biggerapp/views.py000066400000000000000000000014771375611115600224170ustar00rootroot00000000000000import flask from .models import Todo def index(): # As a list to test debug toolbar Todo.objects().delete() # Removes Todo(title="Simple todo A", text="12345678910").save() # Insert Todo(title="Simple todo B", text="12345678910").save() # Insert Todo.objects(title__contains="B").update(set__text="Hello world") # Update todos = list(Todo.objects[:10]) todos = Todo.objects.all() return flask.render_template("index.html", todos=todos) def pagination(): Todo.objects().delete() for i in range(10): Todo(title="Simple todo {}".format(i), text="12345678910").save() # Insert page_num = int(flask.request.args.get("page") or 1) todos_page = Todo.objects.paginate(page=page_num, per_page=3) return flask.render_template("pagination.html", todos_page=todos_page) flask-mongoengine-1.0.0/examples/simpleapp/000077500000000000000000000000001375611115600207315ustar00rootroot00000000000000flask-mongoengine-1.0.0/examples/simpleapp/README.rst000066400000000000000000000005311375611115600224170ustar00rootroot00000000000000Sample app to test the toolbar ============================== A simple one file app - to help get you started 1. Install all the requirements from: flask-mongoengine/requirements.txt:: pip install -r requirements.txt 2. From the root folder run the app eg:: python ./examples/simpleapp/app.py 3. Point your browser to localhost:4000/ flask-mongoengine-1.0.0/examples/simpleapp/app.py000066400000000000000000000032301375611115600220610ustar00rootroot00000000000000import datetime import os import sys import flask sys.path.insert(0, os.path.realpath(os.path.join(os.path.dirname(__file__), "../../"))) from flask_mongoengine import MongoEngine from flask_debugtoolbar import DebugToolbarExtension app = flask.Flask(__name__) app.config.from_object(__name__) app.config["MONGODB_SETTINGS"] = {"DB": "testing"} app.config["TESTING"] = True app.config["SECRET_KEY"] = "flask+mongoengine=<3" app.debug = True app.config["DEBUG_TB_PANELS"] = ( "flask_debugtoolbar.panels.versions.VersionDebugPanel", "flask_debugtoolbar.panels.timer.TimerDebugPanel", "flask_debugtoolbar.panels.headers.HeaderDebugPanel", "flask_debugtoolbar.panels.request_vars.RequestVarsDebugPanel", "flask_debugtoolbar.panels.template.TemplateDebugPanel", "flask_debugtoolbar.panels.logger.LoggingPanel", "flask_mongoengine.panels.MongoDebugPanel", ) app.config["DEBUG_TB_INTERCEPT_REDIRECTS"] = False db = MongoEngine() db.init_app(app) DebugToolbarExtension(app) class Todo(db.Document): title = db.StringField(max_length=60) text = db.StringField() done = db.BooleanField(default=False) pub_date = db.DateTimeField(default=datetime.datetime.now) @app.route("/") def index(): # As a list to test debug toolbar Todo.objects().delete() # Removes Todo(title="Simple todo A", text="12345678910").save() # Insert Todo(title="Simple todo B", text="12345678910").save() # Insert Todo.objects(title__contains="B").update(set__text="Hello world") # Update todos = Todo.objects.all() return flask.render_template("index.html", todos=todos) if __name__ == "__main__": app.run(host="0.0.0.0", port=4000) flask-mongoengine-1.0.0/examples/simpleapp/static/000077500000000000000000000000001375611115600222205ustar00rootroot00000000000000flask-mongoengine-1.0.0/examples/simpleapp/static/style.css000066400000000000000000000016271375611115600241000ustar00rootroot00000000000000body { font-family: sans-serif; background: #eee; } a, h1, h2 { color: #377BA8; } h1, h2 { font-family: 'Georgia', serif; margin: 0; } h1 { border-bottom: 2px solid #eee; } h2 { font-size: 1.2em; } .page { margin: 2em auto; width: 35em; border: 5px solid #ccc; padding: 0.8em; background: white; } .entries { list-style: none; margin: 0; padding: 0; } .entries li { margin: 0.8em 1.2em; } .entries li h2 { margin-left: -1em; } .add-entry { font-size: 0.9em; border-bottom: 1px solid #ccc; } .add-entry dl { font-weight: bold; } .metanav { text-align: right; font-size: 0.8em; padding: 0.3em; margin-bottom: 1em; background: #fafafa; } .flash { background: #CEE5F5; padding: 0.5em; border: 1px solid #AACBE2; } .error { background: #F0D6D6; padding: 0.5em; } flask-mongoengine-1.0.0/examples/simpleapp/templates/000077500000000000000000000000001375611115600227275ustar00rootroot00000000000000flask-mongoengine-1.0.0/examples/simpleapp/templates/index.html000066400000000000000000000004161375611115600247250ustar00rootroot00000000000000{% extends "layout.html" %} {% block body %} {% for todo in todos %}

{{ todo.title }}

{{ todo.text|safe }}
{% else %} Unbelievable. No todos here so far Add one {% endfor %} {% endblock %} flask-mongoengine-1.0.0/examples/simpleapp/templates/layout.html000066400000000000000000000004341375611115600251330ustar00rootroot00000000000000 Flask MongoEngine

Toolbar example

{% block body %}{% endblock %}
flask-mongoengine-1.0.0/flask_mongoengine/000077500000000000000000000000001375611115600206065ustar00rootroot00000000000000flask-mongoengine-1.0.0/flask_mongoengine/__init__.py000066400000000000000000000151601375611115600227220ustar00rootroot00000000000000import inspect import mongoengine from flask import Flask, abort, current_app from mongoengine.base.fields import BaseField from mongoengine.errors import DoesNotExist from mongoengine.queryset import QuerySet from .connection import * from .json import override_json_encoder from .pagination import * from .sessions import * from .wtf import WtfBaseField VERSION = (1, 0, 0) def get_version(): """Return the VERSION as a string.""" return ".".join(map(str, VERSION)) __version__ = get_version() def _patch_base_field(obj, name): """ If the object submitted has a class whose base class is mongoengine.base.fields.BaseField, then monkey patch to replace it with flask_mongoengine.wtf.WtfBaseField. @note: WtfBaseField is an instance of BaseField - but gives us the flexibility to extend field parameters and settings required of WTForm via model form generator. @see: flask_mongoengine.wtf.base.WtfBaseField. @see: model_form in flask_mongoengine.wtf.orm @param obj: MongoEngine instance in which we should locate the class. @param name: Name of an attribute which may or may not be a BaseField. """ # TODO is there a less hacky way to accomplish the same level of # extensibility/control? # get an attribute of the MongoEngine class and return if it's not # a class cls = getattr(obj, name) if not inspect.isclass(cls): return # if it is a class, inspect all of its parent classes cls_bases = list(cls.__bases__) # if any of them is a BaseField, replace it with WtfBaseField for index, base in enumerate(cls_bases): if base == BaseField: cls_bases[index] = WtfBaseField cls.__bases__ = tuple(cls_bases) break # re-assign the class back to the MongoEngine instance delattr(obj, name) setattr(obj, name, cls) def _include_mongoengine(obj): """ Copy all of the attributes from mongoengine and mongoengine.fields onto obj (which should be an instance of the MongoEngine class). """ # TODO why do we need this? What's wrong with importing from the # original modules? for module in (mongoengine, mongoengine.fields): for attr_name in module.__all__: if not hasattr(obj, attr_name): setattr(obj, attr_name, getattr(module, attr_name)) # patch BaseField if available _patch_base_field(obj, attr_name) def current_mongoengine_instance(): """Return a MongoEngine instance associated with current Flask app.""" me = current_app.extensions.get("mongoengine", {}) for k, v in me.items(): if isinstance(k, MongoEngine): return k class MongoEngine(object): """Main class used for initialization of Flask-MongoEngine.""" def __init__(self, app=None, config=None): _include_mongoengine(self) self.app = None self.config = config self.Document = Document self.DynamicDocument = DynamicDocument if app is not None: self.init_app(app, config) def init_app(self, app, config=None): if not app or not isinstance(app, Flask): raise TypeError("Invalid Flask application instance") self.app = app app.extensions = getattr(app, "extensions", {}) # Make documents JSON serializable override_json_encoder(app) if "mongoengine" not in app.extensions: app.extensions["mongoengine"] = {} if self in app.extensions["mongoengine"]: # Raise an exception if extension already initialized as # potentially new configuration would not be loaded. raise ValueError("Extension already initialized") if config: # Passed config have max priority, over init config. self.config = config if not self.config: # If no configs passed, use app.config. config = app.config # Obtain db connection(s) connections = create_connections(config) # Store objects in application instance so that multiple apps do not # end up accessing the same objects. s = {"app": app, "conn": connections} app.extensions["mongoengine"][self] = s @property def connection(self): """ Return MongoDB connection(s) associated with this MongoEngine instance. """ return current_app.extensions["mongoengine"][self]["conn"] class BaseQuerySet(QuerySet): """Mongoengine's queryset extended with handy extras.""" def get_or_404(self, *args, **kwargs): """ Get a document and raise a 404 Not Found error if it doesn't exist. """ try: return self.get(*args, **kwargs) except DoesNotExist: message = kwargs.get("message", None) abort(404, message) if message else abort(404) def first_or_404(self, message=None): """Same as get_or_404, but uses .first, not .get.""" obj = self.first() return obj if obj else abort(404, message) if message else abort(404) def paginate(self, page, per_page, **kwargs): """ Paginate the QuerySet with a certain number of docs per page and return docs for a given page. """ return Pagination(self, page, per_page) def paginate_field(self, field_name, doc_id, page, per_page, total=None): """ Paginate items within a list field from one document in the QuerySet. """ # TODO this doesn't sound useful at all - remove in next release? item = self.get(id=doc_id) count = getattr(item, field_name + "_count", "") total = total or count or len(getattr(item, field_name)) return ListFieldPagination( self, doc_id, field_name, page, per_page, total=total ) class Document(mongoengine.Document): """Abstract document with extra helpers in the queryset class""" meta = {"abstract": True, "queryset_class": BaseQuerySet} def paginate_field(self, field_name, page, per_page, total=None): """Paginate items within a list field.""" # TODO this doesn't sound useful at all - remove in next release? count = getattr(self, field_name + "_count", "") total = total or count or len(getattr(self, field_name)) return ListFieldPagination( self.__class__.objects, self.pk, field_name, page, per_page, total=total ) class DynamicDocument(mongoengine.DynamicDocument): """Abstract Dynamic document with extra helpers in the queryset class""" meta = {"abstract": True, "queryset_class": BaseQuerySet} flask-mongoengine-1.0.0/flask_mongoengine/connection.py000066400000000000000000000121101375611115600233120ustar00rootroot00000000000000import mongoengine from pymongo import ReadPreference, uri_parser __all__ = ( "create_connections", "get_connection_settings", "InvalidSettingsError", ) MONGODB_CONF_VARS = ( "MONGODB_ALIAS", "MONGODB_DB", "MONGODB_HOST", "MONGODB_IS_MOCK", "MONGODB_PASSWORD", "MONGODB_PORT", "MONGODB_USERNAME", "MONGODB_CONNECT", "MONGODB_TZ_AWARE", ) class InvalidSettingsError(Exception): pass def _sanitize_settings(settings): """Given a dict of connection settings, sanitize the keys and fall back to some sane defaults. """ # Remove the "MONGODB_" prefix and make all settings keys lower case. resolved_settings = {} for k, v in settings.items(): if k.startswith("MONGODB_"): k = k[len("MONGODB_") :] k = k.lower() resolved_settings[k] = v # Handle uri style connections if "://" in resolved_settings.get("host", ""): # this section pulls the database name from the URI # PyMongo requires URI to start with mongodb:// to parse # this workaround allows mongomock to work uri_to_check = resolved_settings["host"] if uri_to_check.startswith("mongomock://"): uri_to_check = uri_to_check.replace("mongomock://", "mongodb://") uri_dict = uri_parser.parse_uri(uri_to_check) resolved_settings["db"] = uri_dict["database"] # Add a default name param or use the "db" key if exists if resolved_settings.get("db"): resolved_settings["name"] = resolved_settings.pop("db") else: resolved_settings["name"] = "test" # Add various default values. resolved_settings["alias"] = resolved_settings.get( "alias", mongoengine.DEFAULT_CONNECTION_NAME ) # TODO do we have to specify it here? MongoEngine should take care of that resolved_settings["host"] = resolved_settings.get( "host", "localhost" ) # TODO this is the default host in pymongo.mongo_client.MongoClient, we may not need to explicitly set a default here resolved_settings["port"] = resolved_settings.get( "port", 27017 ) # TODO this is the default port in pymongo.mongo_client.MongoClient, we may not need to explicitly set a default here # Default to ReadPreference.PRIMARY if no read_preference is supplied resolved_settings["read_preference"] = resolved_settings.get( "read_preference", ReadPreference.PRIMARY ) # Clean up empty values for k, v in list(resolved_settings.items()): if v is None: del resolved_settings[k] return resolved_settings def get_connection_settings(config): """ Given a config dict, return a sanitized dict of MongoDB connection settings that we can then use to establish connections. For new applications, settings should exist in a "MONGODB_SETTINGS" key, but for backward compactibility we also support several config keys prefixed by "MONGODB_", e.g. "MONGODB_HOST", "MONGODB_PORT", etc. """ # Sanitize all the settings living under a "MONGODB_SETTINGS" config var if "MONGODB_SETTINGS" in config: settings = config["MONGODB_SETTINGS"] # If MONGODB_SETTINGS is a list of settings dicts, sanitize each # dict separately. if isinstance(settings, list): # List of connection settings. settings_list = [] for setting in settings: settings_list.append(_sanitize_settings(setting)) return settings_list # Otherwise, it should be a single dict describing a single connection. else: return _sanitize_settings(settings) # If "MONGODB_SETTINGS" doesn't exist, sanitize the "MONGODB_" keys # as if they all describe a single connection. else: config = dict( (k, v) for k, v in config.items() if k in MONGODB_CONF_VARS ) # ugly dict comprehention in order to support python 2.6 return _sanitize_settings(config) def create_connections(config): """ Given Flask application's config dict, extract relevant config vars out of it and establish MongoEngine connection(s) based on them. """ # Validate that the config is a dict if config is None or not isinstance(config, dict): raise InvalidSettingsError("Invalid application configuration") # Get sanitized connection settings based on the config conn_settings = get_connection_settings(config) # If conn_settings is a list, set up each item as a separate connection # and return a dict of connection aliases and their connections. if isinstance(conn_settings, list): connections = {} for each in conn_settings: alias = each["alias"] connections[alias] = _connect(each) return connections # Otherwise, return a single connection return _connect(conn_settings) def _connect(conn_settings): """Given a dict of connection settings, create a connection to MongoDB by calling mongoengine.connect and return its result. """ db_name = conn_settings.pop("name") return mongoengine.connect(db_name, **conn_settings) flask-mongoengine-1.0.0/flask_mongoengine/json.py000066400000000000000000000023321375611115600221310ustar00rootroot00000000000000from bson import json_util from flask.json import JSONEncoder from mongoengine.base import BaseDocument from mongoengine.queryset import QuerySet def _make_encoder(superclass): class MongoEngineJSONEncoder(superclass): """ A JSONEncoder which provides serialization of MongoEngine documents and queryset objects. """ def default(self, obj): if isinstance(obj, BaseDocument): return json_util._json_convert(obj.to_mongo()) elif isinstance(obj, QuerySet): return json_util._json_convert(obj.as_pymongo()) return superclass.default(self, obj) return MongoEngineJSONEncoder MongoEngineJSONEncoder = _make_encoder(JSONEncoder) def override_json_encoder(app): """ A function to dynamically create a new MongoEngineJSONEncoder class based upon a custom base class. This function allows us to combine MongoEngine serialization with any changes to Flask's JSONEncoder which a user may have made prior to calling init_app. NOTE: This does not cover situations where users override an instance's json_encoder after calling init_app. """ app.json_encoder = _make_encoder(app.json_encoder) flask-mongoengine-1.0.0/flask_mongoengine/operation_tracker.py000066400000000000000000000226111375611115600246750ustar00rootroot00000000000000import copy import functools import inspect import os import socketserver import sys import time import bson import pymongo.collection import pymongo.command_cursor import pymongo.cursor import pymongo.helpers __all__ = [ "queries", "inserts", "updates", "removes", "install_tracker", "uninstall_tracker", "reset", "response_sizes", ] _original_methods = { "insert": pymongo.collection.Collection.insert, "update": pymongo.collection.Collection.update, "remove": pymongo.collection.Collection.remove, "refresh": pymongo.cursor.Cursor._refresh, "_unpack_response": pymongo.command_cursor.CommandCursor._unpack_response, } queries = [] inserts = [] updates = [] removes = [] response_sizes = [] if sys.version_info >= (3, 0): unicode = str # Wrap helpers._unpack_response for getting response size @functools.wraps(_original_methods["_unpack_response"]) def _unpack_response(response, *args, **kwargs): result = _original_methods["_unpack_response"](response, *args, **kwargs) response_sizes.append(sys.getsizeof(response, len(response)) / 1024.0) return result # Wrap Cursor.insert for getting queries @functools.wraps(_original_methods["insert"]) def _insert( collection_self, doc_or_docs, manipulate=True, safe=None, check_keys=True, **kwargs ): start_time = time.time() result = _original_methods["insert"]( collection_self, doc_or_docs, check_keys=check_keys, **kwargs ) total_time = (time.time() - start_time) * 1000 __traceback_hide__ = True # noqa stack_trace, internal = _tidy_stacktrace() inserts.append( { "document": doc_or_docs, "time": total_time, "stack_trace": stack_trace, "size": response_sizes[-1] if response_sizes else 0, "internal": internal, } ) return result # Wrap Cursor.update for getting queries @functools.wraps(_original_methods["update"]) def _update( collection_self, spec, document, upsert=False, maniuplate=False, safe=None, multi=False, **kwargs, ): start_time = time.time() result = _original_methods["update"]( collection_self, spec, document, upsert=upsert, multi=multi, **kwargs ) total_time = (time.time() - start_time) * 1000 __traceback_hide__ = True # noqa stack_trace, internal = _tidy_stacktrace() updates.append( { "document": document, "upsert": upsert, "multi": multi, "spec": spec, "time": total_time, "stack_trace": stack_trace, "size": response_sizes[-1] if response_sizes else 0, "internal": internal, } ) return result # Wrap Cursor.remove for getting queries @functools.wraps(_original_methods["remove"]) def _remove(collection_self, spec_or_id, safe=None, **kwargs): start_time = time.time() result = _original_methods["remove"](collection_self, spec_or_id, **kwargs) total_time = (time.time() - start_time) * 1000 __traceback_hide__ = True # noqa stack_trace, internal = _tidy_stacktrace() removes.append( { "spec_or_id": spec_or_id, "time": total_time, " ": stack_trace, "size": response_sizes[-1] if response_sizes else 0, "internal": internal, } ) return result # Wrap Cursor._refresh for getting queries @functools.wraps(_original_methods["refresh"]) def _cursor_refresh(cursor_self): # Look up __ private instance variables def privar(name): return getattr(cursor_self, "_Cursor__{0}".format(name), None) if privar("id") is not None: # getMore not query - move on return _original_methods["refresh"](cursor_self) # NOTE: See pymongo/cursor.py+557 [_refresh()] and # pymongo/message.py for where information is stored # Time the actual query start_time = time.time() result = _original_methods["refresh"](cursor_self) total_time = (time.time() - start_time) * 1000 query_son = privar("query_spec")() if not isinstance(query_son, bson.SON): if "$query" not in query_son: query_son = {"$query": query_son} data = privar("data") if data: query_son["data"] = data orderby = privar("ordering") if orderby: query_son["$orderby"] = orderby hint = privar("hint") if hint: query_son["$hint"] = hint snapshot = privar("snapshot") if snapshot: query_son["$snapshot"] = snapshot maxScan = privar("max_scan") if maxScan: query_son["$maxScan"] = maxScan __traceback_hide__ = True # noqa stack_trace, internal = _tidy_stacktrace() query_data = { "time": total_time, "operation": "query", "stack_trace": stack_trace, "size": response_sizes[-1] if response_sizes else 0, "data": copy.copy(privar("data")), "internal": internal, } # Collection in format . collection_name = privar("collection") query_data["collection"] = collection_name.full_name.split(".")[1] if query_data["collection"] == "$cmd": query_data["operation"] = "command" # Handle count as a special case if "count" in query_son: # Information is in a different format to a standard query query_data["collection"] = query_son["count"] query_data["operation"] = "count" query_data["skip"] = query_son.get("skip") query_data["limit"] = query_son.get("limit") query_data["query"] = query_son["query"] else: # Normal Query query_data["skip"] = privar("skip") query_data["limit"] = privar("limit") query_data["query"] = query_son["$query"] query_data["ordering"] = _get_ordering(query_son) queries.append(query_data) return result def install_tracker(): if pymongo.collection.Collection.insert != _insert: pymongo.collection.Collection.insert = _insert if pymongo.collection.Collection.update != _update: pymongo.collection.Collection.update = _update if pymongo.collection.Collection.remove != _remove: pymongo.collection.Collection.remove = _remove if pymongo.cursor.Cursor._refresh != _cursor_refresh: pymongo.cursor.Cursor._refresh = _cursor_refresh if pymongo.command_cursor.CommandCursor._unpack_response != _unpack_response: pymongo.command_cursor.CommandCursor._unpack_response = _unpack_response def uninstall_tracker(): if pymongo.collection.Collection.insert == _insert: pymongo.collection.Collection.insert = _original_methods["insert"] if pymongo.collection.Collection.update == _update: pymongo.collection.Collection.update = _original_methods["update"] if pymongo.collection.Collection.remove == _remove: pymongo.collection.Collection.remove = _original_methods["remove"] if pymongo.cursor.Cursor._refresh == _cursor_refresh: pymongo.cursor.Cursor._refresh = _original_methods["cursor_refresh"] if pymongo.command_cursor.CommandCursor._unpack_response == _unpack_response: pymongo.command_cursor.CommandCursor._unpack_response = _original_methods[ "_unpack_response" ] def reset(): global queries, inserts, updates, removes, response_sizes queries = [] inserts = [] updates = [] removes = [] response_sizes = [] def _get_ordering(son): """Helper function to extract formatted ordering from dict.""" def fmt(field, direction): return "{0}{1}".format({-1: "-", 1: "+"}[direction], field) if "$orderby" in son: return ", ".join(fmt(f, d) for f, d in son["$orderby"].items()) def _tidy_stacktrace(): """ Tidy the stack_trace """ socketserver_path = os.path.realpath(os.path.dirname(socketserver.__file__)) pymongo_path = os.path.realpath(os.path.dirname(pymongo.__file__)) paths = ["/site-packages/", "/flaskext/", socketserver_path, pymongo_path] internal = False # Check html templates fnames = [] for i in range(100): try: fname = sys._getframe(i).f_code.co_filename if ".html" in fname: fnames.append(fname) except Exception: break fnames = list(set(fnames)) trace = [] for path in fnames: if "flask_debugtoolbar" in path: internal = True trace.append((path, "?", "?", "?", False)) if trace: return trace, internal stack = inspect.stack() reversed(stack) trace = [] for frame, path, line_no, func_name, text in (f[:5] for f in stack): s_path = os.path.realpath(path) # Support hiding of frames -- used in various utilities that provide # inspection. if "__traceback_hide__" in frame.f_locals: continue hidden = False if func_name == "": hidden = True if any([p for p in paths if p in s_path]): hidden = True if not text: text = "" else: if sys.version_info >= (3, 0): text = "".join(text).strip() else: try: text = unicode("".join(text).strip()) except Exception: pass trace.append((path, line_no, func_name, text, hidden)) return trace, internal flask-mongoengine-1.0.0/flask_mongoengine/pagination.py000066400000000000000000000132621375611115600233150ustar00rootroot00000000000000import math from flask import abort from mongoengine.queryset import QuerySet __all__ = ("Pagination", "ListFieldPagination") class Pagination(object): def __init__(self, iterable, page, per_page): if page < 1: abort(404) self.iterable = iterable self.page = page self.per_page = per_page if isinstance(iterable, QuerySet): self.total = iterable.count() else: self.total = len(iterable) start_index = (page - 1) * per_page end_index = page * per_page self.items = iterable[start_index:end_index] if isinstance(self.items, QuerySet): self.items = self.items.select_related() if not self.items and page != 1: abort(404) @property def pages(self): """The total number of pages""" return int(math.ceil(self.total / float(self.per_page))) def prev(self, error_out=False): """Returns a :class:`Pagination` object for the previous page.""" assert self.iterable is not None, ( "an object is required " "for this method to work" ) iterable = self.iterable if isinstance(iterable, QuerySet): iterable._skip = None iterable._limit = None return self.__class__(iterable, self.page - 1, self.per_page) @property def prev_num(self): """Number of the previous page.""" return self.page - 1 @property def has_prev(self): """True if a previous page exists""" return self.page > 1 def next(self, error_out=False): """Returns a :class:`Pagination` object for the next page.""" assert self.iterable is not None, ( "an object is required " "for this method to work" ) iterable = self.iterable if isinstance(iterable, QuerySet): iterable._skip = None iterable._limit = None return self.__class__(iterable, self.page + 1, self.per_page) @property def has_next(self): """True if a next page exists.""" return self.page < self.pages @property def next_num(self): """Number of the next page""" return self.page + 1 def iter_pages(self, left_edge=2, left_current=2, right_current=5, right_edge=2): """Iterates over the page numbers in the pagination. The four parameters control the thresholds how many numbers should be produced from the sides. Skipped page numbers are represented as `None`. This is how you could render such a pagination in the templates: .. sourcecode:: html+jinja {% macro render_pagination(pagination, endpoint) %} {% endmacro %} """ last = 0 for num in range(1, self.pages + 1): if ( num <= left_edge or num > self.pages - right_edge or ( num >= self.page - left_current and num <= self.page + right_current ) ): if last + 1 != num: yield None yield num last = num if last != self.pages: yield None class ListFieldPagination(Pagination): def __init__(self, queryset, doc_id, field_name, page, per_page, total=None): """Allows an array within a document to be paginated. Queryset must contain the document which has the array we're paginating, and doc_id should be it's _id. Field name is the name of the array we're paginating. Page and per_page work just like in Pagination. Total is an argument because it can be computed more efficiently elsewhere, but we still use array.length as a fallback. """ if page < 1: abort(404) self.page = page self.per_page = per_page self.queryset = queryset self.doc_id = doc_id self.field_name = field_name start_index = (page - 1) * per_page field_attrs = {field_name: {"$slice": [start_index, per_page]}} qs = queryset(pk=doc_id) self.items = getattr(qs.fields(**field_attrs).first(), field_name) self.total = total or len( getattr(qs.fields(**{field_name: 1}).first(), field_name) ) if not self.items and page != 1: abort(404) def prev(self, error_out=False): """Returns a :class:`Pagination` object for the previous page.""" assert self.items is not None, ( "a query object is required " "for this method to work" ) return self.__class__( self.queryset, self.doc_id, self.field_name, self.page - 1, self.per_page, self.total, ) def next(self, error_out=False): """Returns a :class:`Pagination` object for the next page.""" assert self.items is not None, ( "a query object is required " "for this method to work" ) return self.__class__( self.queryset, self.doc_id, self.field_name, self.page + 1, self.per_page, self.total, ) flask-mongoengine-1.0.0/flask_mongoengine/panels.py000066400000000000000000000042061375611115600224440ustar00rootroot00000000000000from flask import current_app from flask_debugtoolbar.panels import DebugPanel from jinja2 import ChoiceLoader, PackageLoader from flask_mongoengine import operation_tracker package_loader = PackageLoader("flask_mongoengine", "templates") def _maybe_patch_jinja_loader(jinja_env): """Patch the jinja_env loader to include flaskext.mongoengine templates folder if necessary. """ if not isinstance(jinja_env.loader, ChoiceLoader): jinja_env.loader = ChoiceLoader([jinja_env.loader, package_loader]) elif package_loader not in jinja_env.loader.loaders: jinja_env.loader.loaders.append(package_loader) class MongoDebugPanel(DebugPanel): """Panel that shows information about MongoDB operations (including stack) Adapted from https://github.com/hmarr/django-debug-toolbar-mongo """ name = "MongoDB" has_content = True def __init__(self, *args, **kwargs): super(MongoDebugPanel, self).__init__(*args, **kwargs) _maybe_patch_jinja_loader(self.jinja_env) operation_tracker.install_tracker() def process_request(self, request): operation_tracker.reset() def nav_title(self): return "MongoDB" def nav_subtitle(self): attrs = ["queries", "inserts", "updates", "removes"] ops = sum( sum((1 for o in getattr(operation_tracker, a) if not o["internal"])) for a in attrs ) total_time = sum( sum(o["time"] for o in getattr(operation_tracker, a)) for a in attrs ) return "{0} operations in {1:.2f}ms".format(ops, total_time) def title(self): return "MongoDB Operations" def url(self): return "" def content(self): context = self.context.copy() context["queries"] = operation_tracker.queries context["inserts"] = operation_tracker.inserts context["updates"] = operation_tracker.updates context["removes"] = operation_tracker.removes context["slow_query_limit"] = current_app.config.get( "MONGO_DEBUG_PANEL_SLOW_QUERY_LIMIT", 100 ) return self.render("panels/mongo-panel.html", context) flask-mongoengine-1.0.0/flask_mongoengine/sessions.py000066400000000000000000000062541375611115600230350ustar00rootroot00000000000000import datetime import sys import uuid from bson.tz_util import utc from flask.sessions import SessionInterface, SessionMixin from werkzeug.datastructures import CallbackDict __all__ = ("MongoEngineSession", "MongoEngineSessionInterface") if sys.version_info >= (3, 0): basestring = str class MongoEngineSession(CallbackDict, SessionMixin): def __init__(self, initial=None, sid=None): def on_update(self): self.modified = True CallbackDict.__init__(self, initial, on_update) self.sid = sid self.modified = False class MongoEngineSessionInterface(SessionInterface): """SessionInterface for mongoengine""" def __init__(self, db, collection="session"): """ The MongoSessionInterface :param db: The app's db eg: MongoEngine() :param collection: The session collection name defaults to "session" """ if not isinstance(collection, basestring): raise ValueError("collection argument should be string or unicode") class DBSession(db.Document): sid = db.StringField(primary_key=True) data = db.DictField() expiration = db.DateTimeField() meta = { "allow_inheritance": False, "collection": collection, "indexes": [ { "fields": ["expiration"], "expireAfterSeconds": 60 * 60 * 24 * 7 * 31, } ], } self.cls = DBSession def get_expiration_time(self, app, session): if session.permanent: return app.permanent_session_lifetime if "SESSION_TTL" in app.config: return datetime.timedelta(**app.config["SESSION_TTL"]) return datetime.timedelta(days=1) def open_session(self, app, request): sid = request.cookies.get(app.session_cookie_name) if sid: stored_session = self.cls.objects(sid=sid).first() if stored_session: expiration = stored_session.expiration if not expiration.tzinfo: expiration = expiration.replace(tzinfo=utc) if expiration > datetime.datetime.utcnow().replace(tzinfo=utc): return MongoEngineSession( initial=stored_session.data, sid=stored_session.sid ) return MongoEngineSession(sid=str(uuid.uuid4())) def save_session(self, app, session, response): domain = self.get_cookie_domain(app) httponly = self.get_cookie_httponly(app) if not session: if session.modified: response.delete_cookie(app.session_cookie_name, domain=domain) return expiration = datetime.datetime.utcnow().replace( tzinfo=utc ) + self.get_expiration_time(app, session) if session.modified: self.cls(sid=session.sid, data=session, expiration=expiration).save() response.set_cookie( app.session_cookie_name, session.sid, expires=expiration, httponly=httponly, domain=domain, ) flask-mongoengine-1.0.0/flask_mongoengine/templates/000077500000000000000000000000001375611115600226045ustar00rootroot00000000000000flask-mongoengine-1.0.0/flask_mongoengine/templates/panels/000077500000000000000000000000001375611115600240665ustar00rootroot00000000000000flask-mongoengine-1.0.0/flask_mongoengine/templates/panels/mongo-panel.html000066400000000000000000000145241375611115600271760ustar00rootroot00000000000000 {% macro render_stats(title, queries, slow_query_limit=100) %}

{{ title }}

{% if queries %} {% if title == 'Queries' %} {% elif title == 'Inserts' %} {% elif title == 'Removes' %} {% elif title == 'Updates' %} {% endif %} {% for query in queries %} {% if title == "Queries" %} {% set colspan = 10 %} {% elif title == "Inserts" %} {% set colspan = 5 %} {% elif title == 'Removes' %} {% set colspan = 5 %} {% elif title == 'Updates' %} {% set colspan = 8 %} {% endif %} {% if title == "Queries" %} {% endif %} {% endfor %}
Time (ms) SizeOperation Collection Query Ordering Skip Limit DataDocument SafeQuery / Id SafeQuery Update Safe Multi UpsertStack Trace
slow_query_limit %}style="color:red;" {% endif %}> {{ query.time|round(3) }} {{ query.size|round(2) }}Kb{{ query.operation|title }} {{ query.collection }} {% if query.query %}{{ query.query|safe }}{% endif %} {% if query.ordering %}{{ query.ordering }}{% endif %} {% if query.skip %}{{ query.skip }}{% endif %} {% if query.limit %}{{ query.limit }}{% endif %} Toggle{{ query.document|safe }} {{ query.safe }}
{{ query.spec_or_id|safe }}
{{ query.safe }}
{{ query.spec|safe }}
{{ query.document|safe }}
{{ query.safe }} {{ query.multi }} {{ query.upsert }}Toggle
{{ query.data|pprint }}
{% for line in query.stack_trace %} {% endfor %}
Line File Function Code
{{ line.1 }} {{ line.0 }} {{ line.2 }} {{ line.3|safe }}
Toggle full trace
{% else %}

No {{ title|lower }} recorded

{% endif %} {% endmacro %} {{ render_stats("Queries", queries, slow_query_limit)}} {{ render_stats("Removes", removes, slow_query_limit)}} {{ render_stats("Inserts", inserts, slow_query_limit)}} {{ render_stats("Updates", updates, slow_query_limit)}} flask-mongoengine-1.0.0/flask_mongoengine/wtf/000077500000000000000000000000001375611115600214065ustar00rootroot00000000000000flask-mongoengine-1.0.0/flask_mongoengine/wtf/__init__.py000066400000000000000000000002031375611115600235120ustar00rootroot00000000000000from flask_mongoengine.wtf.base import WtfBaseField # noqa from flask_mongoengine.wtf.orm import model_fields, model_form # noqa flask-mongoengine-1.0.0/flask_mongoengine/wtf/base.py000066400000000000000000000023131375611115600226710ustar00rootroot00000000000000from mongoengine.base import BaseField __all__ = "WtfBaseField" class WtfBaseField(BaseField): """ Extension wrapper class for mongoengine BaseField. This enables flask-mongoengine wtf to extend the number of field parameters, and settings on behalf of document model form generator for WTForm. @param validators: wtf model form field validators. @param filters: wtf model form field filters. """ def __init__(self, validators=None, filters=None, **kwargs): self.validators = self._ensure_callable_or_list(validators, "validators") self.filters = self._ensure_callable_or_list(filters, "filters") BaseField.__init__(self, **kwargs) def _ensure_callable_or_list(self, field, msg_flag): """ Ensure the value submitted via field is either a callable object to convert to list or it is in fact a valid list value. """ if field is not None: if callable(field): field = [field] else: msg = "Argument '%s' must be a list value" % msg_flag if not isinstance(field, list): raise TypeError(msg) return field flask-mongoengine-1.0.0/flask_mongoengine/wtf/fields.py000066400000000000000000000140121375611115600232240ustar00rootroot00000000000000""" Useful form fields for use with the mongoengine. """ import json import sys from gettext import gettext as _ from mongoengine.queryset import DoesNotExist from wtforms import widgets from wtforms.fields import SelectFieldBase, StringField, TextAreaField from wtforms.validators import ValidationError __all__ = ( "ModelSelectField", "QuerySetSelectField", ) if sys.version_info >= (3, 0): unicode = str class QuerySetSelectField(SelectFieldBase): """ Given a QuerySet either at initialization or inside a view, will display a select drop-down field of choices. The `data` property actually will store/keep an ORM model instance, not the ID. Submitting a choice which is not in the queryset will result in a validation error. Specifying `label_attr` in the constructor will use that property of the model instance for display in the list, else the model object's `__str__` or `__unicode__` will be used. If `allow_blank` is set to `True`, then a blank choice will be added to the top of the list. Selecting this choice will result in the `data` property being `None`. The label for the blank choice can be set by specifying the `blank_text` parameter. """ widget = widgets.Select() def __init__( self, label="", validators=None, queryset=None, label_attr="", allow_blank=False, blank_text="---", label_modifier=None, **kwargs, ): super(QuerySetSelectField, self).__init__(label, validators, **kwargs) self.label_attr = label_attr self.allow_blank = allow_blank self.blank_text = blank_text self.label_modifier = label_modifier self.queryset = queryset def iter_choices(self): if self.allow_blank: yield ("__None", self.blank_text, self.data is None) if self.queryset is None: return self.queryset.rewind() for obj in self.queryset: label = ( self.label_modifier(obj) if self.label_modifier else (self.label_attr and getattr(obj, self.label_attr) or obj) ) if isinstance(self.data, list): selected = obj in self.data else: selected = self._is_selected(obj) yield (obj.id, label, selected) def process_formdata(self, valuelist): if valuelist: if valuelist[0] == "__None": self.data = None else: if self.queryset is None: self.data = None return try: obj = self.queryset.get(pk=valuelist[0]) self.data = obj except DoesNotExist: self.data = None def pre_validate(self, form): if not self.allow_blank or self.data is not None: if not self.data: raise ValidationError(_("Not a valid choice")) def _is_selected(self, item): return item == self.data class QuerySetSelectMultipleField(QuerySetSelectField): widget = widgets.Select(multiple=True) def __init__( self, label="", validators=None, queryset=None, label_attr="", allow_blank=False, blank_text="---", **kwargs, ): super(QuerySetSelectMultipleField, self).__init__( label, validators, queryset, label_attr, allow_blank, blank_text, **kwargs ) def process_formdata(self, valuelist): if valuelist: if valuelist[0] == "__None": self.data = None else: if not self.queryset: self.data = None return self.queryset.rewind() self.data = list(self.queryset(pk__in=valuelist)) if not len(self.data): self.data = None # If no value passed, empty the list else: self.data = None def _is_selected(self, item): return item in self.data if self.data else False class ModelSelectField(QuerySetSelectField): """ Like a QuerySetSelectField, except takes a model class instead of a queryset and lists everything in it. """ def __init__(self, label="", validators=None, model=None, **kwargs): queryset = kwargs.pop("queryset", model.objects) super(ModelSelectField, self).__init__( label, validators, queryset=queryset, **kwargs ) class ModelSelectMultipleField(QuerySetSelectMultipleField): """ Allows multiple select """ def __init__(self, label="", validators=None, model=None, **kwargs): queryset = kwargs.pop("queryset", model.objects) super(ModelSelectMultipleField, self).__init__( label, validators, queryset=queryset, **kwargs ) class JSONField(TextAreaField): def _value(self): if self.raw_data: return self.raw_data[0] else: return self.data and unicode(json.dumps(self.data)) or "" def process_formdata(self, value): if value: try: self.data = json.loads(value[0]) except ValueError: raise ValueError(self.gettext("Invalid JSON data.")) class DictField(JSONField): def process_formdata(self, value): super(DictField, self).process_formdata(value) if value and not isinstance(self.data, dict): raise ValueError(self.gettext("Not a valid dictionary.")) class NoneStringField(StringField): """ Custom StringField that counts "" as None """ def process_formdata(self, valuelist): if valuelist: self.data = valuelist[0] if self.data == "": self.data = None class BinaryField(TextAreaField): """ Custom TextAreaField that converts its value with binary_type. """ def process_formdata(self, valuelist): if valuelist: self.data = bytes(valuelist[0], "utf-8") flask-mongoengine-1.0.0/flask_mongoengine/wtf/models.py000066400000000000000000000013161375611115600232440ustar00rootroot00000000000000from flask_wtf import FlaskForm from flask_wtf.form import _Auto class ModelForm(FlaskForm): """A WTForms mongoengine model form""" def __init__(self, formdata=_Auto, **kwargs): self.instance = kwargs.pop("instance", None) or kwargs.get("obj") if self.instance and not formdata: kwargs["obj"] = self.instance self.formdata = formdata super(ModelForm, self).__init__(formdata, **kwargs) def save(self, commit=True, **kwargs): if self.instance: self.populate_obj(self.instance) else: self.instance = self.model_class(**self.data) if commit: self.instance.save(**kwargs) return self.instance flask-mongoengine-1.0.0/flask_mongoengine/wtf/orm.py000066400000000000000000000237031375611115600225620ustar00rootroot00000000000000""" Tools for generating forms based on mongoengine Document schemas. """ import decimal import sys from collections import OrderedDict from bson import ObjectId from mongoengine import ReferenceField from wtforms import fields as f, validators from flask_mongoengine.wtf.fields import ( BinaryField, DictField, ModelSelectField, ModelSelectMultipleField, NoneStringField, ) from flask_mongoengine.wtf.models import ModelForm __all__ = ( "model_fields", "model_form", ) if sys.version_info >= (3, 0): unicode = str def converts(*args): def _inner(func): func._converter_for = frozenset(args) return func return _inner class ModelConverter(object): def __init__(self, converters=None): if not converters: converters = {} for name in dir(self): obj = getattr(self, name) if hasattr(obj, "_converter_for"): for classname in obj._converter_for: converters[classname] = obj self.converters = converters def convert(self, model, field, field_args): kwargs = { "label": getattr(field, "verbose_name", field.name), "description": getattr(field, "help_text", None) or "", "validators": getattr(field, "validators", None) or [], "filters": getattr(field, "filters", None) or [], "default": field.default, } if field_args: kwargs.update(field_args) if kwargs["validators"]: # Create a copy of the list since we will be modifying it. kwargs["validators"] = list(kwargs["validators"]) if field.required: kwargs["validators"].append(validators.InputRequired()) else: kwargs["validators"].append(validators.Optional()) ftype = type(field).__name__ if field.choices: kwargs["choices"] = field.choices if ftype in self.converters: kwargs["coerce"] = self.coerce(ftype) multiple_field = kwargs.pop("multiple", False) radio_field = kwargs.pop("radio", False) if multiple_field: return f.SelectMultipleField(**kwargs) if radio_field: return f.RadioField(**kwargs) return f.SelectField(**kwargs) ftype = type(field).__name__ if hasattr(field, "to_form_field"): return field.to_form_field(model, kwargs) if hasattr(field, "field") and type(field.field) == ReferenceField: kwargs["label_modifier"] = getattr( model, field.name + "_label_modifier", None ) if ftype in self.converters: return self.converters[ftype](model, field, kwargs) @classmethod def _string_common(cls, model, field, kwargs): if field.max_length or field.min_length: kwargs["validators"].append( validators.Length( max=field.max_length or -1, min=field.min_length or -1 ) ) @classmethod def _number_common(cls, model, field, kwargs): if field.max_value or field.min_value: kwargs["validators"].append( validators.NumberRange(max=field.max_value, min=field.min_value) ) @converts("StringField") def conv_String(self, model, field, kwargs): if field.regex: kwargs["validators"].append(validators.Regexp(regex=field.regex)) self._string_common(model, field, kwargs) password_field = kwargs.pop("password", False) textarea_field = kwargs.pop("textarea", False) or not field.max_length if password_field: return f.PasswordField(**kwargs) if textarea_field: return f.TextAreaField(**kwargs) return f.StringField(**kwargs) @converts("URLField") def conv_URL(self, model, field, kwargs): kwargs["validators"].append(validators.URL()) self._string_common(model, field, kwargs) return NoneStringField(**kwargs) @converts("EmailField") def conv_Email(self, model, field, kwargs): kwargs["validators"].append(validators.Email()) self._string_common(model, field, kwargs) return NoneStringField(**kwargs) @converts("IntField") def conv_Int(self, model, field, kwargs): self._number_common(model, field, kwargs) return f.IntegerField(**kwargs) @converts("FloatField") def conv_Float(self, model, field, kwargs): self._number_common(model, field, kwargs) return f.FloatField(**kwargs) @converts("DecimalField") def conv_Decimal(self, model, field, kwargs): self._number_common(model, field, kwargs) kwargs["places"] = getattr(field, "precision", None) return f.DecimalField(**kwargs) @converts("BooleanField") def conv_Boolean(self, model, field, kwargs): return f.BooleanField(**kwargs) @converts("DateTimeField") def conv_DateTime(self, model, field, kwargs): return f.DateTimeField(**kwargs) @converts("DateField") def conv_Date(self, model, field, kwargs): return f.DateField(**kwargs) @converts("BinaryField") def conv_Binary(self, model, field, kwargs): # TODO: may be set file field that will save file`s data to MongoDB if field.max_bytes: kwargs["validators"].append(validators.Length(max=field.max_bytes)) return BinaryField(**kwargs) @converts("DictField") def conv_Dict(self, model, field, kwargs): return DictField(**kwargs) @converts("ListField") def conv_List(self, model, field, kwargs): if isinstance(field.field, ReferenceField): return ModelSelectMultipleField(model=field.field.document_type, **kwargs) if field.field.choices: kwargs["multiple"] = True return self.convert(model, field.field, kwargs) field_args = kwargs.pop("field_args", {}) unbound_field = self.convert(model, field.field, field_args) unacceptable = { "validators": [], "filters": [], "min_entries": kwargs.get("min_entries", 0), } kwargs.update(unacceptable) return f.FieldList(unbound_field, **kwargs) @converts("SortedListField") def conv_SortedList(self, model, field, kwargs): # TODO: sort functionality, may be need sortable widget return self.conv_List(model, field, kwargs) @converts("GeoLocationField") def conv_GeoLocation(self, model, field, kwargs): # TODO: create geo field and widget (also GoogleMaps) return @converts("ObjectIdField") def conv_ObjectId(self, model, field, kwargs): return @converts("EmbeddedDocumentField") def conv_EmbeddedDocument(self, model, field, kwargs): kwargs = { "validators": [], "filters": [], "default": field.default or field.document_type_obj, } form_class = model_form(field.document_type_obj, field_args={}) return f.FormField(form_class, **kwargs) @converts("ReferenceField") def conv_Reference(self, model, field, kwargs): return ModelSelectField(model=field.document_type, **kwargs) @converts("GenericReferenceField") def conv_GenericReference(self, model, field, kwargs): return def coerce(self, field_type): coercions = { "IntField": int, "BooleanField": bool, "FloatField": float, "DecimalField": decimal.Decimal, "ObjectIdField": ObjectId, } return coercions.get(field_type, unicode) def model_fields(model, only=None, exclude=None, field_args=None, converter=None): """ Generate a dictionary of fields for a given database model. See `model_form` docstring for description of parameters. """ from mongoengine.base import BaseDocument, DocumentMetaclass if not isinstance(model, (BaseDocument, DocumentMetaclass)): raise TypeError("model must be a mongoengine Document schema") converter = converter or ModelConverter() field_args = field_args or {} names = ((k, v.creation_counter) for k, v in model._fields.items()) field_names = [n[0] for n in sorted(names, key=lambda n: n[1])] if only: field_names = [x for x in only if x in set(field_names)] elif exclude: field_names = [x for x in field_names if x not in set(exclude)] field_dict = OrderedDict() for name in field_names: model_field = model._fields[name] field = converter.convert(model, model_field, field_args.get(name)) if field is not None: field_dict[name] = field return field_dict def model_form( model, base_class=ModelForm, only=None, exclude=None, field_args=None, converter=None, ): """ Create a wtforms Form for a given mongoengine Document schema:: from flask_mongoengine.wtf import model_form from myproject.myapp.schemas import Article ArticleForm = model_form(Article) :param model: A mongoengine Document schema class :param base_class: Base form class to extend from. Must be a ``wtforms.Form`` subclass. :param only: An optional iterable with the property names that should be included in the form. Only these properties will have fields. :param exclude: An optional iterable with the property names that should be excluded from the form. All other properties will have fields. :param field_args: An optional dictionary of field names mapping to keyword arguments used to construct each field object. :param converter: A converter to generate the fields based on the model properties. If not set, ``ModelConverter`` is used. """ field_dict = model_fields(model, only, exclude, field_args, converter) field_dict["model_class"] = model return type(model.__name__ + "Form", (base_class,), field_dict) flask-mongoengine-1.0.0/pyproject.toml000066400000000000000000000003131375611115600200320ustar00rootroot00000000000000[tool.black] line-length = 88 target-version = ['py36', 'py37', 'py38'] exclude = ''' /( \.eggs | \.git | \.tox | \.venv | \.vscode | docs | _build | buck-out | build | dist )/ ''' flask-mongoengine-1.0.0/requirements-dev.txt000066400000000000000000000002031375611115600211540ustar00rootroot00000000000000-r requirements.txt # Dev and test dependencies without version limiting, to test with latest. black pre-commit pytest pytest-cov flask-mongoengine-1.0.0/requirements.txt000066400000000000000000000001711375611115600204040ustar00rootroot00000000000000Flask>=1.1.2 Flask-DebugToolbar>=0.11.0 WTForms[email]>=2.3.1 Flask-WTF>=0.14.3 mongoengine>=0.20 pymongo>=3.10.1 flake8 flask-mongoengine-1.0.0/setup.cfg000066400000000000000000000003771375611115600167510ustar00rootroot00000000000000[tool:pytest] addopts = --cov=flask_mongoengine --cov-config=setup.cfg testpaths = tests env_override_existing_values = 1 [flake8] ignore=E501,F403,F405,I201,W503,E203 max-line-length=90 exclude=build,dist,docs,examples,venv,.tox,.eggs max-complexity=17 flask-mongoengine-1.0.0/setup.py000066400000000000000000000050431375611115600166350ustar00rootroot00000000000000import os from setuptools import setup description = ( "Flask-MongoEngine is a Flask extension " "that provides integration with MongoEngine and WTF model forms." ) # Load index.rst as long_description doc_path = os.path.join(os.path.dirname(__file__), "docs", "index.rst") long_description = open(doc_path, encoding="utf-8").read() long_description_content_type = "text/x-rst" # Stops exit traceback on tests try: import multiprocessing # noqa except ImportError: pass def get_version(version_tuple): """Return the version tuple as a string, e.g. for (0, 10, 7), return '0.10.7'. """ return ".".join(map(str, version_tuple)) # Dirty hack to get version number from flask_monogengine/__init__.py - we # can't import it as it depends on PyMongo and PyMongo isn't installed until # this file is read init = os.path.join(os.path.dirname(__file__), "flask_mongoengine", "__init__.py") version_line = list(filter(lambda l: l.startswith("VERSION"), open(init)))[0] version = get_version(eval(version_line.split("=")[-1])) test_requirements = ["coverage", "pytest", "pytest-cov"] setup( name="flask-mongoengine", version=version, url="https://github.com/mongoengine/flask-mongoengine", license="BSD", author="Ross Lawley", author_email="ross.lawley@gmail.com", zip_safe=False, platforms="any", install_requires=[ "Flask>=1.1.2", "WTForms[email]>=2.3.1", "Flask-WTF>=0.14.3", "mongoengine>=0.20", ], packages=["flask_mongoengine", "flask_mongoengine.wtf"], include_package_data=True, tests_require=test_requirements, description=description, long_description=long_description, classifiers=[ "Development Status :: 4 - Beta", "Environment :: Web Environment", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: PyPy", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Software Development :: Libraries :: Python Modules", "Framework :: Flask", ], ) flask-mongoengine-1.0.0/tests/000077500000000000000000000000001375611115600162635ustar00rootroot00000000000000flask-mongoengine-1.0.0/tests/__init__.py000066400000000000000000000000001375611115600203620ustar00rootroot00000000000000flask-mongoengine-1.0.0/tests/conftest.py000066400000000000000000000025771375611115600204750ustar00rootroot00000000000000from datetime import datetime import mongoengine import pytest from flask import Flask from flask_mongoengine import MongoEngine @pytest.fixture() def app(): app = Flask(__name__) app.config["TESTING"] = True app.config["WTF_CSRF_ENABLED"] = False with app.app_context(): yield app mongoengine.connection.disconnect_all() @pytest.fixture() def db(app): app.config["MONGODB_HOST"] = "mongodb://localhost:27017/flask_mongoengine_test_db" test_db = MongoEngine(app) db_name = test_db.connection.get_database("flask_mongoengine_test_db").name if not db_name.endswith("_test_db"): raise RuntimeError( f"DATABASE_URL must point to testing db, not to master db ({db_name})" ) # Clear database before tests, for cases when some test failed before. test_db.connection.drop_database(db_name) yield test_db # Clear database after tests, for graceful exit. test_db.connection.drop_database(db_name) @pytest.fixture() def todo(db): class Todo(db.Document): title = mongoengine.StringField(max_length=60) text = mongoengine.StringField() done = mongoengine.BooleanField(default=False) pub_date = mongoengine.DateTimeField(default=datetime.utcnow) comments = mongoengine.ListField(mongoengine.StringField()) comment_count = mongoengine.IntField() return Todo flask-mongoengine-1.0.0/tests/test_base.py000066400000000000000000000013531375611115600206100ustar00rootroot00000000000000"""Tests for base MongoEngine class.""" from flask_mongoengine import MongoEngine import pytest def test_mongoengine_class__should_raise_type_error__if_config_not_dict(): """MongoEngine will handle None values, but will pass anything else as app.""" input_value = "Not dict type" with pytest.raises(TypeError) as error: MongoEngine(input_value) assert str(error.value) == "Invalid Flask application instance" @pytest.mark.parametrize("input_value", [None, "Not dict type"]) def test_init_app__should_raise_type_error__if_config_not_dict(input_value): db = MongoEngine() with pytest.raises(TypeError) as error: db.init_app(input_value) assert str(error.value) == "Invalid Flask application instance" flask-mongoengine-1.0.0/tests/test_basic_app.py000066400000000000000000000024161375611115600216200ustar00rootroot00000000000000import flask import pytest from bson import ObjectId @pytest.fixture(autouse=True) def setup_endpoints(app, todo): Todo = todo @app.route("/") def index(): return "\n".join(x.title for x in Todo.objects) @app.route("/add", methods=["POST"]) def add(): form = flask.request.form todo = Todo(title=form["title"], text=form["text"]) todo.save() return "added" @app.route("/show//") def show(id): todo = Todo.objects.get_or_404(id=id) return "\n".join([todo.title, todo.text]) def test_with_id(app, todo): Todo = todo client = app.test_client() response = client.get("/show/%s/" % ObjectId()) assert response.status_code == 404 client.post("/add", data={"title": "First Item", "text": "The text"}) response = client.get("/show/%s/" % Todo.objects.first_or_404().id) assert response.status_code == 200 assert response.data.decode("utf-8") == "First Item\nThe text" def test_basic_insert(app): client = app.test_client() client.post("/add", data={"title": "First Item", "text": "The text"}) client.post("/add", data={"title": "2nd Item", "text": "The text"}) response = client.get("/") assert response.data.decode("utf-8") == "First Item\n2nd Item" flask-mongoengine-1.0.0/tests/test_connection.py000066400000000000000000000247711375611115600220460ustar00rootroot00000000000000import mongoengine import pymongo import pytest from mongoengine.connection import ConnectionFailure from mongoengine.context_managers import switch_db from pymongo.database import Database from pymongo.errors import InvalidURI from pymongo.mongo_client import MongoClient from pymongo.read_preferences import ReadPreference from flask_mongoengine import MongoEngine, current_mongoengine_instance def test_connection__should_use_defaults__if_no_settings_provided(app): """Make sure a simple connection to a standalone MongoDB works.""" db = MongoEngine() # Verify no extension for Mongoengine yet created for app assert app.extensions == {} assert current_mongoengine_instance() is None # Create db connection. Should return None. assert db.init_app(app) is None # Verify db added to Flask extensions. assert current_mongoengine_instance() == db # Verify db settings passed to pymongo driver. # Default mongoengine db is 'default', default Flask-Mongoengine db is 'test'. connection = mongoengine.get_connection() mongo_engine_db = mongoengine.get_db() assert isinstance(mongo_engine_db, Database) assert isinstance(connection, MongoClient) assert mongo_engine_db.name == "test" assert connection.HOST == "localhost" assert connection.PORT == 27017 @pytest.mark.parametrize( ("config_extension"), [ { "MONGODB_SETTINGS": { "ALIAS": "simple_conn", "HOST": "localhost", "PORT": 27017, "DB": "flask_mongoengine_test_db", } }, { "MONGODB_HOST": "localhost", "MONGODB_PORT": 27017, "MONGODB_DB": "flask_mongoengine_test_db", "MONGODB_ALIAS": "simple_conn", }, ], ids=("Dict format", "Config variable format"), ) def test_connection__should_pass_alias__if_provided(app, config_extension): """Make sure a simple connection pass ALIAS setting variable.""" db = MongoEngine() app.config.update(config_extension) # Verify no extension for Mongoengine yet created for app assert app.extensions == {} assert current_mongoengine_instance() is None # Create db connection. Should return None. assert db.init_app(app) is None # Verify db added to Flask extensions. assert current_mongoengine_instance() == db # Verify db settings passed to pymongo driver. # ALIAS is used to find correct connection. # As we do not use default alias, default call to mongoengine.get_connection # should raise. with pytest.raises(ConnectionFailure): mongoengine.get_connection() connection = mongoengine.get_connection("simple_conn") mongo_engine_db = mongoengine.get_db("simple_conn") assert isinstance(mongo_engine_db, Database) assert isinstance(connection, MongoClient) assert mongo_engine_db.name == "flask_mongoengine_test_db" assert connection.HOST == "localhost" assert connection.PORT == 27017 @pytest.mark.parametrize( ("config_extension"), [ { "MONGODB_SETTINGS": { "HOST": "mongodb://localhost:27017/flask_mongoengine_test_db" } }, { "MONGODB_HOST": "mongodb://localhost:27017/flask_mongoengine_test_db", "MONGODB_PORT": 27017, "MONGODB_DB": "should_ignore_it", }, ], ids=("Dict format", "Config variable format"), ) def test_connection__should_parse_host_uri__if_host_formatted_as_uri( app, config_extension ): """Make sure a simple connection pass ALIAS setting variable.""" db = MongoEngine() app.config.update(config_extension) # Verify no extension for Mongoengine yet created for app assert app.extensions == {} assert current_mongoengine_instance() is None # Create db connection. Should return None. assert db.init_app(app) is None # Verify db added to Flask extensions. assert current_mongoengine_instance() == db connection = mongoengine.get_connection() mongo_engine_db = mongoengine.get_db() assert isinstance(mongo_engine_db, Database) assert isinstance(connection, MongoClient) assert mongo_engine_db.name == "flask_mongoengine_test_db" assert connection.HOST == "localhost" assert connection.PORT == 27017 @pytest.mark.parametrize( ("config_extension"), [ { "MONGODB_SETTINGS": { "HOST": "mongomock://localhost:27017/flask_mongoengine_test_db" } }, { "MONGODB_SETTINGS": { "ALIAS": "simple_conn", "HOST": "localhost", "PORT": 27017, "DB": "flask_mongoengine_test_db", "IS_MOCK": True, } }, {"MONGODB_HOST": "mongomock://localhost:27017/flask_mongoengine_test_db"}, ], ids=("Dict format as URI", "Dict format as Param", "Config variable format as URI"), ) def test_connection__should_parse_mongo_mock_uri__as_uri_and_as_settings( app, config_extension ): """Make sure a simple connection pass ALIAS setting variable.""" db = MongoEngine() app.config.update(config_extension) # Verify no extension for Mongoengine yet created for app assert app.extensions == {} assert current_mongoengine_instance() is None # Create db connection. Should return None. with pytest.raises(RuntimeError) as error: assert db.init_app(app) is None assert str(error.value) == "You need mongomock installed to mock MongoEngine." @pytest.mark.parametrize( ("config_extension"), [ { "MONGODB_SETTINGS": { "HOST": "postgre://localhost:27017/flask_mongoengine_test_db" } }, {"MONGODB_HOST": "mysql://localhost:27017/flask_mongoengine_test_db"}, ], ids=("Dict format as URI", "Config variable format as URI"), ) def test_connection__should_raise__if_uri_not_properly_formatted(app, config_extension): """Make sure a simple connection pass ALIAS setting variable.""" db = MongoEngine() app.config.update(config_extension) # Verify no extension for Mongoengine yet created for app assert app.extensions == {} assert current_mongoengine_instance() is None # Create db connection. Should return None. with pytest.raises(InvalidURI) as error: assert db.init_app(app) is None assert ( str(error.value) == "Invalid URI scheme: URI must begin with 'mongodb://' or 'mongodb+srv://'" ) def test_connection__should_accept_host_as_list(app): """Make sure MONGODB_HOST can be a list hosts.""" db = MongoEngine() app.config["MONGODB_SETTINGS"] = { "ALIAS": "host_list", "HOST": ["localhost:27017"], "DB": "flask_mongoengine_list_test_db", } db.init_app(app) connection = mongoengine.get_connection("host_list") mongo_engine_db = mongoengine.get_db("host_list") assert isinstance(mongo_engine_db, Database) assert isinstance(connection, MongoClient) assert mongo_engine_db.name == "flask_mongoengine_list_test_db" assert connection.HOST == "localhost" assert connection.PORT == 27017 def test_multiple_connections(app): """Make sure establishing multiple connections to a standalone MongoDB and switching between them works. """ db = MongoEngine() app.config["MONGODB_SETTINGS"] = [ { "ALIAS": "default", "DB": "flask_mongoengine_test_db_1", "HOST": "localhost", "PORT": 27017, }, { "ALIAS": "alternative", "DB": "flask_mongoengine_test_db_2", "HOST": "localhost", "PORT": 27017, }, ] class Todo(db.Document): title = db.StringField(max_length=60) db.init_app(app) # Drop default collection from init Todo.drop_collection() Todo.meta = {"db_alias": "alternative"} # Drop 'alternative' collection initiated early. Todo.drop_collection() # Make sure init correct and both databases are clean with switch_db(Todo, "default") as Todo: doc = Todo.objects().first() assert doc is None with switch_db(Todo, "alternative") as Todo: doc = Todo.objects().first() assert doc is None # Test saving a doc via the default connection with switch_db(Todo, "default") as Todo: todo = Todo() todo.text = "Sample" todo.title = "Testing" todo.done = True s_todo = todo.save() f_to = Todo.objects().first() assert s_todo.title == f_to.title # Make sure the doc still doesn't exist in the alternative db with switch_db(Todo, "alternative") as Todo: doc = Todo.objects().first() assert doc is None # Make sure switching back to the default connection shows the doc with switch_db(Todo, "default") as Todo: doc = Todo.objects().first() assert doc is not None def test_ingnored_mongodb_prefix_config(app): """Config starting by MONGODB_ but not used by flask-mongoengine should be ignored. """ db = MongoEngine() app.config[ "MONGODB_HOST" ] = "mongodb://localhost:27017/flask_mongoengine_test_db_prod" # Invalid host, should trigger exception if used app.config["MONGODB_TEST_HOST"] = "dummy://localhost:27017/test" db.init_app(app) connection = mongoengine.get_connection() mongo_engine_db = mongoengine.get_db() assert isinstance(mongo_engine_db, Database) assert isinstance(connection, MongoClient) assert mongo_engine_db.name == "flask_mongoengine_test_db_prod" assert connection.HOST == "localhost" assert connection.PORT == 27017 def test_connection_kwargs(app): """Make sure additional connection kwargs work.""" # Figure out whether to use "MAX_POOL_SIZE" or "MAXPOOLSIZE" based # on PyMongo version (former was changed to the latter as described # in https://jira.mongodb.org/browse/PYTHON-854) # TODO remove once PyMongo < 3.0 support is dropped if pymongo.version_tuple[0] >= 3: MAX_POOL_SIZE_KEY = "MAXPOOLSIZE" else: MAX_POOL_SIZE_KEY = "MAX_POOL_SIZE" app.config["MONGODB_SETTINGS"] = { "ALIAS": "tz_aware_true", "DB": "flask_mongoengine_testing_tz_aware", "TZ_AWARE": True, "READ_PREFERENCE": ReadPreference.SECONDARY, MAX_POOL_SIZE_KEY: 10, } db = MongoEngine(app) assert db.connection.codec_options.tz_aware assert db.connection.max_pool_size == 10 assert db.connection.read_preference == ReadPreference.SECONDARY flask-mongoengine-1.0.0/tests/test_forms.py000066400000000000000000000365471375611115600210410ustar00rootroot00000000000000import datetime import re import bson import flask import pytest import wtforms from mongoengine import NotUniqueError, queryset_manager from werkzeug.datastructures import MultiDict from flask_mongoengine.wtf import model_form def test_binaryfield(app, db): with app.test_request_context("/"): class Binary(db.Document): binary = db.BinaryField() BinaryForm = model_form(Binary) form = BinaryForm(MultiDict({"binary": "1"})) assert form.validate() form.save() def test_choices_coerce(app, db): with app.test_request_context("/"): CHOICES = ((1, "blue"), (2, "red")) class MyChoices(db.Document): pill = db.IntField(choices=CHOICES) MyChoicesForm = model_form(MyChoices) form = MyChoicesForm(MultiDict({"pill": "1"})) assert form.validate() form.save() assert MyChoices.objects.first().pill == 1 def test_list_choices_coerce(app, db): with app.test_request_context("/"): CHOICES = ((1, "blue"), (2, "red")) class MyChoices(db.Document): pill = db.ListField(db.IntField(choices=CHOICES)) MyChoicesForm = model_form(MyChoices) form = MyChoicesForm(MultiDict({"pill": "1"})) assert form.validate() form.save() assert MyChoices.objects.first().pill[0] == 1 def test_emailfield(app, db): with app.test_request_context("/"): class Email(db.Document): email = db.EmailField(required=False) EmailForm = model_form(Email) form = EmailForm(instance=Email()) assert "None" not in "%s" % form.email assert form.validate() form = EmailForm(MultiDict({"email": ""})) assert "None" not in "%s" % form.email assert form.validate() # Ensure required works class Email(db.Document): email = db.EmailField(required=True) EmailForm = model_form(Email) form = EmailForm(MultiDict({"email": ""})) assert "None" not in "%s" % form.email assert not form.validate() def test_model_form(app, db): with app.test_request_context("/"): class BlogPost(db.Document): meta = {"allow_inheritance": True} title = db.StringField(required=True, max_length=200) posted = db.DateTimeField(default=datetime.datetime.now) tags = db.ListField(db.StringField()) class TextPost(BlogPost): email = db.EmailField(required=False) lead_paragraph = db.StringField(max_length=200) content = db.StringField(required=True) class LinkPost(BlogPost): url = db.StringField(required=True, max_length=200) interest = db.DecimalField(required=True) # Create a text-based post TextPostForm = model_form( TextPost, field_args={"lead_paragraph": {"textarea": True}} ) form = TextPostForm( MultiDict( {"title": "Using MongoEngine", "tags": ["mongodb", "mongoengine"]} ) ) assert not form.validate() form = TextPostForm( MultiDict( { "title": "Using MongoEngine", "content": "See the tutorial", "tags": ["mongodb", "mongoengine"], } ) ) assert form.validate() form.save() assert form.title.type == "StringField" assert form.content.type == "TextAreaField" assert form.lead_paragraph.type == "TextAreaField" assert BlogPost.objects.first().title == "Using MongoEngine" assert BlogPost.objects.count() == 1 form = TextPostForm( MultiDict( { "title": "Using Flask-MongoEngine", "content": "See the tutorial", "tags": ["flask", "mongodb", "mongoengine"], } ) ) assert form.validate() form.save() assert BlogPost.objects.count() == 2 post = BlogPost.objects(title="Using Flask-MongoEngine").get() form = TextPostForm( MultiDict( { "title": "Using Flask-MongoEngine", "content": "See the tutorial", "tags-0": "flask", "tags-1": "mongodb", "tags-2": "mongoengine", "tags-3": "flask-mongoengine", } ), instance=post, ) assert form.validate() form.save() post = post.reload() assert post.tags == ["flask", "mongodb", "mongoengine", "flask-mongoengine"] # Create a link post LinkPostForm = model_form(LinkPost) form = LinkPostForm( MultiDict( { "title": "Using Flask-MongoEngine", "url": "http://flask-mongoengine.org", "interest": "0", } ) ) form.validate() assert form.validate() def test_model_form_only(app, db): with app.test_request_context("/"): class BlogPost(db.Document): title = db.StringField(required=True, max_length=200) posted = db.DateTimeField(default=datetime.datetime.now) tags = db.ListField(db.StringField()) BlogPost.drop_collection() BlogPostForm = model_form(BlogPost, only=["tags"]) form = BlogPostForm() assert hasattr(form, "tags") assert not hasattr(form, "posted") BlogPostForm = model_form(BlogPost, exclude=["posted"]) form = BlogPostForm() assert hasattr(form, "tags") assert not hasattr(form, "posted") def test_model_form_with_custom_query_set(app, db): with app.test_request_context("/"): class Dog(db.Document): breed = db.StringField() @queryset_manager def large_objects(cls, queryset): return queryset(breed__in=["german sheppard", "wolfhound"]) class DogOwner(db.Document): dog = db.ReferenceField(Dog) big_dogs = [Dog(breed="german sheppard"), Dog(breed="wolfhound")] dogs = [Dog(breed="poodle")] + big_dogs for dog in dogs: dog.save() BigDogForm = model_form( DogOwner, field_args={"dog": {"queryset": Dog.large_objects}} ) form = BigDogForm(dog=big_dogs[0]) assert form.validate() assert big_dogs == [d[1] for d in form.dog.iter_choices()] def test_modelselectfield(app, db): with app.test_request_context("/"): class Dog(db.Document): name = db.StringField() class DogOwner(db.Document): dog = db.ReferenceField(Dog) DogOwnerForm = model_form(DogOwner, field_args={"dog": {"allow_blank": True}}) dog = Dog(name="fido") dog.save() form = DogOwnerForm(dog=dog) assert form.validate() assert isinstance(form.dog.widget, wtforms.widgets.Select) assert not form.dog.widget.multiple # Validate the options - should contain a dog (selected) and a # blank option there should be an extra blank option. choices = list(form.dog) assert len(choices) == 2 assert not choices[0].checked assert choices[0].data == "__None" assert choices[1].checked assert choices[1].data == dog.pk # Validate selecting one item form = DogOwnerForm(MultiDict({"dog": dog.id})) assert form.dog.data == dog # Validate selecting no item form = DogOwnerForm(MultiDict({"dog": "__None"}), dog=dog) assert form.dog.data is None def test_modelselectfield_multiple(app, db): with app.test_request_context("/"): class Dog(db.Document): name = db.StringField() class DogOwner(db.Document): dogs = db.ListField(db.ReferenceField(Dog)) DogOwnerForm = model_form(DogOwner, field_args={"dogs": {"allow_blank": True}}) dogs = [Dog(name="fido"), Dog(name="rex")] for dog in dogs: dog.save() form = DogOwnerForm(dogs=dogs) assert form.validate() assert isinstance(form.dogs.widget, wtforms.widgets.Select) assert form.dogs.widget.multiple # Validate the options - both dogs should be selected and # there should be an extra blank option. choices = list(form.dogs) assert len(choices) == 3 assert not choices[0].checked assert choices[0].data == "__None" assert choices[1].checked assert choices[1].data == dogs[0].pk assert choices[2].checked assert choices[2].data == dogs[1].pk # Validate selecting two items form = DogOwnerForm(MultiDict({"dogs": [dog.id for dog in dogs]})) assert form.dogs.data == dogs # Validate selecting none actually empties the list form = DogOwnerForm(MultiDict({"dogs": "__None"}), dogs=dogs) assert form.dogs.data is None def test_modelselectfield_multiple_initalvalue_None(app, db): with app.test_request_context("/"): class Dog(db.Document): name = db.StringField() class DogOwner(db.Document): dogs = db.ListField(db.ReferenceField(Dog)) DogOwnerForm = model_form(DogOwner) dogs = [Dog(name="fido"), Dog(name="rex")] for dog in dogs: dog.save() form = DogOwnerForm(dogs=None) assert form.validate() assert isinstance(form.dogs.widget, wtforms.widgets.Select) assert form.dogs.widget.multiple # Validate if both dogs are selected choices = list(form.dogs) assert len(choices) == 2 assert not choices[0].checked assert not choices[1].checked def test_modelradiofield(app, db): with app.test_request_context("/"): choices = [("male", "Male"), ("female", "Female"), ("other", "Other")] class Poll(db.Document): answer = db.StringField(choices=choices) PollForm = model_form(Poll, field_args={"answer": {"radio": True}}) form = PollForm(answer=None) assert form.validate() assert form.answer.type == "RadioField" assert form.answer.choices == choices def test_passwordfield(app, db): with app.test_request_context("/"): class User(db.Document): password = db.StringField() UserForm = model_form(User, field_args={"password": {"password": True}}) form = UserForm(password="12345") assert isinstance(form.password.widget, wtforms.widgets.PasswordInput) def test_unique_with(app, db): with app.test_request_context("/"): class Item(db.Document): owner_id = db.ObjectIdField(required=True) owner_item_id = db.StringField(required=True, unique_with="owner_id") Item.drop_collection() object_id = bson.ObjectId() Item(owner_id=object_id, owner_item_id="1").save() with pytest.raises(NotUniqueError): Item(owner_id=object_id, owner_item_id="1").save() assert 1 == Item.objects.count() def test_sub_field_args(app, db): with app.test_request_context("/"): class TestModel(db.Document): lst = db.ListField(db.StringField()) field_args = { "lst": { "label": "Custom Label", "field_args": { "widget": wtforms.widgets.HiddenInput(), "label": "Hidden Input", }, } } CustomForm = model_form(TestModel, field_args=field_args) custom_form = CustomForm(obj=TestModel(lst=["Foo"])) list_label = flask.render_template_string( "{{ custom_form.lst.label }}", custom_form=custom_form ) assert "Custom Label" in list_label assert "Hidden Input" not in list_label sub_label = flask.render_template_string( "{{ custom_form.lst }}", custom_form=custom_form ) assert "Hidden Input" in sub_label def test_modelselectfield_multiple_selected_elements_must_be_retained(app, db): with app.test_request_context("/"): class Dog(db.Document): name = db.StringField() def __unicode__(self): return self.name class DogOwner(db.Document): dogs = db.ListField(db.ReferenceField(Dog)) DogOwnerForm = model_form(DogOwner) fido = Dog(name="fido").save() Dog(name="rex").save() dogOwner = DogOwner(dogs=[fido]) form = DogOwnerForm(obj=dogOwner) html = form.dogs() m = re.search("", html) # Should have one selected option assert m is not None assert "fido" == m.group(1) def test_model_form_help_text(app, db): with app.test_request_context("/"): class BlogPost(db.Document): title = db.StringField( required=True, help_text="Some imaginative title to set the world on fire", ) post = BlogPost(title="hello world").save() BlogPostForm = model_form(BlogPost) form = BlogPostForm(instance=post) assert ( form.title.description == "Some imaginative title to set the world on fire" ) def test_shared_field_args(app, db): with app.test_request_context("/"): class BlogPost(db.Document): title = db.StringField(required=True) content = db.StringField(required=False) shared_field_args = { "title": {"validators": [wtforms.validators.Regexp("test")]} } TitleOnlyForm = model_form( BlogPost, field_args=shared_field_args, exclude=["content"] ) BlogPostForm = model_form(BlogPost, field_args=shared_field_args) # ensure shared field_args don't create duplicate validators title_only_form = TitleOnlyForm() assert len(title_only_form.title.validators) == 2 blog_post_form = BlogPostForm() assert len(blog_post_form.title.validators) == 2 def test_embedded_model_form(app, db): with app.test_request_context("/"): class Content(db.EmbeddedDocument): text = db.StringField() lang = db.StringField(max_length=3) class Post(db.Document): title = db.StringField(max_length=120, required=True) tags = db.ListField(db.StringField(max_length=30)) content = db.EmbeddedDocumentField("Content") PostForm = model_form(Post) form = PostForm() assert "content-text" in "%s" % form.content.text def test_form_label_modifier(app, db): with app.test_request_context("/"): class FoodItem(db.Document): title = db.StringField() class FoodStore(db.Document): title = db.StringField(max_length=120, required=True) food_items = db.ListField(db.ReferenceField(FoodItem)) def food_items_label_modifier(obj): return obj.title fruit_names = ["banana", "apple", "pear"] food_items = [FoodItem(title=name).save() for name in fruit_names] FoodStore(title="John's fruits", food_items=food_items).save() FoodStoreForm = model_form(FoodStore) form = FoodStoreForm() assert [obj.label.text for obj in form.food_items] == fruit_names flask-mongoengine-1.0.0/tests/test_json.py000066400000000000000000000024301375611115600206440ustar00rootroot00000000000000import flask import pytest from flask_mongoengine import MongoEngine @pytest.fixture() def extended_db(app): app.json_encoder = DummyEncoder app.config["MONGODB_HOST"] = "mongodb://localhost:27017/flask_mongoengine_test_db" test_db = MongoEngine(app) db_name = test_db.connection.get_database("flask_mongoengine_test_db").name if not db_name.endswith("_test_db"): raise RuntimeError( f"DATABASE_URL must point to testing db, not to master db ({db_name})" ) # Clear database before tests, for cases when some test failed before. test_db.connection.drop_database(db_name) yield test_db # Clear database after tests, for graceful exit. test_db.connection.drop_database(db_name) class DummyEncoder(flask.json.JSONEncoder): """ An example encoder which a user may create and override the apps json_encoder with. This class is a NO-OP, but used to test proper inheritance. """ @pytest.mark.usefixtures("extended_db") def test_inheritance(app): assert issubclass(app.json_encoder, DummyEncoder) json_encoder_name = app.json_encoder.__name__ # Since the class is dynamically derrived, must compare class names # rather than class objects. assert json_encoder_name == "MongoEngineJSONEncoder" flask-mongoengine-1.0.0/tests/test_json_app.py000066400000000000000000000025431375611115600215110ustar00rootroot00000000000000import flask import pytest from bson import ObjectId @pytest.fixture(autouse=True) def setup_endpoints(app, todo): Todo = todo @app.route("/") def index(): return flask.jsonify(result=Todo.objects()) @app.route("/add", methods=["POST"]) def add(): form = flask.request.form todo = Todo(title=form["title"], text=form["text"]) todo.save() return flask.jsonify(result=todo) @app.route("/show//") def show(id): return flask.jsonify(result=Todo.objects.get_or_404(id=id)) def test_with_id(app, todo): Todo = todo client = app.test_client() response = client.get("/show/%s/" % ObjectId()) assert response.status_code == 404 response = client.post("/add", data={"title": "First Item", "text": "The text"}) assert response.status_code == 200 response = client.get("/show/%s/" % Todo.objects.first().id) assert response.status_code == 200 result = flask.json.loads(response.data).get("result") assert ("title", "First Item") in result.items() def test_basic_insert(app): client = app.test_client() client.post("/add", data={"title": "First Item", "text": "The text"}) client.post("/add", data={"title": "2nd Item", "text": "The text"}) rv = client.get("/") result = flask.json.loads(rv.data).get("result") assert len(result) == 2 flask-mongoengine-1.0.0/tests/test_pagination.py000066400000000000000000000044651375611115600220360ustar00rootroot00000000000000import pytest from werkzeug.exceptions import NotFound from flask_mongoengine import ListFieldPagination, Pagination def test_queryset_paginator(app, todo): Todo = todo for i in range(42): Todo(title="post: %s" % i).save() with pytest.raises(NotFound): Pagination(iterable=Todo.objects, page=0, per_page=10) with pytest.raises(NotFound): Pagination(iterable=Todo.objects, page=6, per_page=10) paginator = Pagination(Todo.objects, 1, 10) _test_paginator(paginator) def test_paginate_plain_list(): with pytest.raises(NotFound): Pagination(iterable=range(1, 42), page=0, per_page=10) with pytest.raises(NotFound): Pagination(iterable=range(1, 42), page=6, per_page=10) paginator = Pagination(range(1, 42), 1, 10) _test_paginator(paginator) def test_list_field_pagination(app, todo): Todo = todo comments = ["comment: %s" % i for i in range(42)] todo = Todo( title="todo has comments", comments=comments, comment_count=len(comments), ).save() # Check without providing a total paginator = ListFieldPagination(Todo.objects, todo.id, "comments", 1, 10) _test_paginator(paginator) # Check with providing a total (saves a query) paginator = ListFieldPagination( Todo.objects, todo.id, "comments", 1, 10, todo.comment_count ) _test_paginator(paginator) paginator = todo.paginate_field("comments", 1, 10) _test_paginator(paginator) def _test_paginator(paginator): assert 5 == paginator.pages assert [1, 2, 3, 4, 5] == list(paginator.iter_pages()) for i in [1, 2, 3, 4, 5]: if i == 1: assert not paginator.has_prev with pytest.raises(NotFound): paginator.prev() else: assert paginator.has_prev if i == 5: assert not paginator.has_next with pytest.raises(NotFound): paginator.next() else: assert paginator.has_next if i == 3: assert [None, 2, 3, 4, None] == list(paginator.iter_pages(0, 1, 1, 0)) assert i == paginator.page assert i - 1 == paginator.prev_num assert i + 1 == paginator.next_num # Paginate to the next page if i < 5: paginator = paginator.next() flask-mongoengine-1.0.0/tests/test_session.py000066400000000000000000000021241375611115600213560ustar00rootroot00000000000000import pytest from flask import session from flask_mongoengine import MongoEngineSessionInterface @pytest.fixture(autouse=True) def setup_endpoints(app, db): app.session_interface = MongoEngineSessionInterface(db) @app.route("/") def index(): session["a"] = "hello session" return session["a"] @app.route("/check-session") def check_session(): return "session: %s" % session["a"] @app.route("/check-session-database") def check_session_database(): sessions = app.session_interface.cls.objects.count() return "sessions: %s" % sessions def test_setting_session(app): client = app.test_client() response = client.get("/") assert response.status_code == 200 assert response.data.decode("utf-8") == "hello session" response = client.get("/check-session") assert response.status_code == 200 assert response.data.decode("utf-8") == "session: hello session" response = client.get("/check-session-database") assert response.status_code == 200 assert response.data.decode("utf-8") == "sessions: 1" flask-mongoengine-1.0.0/tox.ini000066400000000000000000000003671375611115600164420ustar00rootroot00000000000000[tox] envlist = {py36,py37,py38,py39,pypy3},lint [testenv] commands = python -m pytest {posargs} deps = PyMongo>3.9.0 pytest pytest-cov [testenv:lint] deps = pre-commit commands = python -m pre_commit run {posargs:--all}