├── .codeclimate.yml ├── .editorconfig ├── .github ├── labels.yml ├── release-drafter.yml └── workflows │ ├── coverage.yml │ ├── drafter.yml │ ├── labeler-check.yml │ ├── labeler.yml │ ├── linting.yml │ └── tests.yml ├── .gitignore ├── .markdownlint.json ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── .sourcery.yaml ├── AUTHORS.md ├── CONTRIBUTING.md ├── LICENSE.md ├── MANIFEST.in ├── README.md ├── codecov.yml ├── docker-compose.yml ├── docs ├── AUTHORS.md ├── CONTRIBUTING.md ├── LICENSE.md ├── _static │ ├── css │ │ └── custom.css │ └── debug_toolbar.png ├── api │ ├── base.rst │ ├── index.rst │ └── wtf.rst ├── changelog.rst ├── conf.py ├── custom_queryset.md ├── db_model.md ├── debug_toolbar.md ├── example_app.md ├── flask_config.md ├── forms.md ├── index.rst ├── migration_to_v2.md ├── requirements.txt ├── session_interface.md └── wtf_forms.md ├── example_app ├── __init__.py ├── app.py ├── boolean_demo.py ├── compose │ └── flask │ │ └── Dockerfile ├── dates_demo.py ├── dict_demo.py ├── models.py ├── numbers_demo.py ├── strings_demo.py ├── templates │ ├── _formhelpers.html │ ├── form_demo.html │ ├── index.html │ ├── layout.html │ └── pagination.html └── views.py ├── flask_mongoengine ├── __init__.py ├── connection.py ├── db_fields.py ├── decorators.py ├── documents.py ├── json.py ├── pagination.py ├── panels.py ├── sessions.py ├── templates │ └── panels │ │ └── mongo-panel.html └── wtf │ ├── __init__.py │ ├── fields.py │ ├── models.py │ └── orm.py ├── noxfile.py ├── pyproject.toml ├── setup.cfg ├── setup.py └── tests ├── __init__.py ├── conftest.py ├── test_base.py ├── test_basic_app.py ├── test_connection.py ├── test_db_fields.py ├── test_db_fields_import_protection.py ├── test_debug_panel.py ├── test_decorators.py ├── test_forms.py ├── test_forms_v2.py ├── test_json.py ├── test_json_app.py ├── test_pagination.py └── test_session.py /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | checks: 3 | file-lines: 4 | enabled: false 5 | method-count: 6 | enabled: false 7 | method-lines: 8 | enabled: false 9 | complex-logic: 10 | config: 11 | threshold: 10 12 | method-complexity: 13 | config: 14 | threshold: 10 15 | argument-count: 16 | config: 17 | threshold: 20 18 | exclude_patterns: 19 | - "tests/" 20 | - "example_app/" 21 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | # https://editorconfig-specification.readthedocs.io/en/latest/ 3 | 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.{py,ini,sh}] 13 | indent_style = space 14 | indent_size = 4 15 | 16 | [*.{html,css,scss,json,yml,tpl,js,jsx}] 17 | indent_style = space 18 | indent_size = 2 19 | 20 | [*.md] 21 | indent_style = space 22 | indent_size = 4 23 | tab_width = 4 24 | 25 | [*.rst] 26 | indent_style = space 27 | indent_size = 3 28 | 29 | [Makefile] 30 | indent_style = tab 31 | -------------------------------------------------------------------------------- /.github/labels.yml: -------------------------------------------------------------------------------- 1 | - name: "topic: CI/CD" 2 | color: "54f449" 3 | description: "This issue/PR relates to CI/CD pipeline change" 4 | - name: "topic: code style" 5 | color: "54f449" 6 | description: "This issue/PR relates to code style." 7 | from_name: "code quality" 8 | - name: "topic: tests" 9 | color: "54f449" 10 | description: "This issue/PR relates to tests, QA, CI." 11 | - name: "topic: deprecated" 12 | color: "999999" 13 | description: "Feature or component marked as deprecated and will be removed" 14 | - name: "topic: documentation" 15 | color: "54f449" 16 | description: "This issue/PR relates to or includes documentation." 17 | from_name: "Documentation" 18 | - name: "type: breaking-change" 19 | color: "310372" 20 | description: "Marks an important and likely breaking old interface change." 21 | - name: "type: feature" 22 | color: "310372" 23 | description: "New feature implementation" 24 | - name: "type: enhancement" 25 | color: "310372" 26 | description: "Enhancement update for old feature" 27 | from_name: "enhancement" 28 | - name: "type: good first issue" 29 | color: "310372" 30 | description: "Good for newcomers" 31 | from_name: "Good first contrib" 32 | - name: "type: bug" 33 | color: "b71914" 34 | description: "Something isn't working" 35 | from_name: "bug" 36 | - name: "log:added" 37 | color: "ff935e" 38 | description: "Changelog mark label. Marks new added features." 39 | - name: "log:breaking-change" 40 | color: "ff935e" 41 | description: "Changelog mark label. Marks breaking changes." 42 | - name: "log:changed" 43 | color: "ff935e" 44 | description: "Changelog mark label. Marks old, but changed features." 45 | - name: "log:deprecated" 46 | color: "ff935e" 47 | description: "Changelog mark label. Marks deprecated features, that will be removed in next major release." 48 | - name: "log:fixed" 49 | color: "ff935e" 50 | description: "Changelog mark label. Marks fixed bug issues." 51 | - name: "log:removed" 52 | color: "ff935e" 53 | description: "Changelog mark label. Marks removed features." 54 | - name: "log:skip-changelog" 55 | color: "ff935e" 56 | description: "Should be excluded from the changelog." 57 | - name: "os: linux" 58 | color: "fbca04" 59 | description: "This issue/PR related to linux systems" 60 | - name: "os: mac" 61 | color: "fbca04" 62 | description: "This issue/PR related to mac systems" 63 | - name: "os: windows" 64 | color: "fbca04" 65 | description: "This issue/PR related to windows systems" 66 | - name: "question" 67 | color: "d876e3" 68 | description: "User questions, not related to issues" 69 | from_name: "question" 70 | - name: "decision: duplicate" 71 | color: "eeeeee" 72 | description: "This issue or pull request already exists" 73 | from_name: "duplicate" 74 | - name: "decision: wontfix" 75 | color: "eeeeee" 76 | description: "This will not be worked on" 77 | from_name: "wontfix" 78 | - name: "decision: wontmerge" 79 | color: "eeeeee" 80 | description: "PR Only: This pull request will not be merged (problem described in comments)" 81 | - name: "stage: waiting-for-contributor" 82 | color: "efc1ae" 83 | from_name: "awaiting response" 84 | description: "Waiting for answer from original contributor." 85 | - name: "stage: WIP" 86 | color: "efc1ae" 87 | description: "Work In Progress" 88 | - name: "stage: help wanted" 89 | color: "efc1ae" 90 | description: "Extra attention is needed" 91 | from_name: "Help wanted" 92 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | categories: 2 | - title: 'Breaking Changes' 3 | labels: 4 | - 'log:breaking-change' 5 | - title: 'Added' 6 | labels: 7 | - 'log:added' 8 | - title: 'Changed' 9 | labels: 10 | - 'log:changed' 11 | - title: 'Fixed' 12 | labels: 13 | - 'log:fixed' 14 | - title: 'Deprecated' 15 | labels: 16 | - 'log:deprecated' 17 | - title: 'Removed' 18 | labels: 19 | - 'log:removed' 20 | exclude-labels: 21 | - 'log:skip-changelog' 22 | sort-by: 'title' 23 | template: | 24 | ## Changes 25 | 26 | $CHANGES 27 | 28 | ## This release is made by wonderful contributors: 29 | 30 | $CONTRIBUTORS 31 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: Send Coverage to different analytic engines 2 | # Only for last versions of python and mongo 3 | 4 | on: 5 | push: 6 | branches: 7 | - master 8 | tags: 9 | - "*" 10 | pull_request: 11 | branches: 12 | - "*" 13 | 14 | jobs: 15 | coverage: 16 | runs-on: ubuntu-latest 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | mongodb-version: [5.0] 21 | include: 22 | - name: "coverage" 23 | python: "3.10" 24 | 25 | steps: 26 | - uses: actions/checkout@v3 27 | - name: Set up Python ${{ matrix.python }} 28 | uses: actions/setup-python@v4 29 | with: 30 | python-version: ${{ matrix.python }} 31 | - name: Install dependencies 32 | run: | 33 | python -m pip install --upgrade pip 34 | pip install nox virtualenv 35 | - name: Start MongoDB 36 | uses: supercharge/mongodb-github-action@1.7.0 37 | with: 38 | mongodb-version: ${{ matrix.mongodb-version }} 39 | - name: Test build 40 | run: "nox -s 'latest-${{ matrix.python }}(wtf=True, toolbar=True)' -- --cov-report=xml --cov-report=html" 41 | - name: Send coverage report to codecov 42 | uses: codecov/codecov-action@v3 43 | with: 44 | file: ./coverage.xml 45 | - name: Send coverage report to codeclimate 46 | uses: paambaati/codeclimate-action@v3.0.0 47 | continue-on-error: true 48 | with: 49 | coverageCommand: echo "Ignore rerun" 50 | coverageLocations: ${{github.workspace}}/coverage.xml:coverage.py 51 | env: 52 | CC_TEST_REPORTER_ID: ${{secrets.CC_TEST_REPORTER_ID}} 53 | -------------------------------------------------------------------------------- /.github/workflows/drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | update_release_draft: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: release-drafter/release-drafter@v5 13 | env: 14 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 15 | -------------------------------------------------------------------------------- /.github/workflows/labeler-check.yml: -------------------------------------------------------------------------------- 1 | name: Labels verification 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | labeler: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v3 12 | - name: Run Labeler 13 | uses: crazy-max/ghaction-github-labeler@v4 14 | with: 15 | yaml_file: .github/labels.yml 16 | dry_run: true 17 | env: 18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 19 | -------------------------------------------------------------------------------- /.github/workflows/labeler.yml: -------------------------------------------------------------------------------- 1 | name: Labels verification 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | 7 | jobs: 8 | labeler: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v3 13 | - name: Run Labeler 14 | uses: crazy-max/ghaction-github-labeler@v4 15 | with: 16 | yaml_file: .github/labels.yml 17 | env: 18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 19 | -------------------------------------------------------------------------------- /.github/workflows/linting.yml: -------------------------------------------------------------------------------- 1 | name: Linting Tests 2 | # Only for least supported python version 3 | 4 | on: 5 | push: 6 | branches: 7 | - master 8 | tags: 9 | - "*" 10 | pull_request: 11 | branches: 12 | - "*" 13 | 14 | jobs: 15 | linting: 16 | runs-on: ubuntu-latest 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | include: 21 | - name: "linting" 22 | python: "3.7" 23 | 24 | steps: 25 | - uses: actions/checkout@v3 26 | - name: Set up Python ${{ matrix.python }} 27 | uses: actions/setup-python@v4 28 | with: 29 | python-version: ${{ matrix.python }} 30 | - name: Install dependencies 31 | run: | 32 | python -m pip install --upgrade pip 33 | pip install nox virtualenv 34 | - name: Test build 35 | run: "nox -s lint" 36 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: CI Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | tags: 8 | - "*" 9 | pull_request: 10 | branches: 11 | - "*" 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | mongodb-version: ["3.6", "4.0", "4.2", "4.4", "5.0"] 20 | python: ["3.7", "3.8", "3.9", "3.10", "pypy3.7"] 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | - name: Set up Python ${{ matrix.python }} 25 | uses: actions/setup-python@v4 26 | with: 27 | python-version: ${{ matrix.python }} 28 | - name: Install dependencies 29 | run: | 30 | python -m pip install --upgrade pip 31 | pip install nox virtualenv 32 | - name: Start MongoDB 33 | uses: supercharge/mongodb-github-action@1.7.0 34 | with: 35 | mongodb-version: ${{ matrix.mongodb-version }} 36 | - name: Test build 37 | run: "nox -s ci_cd_tests --python ${{ matrix.python }}" 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .*.swp 3 | *.egg 4 | docs/.build 5 | docs/_build 6 | build/ 7 | dist/ 8 | *.egg-info/ 9 | env/ 10 | venv/ 11 | ._* 12 | .DS_Store 13 | .coverage 14 | .project 15 | .pydevproject 16 | .tox 17 | .nox 18 | .eggs 19 | .idea 20 | .vscode 21 | htmlcov/ 22 | coverage.xml 23 | _version.py 24 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "MD003": true, 3 | "MD004": false, 4 | "MD007": false, 5 | "MD013": { 6 | "tables": false, 7 | "code_blocks": false, 8 | "line_length": 88 9 | }, 10 | "MD024": false, 11 | "MD026": false, 12 | "MD029": false, 13 | "MD034": true, 14 | "MD046": true, 15 | "code-block-style": { 16 | "style": "fenced" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.3.0 4 | hooks: 5 | - id: trailing-whitespace 6 | - id: end-of-file-fixer 7 | - id: check-yaml 8 | - id: check-added-large-files 9 | - id: check-ast 10 | - id: check-case-conflict 11 | - id: check-docstring-first 12 | - id: check-executables-have-shebangs 13 | - id: check-json 14 | - id: check-shebang-scripts-are-executable 15 | - id: check-symlinks 16 | - id: check-toml 17 | - id: check-xml 18 | - id: check-yaml 19 | - id: debug-statements 20 | - id: destroyed-symlinks 21 | - id: detect-private-key 22 | - id: fix-byte-order-marker 23 | - id: fix-encoding-pragma 24 | args: [--remove] 25 | - id: mixed-line-ending 26 | - repo: https://github.com/psf/black 27 | rev: 22.6.0 28 | hooks: 29 | - id: black 30 | language_version: python3.7 31 | exclude: ^docs/ 32 | - repo: https://gitlab.com/pycqa/flake8 33 | rev: 4.0.1 34 | hooks: 35 | - id: flake8 36 | exclude: ^docs/|^examples/ 37 | - repo: https://github.com/igorshubovych/markdownlint-cli 38 | rev: v0.31.1 39 | hooks: 40 | - id: markdownlint 41 | args: [-f] 42 | exclude: ^(.github|tests) 43 | - repo: https://github.com/PyCQA/isort 44 | rev: 5.10.1 45 | hooks: 46 | - id: isort 47 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | version: 2 5 | build: 6 | os: ubuntu-22.04 7 | tools: 8 | python: "3.10" 9 | sphinx: 10 | configuration: docs/conf.py 11 | formats: 12 | - htmlzip 13 | - pdf 14 | - epub 15 | python: 16 | install: 17 | - requirements: docs/requirements.txt 18 | - method: pip 19 | path: . 20 | extra_requirements: 21 | - wtf 22 | - toolbar 23 | -------------------------------------------------------------------------------- /.sourcery.yaml: -------------------------------------------------------------------------------- 1 | ignore: [] 2 | 3 | refactor: 4 | include: [] 5 | skip: 6 | - use-contextlib-suppress 7 | rule_types: 8 | - refactoring 9 | - suggestion 10 | - comment 11 | python_version: '3.7' 12 | 13 | metrics: 14 | quality_threshold: 25.0 15 | 16 | clone_detection: 17 | min_lines: 3 18 | min_duplicates: 2 19 | identical_clones_only: false 20 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | # Credits 2 | 3 | ## Current maintainers: 4 | 5 | - Andrey Shpak 6 | 7 | This project is made by incredible authors: 8 | 9 | - Adam Chainz 10 | - aliaksandr.askerka 11 | - Alistair Roche 12 | - Almog Cohen 13 | - Aly Sivji 14 | - Andrew Elkins 15 | - Andrey Shpak 16 | - Anthony Nemitz 17 | - Anton Antonov 18 | - Axel Haustant 19 | - bdadson 20 | - bioneddy 21 | - Boullier Jocelyn 22 | - bright 23 | - Bright Dadson 24 | - BrightPan 25 | - Bruno Belarmino 26 | - Bruno Rocha 27 | - Christian Wygoda 28 | - Clay McClure 29 | - Cory Dolphin 30 | - Denny Huang 31 | - Dragos Catarahia 32 | - Emmanuel Leblond 33 | - Fengda Huang 34 | - fieliapm 35 | - Garito 36 | - Gregg Lind 37 | - Ido Shraga 38 | - icoz 39 | - Jack Stouffer 40 | - Jamar Parris 41 | - jcass 42 | - Jeff Tharp 43 | - jmesquita 44 | - Joao Mesquita 45 | - Joe Shaw 46 | - Joe Shaw 47 | - Jorge Bastida 48 | - jorgebastida 49 | - JTG 50 | - Jérôme Lafréchoux 51 | - Jérôme Lafréchoux 52 | - Len Buckens 53 | - Liam McLoughlin 54 | - losintikfos 55 | - Lucas Nemeth 56 | - Lucas Vogelsang 57 | - Marcel Tschopp 58 | - Marcus Carlsson 59 | - Martin Hanzík 60 | - Massimo Santini 61 | - Mattias Granlund 62 | - Max Countryman 63 | - mickey06 64 | - mxck 65 | - Nauman Ahmad 66 | - onlinejudge95 67 | - Paul Brown 68 | - Peter Kristensen 69 | - PeterKharchenko 70 | - Philip House 71 | - qisoster 72 | - rma4ok 73 | - Rod Cloutier 74 | - Ross Lawley 75 | - Sebastian Karlsson 76 | - Serge S. Koval 77 | - Sibelius Seraphini 78 | - Simon Eames 79 | - Slavek Kabrda 80 | - Stefan Wójcik 81 | - Stephen Brown II 82 | - Stormxx 83 | - Thanathip Limna 84 | - Thomas Steinacher 85 | - Tim Gates 86 | - Ty3uK 87 | - vantastic 88 | - wcdolphin 89 | - woo 90 | - yoyicue 91 | - Zephor 武智辉 92 | - Иришка 93 | - Павел Литвиненко 94 | - 庄天翼 95 | 96 | This project inspired by two repos: 97 | 98 | - [danjac] 99 | - [maratfm] 100 | 101 | [danjac]: https://bitbucket.org/danjac/flask-mongoengine 102 | [maratfm]: https://bitbucket.org/maratfm/wtforms 103 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing guide 2 | 3 | MongoEngine has a large [community] and contributions are always encouraged. 4 | Contributions can be as simple as typo fix in the documentation, as well as complete 5 | new features and integrations. Please read these guidelines before sending a pull 6 | request. 7 | 8 | ## Bugfixes and new features 9 | 10 | Before starting to write code, look for existing [tickets] or create one for your 11 | specific issue or feature request. That way you avoid working on something 12 | that might not be of interest or that has already been addressed. 13 | 14 | For new integrations do you best to make integration optional, to leave user 15 | opportunity to exclude additional dependencies, in case when user do not need 16 | integration or feature. 17 | 18 | ## Supported interpreters 19 | 20 | Flask-MongoEngine supports CPython 3.7 and newer, PyPy 3.7 and newer. Language features 21 | not supported by all interpreters can not be used. 22 | 23 | ## Running tests 24 | 25 | All development requirements, except [docker] are included in package extra options 26 | `dev`. So, to install full development environment you need just run package with 27 | all related options installation. 28 | 29 | Our local test environment related on [docker] and [nox] to test project on real 30 | database engine and not use any database mocking, as such mocking can raise unexpected 31 | behaviour, that is not seen in real database engines. 32 | 33 | Before running tests, please ensure that real database not launched on local port 34 | ``27017``, otherwise tests will fail. If you want to run tests with local launched 35 | database engine, run tests in non-interactive mode (see below), in this case [docker] 36 | will not be used at all. 37 | 38 | We do not include docker python package to requirements, to exclude harming local 39 | developer's system, if docker installed in recommended/other way. 40 | 41 | To run minimum amount of required tests with [docker], use: 42 | 43 | ```bash 44 | nox 45 | ``` 46 | 47 | To run minimum amount of required tests with local database, use: 48 | 49 | ```bash 50 | nox --non-interactive 51 | ``` 52 | 53 | To run one or mode nox sessions only, use `-s` option. For example to run only 54 | documentation and linting tests, run: 55 | 56 | ```bash 57 | nox -s documentation_tests lint 58 | ``` 59 | 60 | In some cases you will want to bypass arguments to pytest itself, to run single test 61 | or single test file. It is easy to do, everything after double dash will be bypassed 62 | to pytest directly. For example, to run ``test__normal_command__logged`` test only, use: 63 | 64 | ```bash 65 | nox -- -k test__normal_command__logged 66 | ``` 67 | 68 | ## Setting up the code for local development and tests 69 | 70 | 1. Fork the `flask-mongoengine` repo on GitHub. 71 | 2. Clone your fork locally: 72 | 73 | ```bash 74 | git clone git@github.com:your_name_here/flask-mongoengine.git 75 | ``` 76 | 77 | 3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper 78 | installed, this is how you set up your fork for local development with all 79 | required development and testing dependencies (except docker): 80 | 81 | ```bash 82 | cd flask-mongoengine/ 83 | # With all development and package requirements, except docker 84 | pip install -e .[wtf,toolbar,dev] 85 | ``` 86 | 87 | 4. Create a branch for local development: 88 | 89 | ```bash 90 | git checkout -b name-of-your-bugfix-or-feature 91 | ``` 92 | 93 | Now you can make your changes locally. 94 | 5. When you're done making changes, check that your changes pass the tests and lint 95 | check: 96 | 97 | ```bash 98 | nox 99 | ``` 100 | 101 | Please note that [nox] runs lint and documentation builds check automatically, 102 | since we have a test environment for it. During lint check run, some common 103 | issues will be fixed automatically, so in case of failed status, firstly just try 104 | to rerun check again. If issue is not fixed automatically, check run log. 105 | 106 | If you feel like running only the lint environment, please use the following command: 107 | 108 | ```bash 109 | nox -s lint 110 | ``` 111 | 112 | 6. Ensure that your feature or commit is fully covered by tests. Check report after 113 | regular nox run. 114 | 7. Please install [pre-commit] git hook before adding any commits. This will 115 | ensure/fix 95% of linting issues. Otherwise, GitHub CI/CD pipeline may fail. This 116 | is linting double check (same as in [nox]), but it will run always, even if you 117 | forget to run tests before commit. 118 | 119 | ```bash 120 | pre-commit install 121 | ``` 122 | 123 | 8. Commit your changes and push your branch to GitHub: 124 | 125 | ```bash 126 | git add . 127 | git commit -m "Your detailed description of your changes." 128 | git push origin name-of-your-bugfix-or-feature 129 | ``` 130 | 131 | 9. Submit a pull request through the GitHub website. 132 | 133 | ## Interactive documentation development 134 | 135 | Our nox configuration include special session for simplifying documentation 136 | development. When launched, documentation will be re-rendered after any saved 137 | code/documentation files changes. To use this session, after local environment setup, 138 | just run: 139 | 140 | ```bash 141 | nox -s docs 142 | ``` 143 | 144 | Rendered documentation will be available under: 145 | 146 | ## Style guide 147 | 148 | ### General guidelines 149 | 150 | - Avoid backward breaking changes if at all possible. 151 | - Avoid mocking of external libraries; Mocking allowed only in tests. 152 | - Avoid complex structures, for complex classes/methods/functions try to separate to 153 | little objects, to simplify code reuse and testing. 154 | - Avoid code duplications, try to exctract duplicate code to function. (Code 155 | duplication in tests is allowed.) 156 | - Write docstrings for new classes and methods. 157 | - Write tests and make sure they pass. 158 | - Add yourself to [AUTHORS.md] :) 159 | 160 | ### Code style guide 161 | 162 | - Docstrings are using RST format. Developer encouraged to include any signigicant 163 | information to docstrings. 164 | - Developers are encouraged to include typing information in functions signatures. 165 | - Developers should avoid including typing information to docstrings, if possible. 166 | - Developers should avoid including typing information to docstrings and signatures 167 | in same objects, to avoid rendering conflicts. 168 | - Code formatting is completely done by [black] and following black's style 169 | implementation of [PEP8]. Target python version is the lowest supported version. 170 | - Code formatting should be done before passing any new merge requests. 171 | - Code style should use f-strings if possible, exceptional can be made for expensive 172 | debug level logging statements. 173 | 174 | ### Documentation style guide 175 | 176 | - [Documentation] should use Markdown as main format. Including Rest syntax blocks 177 | inside are allowed. Exceptional is API auto modules documents. Our [documentation] 178 | engine support [MyST] markdown extensions. 179 | - [Documentation] should use same 88 string length requirement, as code. Strings can 180 | be larger for cases, when last word/statement is any kind of link (either to 181 | class/function/attribute or web link). 182 | - Weblinks should be placed at the end of document, to make [documentation] easy 183 | readable without rendering (in editor). 184 | - Docs formatting should be checked and formatted with [pre-commit] plugin before 185 | submitting. 186 | 187 | ## CI/CD testing 188 | 189 | All tests are run on [GitHub actions] and any pull requests are automatically tested 190 | for full range of supported [Flask], [mongoengine], [python] and [mongodb] versions. 191 | Any pull requests without tests will take longer to be integrated and might be refused. 192 | 193 | [community]: AUTHORS.md 194 | 195 | [tickets]: https://github.com/MongoEngine/flask-mongoengine/issues?state=open 196 | 197 | [PEP8]: http://www.python.org/dev/peps/pep-0008/ 198 | 199 | [black]: https://github.com/psf/black 200 | 201 | [pre-commit]: https://pre-commit.com/ 202 | 203 | [GitHub actions]: https://github.com/MongoEngine/flask-mongoengine/actions 204 | 205 | [Flask]: https://github.com/pallets/flask 206 | 207 | [mongoengine]: https://github.com/MongoEngine/mongoengine 208 | 209 | [python]: https://www.python.org/ 210 | 211 | [mongodb]: https://www.mongodb.com/ 212 | 213 | [AUTHORS.md]: AUTHORS.md 214 | 215 | [documentation]: http://docs.mongoengine.org/projects/flask-mongoengine/ 216 | 217 | [nox]: https://nox.thea.codes/en/stable/usage.html 218 | 219 | [docker]: https://www.docker.com/ 220 | 221 | [MyST]: https://myst-parser.readthedocs.io/en/latest/syntax/syntax.html 222 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # BSD 3-Clause License 2 | 3 | Copyright (c) 2010-2022, the respective contributors, as shown by 4 | the [AUTHORS.md](AUTHORS.md) file. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are 8 | met: 9 | 10 | * Redistributions of source code must retain the above copyright 11 | notice, this list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above 14 | copyright notice, this list of conditions and the following 15 | disclaimer in the documentation and/or other materials provided 16 | with the distribution. 17 | 18 | * The names of the contributors may not be used to endorse or 19 | promote products derived from this software without specific 20 | prior written permission. 21 | 22 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 23 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 24 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 25 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 26 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 27 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 28 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 29 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 30 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 31 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 32 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 33 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include MANIFEST.in 2 | include README.md 3 | include AUTHORS.md 4 | include LICENSE.md 5 | recursive-include flask_mongoengine/templates *.html 6 | recursive-include docs * 7 | recursive-exclude docs *.pyc 8 | recursive-exclude docs *.pyo 9 | prune docs/_build 10 | prune docs/_themes/.git 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flask-MongoEngine 2 | 3 | [![PyPI version](https://badge.fury.io/py/flask-mongoengine.svg)](https://badge.fury.io/py/flask-mongoengine) 4 | [![CI Tests](https://github.com/MongoEngine/flask-mongoengine/actions/workflows/tests.yml/badge.svg)](https://github.com/MongoEngine/flask-mongoengine/actions/workflows/tests.yml) 5 | [![Documentation Status](https://readthedocs.org/projects/flask-mongoengine/badge/?version=latest)](http://docs.mongoengine.org/projects/flask-mongoengine/en/latest/?badge=latest) 6 | [![Maintainability](https://api.codeclimate.com/v1/badges/6fb8ae00b1008f5f1b20/maintainability)](https://codeclimate.com/github/MongoEngine/flask-mongoengine/maintainability) 7 | [![Test Coverage](https://api.codeclimate.com/v1/badges/6fb8ae00b1008f5f1b20/test_coverage)](https://codeclimate.com/github/MongoEngine/flask-mongoengine/test_coverage) 8 | ![PyPI - Downloads](https://img.shields.io/pypi/dm/flask-mongoengine) 9 | 10 | Flask-MongoEngine is a Flask extension that provides integration with [MongoEngine], 11 | [WtfForms] and [FlaskDebugToolbar]. 12 | 13 | ## Installation 14 | 15 | By default, Flask-MongoEngine will install integration only between [Flask] and 16 | [MongoEngine]. Integration with [WTFForms] and [FlaskDebugToolbar] are optional and 17 | should be selected as extra option, if required. This is done by users request, to 18 | limit amount of external dependencies in different production setup environments. 19 | 20 | All methods end extras described below are compatible between each other and can be 21 | used together. 22 | 23 | ### Installation with MongoEngine only support 24 | 25 | ```bash 26 | # For Flask >= 2.0.0 27 | pip install flask-mongoengine 28 | ``` 29 | 30 | We still maintain special case for [Flask] = 1.1.4 support (the latest version in 1.x.x 31 | branch). To install flask-mongoengine with required dependencies use ``legacy`` 32 | extra option. 33 | 34 | ```bash 35 | # With Flask 1.1.4 dependencies 36 | pip install flask-mongoengine[legacy] 37 | ``` 38 | 39 | ### Installation with WTFForms and Flask-WTF support 40 | 41 | Flask-mongoengine can be installed with [Flask-WTF] and [WTFForms] support. This 42 | will extend project dependencies with [Flask-WTF], [WTFForms] and related packages. 43 | 44 | ```bash 45 | # With Flask-WTF and WTFForms dependencies 46 | pip install flask-mongoengine[wtf] 47 | ``` 48 | 49 | ### Installation with Flask Debug Toolbar support 50 | 51 | Flask-mongoengine provide beautiful extension to [FlaskDebugToolbar] allowing to monitor 52 | all database requests. To use this extension [FlaskDebugToolbar] itself required. If 53 | you need to install flask-mongoengine with related support, use: 54 | 55 | ```bash 56 | # With FlaskDebugToolbar dependencies 57 | pip install flask-mongoengine[toolbar] 58 | ``` 59 | 60 | ### Installation with all features together 61 | 62 | ```bash 63 | # With Flask-WTF, WTFForms and FlaskDebugToolbar dependencies 64 | pip install flask-mongoengine[wtf,toolbar] 65 | ``` 66 | 67 | ## Flask configuration 68 | 69 | Flask-mongoengine does not provide any configuration defaults. User is responsible 70 | for setting up correct database settings, to exclude any possible misconfiguration 71 | and data corruption. 72 | 73 | There are several options to set connection. Please note, that all except 74 | recommended are deprecated and may be removed in future versions, to lower code base 75 | complexity and bugs. If you use any deprecated connection settings approach, you should 76 | update your application configuration. 77 | 78 | Please refer to [complete connection settings description] for more info. 79 | 80 | ## Usage and API documentation 81 | 82 | Full project documentation available on [read the docs]. 83 | 84 | ## Contributing and testing 85 | 86 | We are welcome for contributors and testers! Check [Contribution guidelines]. 87 | 88 | ## License 89 | 90 | Flask-MongoEngine is distributed under [BSD 3-Clause License]. 91 | 92 | [MongoEngine]: https://github.com/MongoEngine/mongoengine 93 | 94 | [WTFForms]: https://github.com/wtforms/wtforms 95 | 96 | [Flask-WTF]: https://github.com/wtforms/flask-wtf 97 | 98 | [FlaskDebugToolbar]: https://github.com/flask-debugtoolbar/flask-debugtoolbar 99 | 100 | [read the docs]: http://docs.mongoengine.org/projects/flask-mongoengine/ 101 | 102 | [Flask]: https://github.com/pallets/flask 103 | 104 | [BSD 3-Clause License]: LICENSE.md 105 | 106 | [Contribution guidelines]: CONTRIBUTING.md 107 | 108 | [nox]: https://nox.thea.codes/en/stable/usage.html 109 | 110 | [complete connection settings description]: http://docs.mongoengine.org/projects/flask-mongoengine/flask_config.html 111 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: false 2 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.3" 2 | volumes: 3 | db_data: {} 4 | 5 | services: 6 | mongo: 7 | image: mongo:4.4 8 | volumes: 9 | - db_data:/data/db/ 10 | ports: 11 | - 27017:27017 12 | restart: always 13 | 14 | flask: 15 | build: 16 | context: . 17 | dockerfile: ./example_app/compose/flask/Dockerfile 18 | depends_on: 19 | - mongo 20 | command: python ./example_app/app.py 21 | ports: 22 | - 8000:8000 23 | volumes: 24 | - ./:/flask_mongoengine 25 | restart: always 26 | -------------------------------------------------------------------------------- /docs/AUTHORS.md: -------------------------------------------------------------------------------- 1 | ../AUTHORS.md -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ../CONTRIBUTING.md -------------------------------------------------------------------------------- /docs/LICENSE.md: -------------------------------------------------------------------------------- 1 | ../LICENSE.md -------------------------------------------------------------------------------- /docs/_static/css/custom.css: -------------------------------------------------------------------------------- 1 | @import 'theme.css'; 2 | 3 | html.writer-html4 .rst-content dl:not(.docutils) .property, html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) .property { 4 | display: inline; 5 | padding-right: 8px; 6 | max-width: 100% 7 | } 8 | -------------------------------------------------------------------------------- /docs/_static/debug_toolbar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MongoEngine/flask-mongoengine/d4526139cb1e2e94111ab7de96bb629d574c1690/docs/_static/debug_toolbar.png -------------------------------------------------------------------------------- /docs/api/base.rst: -------------------------------------------------------------------------------- 1 | Base module API 2 | =============== 3 | 4 | This is the flask_mongoengine main modules API documentation. 5 | 6 | flask_mongoengine.connection module 7 | ----------------------------------- 8 | 9 | .. automodule:: flask_mongoengine.connection 10 | 11 | flask_mongoengine.db_fields module 12 | ---------------------------------- 13 | 14 | .. automodule:: flask_mongoengine.db_fields 15 | :special-members: __init__ 16 | 17 | flask_mongoengine.decorators module 18 | ----------------------------------- 19 | 20 | .. automodule:: flask_mongoengine.decorators 21 | 22 | flask_mongoengine.documents module 23 | ---------------------------------- 24 | 25 | .. automodule:: flask_mongoengine.documents 26 | 27 | flask_mongoengine.json module 28 | ----------------------------- 29 | 30 | .. automodule:: flask_mongoengine.json 31 | :exclude-members: MongoEngineJSONProvider 32 | 33 | flask_mongoengine.pagination module 34 | ----------------------------------- 35 | 36 | .. automodule:: flask_mongoengine.pagination 37 | 38 | flask_mongoengine.panels module 39 | ------------------------------- 40 | 41 | .. automodule:: flask_mongoengine.panels 42 | :member-order: bysource 43 | 44 | flask_mongoengine.sessions module 45 | --------------------------------- 46 | 47 | .. automodule:: flask_mongoengine.sessions 48 | 49 | Module contents 50 | --------------- 51 | 52 | .. note:: 53 | Parent `mongoengine `_ project docs/docstrings has 54 | some formatting issues. If class/function/method link not clickable, search on 55 | provided parent documentation manually. 56 | 57 | .. automodule:: flask_mongoengine 58 | :private-members: _abort_404 59 | -------------------------------------------------------------------------------- /docs/api/index.rst: -------------------------------------------------------------------------------- 1 | API documentation 2 | ================= 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | base 8 | wtf 9 | -------------------------------------------------------------------------------- /docs/api/wtf.rst: -------------------------------------------------------------------------------- 1 | WTF module API 2 | ============== 3 | 4 | This is the flask_mongoengine.wtf modules API documentation. 5 | 6 | flask_mongoengine.wtf.fields module 7 | ----------------------------------- 8 | 9 | .. automodule:: flask_mongoengine.wtf.fields 10 | :special-members: __init__ 11 | 12 | flask_mongoengine.wtf.models module 13 | ----------------------------------- 14 | 15 | .. automodule:: flask_mongoengine.wtf.models 16 | 17 | flask_mongoengine.wtf.orm module 18 | -------------------------------- 19 | 20 | .. automodule:: flask_mongoengine.wtf.orm 21 | 22 | Module contents 23 | --------------- 24 | 25 | .. automodule:: flask_mongoengine.wtf 26 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | ============= 2 | Old changelog 3 | ============= 4 | 5 | Changes in 1.0.0 6 | ================ 7 | Changelog maintenance automated and latest changelog available at 8 | `github release page `_. 9 | 10 | Use version 0.9.5 if old dependencies required. 11 | 12 | Changes in 0.9.5 13 | ================ 14 | - Disable flake8 on travis. 15 | - Correct `Except` clauses in code. 16 | - Fix warning about undefined unicode variable in orm.py with python 3 17 | 18 | Changes in 0.9.4 19 | ================ 20 | - ADDED: Support for `MONGODB_CONNECT` mongodb parameter (#321) 21 | - ADDED: Support for `MONGODB_TZ_AWARE` mongodb parameter. 22 | 23 | Changes in 0.9.3 24 | ================ 25 | - Fix test and mongomock (#304) 26 | - Run Travis builds in a container-based environment (#301) 27 | 28 | Changes in 0.9.2 29 | ================ 30 | - Travis CI/CD pipeline update to automatically publish 0.9.1. 31 | 32 | Changes in 0.9.1 33 | ================ 34 | - Fixed setup.py for various platforms (#298). 35 | - Added Flask-WTF v0.14 support (#294). 36 | - MongoEngine instance now holds a reference to a particular Flask app it was 37 | initialized with (#261). 38 | 39 | Changes in 0.9.0 40 | ================ 41 | - BREAKING CHANGE: Dropped Python v2.6 support 42 | 43 | Changes in 0.8.2 44 | ================ 45 | - Fixed relying on mongoengine.python_support. 46 | - Fixed cleaning up empty connection settings #285 47 | 48 | Changes in 0.8.1 49 | ================ 50 | 51 | - Fixed connection issues introduced in 0.8 52 | - Removed support for MongoMock 53 | 54 | Changes in 0.8 55 | ============== 56 | 57 | - Dropped MongoEngine 0.7 support 58 | - Added MongoEngine 0.10 support 59 | - Added PyMongo 3 support 60 | - Added Python3 support up to 3.5 61 | - Allowed empying value list in SelectMultipleField 62 | - Fixed paginator issues 63 | - Use InputRequired validator to allow 0 in required field 64 | - Made help_text Field attribute optional 65 | - Added "radio" form_arg to convert field into RadioField 66 | - Added "textarea" form_arg to force conversion into TextAreaField 67 | - Added field parameters (validators, filters...) 68 | - Fixed 'False' connection settings ignored 69 | - Fixed bug to allow multiple instances of extension 70 | - Added MongoEngineSessionInterface support for PyMongo's tz_aware option 71 | - Support arbitrary primary key fields (not "id") 72 | - Configurable httponly flag for MongoEngineSessionInterface 73 | - Various bugfixes, code cleanup and documentation improvements 74 | - Move from deprecated flask.ext.* to flask_* syntax in imports 75 | - Added independent connection handler for FlaskMongoEngine 76 | - All MongoEngine connection calls are proxied via FlaskMongoEngine connection 77 | handler 78 | - Added backward compatibility for settings key names 79 | - Added support for MongoMock and temporary test DB 80 | - Fixed issue with multiple DB support 81 | - Various bugfixes 82 | 83 | Changes in 0.7 84 | ============== 85 | - Fixed only / exclude in model forms (#49) 86 | - Added automatic choices coerce for simple types (#34) 87 | - Fixed EmailField and URLField rendering and validation (#44, #9) 88 | - Use help_text for field description (#43) 89 | - Fixed Pagination and added Document.paginate_field() helper 90 | - Keep model_forms fields in order of creation 91 | - Added MongoEngineSessionInterface (#5) 92 | - Added customisation hooks for FieldList sub fields (#19) 93 | - Handle non ascii chars in the MongoDebugPanel (#22) 94 | - Fixed toolbar stacktrace if a html directory is in the path (#31) 95 | - ModelForms no longer patch Document.update (#32) 96 | - No longer wipe field kwargs in ListField (#20, #19) 97 | - Passthrough ModelField.save-arguments (#26) 98 | - QuerySetSelectMultipleField now supports initial value (#27) 99 | - Clarified configuration documentation (#33) 100 | - Fixed forms when EmbeddedDocument has no default (#36) 101 | - Fixed multiselect restore bug (#37) 102 | - Split out the examples into a single file app and a cross file app 103 | 104 | Changes in 0.6 105 | ============== 106 | - Support for JSON and DictFields 107 | - Speeding up QuerySetSelectField with big querysets 108 | 109 | Changes in 0.5 110 | ============== 111 | - Added support for all connection settings 112 | - Fixed extended DynamicDocument 113 | 114 | Changes in 0.4 115 | ============== 116 | - Added CSRF support and validate_on_save via flask.ext.WTF 117 | - Fixed DateTimeField not required 118 | 119 | Changes in 0.3 120 | =============== 121 | - Reverted mongopanel - got knocked out by a merge 122 | - Updated imports paths 123 | 124 | Changes in 0.2 125 | =============== 126 | - Added support for password StringField 127 | - Added ModelSelectMultiple 128 | 129 | Changes in 0.1 130 | =============== 131 | - Released to PyPi 132 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # flask-script documentation build configuration file, created by 2 | # sphinx-quickstart on Wed Jun 23 08:26:41 2010. 3 | # 4 | # This file is execfile()d with the current directory set to its containing dir. 5 | # 6 | # Note that not all possible configuration values are present in this 7 | # autogenerated file. 8 | # 9 | # All configuration values have a default; values that are commented out 10 | # serve to show the default. 11 | 12 | import os 13 | import sys 14 | from importlib.metadata import version as v 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | sys.path.append(os.path.abspath("_themes")) 20 | 21 | # -- General configuration ----------------------------------------------------- 22 | 23 | # If your documentation needs a minimal Sphinx version, state it here. 24 | # needs_sphinx = '1.0' 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be extensions 27 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 28 | extensions = [ 29 | "sphinx.ext.autodoc", 30 | "sphinx.ext.intersphinx", 31 | "myst_parser", 32 | "sphinx.ext.viewcode", 33 | "sphinx_autodoc_typehints", 34 | ] 35 | 36 | # Add any paths that contain templates here, relative to this directory. 37 | templates_path = ["_templates"] 38 | 39 | # The suffix of source filenames. 40 | source_suffix = { 41 | ".rst": "restructuredtext", 42 | ".md": "markdown", 43 | } 44 | 45 | # The encoding of source files. 46 | # source_encoding = 'utf-8-sig' 47 | 48 | # The master toctree document. 49 | master_doc = "index" 50 | 51 | # General information about the project. 52 | project = "Flask-MongoEngine" 53 | copyright = "2010-2020, Streetlife and others" 54 | 55 | # The version info for the project you're documenting, acts as replacement for 56 | # |version| and |release|, also used in various other places throughout the 57 | # built documents. 58 | # 59 | 60 | # The short X.Y version. 61 | version = v("flask_mongoengine") 62 | # The full version, including alpha/beta/rc tags. 63 | release = v("flask_mongoengine") 64 | 65 | # The language for content autogenerated by Sphinx. Refer to documentation 66 | # for a list of supported languages. 67 | # language = None 68 | 69 | # There are two options for replacing |today|: either, you set today to some 70 | # non-false value, then it is used: 71 | # today = '' 72 | # Else, today_fmt is used as the format for a strftime call. 73 | # today_fmt = '%B %d, %Y' 74 | 75 | # List of patterns, relative to source directory, that match files and 76 | # directories to ignore when looking for source files. 77 | exclude_patterns = ["_build"] 78 | 79 | # The reST default role (used for this markup: `text`) to use for all documents. 80 | # default_role = None 81 | 82 | # If true, '()' will be appended to :func: etc. cross-reference text. 83 | # add_function_parentheses = True 84 | 85 | # If true, the current module name will be prepended to all description 86 | # unit titles (such as .. function::). 87 | # add_module_names = True 88 | 89 | # If true, sectionauthor and moduleauthor directives will be shown in the 90 | # output. They are ignored by default. 91 | # show_authors = False 92 | 93 | # The name of the Pygments (syntax highlighting) style to use. 94 | pygments_style = "sphinx" 95 | 96 | # A list of ignored prefixes for module index sorting. 97 | # modindex_common_prefix = [] 98 | 99 | 100 | # -- Options for HTML output --------------------------------------------------- 101 | 102 | # The theme to use for HTML and HTML Help pages. See the documentation for 103 | # a list of builtin themes. 104 | html_theme = "sphinx_rtd_theme" 105 | 106 | # Theme options are theme-specific and customize the look and feel of a theme 107 | # further. For a list of options available for each theme, see the 108 | # documentation. 109 | # html_theme_options = {} 110 | 111 | # Add any paths that contain custom themes here, relative to this directory. 112 | # html_theme_path = [] 113 | 114 | # The name for this set of Sphinx documents. If None, it defaults to 115 | # " v documentation". 116 | # html_title = None 117 | 118 | # A shorter title for the navigation bar. Default is the same as html_title. 119 | # html_short_title = None 120 | 121 | # The name of an image file (relative to this directory) to place at the top 122 | # of the sidebar. 123 | # html_logo = None 124 | 125 | # The name of an image file (within the static path) to use as favicon of the 126 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 127 | # pixels large. 128 | # html_favicon = None 129 | 130 | # Add any paths that contain custom static files (such as style sheets) here, 131 | # relative to this directory. They are copied after the builtin static files, 132 | # so a file named "default.css" will overwrite the builtin "default.css". 133 | html_static_path = ["_static"] 134 | html_css_files = [ 135 | "css/custom.css", 136 | ] 137 | html_style = "css/custom.css" 138 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 139 | # using the given strftime format. 140 | # html_last_updated_fmt = '%b %d, %Y' 141 | 142 | # If true, SmartyPants will be used to convert quotes and dashes to 143 | # typographically correct entities. 144 | # html_use_smartypants = True 145 | 146 | # Custom sidebar templates, maps document names to template names. 147 | # html_sidebars = {} 148 | 149 | # Additional templates that should be rendered to pages, maps page names to 150 | # template names. 151 | # html_additional_pages = {} 152 | 153 | # If false, no module index is generated. 154 | # html_domain_indices = True 155 | 156 | # If false, no index is generated. 157 | # html_use_index = True 158 | 159 | # If true, the index is split into individual pages for each letter. 160 | # html_split_index = False 161 | 162 | # If true, links to the reST sources are added to the pages. 163 | # html_show_sourcelink = True 164 | 165 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 166 | # html_show_sphinx = True 167 | 168 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 169 | # html_show_copyright = True 170 | 171 | # If true, an OpenSearch description file will be output, and all pages will 172 | # contain a tag referring to it. The value of this option must be the 173 | # base URL from which the finished HTML is served. 174 | # html_use_opensearch = '' 175 | 176 | # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). 177 | # html_file_suffix = '' 178 | 179 | # Output file base name for HTML help builder. 180 | htmlhelp_basename = "flask-mongoenginedoc" 181 | 182 | 183 | # -- Options for LaTeX output -------------------------------------------------- 184 | 185 | # The paper size ('letter' or 'a4'). 186 | # latex_paper_size = 'letter' 187 | 188 | # The font size ('10pt', '11pt' or '12pt'). 189 | # latex_font_size = '10pt' 190 | 191 | # Grouping the document tree into LaTeX files. List of tuples 192 | # (source start file, target name, title, author, documentclass [howto/manual]). 193 | latex_documents = [ 194 | ( 195 | "index", 196 | "flask-mongoengine.tex", 197 | "Flask-MongoEngine Documentation", 198 | "Ross Lawley", 199 | "manual", 200 | ), 201 | ] 202 | 203 | # The name of an image file (relative to this directory) to place at the top of 204 | # the title page. 205 | # latex_logo = None 206 | 207 | # For "manual" documents, if this is true, then toplevel headings are parts, 208 | # not chapters. 209 | # latex_use_parts = False 210 | 211 | # If true, show page references after internal links. 212 | # latex_show_pagerefs = False 213 | 214 | # If true, show URL addresses after external links. 215 | # latex_show_urls = False 216 | 217 | # Additional stuff for the LaTeX preamble. 218 | # latex_preamble = '' 219 | 220 | # Documents to append as an appendix to all manuals. 221 | # latex_appendices = [] 222 | 223 | # If false, no module index is generated. 224 | # latex_domain_indices = True 225 | 226 | 227 | # -- Options for manual page output -------------------------------------------- 228 | 229 | # One entry per manual page. List of tuples 230 | # (source start file, name, description, authors, manual section). 231 | man_pages = [ 232 | ( 233 | "index", 234 | "flask-mongoengine", 235 | "Flask-MongoEngine Documentation", 236 | ["Ross Lawley", "Dan Jacob", "Marat Khabibullin"], 237 | 1, 238 | ) 239 | ] 240 | 241 | intersphinx_mapping = { 242 | "python": ("https://docs.python.org/3", None), 243 | "flask": ("https://flask.palletsprojects.com/en/2.2.x/", None), 244 | "werkzeug": ("https://werkzeug.palletsprojects.com/en/2.1.x/", None), 245 | "pymongo": ("https://pymongo.readthedocs.io/en/stable/", None), 246 | "mongoengine": ("https://docs.mongoengine.org/", None), 247 | "wtforms": ("https://wtforms.readthedocs.io/en/3.0.x/", None), 248 | "flask-wtf": ("https://flask-wtf.readthedocs.io/en/1.0.x/", None), 249 | } 250 | myst_enable_extensions = [ 251 | "tasklist", 252 | "strikethrough", 253 | "fieldlist", 254 | ] 255 | myst_heading_anchors = 3 256 | suppress_warnings = ["autodoc"] 257 | autodoc_default_options = { 258 | "members": True, 259 | "undoc-members": True, 260 | "show-inheritance": True, 261 | "ignore-module-all": True, 262 | "private-members": True, 263 | } 264 | -------------------------------------------------------------------------------- /docs/custom_queryset.md: -------------------------------------------------------------------------------- 1 | # Custom Queryset 2 | 3 | flask-mongoengine attaches the following methods to Mongoengine's default QuerySet: 4 | 5 | * **get_or_404**: works like .get(), but calls abort(404) if the object DoesNotExist. 6 | Optional arguments: *message* - custom message to display. 7 | * **first_or_404**: same as above, except for .first(). 8 | Optional arguments: *message* - custom message to display. 9 | * **paginate**: paginates the QuerySet. Takes two arguments, *page* and *per_page*. 10 | * **paginate_field**: paginates a field from one document in the QuerySet. 11 | Arguments: *field_name*, *doc_id*, *page*, *per_page*. 12 | 13 | Examples: 14 | 15 | ```python 16 | # 404 if object doesn't exist 17 | def view_todo(todo_id): 18 | todo = Todo.objects.get_or_404(_id=todo_id) 19 | 20 | # Paginate through todo 21 | def view_todos(page=1): 22 | paginated_todos = Todo.objects.paginate(page=page, per_page=10) 23 | 24 | # Paginate through tags of todo 25 | def view_todo_tags(todo_id, page=1): 26 | todo = Todo.objects.get_or_404(_id=todo_id) 27 | paginated_tags = todo.paginate_field('tags', page, per_page=10) 28 | ``` 29 | 30 | Properties of the pagination object include: iter_pages, next, prev, has_next, 31 | has_prev, next_num, prev_num. 32 | 33 | In the template: 34 | 35 | ```html 36 | {# Display a page of todos #} 37 |
    38 | {% for todo in paginated_todos.items %} 39 |
  • {{ todo.title }}
  • 40 | {% endfor %} 41 |
42 | 43 | {# Macro for creating navigation links #} 44 | {% macro render_navigation(pagination, endpoint) %} 45 | 58 | {% endmacro %} 59 | 60 | {{ render_navigation(paginated_todos, 'view_todos') }} 61 | ``` 62 | -------------------------------------------------------------------------------- /docs/db_model.md: -------------------------------------------------------------------------------- 1 | # Database model and fields definition 2 | 3 | ```{important} 4 | Flask-Mongoengine does not adjust database level behaviour of [mongoengine] fields 5 | definition, except [keyword only definition] requirement. Everything other on 6 | database level match [mongoengine] project. All parent methods, arguments (as 7 | keyword arguments) and keyword arguments are supported. 8 | 9 | If you are not intend to use WTForms integration, you are free to use fields classes 10 | from parent [mongoengine] project; this should not break anything. 11 | ``` 12 | 13 | ## Supported fields 14 | 15 | Flask-Mongoengine support all **database** fields definition. Even if there will be some 16 | new field type created in parent [mongoengine] project, we will silently bypass 17 | field definition to it, if we do not declare rules on our side. 18 | 19 | ```{note} 20 | Version **2.0.0** Flask-Mongoengine update support [mongoengine] fields, based on 21 | version **mongoengine==0.21**. Any new fields bypassed without modification. 22 | ``` 23 | 24 | ## Keyword only definition 25 | 26 | ```{eval-rst} 27 | .. versionchanged:: 2.0.0 28 | ``` 29 | 30 | Database model definition rules and Flask-WTF/WTForms [integration] was seriously 31 | updated in version **2.0.0**. Unfortunately, these changes implemented without any 32 | deprecation stages. 33 | 34 | Before version **2.0.0** Flask-Mongoengine integration allowed to pass fields 35 | parameters as arguments. To exclude any side effects or keyword parameters 36 | duplication/conflicts, since version **2.0.0** all fields require keyword 37 | only setup. 38 | 39 | Such approach removes number of issues and questions, when users frequently used 40 | Flask-WTF/WTForms definition rules by mistake, or just missed that some arguments 41 | was passed to keyword places silently creating unexpected side effects. You can 42 | check issue [#379] as example of one of such cases. 43 | 44 | [mongoengine]: https://docs.mongoengine.org/ 45 | [#379]: https://github.com/MongoEngine/flask-mongoengine/issues/379 46 | [integration]: forms 47 | [keyword only definition]: #keyword-only-definition 48 | -------------------------------------------------------------------------------- /docs/debug_toolbar.md: -------------------------------------------------------------------------------- 1 | # Mongo Debug Toolbar Panel 2 | 3 | ```{eval-rst} 4 | .. versionchanged:: 2.0.0 5 | ``` 6 | 7 | Mongo Debug Toolbar Panel was completely rewritten in version **2.0.0**. Before this 8 | version Mongo Debug Toolbar Panel used patching of main [pymongo] create/update/delete 9 | methods. This was not the best approach, as raised some compatibility issues during 10 | [pymongo] project updates. Since version **2.0.0** we use [pymongo] monitoring functions 11 | to track requests and timings. This approach completely removes patching of external 12 | packages and more compatibility friendly. 13 | 14 | New approach require some user side actions to proper panel installation. This is done 15 | to exclude 'silent' registration of event loggers, to prevent performance degradation in 16 | external projects and in projects, that do not require Mongo Debug Toolbar Panel 17 | functional. 18 | 19 | Described approach change brings several side effects, that user should be aware of: 20 | 21 | 1. Now Mongo Debug Toolbar Panel shows real database time, as reported by database 22 | engine. Excluding time required for data delivery from database instance to Flask 23 | instance. 24 | 2. Mongo Debug Toolbar Panel do not display and track tracebacks anymore. It only 25 | monitors database requests. Nothing more. 26 | 3. Mongo Debug Toolbar Panel do not do anything, if monitoring engine not registered 27 | before Flask application connection setup. Monitoring listener should be 28 | registered before first database connection. This is external requirement. 29 | 4. Mongo Debug Toolbar Panel code is now covered by internal tests, raising overall 30 | project code quality. 31 | 5. Mongo Debug Toolbar Panel can work without any usage of other 32 | ``flask_mongoengine`` functions. 33 | 6. Mongo Debug Toolbar Panel do not split requests types anymore, this is because 34 | now it handle any requests, including aggregations, collection creating/deleting 35 | and any other, reported by [pymongo] monitoring. Making splitting of incomming 36 | events will bring high complexity to parsers, as there are a lot of mongoDb 37 | commmands exist. 38 | 39 | ## Installation 40 | 41 | To install and use Mongo Debug Toolbar Panel: 42 | 43 | 1. Add ``'flask_mongoengine.panels.MongoDebugPanel'`` to ``DEBUG_TB_PANELS`` of 44 | [Flask Debug Toolbar]. 45 | 2. Import ``mongo_command_logger`` in your Flask application initialization file. 46 | 3. Import ``monitoring`` from ``pymongo`` package in your Flask application 47 | initialization file. 48 | 4. Register ``mongo_command_logger`` as event listener in ``pymongo``. 49 | 5. Init Flask app instance. 50 | 51 | Example: 52 | 53 | ```python 54 | import flask 55 | from flask_debugtoolbar import DebugToolbarExtension 56 | from pymongo import monitoring 57 | 58 | from flask_mongoengine.panels import mongo_command_logger 59 | from flask_mongoengine import MongoEngine 60 | 61 | 62 | app = flask.Flask(__name__) 63 | app.config.from_object(__name__) 64 | app.config["MONGODB_SETTINGS"] = {"DB": "testing", "host": "mongo"} 65 | app.config["TESTING"] = True 66 | app.config["SECRET_KEY"] = "some_key" 67 | app.debug = True 68 | app.config["DEBUG_TB_PANELS"] = ("flask_mongoengine.panels.MongoDebugPanel",) 69 | DebugToolbarExtension(app) 70 | monitoring.register(mongo_command_logger) 71 | db = MongoEngine() 72 | db.init_app(app) 73 | ``` 74 | 75 | ```{note} 76 | Working toolbar installation code can be found and tested in example_app, shipped with 77 | project codebase. 78 | ``` 79 | 80 | ## Configuration 81 | 82 | You can add ``MONGO_DEBUG_PANEL_SLOW_QUERY_LIMIT`` variable to flask application 83 | config, to set a limit for queries highlight in web interface. By default, 100 ms. 84 | 85 | ## Usage 86 | 87 | ```{eval-rst} 88 | .. image:: _static/debug_toolbar.png 89 | :target: _static/debug_toolbar.png 90 | ``` 91 | 92 | - Mongo Debug Toolbar Panel logs every mongoDb query, in executed order. 93 | - You can expand ``Server command`` to check what command was send to server. 94 | - You can expand ``Response data`` to check raw response from server side. 95 | 96 | ## Known issues 97 | 98 | There is some HTML rendering related issues, that I cannot fix, as do not work with 99 | front end at all. If you have a little HTML/CSS knowledge, please help. 100 | 101 | - [#469] Mongo Debug Toolbar Panel: Update HTML view to use wide screens 102 | - Objects sizes may be incorrect, as calculated in python. This part is copied from 103 | previous version, and may be removed in the future, if there will be confirmation, 104 | that this size data completely incorrect. 105 | 106 | [Flask Debug Toolbar]: https://github.com/flask-debugtoolbar/flask-debugtoolbar 107 | [#469]: https://github.com/MongoEngine/flask-mongoengine/issues/469 108 | [pymongo]: https://pymongo.readthedocs.io/en/stable/ 109 | -------------------------------------------------------------------------------- /docs/example_app.md: -------------------------------------------------------------------------------- 1 | # Example app 2 | 3 | A simple multi file app - to help get you started. 4 | 5 | Completely located in docker container. Require ports 8000, 27015 on host 6 | system to be opened (for development needs). 7 | 8 | Usage: 9 | 10 | 1. From flask-mongoengine root folder just run `docker-compose up`. 11 | 2. Wait until dependencies downloaded and containers up and running. 12 | 3. Open `http://localhost:8000/` to check the app. 13 | 4. Hit `ctrl+c` in terminal window to stop docker-compose. 14 | 5. You can use this app for live flask-mongoengine development and testing. 15 | 16 | ```{note} 17 | Example app files located in ``example_app`` directory. 18 | ``` 19 | -------------------------------------------------------------------------------- /docs/flask_config.md: -------------------------------------------------------------------------------- 1 | # Flask configuration 2 | 3 | Flask-mongoengine does not provide any configuration defaults. User is responsible 4 | for setting up correct database settings, to exclude any possible misconfiguration 5 | and data corruption. 6 | 7 | There are several options to set connection. Please note, that all except 8 | recommended are deprecated and may be removed in future versions, to lower code base 9 | complexity and bugs. If you use any deprecated connection settings approach, you should 10 | update your application configuration. 11 | 12 | By default, flask-mongoengine open the connection when extension is instantiated, 13 | but you can configure it to open connection only on first database access by setting 14 | the ``'connect'`` dictionary parameter or its ``MONGODB_CONNECT`` flat equivalent to 15 | ``False``. 16 | 17 | ```{note} 18 | Due lack of developers we are unable to answer/solve not recommended connection methods 19 | errors. Please switch to recommended method before posting any issue. Thank you. 20 | ``` 21 | 22 | ## Recommended: List of dictionaries settings 23 | 24 | Recommended way for setting up connections is to set ``MONGODB_SETTINGS`` in you 25 | application config. ``MONGODB_SETTINGS`` is a list of dictionaries, where each 26 | dictionary is configuration for individual database (for systems with multi-database) 27 | use. 28 | 29 | Each dictionary in ``MONGODB_SETTINGS`` will be passed to {func}`mongoengine.connect`, 30 | which will bypass settings to {func}`mongoengine.register_connection`. All settings 31 | related to {func}`mongoengine.connect` and {func}`mongoengine.register_connection` will 32 | be extracted by mentioned functions, any other keyword arguments will be silently 33 | followed to {class}`pymongo.mongo_client.MongoClient`. 34 | 35 | This allows complete and flexible database configuration. 36 | 37 | Example: 38 | 39 | ```python 40 | import flask 41 | from flask_mongoengine import MongoEngine 42 | 43 | db = MongoEngine() 44 | app = flask.Flask("example_app") 45 | app.config["MONGODB_SETTINGS"] = [ 46 | { 47 | "db": "project1", 48 | "host": "localhost", 49 | "port": 27017, 50 | "alias": "default", 51 | } 52 | ] 53 | db.init_app(app) 54 | ``` 55 | 56 | ## Deprecated: Passing database configuration to MongoEngine class 57 | 58 | ```{eval-rst} 59 | .. deprecated:: 2.0.0 60 | ``` 61 | 62 | You can pass database dictionary of dictionaries directly to {class}`.MongoEngine` 63 | class initialization. Lists of settings not supported. 64 | 65 | ```python 66 | import flask 67 | from flask_mongoengine import MongoEngine 68 | 69 | db_config = { 70 | "db": "project1", 71 | "host": "localhost", 72 | "port": 27017, 73 | "alias": "default", 74 | } 75 | db = MongoEngine(config=db_config) 76 | app = flask.Flask("example_app") 77 | 78 | db.init_app(app) 79 | ``` 80 | 81 | ## Deprecated: Passing database configuration to MongoEngine.init_app method 82 | 83 | ```{eval-rst} 84 | .. deprecated:: 2.0.0 85 | ``` 86 | 87 | You can pass database dictionary of dictionaries directly to 88 | {func}`flask_mongoengine.MongoEngine.init_app` class initialization. Lists of 89 | settings not supported. 90 | 91 | ```python 92 | import flask 93 | from flask_mongoengine import MongoEngine 94 | 95 | db_config = { 96 | "db": "project1", 97 | "host": "localhost", 98 | "port": 27017, 99 | "alias": "default", 100 | } 101 | db = MongoEngine() 102 | app = flask.Flask("example_app") 103 | 104 | db.init_app(app, config=db_config) 105 | ``` 106 | 107 | ## Deprecated: MONGODB_ inside MONGODB_SETTINGS dictionary 108 | 109 | ```{eval-rst} 110 | .. deprecated:: 2.0.0 111 | ``` 112 | 113 | Flask-mongoengine will cut off ``MONGODB_`` prefix from any parameters, specified 114 | inside ``MONGODB_SETTINGS`` dictionary. This is historical behaviour, but may be 115 | removed in the future. Providing such settings may raise config errors, when parent 116 | packets implement case-sensitive keyword arguments checks. Check issue [#451] for 117 | example historical problem. 118 | 119 | Currently, we are handling all possible case-sensitive keyword settings and related 120 | users errors (based on pymongo 4.1.1), but amount of such settings may be increased 121 | in future pymongo versions. 122 | 123 | Usage of recommended settings approach completely remove this possible problem. 124 | 125 | ## Deprecated: URI style settings 126 | 127 | ```{eval-rst} 128 | .. deprecated:: 2.0.0 129 | ``` 130 | 131 | URI style connections supported as supply the uri as the ``host`` in the 132 | ``MONGODB_SETTINGS`` dictionary in ``app.config``. 133 | 134 | ```{warning} 135 | It is not recommended to use URI style settings, as URI style settings parsed and 136 | manupulated in all parent functions/methods. This may lead to unexpected behaviour when 137 | parent packages versions changed. 138 | ``` 139 | 140 | ```{warning} 141 | Database name from uri has priority over name. (MongoEngine behaviour). 142 | ``` 143 | 144 | If uri presents and doesn't contain database name db setting entirely ignore and db 145 | name set to ``test``: 146 | 147 | ```python 148 | import flask 149 | from flask_mongoengine import MongoEngine 150 | 151 | db = MongoEngine() 152 | app = flask.Flask("example_app") 153 | app.config['MONGODB_SETTINGS'] = { 154 | 'db': 'project1', 155 | 'host': 'mongodb://localhost/database_name' 156 | } 157 | db.init_app(app) 158 | ``` 159 | 160 | ## Deprecated: Flat MONGODB_ style configuration settings 161 | 162 | ```{eval-rst} 163 | .. deprecated:: 2.0.0 164 | ``` 165 | 166 | Connection settings may also be provided individually by prefixing the setting with 167 | ``MONGODB_`` in the ``app.config``: 168 | 169 | ```python 170 | import flask 171 | from flask_mongoengine import MongoEngine 172 | 173 | db = MongoEngine() 174 | app = flask.Flask("example_app") 175 | app.config['MONGODB_DB'] = 'project1' 176 | app.config['MONGODB_HOST'] = '192.168.1.35' 177 | app.config['MONGODB_PORT'] = 12345 178 | app.config['MONGODB_USERNAME'] = 'webapp' 179 | app.config['MONGODB_PASSWORD'] = 'pwd123' 180 | db.init_app(app) 181 | ``` 182 | 183 | This method does not support multi-database installations. 184 | 185 | By default, flask-mongoengine open the connection when extension is instantiated, 186 | but you can configure it to open connection only on first database access by setting 187 | the ``MONGODB_SETTINGS['connect']`` parameter or its ``MONGODB_CONNECT`` flat 188 | equivalent to ``False``. 189 | 190 | [#451]: https://github.com/MongoEngine/flask-mongoengine/issues/451 191 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Flask-MongoEngine documentation 2 | =============================== 3 | 4 | A Flask extension that provides integration with `MongoEngine `_. 5 | For more information on MongoEngine please check out the `MongoEngine Documentation `_. 6 | 7 | It handles connection management for your app. 8 | You can also use `WTForms `_ as model forms for your models. 9 | 10 | .. toctree:: 11 | :maxdepth: 2 12 | 13 | flask_config 14 | db_model 15 | forms 16 | migration_to_v2 17 | custom_queryset 18 | wtf_forms 19 | session_interface 20 | debug_toolbar 21 | example_app 22 | api/index 23 | CONTRIBUTING 24 | AUTHORS 25 | changelog 26 | LICENSE 27 | -------------------------------------------------------------------------------- /docs/migration_to_v2.md: -------------------------------------------------------------------------------- 1 | # Migration to 2.0.0 and changes 2 | 3 | ## Empty fields are not created in database 4 | 5 | If you are already aware about empty string vs `None` database conflicts, you should 6 | know that some string based generated forms behaviour are not consistent between 7 | version **<=1.0.0** and **2.0.0+**. 8 | 9 | In version **2.0.0** all fields with empty strings will be considered as `None` and 10 | will not be saved by default (this is easy to change for particular field to keep 11 | old behaviour for existed projects). 12 | 13 | This behaviour is different from previous versions, where 14 | {class}`~flask_mongoengine.db_fields.StringField` consider empty string as valid 15 | value, and save it to database. This is the opposite behavior against original 16 | [mongoengine] project and related database. 17 | 18 | ```{warning} 19 | To keep easy migration and correct deprecation steps {func}`~.model_form` and 20 | {func}`.model_fields` still keep old behaviour. 21 | ``` 22 | 23 | To be completely clear, let look on example. Let make a completely meaningless model, 24 | with all field types from [mongoengine], like this: 25 | 26 | ```python 27 | """example_app.models.py""" 28 | from flask_mongoengine import MongoEngine 29 | 30 | db = MongoEngine() 31 | 32 | 33 | class Todo(db.Document): 34 | """Test model for AllFieldsModel.""" 35 | string = db.StringField() 36 | 37 | 38 | class Embedded(db.EmbeddedDocument): 39 | """Test embedded for AllFieldsModel.""" 40 | string = db.StringField() 41 | 42 | 43 | class AllFieldsModel(db.Document): 44 | """Meaningless Document with all field types.""" 45 | binary_field = db.BinaryField() 46 | boolean_field = db.BooleanField() 47 | date_field = db.DateField() 48 | date_time_field = db.DateTimeField() 49 | decimal_field = db.DecimalField() 50 | dict_field = db.DictField() 51 | email_field = db.EmailField() 52 | embedded_document_field = db.EmbeddedDocumentField(document_type=Embedded) 53 | file_field = db.FileField() 54 | float_field = db.FloatField() 55 | int_field = db.IntField() 56 | list_field = db.ListField(field=db.StringField) 57 | reference_field = db.ReferenceField(document_type=Todo) 58 | sorted_list_field = db.SortedListField(field=db.StringField) 59 | string_field = db.StringField() 60 | url_field = db.URLField() 61 | cached_reference_field = db.CachedReferenceField(document_type=Todo) 62 | complex_date_time_field = db.ComplexDateTimeField() 63 | dynamic_field = db.DynamicField() 64 | embedded_document_list_field = db.EmbeddedDocumentListField(document_type=Embedded) 65 | enum_field = db.EnumField(enum=[1, 2]) 66 | generic_embedded_document_field = db.GenericEmbeddedDocumentField() 67 | generic_lazy_reference_field = db.GenericLazyReferenceField() 68 | geo_json_base_field = db.GeoJsonBaseField() 69 | geo_point_field = db.GeoPointField() 70 | image_field = db.ImageField() 71 | lazy_reference_field = db.LazyReferenceField(document_type=Todo) 72 | line_string_field = db.LineStringField() 73 | long_field = db.LongField() 74 | map_field = db.MapField(field=db.StringField()) 75 | multi_line_string_field = db.MultiLineStringField() 76 | multi_point_field = db.MultiPointField() 77 | multi_polygon_field = db.MultiPolygonField() 78 | point_field = db.PointField() 79 | polygon_field = db.PolygonField() 80 | sequence_field = db.SequenceField() 81 | uuid_field = db.UUIDField() 82 | generic_reference_field = db.GenericReferenceField(document_type=Todo) 83 | object_id_field = db.ObjectIdField() 84 | ``` 85 | 86 | Now let's make an example instance of such object and save it to db. 87 | 88 | ```python 89 | from example_app.models import AllFieldsModel 90 | 91 | obj = AllFieldsModel() 92 | obj.save() 93 | ``` 94 | 95 | On database side this document will be created: 96 | 97 | ```json 98 | { 99 | "_id": { 100 | "$oid": "62df93ac3fe82c8656aae60d" 101 | }, 102 | "dict_field": {}, 103 | "list_field": [], 104 | "sorted_list_field": [], 105 | "embedded_document_list_field": [], 106 | "map_field": {}, 107 | "sequence_field": 1 108 | } 109 | ``` 110 | 111 | For empty form Flask-Mongoengine **2.0.0** will create exactly the same document, 112 | old project version will try to fill some fields, based on 113 | {class}`~flask_mongoengine.db_fields.StringField`. 114 | 115 | [mongoengine]: https://docs.mongoengine.org/ 116 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx-rtd-theme>=1.0.0 2 | myst-parser>=0.17.2 3 | sphinx-autobuild>=2021.3.14 4 | Sphinx>=4.5.0 5 | sphinxcontrib-apidoc>=0.3.0 6 | sphinx-autodoc-typehints>=1.18.2 7 | -------------------------------------------------------------------------------- /docs/session_interface.md: -------------------------------------------------------------------------------- 1 | # Session Interface 2 | 3 | To use MongoEngine as your session store simple configure the session interface: 4 | 5 | ```python 6 | from flask_mongoengine import MongoEngine, MongoEngineSessionInterface 7 | 8 | app = Flask(__name__) 9 | db = MongoEngine(app) 10 | app.session_interface = MongoEngineSessionInterface(db) 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/wtf_forms.md: -------------------------------------------------------------------------------- 1 | # MongoEngine and WTForms 2 | 3 | flask-mongoengine automatically generates WTForms from MongoEngine models: 4 | 5 | ```python 6 | from flask_mongoengine.wtf import model_form 7 | 8 | class User(db.Document): 9 | email = db.StringField(required=True) 10 | first_name = db.StringField(max_length=50) 11 | last_name = db.StringField(max_length=50) 12 | 13 | class Content(db.EmbeddedDocument): 14 | text = db.StringField() 15 | lang = db.StringField(max_length=3) 16 | 17 | class Post(db.Document): 18 | title = db.StringField(max_length=120, required=True, validators=[validators.InputRequired(message='Missing title.'),]) 19 | author = db.ReferenceField(User) 20 | tags = db.ListField(db.StringField(max_length=30)) 21 | content = db.EmbeddedDocumentField(Content) 22 | 23 | PostForm = model_form(Post) 24 | 25 | def add_post(request): 26 | form = PostForm(request.POST) 27 | if request.method == 'POST' and form.validate(): 28 | # do something 29 | redirect('done') 30 | return render_template('add_post.html', form=form) 31 | ``` 32 | 33 | For each MongoEngine field, the most appropriate WTForm field is used. 34 | Parameters allow the user to provide hints if the conversion is not implicit:: 35 | 36 | ```python 37 | PostForm = model_form(Post, field_args={'title': {'textarea': True}}) 38 | ``` 39 | 40 | Supported parameters: 41 | 42 | For fields with `choices`: 43 | 44 | - `multiple` to use a SelectMultipleField 45 | - `radio` to use a RadioField 46 | 47 | For ``StringField``: 48 | 49 | - `password` to use a PasswordField 50 | - `textarea` to use a TextAreaField 51 | 52 | For ``ListField``: 53 | 54 | - `min_entries` to set the minimal number of entries 55 | 56 | (By default, a StringField is converted into a TextAreaField if and only if it has no 57 | max_length.) 58 | 59 | ## Supported fields 60 | 61 | * StringField 62 | * BinaryField 63 | * URLField 64 | * EmailField 65 | * IntField 66 | * FloatField 67 | * DecimalField 68 | * BooleanField 69 | * DateTimeField 70 | * **ListField** (using wtforms.fields.FieldList ) 71 | * SortedListField (duplicate ListField) 72 | * **EmbeddedDocumentField** (using wtforms.fields.FormField and generating inline Form) 73 | * **ReferenceField** (using wtforms.fields.SelectFieldBase with options loaded from 74 | QuerySet or Document) 75 | * DictField 76 | 77 | ## Not currently supported field types: 78 | 79 | * ObjectIdField 80 | * GeoLocationField 81 | * GenericReferenceField 82 | -------------------------------------------------------------------------------- /example_app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MongoEngine/flask-mongoengine/d4526139cb1e2e94111ab7de96bb629d574c1690/example_app/__init__.py -------------------------------------------------------------------------------- /example_app/app.py: -------------------------------------------------------------------------------- 1 | import flask 2 | from flask_debugtoolbar import DebugToolbarExtension 3 | from pymongo import monitoring 4 | 5 | from example_app import views 6 | from example_app.boolean_demo import boolean_demo_view 7 | from example_app.dates_demo import dates_demo_view 8 | from example_app.dict_demo import dict_demo_view 9 | from example_app.models import db 10 | from example_app.numbers_demo import numbers_demo_view 11 | from example_app.strings_demo import strings_demo_view 12 | from flask_mongoengine.panels import mongo_command_logger 13 | 14 | app = flask.Flask("example_app") 15 | # Working multidatabase settings example 16 | app.config["MONGODB_SETTINGS"] = [ 17 | {"db": "example_app", "host": "mongo", "alias": "default"}, 18 | { 19 | "MONGODB_DB": "example_app_2", 20 | "MONGODB_HOST": "mongo", 21 | "MONGODB_ALIAS": "secondary", 22 | }, 23 | ] 24 | app.config["TESTING"] = True 25 | app.config["SECRET_KEY"] = "flask+mongoengine=<3" 26 | app.debug = True 27 | app.config["DEBUG_TB_PANELS"] = ( 28 | "flask_debugtoolbar.panels.config_vars.ConfigVarsDebugPanel", 29 | "flask_debugtoolbar.panels.g.GDebugPanel", 30 | "flask_debugtoolbar.panels.headers.HeaderDebugPanel", 31 | "flask_debugtoolbar.panels.logger.LoggingPanel", 32 | "flask_debugtoolbar.panels.profiler.ProfilerDebugPanel", 33 | "flask_debugtoolbar.panels.request_vars.RequestVarsDebugPanel", 34 | "flask_debugtoolbar.panels.route_list.RouteListDebugPanel", 35 | "flask_debugtoolbar.panels.template.TemplateDebugPanel", 36 | "flask_debugtoolbar.panels.timer.TimerDebugPanel", 37 | "flask_debugtoolbar.panels.versions.VersionDebugPanel", 38 | "flask_mongoengine.panels.MongoDebugPanel", 39 | ) 40 | app.config["DEBUG_TB_INTERCEPT_REDIRECTS"] = False 41 | DebugToolbarExtension(app) 42 | monitoring.register(mongo_command_logger) 43 | db.init_app(app) 44 | 45 | 46 | app.add_url_rule("/", view_func=views.index, methods=["GET", "POST"]) 47 | app.add_url_rule("/pagination", view_func=views.pagination, methods=["GET", "POST"]) 48 | app.add_url_rule("/strings", view_func=strings_demo_view, methods=["GET", "POST"]) 49 | app.add_url_rule("/strings//", view_func=strings_demo_view, methods=["GET", "POST"]) 50 | app.add_url_rule("/numbers", view_func=numbers_demo_view, methods=["GET", "POST"]) 51 | app.add_url_rule("/numbers//", view_func=numbers_demo_view, methods=["GET", "POST"]) 52 | app.add_url_rule("/dates", view_func=dates_demo_view, methods=["GET", "POST"]) 53 | app.add_url_rule("/dates//", view_func=dates_demo_view, methods=["GET", "POST"]) 54 | app.add_url_rule("/bool", view_func=boolean_demo_view, methods=["GET", "POST"]) 55 | app.add_url_rule("/bool//", view_func=boolean_demo_view, methods=["GET", "POST"]) 56 | app.add_url_rule("/dict", view_func=dict_demo_view, methods=["GET", "POST"]) 57 | app.add_url_rule("/dict//", view_func=dict_demo_view, methods=["GET", "POST"]) 58 | 59 | if __name__ == "__main__": 60 | app.run(host="0.0.0.0", port=8000) 61 | -------------------------------------------------------------------------------- /example_app/boolean_demo.py: -------------------------------------------------------------------------------- 1 | """Booleans fields demo model.""" 2 | 3 | from example_app.models import db 4 | 5 | 6 | class BooleanDemoModel(db.Document): 7 | """Documentation example model.""" 8 | 9 | simple_sting_name = db.StringField() 10 | boolean_field = db.BooleanField() 11 | boolean_field_with_null = db.BooleanField(null=True) 12 | true_boolean_field_with_allowed_null = db.BooleanField(null=True, default=True) 13 | boolean_field_with_as_choices_replace = db.BooleanField( 14 | wtf_options={ 15 | "choices": [("", "Not selected"), ("yes", "Positive"), ("no", "Negative")] 16 | } 17 | ) 18 | 19 | 20 | BooleanDemoForm = BooleanDemoModel.to_wtf_form() 21 | 22 | 23 | def boolean_demo_view(pk=None): 24 | """Return all fields demonstration.""" 25 | from example_app.views import demo_view 26 | 27 | return demo_view( 28 | model=BooleanDemoModel, view_name=boolean_demo_view.__name__, pk=pk 29 | ) 30 | -------------------------------------------------------------------------------- /example_app/compose/flask/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7-slim as dev 2 | 3 | ENV PYTHONUNBUFFERED 1 4 | RUN apt-get update && \ 5 | apt-get install git -y --no-install-recommends && \ 6 | rm -rf /var/lib/apt/lists/* 7 | RUN groupadd -r flask && useradd -r -g flask flask 8 | COPY --chown=flask . /flask_mongoengine 9 | RUN pip install --upgrade pip \ 10 | && pip install -e /flask_mongoengine[toolbar,wtf] \ 11 | && pip install Faker Pillow 12 | WORKDIR /flask_mongoengine 13 | -------------------------------------------------------------------------------- /example_app/dates_demo.py: -------------------------------------------------------------------------------- 1 | """Date and DateTime fields demo model.""" 2 | 3 | from wtforms.fields import DateTimeField 4 | 5 | from example_app.models import db 6 | 7 | 8 | class DateTimeModel(db.Document): 9 | """Documentation example model.""" 10 | 11 | any_string = db.StringField() 12 | date = db.DateField() 13 | datetime = db.DateTimeField() 14 | datetime_no_sec = db.DateTimeField(wtf_options={"render_kw": {"step": "60"}}) 15 | datetime_ms = db.DateTimeField(wtf_options={"render_kw": {"step": "0.001"}}) 16 | complex_datetime = db.ComplexDateTimeField() 17 | complex_datetime_sec = db.ComplexDateTimeField( 18 | wtf_options={"render_kw": {"step": "1"}} 19 | ) 20 | complex_datetime_microseconds = db.ComplexDateTimeField( 21 | wtf_field_class=DateTimeField, wtf_options={"format": "%Y-%m-%d %H:%M:%S.%f"} 22 | ) 23 | 24 | 25 | DateTimeDemoForm = DateTimeModel.to_wtf_form() 26 | 27 | 28 | def dates_demo_view(pk=None): 29 | """Return all fields demonstration.""" 30 | from example_app.views import demo_view 31 | 32 | return demo_view(model=DateTimeModel, view_name=dates_demo_view.__name__, pk=pk) 33 | -------------------------------------------------------------------------------- /example_app/dict_demo.py: -------------------------------------------------------------------------------- 1 | """Dict fields demo model.""" 2 | 3 | from example_app.models import db 4 | 5 | 6 | def get_default_dict(): 7 | """Example of default dict specification.""" 8 | return {"alpha": 1, "text": "text", "float": 1.2} 9 | 10 | 11 | class DictDemoModel(db.Document): 12 | """Documentation example model.""" 13 | 14 | string = db.StringField(verbose_name="str") 15 | dict_field = db.DictField() 16 | no_dict_field = db.DictField(default=None) 17 | null_dict_field = db.DictField(default=None, null=True) 18 | dict_default = db.DictField(default=get_default_dict) 19 | null_dict_default = db.DictField(default=get_default_dict, null=True) 20 | no_dict_prefilled = db.DictField( 21 | default=None, 22 | null=False, 23 | wtf_options={"default": get_default_dict, "null": True}, 24 | ) 25 | 26 | 27 | DictDemoForm = DictDemoModel.to_wtf_form() 28 | 29 | 30 | def dict_demo_view(pk=None): 31 | """Return all fields demonstration.""" 32 | from example_app.views import demo_view 33 | 34 | return demo_view(model=DictDemoModel, view_name=dict_demo_view.__name__, pk=pk) 35 | -------------------------------------------------------------------------------- /example_app/models.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from flask_mongoengine import MongoEngine 4 | 5 | db = MongoEngine() 6 | 7 | 8 | class Todo(db.Document): 9 | """Test model for AllFieldsModel and pagination.""" 10 | 11 | title = db.StringField(max_length=60) 12 | text = db.StringField() 13 | done = db.BooleanField(default=False) 14 | pub_date = db.DateTimeField(default=datetime.datetime.now) 15 | 16 | 17 | class Embedded(db.EmbeddedDocument): 18 | """Test embedded for AllFieldsModel.""" 19 | 20 | string = db.StringField() 21 | 22 | 23 | class AllFieldsModel(db.Document): 24 | """Meaningless Document with all field types.""" 25 | 26 | binary_field = db.BinaryField() 27 | boolean_field = db.BooleanField() 28 | date_field = db.DateField() 29 | date_time_field = db.DateTimeField() 30 | decimal_field = db.DecimalField() 31 | dict_field = db.DictField() 32 | email_field = db.EmailField() 33 | embedded_document_field = db.EmbeddedDocumentField(document_type=Embedded) 34 | file_field = db.FileField() 35 | float_field = db.FloatField() 36 | int_field = db.IntField() 37 | list_field = db.ListField(field=db.StringField) 38 | reference_field = db.ReferenceField(document_type=Todo) 39 | sorted_list_field = db.SortedListField(field=db.StringField) 40 | string_field = db.StringField() 41 | url_field = db.URLField() 42 | cached_reference_field = db.CachedReferenceField(document_type=Todo) 43 | complex_date_time_field = db.ComplexDateTimeField() 44 | dynamic_field = db.DynamicField() 45 | embedded_document_list_field = db.EmbeddedDocumentListField(document_type=Embedded) 46 | enum_field = db.EnumField(enum=[1, 2]) 47 | generic_embedded_document_field = db.GenericEmbeddedDocumentField() 48 | generic_lazy_reference_field = db.GenericLazyReferenceField() 49 | geo_json_base_field = db.GeoJsonBaseField() 50 | geo_point_field = db.GeoPointField() 51 | image_field = db.ImageField() 52 | lazy_reference_field = db.LazyReferenceField(document_type=Todo) 53 | line_string_field = db.LineStringField() 54 | long_field = db.LongField() 55 | map_field = db.MapField(field=db.StringField()) 56 | multi_line_string_field = db.MultiLineStringField() 57 | multi_point_field = db.MultiPointField() 58 | multi_polygon_field = db.MultiPolygonField() 59 | point_field = db.PointField() 60 | polygon_field = db.PolygonField() 61 | sequence_field = db.SequenceField() 62 | uuid_field = db.UUIDField() 63 | generic_reference_field = db.GenericReferenceField(document_type=Todo) 64 | object_id_field = db.ObjectIdField() 65 | 66 | 67 | TodoForm = Todo.to_wtf_form() 68 | AllFieldsForm = AllFieldsModel.to_wtf_form() 69 | -------------------------------------------------------------------------------- /example_app/numbers_demo.py: -------------------------------------------------------------------------------- 1 | """Numbers and related fields demo model.""" 2 | 3 | from decimal import Decimal 4 | 5 | from example_app.models import db 6 | 7 | 8 | class NumbersDemoModel(db.Document): 9 | """Documentation example model.""" 10 | 11 | simple_sting_name = db.StringField() 12 | float_field_unlimited = db.FloatField() 13 | decimal_field_unlimited = db.DecimalField() 14 | integer_field_unlimited = db.IntField() 15 | float_field_limited = db.FloatField(min_value=float(1), max_value=200.455) 16 | decimal_field_limited = db.DecimalField( 17 | min_value=Decimal("1"), max_value=Decimal("200.455") 18 | ) 19 | integer_field_limited = db.IntField(min_value=1, max_value=200) 20 | 21 | 22 | NumbersDemoForm = NumbersDemoModel.to_wtf_form() 23 | 24 | 25 | def numbers_demo_view(pk=None): 26 | """Return all fields demonstration.""" 27 | from example_app.views import demo_view 28 | 29 | return demo_view( 30 | model=NumbersDemoModel, view_name=numbers_demo_view.__name__, pk=pk 31 | ) 32 | -------------------------------------------------------------------------------- /example_app/strings_demo.py: -------------------------------------------------------------------------------- 1 | """Strings and strings related fields demo model.""" 2 | import re 3 | 4 | from example_app.models import db 5 | from flask_mongoengine.wtf import fields as mongo_fields 6 | 7 | 8 | class StringsDemoModel(db.Document): 9 | """Documentation example model.""" 10 | 11 | string_field = db.StringField() 12 | regexp_string_field = db.StringField( 13 | regex=re.compile( 14 | r"^(https:\/\/)[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=]+$" 15 | ) 16 | ) 17 | sized_string_field = db.StringField(min_length=5) 18 | tel_field = db.StringField(wtf_field_class=mongo_fields.MongoTelField) 19 | password_field = db.StringField( 20 | wtf_field_class=mongo_fields.MongoPasswordField, 21 | required=True, 22 | min_length=5, 23 | ) 24 | email_field = db.EmailField() 25 | url_field = db.URLField() 26 | 27 | 28 | StringsDemoForm = StringsDemoModel.to_wtf_form() 29 | 30 | 31 | def strings_demo_view(pk=None): 32 | """Return all fields demonstration.""" 33 | from example_app.views import demo_view 34 | 35 | return demo_view( 36 | model=StringsDemoModel, view_name=strings_demo_view.__name__, pk=pk 37 | ) 38 | -------------------------------------------------------------------------------- /example_app/templates/_formhelpers.html: -------------------------------------------------------------------------------- 1 | {# Macro for creating navigation links #} 2 | {% macro render_navigation(pagination, endpoint) %} 3 |
4 | {% for page in pagination.iter_pages() %} 5 | {% if page %} 6 | {% if page != pagination.page %} 7 | {{ page }} 8 | {% else %} 9 | {{ page }} 10 | {% endif %} 11 | {% else %} 12 | nothing to show 13 | {% endif %} 14 | {% endfor %} 15 |
16 | {% endmacro %} 17 | 18 | {# Example macro for rendering fields #} 19 | {% macro render_field(field) %} 20 |
21 | {% if field.errors %} 22 | {% set css_class = 'has_error ' + kwargs.pop('class', '') %} 23 | {{ field.label }}{{ field(class=css_class, **kwargs) }}{% if field.flags.required %}*{% endif %} 24 |
    {% for error in field.errors %} 25 |
  • {{ error|e }}
  • {% endfor %}
26 | {% else %} 27 | {{ field.label }}{{ field(**kwargs) }}{% if field.flags.required %}*{% endif %} 28 | {% endif %} 29 |
30 | {% endmacro %} 31 | -------------------------------------------------------------------------------- /example_app/templates/form_demo.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% from "_formhelpers.html" import render_field %} 3 | {% from "_formhelpers.html" import render_navigation %} 4 | 5 | {% block body %} 6 | 7 |
8 | 9 | 10 | 11 | {% for field in model._fields_ordered %} 12 | 13 | {% endfor %} 14 | 15 | 16 | 17 | 18 | {% for page_object in page.items %} 19 | 20 | {% for field in page_object._fields_ordered %} 21 | 22 | {% endfor %} 23 | 24 | 25 | {% endfor %} 26 | 27 |
{{ model[field].verbose_name or model[field].name}}Edit
{{ page_object[field] }}edit
28 |
29 |
30 | {{ render_navigation(page, view) }} 31 |
32 |
33 |
34 | {% for field in form %} 35 | {{ render_field(field, style='font-weight: bold') }} 36 | {% endfor %} 37 | 38 |
39 |
40 | 41 | {% endblock %} 42 | -------------------------------------------------------------------------------- /example_app/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |

This is demo application that shows Flask-Mongoengine features and integrations.

4 |

Feel free to open Flask debug toolbar after any action to check executed database requests.

5 |

If you use this application for first time, it is good idea to generate some fake data.

6 |

7 | Data will be generated inside docker container database. 8 | Two databases will be used to show Flask Debug Panel integration capabilities. 9 |

10 |

You can also delete all data, to test forms only.

11 | {% if message %} 12 |

{{ message }}

13 | {% endif %} 14 |
15 | 16 | 17 |
18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /example_app/templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Flask MongoEngine 5 | 6 | 7 | 16 | 17 | 28 |
29 |
30 | {% block body %}{% endblock %} 31 |
32 |
33 | 34 | 35 | -------------------------------------------------------------------------------- /example_app/templates/pagination.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% from "_formhelpers.html" import render_field %} 3 | {% from "_formhelpers.html" import render_navigation %} 4 | 5 | {% block body %} 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | {% for todo in todos_page.items %} 19 | 20 | 21 | 22 | 23 | 24 | 25 | {% endfor %} 26 | 27 |
TitleTextDonePublication date
{{ todo.title }}{{ todo.text }}{{ todo.done }}{{ todo.pub_date }}
28 |
29 |
30 | {{ render_navigation(todos_page, "pagination") }} 31 |
32 |
33 |
34 | {% for field in form %} 35 | {{ render_field(field, style='font-weight: bold') }} 36 | {% endfor %} 37 | 38 |
39 |
40 | 41 | {% endblock %} 42 | -------------------------------------------------------------------------------- /example_app/views.py: -------------------------------------------------------------------------------- 1 | """Demo views for example application.""" 2 | from faker import Faker 3 | from flask import render_template, request 4 | from mongoengine.context_managers import switch_db 5 | 6 | from example_app import models 7 | from example_app.boolean_demo import BooleanDemoModel 8 | from example_app.dates_demo import DateTimeModel 9 | from example_app.dict_demo import DictDemoModel 10 | from example_app.numbers_demo import NumbersDemoModel 11 | from example_app.strings_demo import StringsDemoModel 12 | 13 | 14 | def generate_data(): 15 | """Generates fake data for all supported models and views.""" 16 | fake = Faker(locale=["en_US"]) 17 | 18 | with switch_db(models.Todo, "default"): 19 | models.Todo.objects().delete() # Removes 20 | models.Todo( 21 | title="Simple todo A", text=fake.sentence(), done=False 22 | ).save() # Insert 23 | models.Todo( 24 | title="Simple todo B", text=fake.sentence(), done=True 25 | ).save() # Insert 26 | models.Todo( 27 | title="Simple todo C", text=fake.sentence(), done=True 28 | ).save() # Insert 29 | # Bulk insert 30 | bulk = ( 31 | models.Todo(title="Bulk 1", text=fake.sentence(), done=False), 32 | models.Todo(title="Bulk 2", text=fake.sentence(), done=True), 33 | ) 34 | models.Todo.objects().insert(bulk) 35 | models.Todo.objects(title__contains="B").order_by("-title").update( 36 | set__text="Hello world" 37 | ) # Update 38 | models.Todo.objects(title__contains="C").order_by("-title").delete() # Removes 39 | 40 | with switch_db(models.Todo, "secondary"): 41 | models.Todo.objects().delete() 42 | for _ in range(10): 43 | models.Todo( 44 | title=fake.text(max_nb_chars=59), 45 | text=fake.sentence(), 46 | done=fake.pybool(), 47 | pub_date=fake.date(), 48 | ).save() 49 | 50 | 51 | def delete_data(): 52 | """Clear database.""" 53 | with switch_db(models.Todo, "default"): 54 | models.Todo.objects().delete() 55 | BooleanDemoModel.objects().delete() 56 | DateTimeModel.objects().delete() 57 | DictDemoModel.objects().delete() 58 | StringsDemoModel.objects().delete() 59 | NumbersDemoModel.objects().delete() 60 | with switch_db(models.Todo, "secondary"): 61 | models.Todo.objects().delete() 62 | 63 | 64 | def index(): 65 | """Return page with welcome words and instructions.""" 66 | message = None 67 | if request.method == "POST": 68 | if request.form["button"] == "Generate data": 69 | generate_data() 70 | message = "Fake data generated" 71 | if request.form["button"] == "Delete data": 72 | delete_data() 73 | message = "All data deleted" 74 | return render_template("index.html", message=message) 75 | 76 | 77 | def pagination(): 78 | """Return pagination and easy form demonstration.""" 79 | form = models.TodoForm() 80 | 81 | with switch_db(models.Todo, "secondary"): 82 | if request.method == "POST": 83 | form.validate_on_submit() 84 | form.save() 85 | page_num = int(request.args.get("page") or 1) 86 | todos_page = models.Todo.objects.paginate(page=page_num, per_page=3) 87 | 88 | return render_template("pagination.html", todos_page=todos_page, form=form) 89 | 90 | 91 | def demo_view(model, view_name, pk=None): 92 | """Return all fields demonstration.""" 93 | FormClass = model.to_wtf_form() 94 | form = FormClass() 95 | if pk: 96 | obj = model.objects.get(pk=pk) 97 | form = FormClass(obj=obj) 98 | 99 | if request.method == "POST" and form.validate_on_submit(): 100 | form.save() 101 | # form = FormClass(obj=form.instance) 102 | page_num = int(request.args.get("page") or 1) 103 | page = model.objects.paginate(page=page_num, per_page=100) 104 | 105 | return render_template( 106 | "form_demo.html", view=view_name, page=page, form=form, model=model 107 | ) 108 | -------------------------------------------------------------------------------- /flask_mongoengine/__init__.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | import mongoengine 4 | from flask import Flask, current_app 5 | 6 | from flask_mongoengine import db_fields, documents 7 | from flask_mongoengine.connection import * 8 | from flask_mongoengine.json import override_json_encoder 9 | from flask_mongoengine.pagination import * 10 | from flask_mongoengine.sessions import * 11 | 12 | 13 | def current_mongoengine_instance(): 14 | """Return a MongoEngine instance associated with current Flask app.""" 15 | me = current_app.extensions.get("mongoengine", {}) 16 | for k, v in me.items(): 17 | if isinstance(k, MongoEngine): 18 | return k 19 | 20 | 21 | class MongoEngine: 22 | """Main class used for initialization of Flask-MongoEngine.""" 23 | 24 | def __init__(self, app=None, config=None): 25 | if config is not None: 26 | warnings.warn( 27 | ( 28 | "Passing flat configuration is deprecated. Please check " 29 | "http://docs.mongoengine.org/projects/flask-mongoengine/flask_config.html " 30 | "for more info." 31 | ), 32 | DeprecationWarning, 33 | stacklevel=2, 34 | ) 35 | # Extended database fields 36 | self.BinaryField = db_fields.BinaryField 37 | self.BooleanField = db_fields.BooleanField 38 | self.CachedReferenceField = db_fields.CachedReferenceField 39 | self.ComplexDateTimeField = db_fields.ComplexDateTimeField 40 | self.DateField = db_fields.DateField 41 | self.DateTimeField = db_fields.DateTimeField 42 | self.DecimalField = db_fields.DecimalField 43 | self.DictField = db_fields.DictField 44 | self.DynamicField = db_fields.DynamicField 45 | self.EmailField = db_fields.EmailField 46 | self.EmbeddedDocumentField = db_fields.EmbeddedDocumentField 47 | self.EmbeddedDocumentListField = db_fields.EmbeddedDocumentListField 48 | self.EnumField = db_fields.EnumField 49 | self.FileField = db_fields.FileField 50 | self.FloatField = db_fields.FloatField 51 | self.GenericEmbeddedDocumentField = db_fields.GenericEmbeddedDocumentField 52 | self.GenericLazyReferenceField = db_fields.GenericLazyReferenceField 53 | self.GenericReferenceField = db_fields.GenericReferenceField 54 | self.GeoJsonBaseField = db_fields.GeoJsonBaseField 55 | self.GeoPointField = db_fields.GeoPointField 56 | self.ImageField = db_fields.ImageField 57 | self.IntField = db_fields.IntField 58 | self.LazyReferenceField = db_fields.LazyReferenceField 59 | self.LineStringField = db_fields.LineStringField 60 | self.ListField = db_fields.ListField 61 | self.LongField = db_fields.LongField 62 | self.MapField = db_fields.MapField 63 | self.MultiLineStringField = db_fields.MultiLineStringField 64 | self.MultiPointField = db_fields.MultiPointField 65 | self.MultiPolygonField = db_fields.MultiPolygonField 66 | self.ObjectIdField = db_fields.ObjectIdField 67 | self.PointField = db_fields.PointField 68 | self.PolygonField = db_fields.PolygonField 69 | self.ReferenceField = db_fields.ReferenceField 70 | self.SequenceField = db_fields.SequenceField 71 | self.SortedListField = db_fields.SortedListField 72 | self.StringField = db_fields.StringField 73 | self.URLField = db_fields.URLField 74 | self.UUIDField = db_fields.UUIDField 75 | 76 | # Flask related data 77 | self.app = None 78 | self.config = config 79 | 80 | # Extended documents classes 81 | self.Document = documents.Document 82 | self.DynamicDocument = documents.DynamicDocument 83 | 84 | if app is not None: 85 | self.init_app(app, config) 86 | 87 | def init_app(self, app, config=None): 88 | if not app or not isinstance(app, Flask): 89 | raise TypeError("Invalid Flask application instance") 90 | 91 | if config is not None: 92 | warnings.warn( 93 | ( 94 | "Passing flat configuration is deprecated. Please check " 95 | "http://docs.mongoengine.org/projects/flask-mongoengine/flask_config.html " 96 | "for more info." 97 | ), 98 | DeprecationWarning, 99 | stacklevel=2, 100 | ) 101 | self.app = app 102 | 103 | app.extensions = getattr(app, "extensions", {}) 104 | 105 | # Make documents JSON serializable 106 | override_json_encoder(app) 107 | 108 | if "mongoengine" not in app.extensions: 109 | app.extensions["mongoengine"] = {} 110 | 111 | if self in app.extensions["mongoengine"]: 112 | # Raise an exception if extension already initialized as 113 | # potentially new configuration would not be loaded. 114 | raise ValueError("Extension already initialized") 115 | 116 | if config: 117 | # Passed config have max priority, over init config. 118 | self.config = config 119 | 120 | if not self.config: 121 | # If no configs passed, use app.config. 122 | self.config = app.config 123 | 124 | # Obtain db connection(s) 125 | connections = create_connections(self.config) 126 | 127 | # Store objects in application instance so that multiple apps do not 128 | # end up accessing the same objects. 129 | s = {"app": app, "conn": connections} 130 | app.extensions["mongoengine"][self] = s 131 | 132 | @property 133 | def connection(self) -> dict: 134 | """ 135 | Return MongoDB connection(s) associated with this MongoEngine 136 | instance. 137 | """ 138 | return current_app.extensions["mongoengine"][self]["conn"] 139 | 140 | def __getattr__(self, attr_name): 141 | """ 142 | Mongoengine backward compatibility handler. 143 | 144 | Provide original :module:``mongoengine`` module methods/classes if they are not 145 | modified by us, and not mapped directly. 146 | """ 147 | return getattr(mongoengine, attr_name) 148 | -------------------------------------------------------------------------------- /flask_mongoengine/connection.py: -------------------------------------------------------------------------------- 1 | """Module responsible for connection setup.""" 2 | import warnings 3 | from typing import List 4 | 5 | import mongoengine 6 | 7 | __all__ = ( 8 | "create_connections", 9 | "get_connection_settings", 10 | ) 11 | 12 | 13 | def _get_name(setting_name: str) -> str: 14 | """ 15 | Return known pymongo setting name, or lower case name for unknown. 16 | 17 | This problem discovered in issue #451. As mentioned there pymongo settings are not 18 | case-sensitive, but mongoengine use exact name of some settings for matching, 19 | overwriting pymongo behaviour. 20 | 21 | This function address this issue, and potentially address cases when pymongo will 22 | become case-sensitive in some settings by same reasons as mongoengine done. 23 | 24 | Based on pymongo 4.1.1 settings. 25 | """ 26 | KNOWN_CAMEL_CASE_SETTINGS = { 27 | "directconnection": "directConnection", 28 | "maxpoolsize": "maxPoolSize", 29 | "minpoolsize": "minPoolSize", 30 | "maxidletimems": "maxIdleTimeMS", 31 | "maxconnecting": "maxConnecting", 32 | "sockettimeoutms": "socketTimeoutMS", 33 | "connecttimeoutms": "connectTimeoutMS", 34 | "serverselectiontimeoutms": "serverSelectionTimeoutMS", 35 | "waitqueuetimeoutms": "waitQueueTimeoutMS", 36 | "heartbeatfrequencyms": "heartbeatFrequencyMS", 37 | "retrywrites": "retryWrites", 38 | "retryreads": "retryReads", 39 | "zlibcompressionlevel": "zlibCompressionLevel", 40 | "uuidrepresentation": "uuidRepresentation", 41 | "srvservicename": "srvServiceName", 42 | "wtimeoutms": "wTimeoutMS", 43 | "replicaset": "replicaSet", 44 | "readpreference": "readPreference", 45 | "readpreferencetags": "readPreferenceTags", 46 | "maxstalenessseconds": "maxStalenessSeconds", 47 | "authsource": "authSource", 48 | "authmechanism": "authMechanism", 49 | "authmechanismproperties": "authMechanismProperties", 50 | "tlsinsecure": "tlsInsecure", 51 | "tlsallowinvalidcertificates": "tlsAllowInvalidCertificates", 52 | "tlsallowinvalidhostnames": "tlsAllowInvalidHostnames", 53 | "tlscafile": "tlsCAFile", 54 | "tlscertificatekeyfile": "tlsCertificateKeyFile", 55 | "tlscrlfile": "tlsCRLFile", 56 | "tlscertificatekeyfilepassword": "tlsCertificateKeyFilePassword", 57 | "tlsdisableocspendpointcheck": "tlsDisableOCSPEndpointCheck", 58 | "readconcernlevel": "readConcernLevel", 59 | } 60 | _setting_name = KNOWN_CAMEL_CASE_SETTINGS.get(setting_name.lower()) 61 | return setting_name.lower() if _setting_name is None else _setting_name 62 | 63 | 64 | def _sanitize_settings(settings: dict) -> dict: 65 | """Remove ``MONGODB_`` prefix from dict values, to correct bypass to mongoengine.""" 66 | resolved_settings = {} 67 | for k, v in settings.items(): 68 | # Replace with k.lower().removeprefix("mongodb_") when python 3.8 support ends. 69 | key = _get_name(k[8:]) if k.lower().startswith("mongodb_") else _get_name(k) 70 | resolved_settings[key] = v 71 | 72 | return resolved_settings 73 | 74 | 75 | def get_connection_settings(config: dict) -> List[dict]: 76 | """ 77 | Given a config dict, return a sanitized dict of MongoDB connection 78 | settings that we can then use to establish connections. For new 79 | applications, settings should exist in a ``MONGODB_SETTINGS`` key, but 80 | for backward compatibility we also support several config keys 81 | prefixed by ``MONGODB_``, e.g. ``MONGODB_HOST``, ``MONGODB_PORT``, etc. 82 | """ 83 | 84 | # If no "MONGODB_SETTINGS", sanitize the "MONGODB_" keys as single connection. 85 | if "MONGODB_SETTINGS" not in config: 86 | warnings.warn( 87 | ( 88 | "Passing flat configuration is deprecated. Please check " 89 | "http://docs.mongoengine.org/projects/flask-mongoengine/flask_config.html " 90 | "for more info." 91 | ), 92 | DeprecationWarning, 93 | stacklevel=2, 94 | ) 95 | config = {k: v for k, v in config.items() if k.lower().startswith("mongodb_")} 96 | return [_sanitize_settings(config)] 97 | 98 | # Sanitize all the settings living under a "MONGODB_SETTINGS" config var 99 | settings = config["MONGODB_SETTINGS"] 100 | 101 | # If MONGODB_SETTINGS is a list of settings dicts, sanitize each dict separately. 102 | if isinstance(settings, list): 103 | return [_sanitize_settings(settings_dict) for settings_dict in settings] 104 | 105 | # Otherwise, it should be a single dict describing a single connection. 106 | return [_sanitize_settings(settings)] 107 | 108 | 109 | def create_connections(config: dict): 110 | """ 111 | Given Flask application's config dict, extract relevant config vars 112 | out of it and establish MongoEngine connection(s) based on them. 113 | """ 114 | # Validate that the config is a dict and dict is not empty 115 | if not config or not isinstance(config, dict): 116 | raise TypeError(f"Config dictionary expected, but {type(config)} received.") 117 | 118 | # Get sanitized connection settings based on the config 119 | connection_settings = get_connection_settings(config) 120 | 121 | connections = {} 122 | for connection_setting in connection_settings: 123 | alias = connection_setting.setdefault( 124 | "alias", 125 | mongoengine.DEFAULT_CONNECTION_NAME, 126 | ) 127 | connection_setting.setdefault("uuidRepresentation", "standard") 128 | connections[alias] = mongoengine.connect(**connection_setting) 129 | 130 | return connections 131 | -------------------------------------------------------------------------------- /flask_mongoengine/decorators.py: -------------------------------------------------------------------------------- 1 | """Collection of project wide decorators.""" 2 | import functools 3 | import logging 4 | import warnings 5 | 6 | try: 7 | import wtforms # noqa 8 | 9 | wtf_installed = True 10 | except ImportError: # pragma: no cover 11 | wtf_installed = False 12 | 13 | logger = logging.getLogger("flask_mongoengine") 14 | 15 | 16 | def wtf_required(func): 17 | """Special decorator to warn user on incorrect installation.""" 18 | 19 | @functools.wraps(func) 20 | def wrapped(*args, **kwargs): 21 | if not wtf_installed: 22 | logger.error(f"WTForms not installed. Function '{func.__name__}' aborted.") 23 | return None 24 | 25 | return func(*args, **kwargs) 26 | 27 | return wrapped 28 | 29 | 30 | def orm_deprecated(func): 31 | """Warning about usage of deprecated functions, that will be removed in the future.""" 32 | 33 | @functools.wraps(func) 34 | def wrapped(*args, **kwargs): 35 | # TODO: Insert URL 36 | warnings.warn( 37 | ( 38 | f"ORM module and function '{func.__name__}' are deprecated and will be " 39 | "removed in version 3.0.0. Please switch to form generation from full " 40 | "model nesting. Support and bugfixes are not available for stalled code. " 41 | "Please read: " 42 | ), 43 | DeprecationWarning, 44 | stacklevel=2, 45 | ) 46 | 47 | return func(*args, **kwargs) 48 | 49 | return wrapped 50 | -------------------------------------------------------------------------------- /flask_mongoengine/documents.py: -------------------------------------------------------------------------------- 1 | """Extended version of :mod:`mongoengine.document`.""" 2 | import logging 3 | from typing import Dict, List, Optional, Type, Union 4 | 5 | import mongoengine 6 | from flask import abort 7 | from mongoengine.errors import DoesNotExist 8 | from mongoengine.queryset import QuerySet 9 | 10 | from flask_mongoengine.decorators import wtf_required 11 | from flask_mongoengine.pagination import ListFieldPagination, Pagination 12 | 13 | try: 14 | from flask_mongoengine.wtf.models import ModelForm 15 | except ImportError: # pragma: no cover 16 | ModelForm = None 17 | logger = logging.getLogger("flask_mongoengine") 18 | 19 | 20 | class BaseQuerySet(QuerySet): 21 | """Extends :class:`~mongoengine.queryset.QuerySet` class with handly methods.""" 22 | 23 | def _abort_404(self, _message_404): 24 | """Returns 404 error with message, if message provided. 25 | 26 | :param _message_404: Message for 404 comment 27 | """ 28 | abort(404, _message_404) if _message_404 else abort(404) 29 | 30 | def get_or_404(self, *args, _message_404=None, **kwargs): 31 | """Get a document and raise a 404 Not Found error if it doesn't exist. 32 | 33 | :param _message_404: Message for 404 comment, not forwarded to 34 | :func:`~mongoengine.queryset.QuerySet.get` 35 | :param args: args list, silently forwarded to 36 | :func:`~mongoengine.queryset.QuerySet.get` 37 | :param kwargs: keywords arguments, silently forwarded to 38 | :func:`~mongoengine.queryset.QuerySet.get` 39 | """ 40 | try: 41 | return self.get(*args, **kwargs) 42 | except DoesNotExist: 43 | self._abort_404(_message_404) 44 | 45 | def first_or_404(self, _message_404=None): 46 | """ 47 | Same as :func:`~BaseQuerySet.get_or_404`, but uses 48 | :func:`~mongoengine.queryset.QuerySet.first`, not 49 | :func:`~mongoengine.queryset.QuerySet.get`. 50 | 51 | :param _message_404: Message for 404 comment, not forwarded to 52 | :func:`~mongoengine.queryset.QuerySet.get` 53 | """ 54 | return self.first() or self._abort_404(_message_404) 55 | 56 | def paginate(self, page, per_page): 57 | """ 58 | Paginate the QuerySet with a certain number of docs per page 59 | and return docs for a given page. 60 | """ 61 | return Pagination(self, page, per_page) 62 | 63 | def paginate_field(self, field_name, doc_id, page, per_page, total=None): 64 | """ 65 | Paginate items within a list field from one document in the 66 | QuerySet. 67 | """ 68 | # TODO this doesn't sound useful at all - remove in next release? 69 | item = self.get(id=doc_id) 70 | count = getattr(item, f"{field_name}_count", "") 71 | total = total or count or len(getattr(item, field_name)) 72 | return ListFieldPagination( 73 | self, doc_id, field_name, page, per_page, total=total 74 | ) 75 | 76 | 77 | class WtfFormMixin: 78 | """Special mixin, for form generation functions.""" 79 | 80 | @classmethod 81 | def _get_fields_names( 82 | cls: Union["WtfFormMixin", mongoengine.document.BaseDocument], 83 | only: Optional[List[str]], 84 | exclude: Optional[List[str]], 85 | ): 86 | """ 87 | Filter fields names for further form generation. 88 | 89 | :param only: 90 | An optional iterable with the property names that should be included in 91 | the form. Only these properties will have fields. 92 | Fields are always appear in provided order, this allows user to change form 93 | fields ordering, without changing database model. 94 | :param exclude: 95 | An optional iterable with the property names that should be excluded 96 | from the form. All other properties will have fields. 97 | Fields are appears in order, defined in model, excluding provided fields 98 | names. For adjusting fields ordering, use :attr:`only`. 99 | """ 100 | field_names = cls._fields_ordered 101 | 102 | if only: 103 | field_names = [field for field in only if field in field_names] 104 | elif exclude: 105 | field_names = [field for field in field_names if field not in exclude] 106 | 107 | return field_names 108 | 109 | @classmethod 110 | @wtf_required 111 | def to_wtf_form( 112 | cls: Union["WtfFormMixin", mongoengine.document.BaseDocument], 113 | base_class: Type[ModelForm] = ModelForm, 114 | only: Optional[List[str]] = None, 115 | exclude: Optional[List[str]] = None, 116 | fields_kwargs: Optional[Dict[str, Dict]] = None, 117 | ) -> Type[ModelForm]: 118 | """ 119 | Generate WTForm from Document model. 120 | 121 | :param base_class: 122 | Base form class to extend from. Must be a :class:`.ModelForm` subclass. 123 | :param only: 124 | An optional iterable with the property names that should be included in 125 | the form. Only these properties will have fields. 126 | Fields are always appear in provided order, this allows user to change form 127 | fields ordering, without changing database model. 128 | :param exclude: 129 | An optional iterable with the property names that should be excluded 130 | from the form. All other properties will have fields. 131 | Fields are appears in order, defined in model, excluding provided fields 132 | names. For adjusting fields ordering, use :attr:`only`. 133 | :param fields_kwargs: 134 | An optional dictionary of dictionaries, where field names mapping to keyword 135 | arguments used to construct each field object. Has the highest priority over 136 | all fields settings (made in Document field definition). Field options are 137 | directly passed to field generation, so must match WTForm Field keyword 138 | arguments. Support special field keyword option ``wtf_field_class``, that 139 | can be used for complete field class replacement. 140 | 141 | Dictionary format example:: 142 | 143 | dictionary = { 144 | "field_name":{ 145 | "label":"new", 146 | "default": "new", 147 | "wtf_field_class": wtforms.fields.StringField 148 | } 149 | } 150 | 151 | With such dictionary for field with name ``field_name`` 152 | :class:`wtforms.fields.StringField` will be called like:: 153 | 154 | field_name = wtforms.fields.StringField(label="new", default="new") 155 | """ 156 | form_fields_dict = {} 157 | fields_kwargs = fields_kwargs or {} 158 | fields_names = cls._get_fields_names(only, exclude) 159 | 160 | for field_name in fields_names: 161 | # noinspection PyUnresolvedReferences 162 | field_class = cls._fields[field_name] 163 | try: 164 | form_fields_dict[field_name] = field_class.to_wtf_field( 165 | model=cls, 166 | field_kwargs=fields_kwargs.get(field_name, {}), 167 | ) 168 | except (AttributeError, NotImplementedError): 169 | logger.warning( 170 | f"Field {field_name} ignored, field type does not have " 171 | f".to_wtf_field() method or method raised NotImplementedError." 172 | ) 173 | 174 | form_fields_dict["model_class"] = cls 175 | # noinspection PyTypeChecker 176 | return type(f"{cls.__name__}Form", (base_class,), form_fields_dict) 177 | 178 | 179 | class Document(WtfFormMixin, mongoengine.Document): 180 | """Abstract Document with QuerySet and WTForms extra helpers.""" 181 | 182 | meta = {"abstract": True, "queryset_class": BaseQuerySet} 183 | 184 | def paginate_field(self, field_name, page, per_page, total=None): 185 | """Paginate items within a list field.""" 186 | # TODO this doesn't sound useful at all - remove in next release? 187 | count = getattr(self, f"{field_name}_count", "") 188 | total = total or count or len(getattr(self, field_name)) 189 | return ListFieldPagination( 190 | self.__class__.objects, self.pk, field_name, page, per_page, total=total 191 | ) 192 | 193 | 194 | class DynamicDocument(WtfFormMixin, mongoengine.DynamicDocument): 195 | """Abstract DynamicDocument with QuerySet and WTForms extra helpers.""" 196 | 197 | meta = {"abstract": True, "queryset_class": BaseQuerySet} 198 | 199 | 200 | class EmbeddedDocument(WtfFormMixin, mongoengine.EmbeddedDocument): 201 | """Abstract EmbeddedDocument document with extra WTForms helpers.""" 202 | 203 | meta = {"abstract": True} 204 | 205 | 206 | class DynamicEmbeddedDocument(WtfFormMixin, mongoengine.DynamicEmbeddedDocument): 207 | """Abstract DynamicEmbeddedDocument document with extra WTForms helpers.""" 208 | 209 | meta = {"abstract": True} 210 | -------------------------------------------------------------------------------- /flask_mongoengine/json.py: -------------------------------------------------------------------------------- 1 | """Flask application JSON extension functions.""" 2 | from functools import lru_cache 3 | 4 | from bson import DBRef, ObjectId, json_util 5 | from mongoengine.base import BaseDocument 6 | from mongoengine.queryset import QuerySet 7 | from pymongo.command_cursor import CommandCursor 8 | 9 | 10 | @lru_cache(maxsize=1) 11 | def use_json_provider() -> bool: 12 | """Split Flask before 2.2.0 and after, to use/not use JSON provider approach.""" 13 | from flask import __version__ 14 | 15 | version = list(__version__.split(".")) 16 | return int(version[0]) > 2 or (int(version[0]) == 2 and int(version[1]) > 1) 17 | 18 | 19 | # noinspection PyProtectedMember 20 | def _convert_mongo_objects(obj): 21 | """Convert objects, related to Mongo database to JSON.""" 22 | converted = None 23 | if isinstance(obj, BaseDocument): 24 | converted = json_util._json_convert(obj.to_mongo()) 25 | elif isinstance(obj, QuerySet): 26 | converted = json_util._json_convert(obj.as_pymongo()) 27 | elif isinstance(obj, CommandCursor): 28 | converted = json_util._json_convert(obj) 29 | elif isinstance(obj, DBRef): 30 | converted = obj.id 31 | elif isinstance(obj, ObjectId): 32 | converted = obj.__str__() 33 | return converted 34 | 35 | 36 | def _make_encoder(superclass): 37 | """Extend Flask JSON Encoder 'default' method with support of Mongo objects.""" 38 | import warnings 39 | 40 | warnings.warn( 41 | ( 42 | "JSONEncoder/JSONDecoder are deprecated in Flask 2.2 and will be removed " 43 | "in Flask 2.3." 44 | ), 45 | DeprecationWarning, 46 | stacklevel=2, 47 | ) 48 | 49 | class MongoEngineJSONEncoder(superclass): 50 | """ 51 | A JSONEncoder which provides serialization of MongoEngine 52 | documents and queryset objects. 53 | """ 54 | 55 | def default(self, obj): 56 | """Extend JSONEncoder default method, with Mongo objects.""" 57 | if isinstance( 58 | obj, 59 | (BaseDocument, QuerySet, CommandCursor, DBRef, ObjectId), 60 | ): 61 | return _convert_mongo_objects(obj) 62 | return super().default(obj) 63 | 64 | return MongoEngineJSONEncoder 65 | 66 | 67 | def _update_json_provider(superclass): 68 | """Extend Flask Provider 'default' static method with support of Mongo objects.""" 69 | 70 | class MongoEngineJSONProvider(superclass): 71 | """A JSON Provider update for Flask 2.2.0+""" 72 | 73 | @staticmethod 74 | def default(obj): 75 | """Extend JSONProvider default static method, with Mongo objects.""" 76 | if isinstance( 77 | obj, 78 | (BaseDocument, QuerySet, CommandCursor, DBRef, ObjectId), 79 | ): 80 | return _convert_mongo_objects(obj) 81 | return super().default(obj) 82 | 83 | return MongoEngineJSONProvider 84 | 85 | 86 | # Compatibility code for Flask 2.2.0+ support 87 | MongoEngineJSONEncoder = None 88 | MongoEngineJSONProvider = None 89 | 90 | if use_json_provider(): 91 | from flask.json.provider import DefaultJSONProvider 92 | 93 | MongoEngineJSONProvider = _update_json_provider(DefaultJSONProvider) 94 | else: 95 | from flask.json import JSONEncoder 96 | 97 | MongoEngineJSONEncoder = _make_encoder(JSONEncoder) 98 | # End of compatibility code 99 | 100 | 101 | def override_json_encoder(app): 102 | """ 103 | A function to dynamically create a new MongoEngineJSONEncoder class 104 | based upon a custom base class. 105 | This function allows us to combine MongoEngine serialization with 106 | any changes to Flask's JSONEncoder which a user may have made 107 | prior to calling init_app. 108 | 109 | NOTE: This does not cover situations where users override 110 | an instance's json_encoder after calling init_app. 111 | """ 112 | 113 | if use_json_provider(): 114 | app.json_provider_class = _update_json_provider(app.json_provider_class) 115 | app.json = app.json_provider_class(app) 116 | else: 117 | app.json_encoder = _make_encoder(app.json_encoder) 118 | -------------------------------------------------------------------------------- /flask_mongoengine/pagination.py: -------------------------------------------------------------------------------- 1 | """Module responsible for custom pagination.""" 2 | import math 3 | 4 | from flask import abort 5 | from mongoengine.queryset import QuerySet 6 | 7 | __all__ = ("Pagination", "ListFieldPagination") 8 | 9 | 10 | class Pagination(object): 11 | def __init__(self, iterable, page, per_page): 12 | 13 | if page < 1: 14 | abort(404) 15 | 16 | self.iterable = iterable 17 | self.page = page 18 | self.per_page = per_page 19 | 20 | if isinstance(self.iterable, QuerySet): 21 | self.total = iterable.count() 22 | self.items = ( 23 | self.iterable.skip(self.per_page * (self.page - 1)) 24 | .limit(self.per_page) 25 | .select_related() 26 | ) 27 | else: 28 | start_index = (page - 1) * per_page 29 | end_index = page * per_page 30 | 31 | self.total = len(iterable) 32 | self.items = iterable[start_index:end_index] 33 | if not self.items and page != 1: 34 | abort(404) 35 | 36 | @property 37 | def pages(self): 38 | """The total number of pages""" 39 | return int(math.ceil(self.total / float(self.per_page))) 40 | 41 | def prev(self, error_out=False): 42 | """Returns a :class:`Pagination` object for the previous page.""" 43 | assert ( 44 | self.iterable is not None 45 | ), "an object is required for this method to work" 46 | iterable = self.iterable 47 | if isinstance(iterable, QuerySet): 48 | iterable._skip = None 49 | iterable._limit = None 50 | return self.__class__(iterable, self.page - 1, self.per_page) 51 | 52 | @property 53 | def prev_num(self): 54 | """Number of the previous page.""" 55 | return self.page - 1 56 | 57 | @property 58 | def has_prev(self): 59 | """True if a previous page exists""" 60 | return self.page > 1 61 | 62 | def next(self, error_out=False): 63 | """Returns a :class:`Pagination` object for the next page.""" 64 | assert ( 65 | self.iterable is not None 66 | ), "an object is required for this method to work" 67 | iterable = self.iterable 68 | if isinstance(iterable, QuerySet): 69 | iterable._skip = None 70 | iterable._limit = None 71 | return self.__class__(iterable, self.page + 1, self.per_page) 72 | 73 | @property 74 | def has_next(self): 75 | """True if a next page exists.""" 76 | return self.page < self.pages 77 | 78 | @property 79 | def next_num(self): 80 | """Number of the next page""" 81 | return self.page + 1 82 | 83 | def iter_pages(self, left_edge=2, left_current=2, right_current=5, right_edge=2): 84 | """Iterates over the page numbers in the pagination. The four 85 | parameters control the thresholds how many numbers should be produced 86 | from the sides. Skipped page numbers are represented as `None`. 87 | This is how you could render such a pagination in the templates: 88 | 89 | .. sourcecode:: html+jinja 90 | 91 | {% macro render_pagination(pagination, endpoint) %} 92 | 105 | {% endmacro %} 106 | """ 107 | last = 0 108 | for num in range(1, self.pages + 1): 109 | if ( 110 | num <= left_edge 111 | or num > self.pages - right_edge 112 | or ( 113 | num >= self.page - left_current and num <= self.page + right_current 114 | ) 115 | ): 116 | if last + 1 != num: 117 | yield None 118 | yield num 119 | last = num 120 | if last != self.pages: 121 | yield None 122 | 123 | 124 | class ListFieldPagination(Pagination): 125 | def __init__(self, queryset, doc_id, field_name, page, per_page, total=None): 126 | """Allows an array within a document to be paginated. 127 | 128 | Queryset must contain the document which has the array we're 129 | paginating, and doc_id should be it's _id. 130 | Field name is the name of the array we're paginating. 131 | Page and per_page work just like in Pagination. 132 | Total is an argument because it can be computed more efficiently 133 | elsewhere, but we still use array.length as a fallback. 134 | """ 135 | if page < 1: 136 | abort(404) 137 | 138 | self.page = page 139 | self.per_page = per_page 140 | 141 | self.queryset = queryset 142 | self.doc_id = doc_id 143 | self.field_name = field_name 144 | 145 | start_index = (page - 1) * per_page 146 | 147 | field_attrs = {field_name: {"$slice": [start_index, per_page]}} 148 | 149 | qs = queryset(pk=doc_id) 150 | self.items = getattr(qs.fields(**field_attrs).first(), field_name) 151 | self.total = total or len( 152 | getattr(qs.fields(**{field_name: 1}).first(), field_name) 153 | ) 154 | 155 | if not self.items and page != 1: 156 | abort(404) 157 | 158 | def prev(self, error_out=False): 159 | """Returns a :class:`Pagination` object for the previous page.""" 160 | assert ( 161 | self.items is not None 162 | ), "a query object is required for this method to work" 163 | return self.__class__( 164 | self.queryset, 165 | self.doc_id, 166 | self.field_name, 167 | self.page - 1, 168 | self.per_page, 169 | self.total, 170 | ) 171 | 172 | def next(self, error_out=False): 173 | """Returns a :class:`Pagination` object for the next page.""" 174 | assert ( 175 | self.items is not None 176 | ), "a query object is required for this method to work" 177 | return self.__class__( 178 | self.queryset, 179 | self.doc_id, 180 | self.field_name, 181 | self.page + 1, 182 | self.per_page, 183 | self.total, 184 | ) 185 | -------------------------------------------------------------------------------- /flask_mongoengine/panels.py: -------------------------------------------------------------------------------- 1 | """Debug panel views and logic and related mongoDb event listeners.""" 2 | __all__ = ["mongo_command_logger", "MongoDebugPanel"] 3 | import logging 4 | import sys 5 | from dataclasses import dataclass 6 | from typing import List, Union 7 | 8 | from flask import current_app 9 | from flask_debugtoolbar.panels import DebugPanel 10 | from jinja2 import ChoiceLoader, PackageLoader 11 | from pymongo import monitoring 12 | 13 | logger = logging.getLogger("flask_mongoengine") 14 | 15 | 16 | @dataclass 17 | class RawQueryEvent: 18 | # noinspection PyUnresolvedReferences 19 | """Responsible for parsing monitoring events to web panel interface. 20 | 21 | :param _event: Succeeded or Failed event object from pymongo monitoring. 22 | :param _start_event: Started event object from pymongo monitoring. 23 | :param _is_query_pass: Boolean status of db query reported by pymongo monitoring. 24 | """ 25 | 26 | _event: Union[monitoring.CommandSucceededEvent, monitoring.CommandFailedEvent] 27 | _start_event: monitoring.CommandStartedEvent 28 | _is_query_pass: bool 29 | 30 | @property 31 | def time(self): 32 | """Query execution time.""" 33 | return self._event.duration_micros * 0.001 34 | 35 | @property 36 | def size(self): 37 | """Query object size.""" 38 | return sys.getsizeof(self.server_response, 0) / 1024 39 | 40 | @property 41 | def database(self): 42 | """Query database target.""" 43 | return self._start_event.database_name 44 | 45 | @property 46 | def collection(self): 47 | """Query collection target.""" 48 | return self.server_command.get(self.command_name) 49 | 50 | @property 51 | def command_name(self): 52 | """Query db level operation/command name.""" 53 | return self._event.command_name 54 | 55 | @property 56 | def operation_id(self): 57 | """MongoDb operation_id used to match 'start' and 'final' monitoring events.""" 58 | return self._start_event.operation_id 59 | 60 | @property 61 | def server_command(self): 62 | """Raw MongoDb command send to server.""" 63 | return self._start_event.command 64 | 65 | @property 66 | def server_response(self): 67 | """Raw MongoDb response received from server.""" 68 | return self._event.reply if self._is_query_pass else self._event.failure 69 | 70 | @property 71 | def request_status(self): 72 | """Query execution status.""" 73 | return "Succeed" if self._is_query_pass else "Failed" 74 | 75 | 76 | class MongoCommandLogger(monitoring.CommandListener): 77 | """Receive point for :class:`~.pymongo.monitoring.CommandListener` events. 78 | 79 | Count and parse incoming events for display in debug panel. 80 | """ 81 | 82 | def __init__(self): 83 | self.total_time: float = 0 84 | self.started_operations_count: int = 0 85 | self.succeeded_operations_count: int = 0 86 | self.failed_operations_count: int = 0 87 | self.queries: List[RawQueryEvent] = [] 88 | self.started_events: dict = {} 89 | 90 | def append_raw_query(self, event, request_status): 91 | """Pass 'unknown' events to parser and include final result to final list.""" 92 | self.total_time += event.duration_micros 93 | start_event = self.started_events.pop(event.operation_id, {}) 94 | self.queries.append(RawQueryEvent(event, start_event, request_status)) 95 | logger.debug(f"Added record to 'Unknown' section: {self.queries[-1]}") 96 | 97 | def failed(self, event): 98 | """Receives 'failed' events. Required to track database answer to request.""" 99 | logger.debug(f"Received 'Failed' event from driver: {event}") 100 | self.failed_operations_count += 1 101 | self.append_raw_query(event, False) 102 | 103 | def reset_tracker(self): 104 | """Resets all counters to default, keeping instance itself the same.""" 105 | self.__class__.__init__(self) 106 | 107 | def started(self, event): 108 | """Receives 'started' events. Required to track original request context.""" 109 | logger.debug(f"Received 'Started' event from driver: {event}") 110 | self.started_operations_count += 1 111 | self.started_events[event.operation_id] = event 112 | 113 | def succeeded(self, event): 114 | """Receives 'succeeded' events. Required to track database answer to request.""" 115 | logger.debug(f"Received 'Succeeded' event from driver: {event}") 116 | self.succeeded_operations_count += 1 117 | self.append_raw_query(event, True) 118 | 119 | 120 | mongo_command_logger = MongoCommandLogger() 121 | 122 | 123 | def _maybe_patch_jinja_loader(jinja_env): 124 | """Extend jinja_env loader with flask_mongoengine templates folder.""" 125 | package_loader = PackageLoader("flask_mongoengine", "templates") 126 | if not isinstance(jinja_env.loader, ChoiceLoader): 127 | jinja_env.loader = ChoiceLoader([jinja_env.loader, package_loader]) 128 | elif package_loader not in jinja_env.loader.loaders: 129 | jinja_env.loader.loaders += [package_loader] 130 | 131 | 132 | class MongoDebugPanel(DebugPanel): 133 | """Panel that shows information about MongoDB operations.""" 134 | 135 | config_error_message = ( 136 | "Pymongo monitoring configuration error. mongo_command_logger should be " 137 | "registered before database connection." 138 | ) 139 | name = "MongoDB" 140 | has_content = True 141 | 142 | def __init__(self, *args, **kwargs): 143 | super().__init__(*args, **kwargs) 144 | _maybe_patch_jinja_loader(self.jinja_env) 145 | 146 | @property 147 | def _context(self) -> dict: 148 | """Context for rendering, as property for easy testing.""" 149 | return { 150 | "queries": mongo_command_logger.queries, 151 | "slow_query_limit": current_app.config.get( 152 | "MONGO_DEBUG_PANEL_SLOW_QUERY_LIMIT", 100 153 | ), 154 | } 155 | 156 | @property 157 | def is_properly_configured(self) -> bool: 158 | """Checks that all required watchers registered before Flask application init.""" 159 | # noinspection PyProtectedMember 160 | if mongo_command_logger not in monitoring._LISTENERS.command_listeners: 161 | logger.error(self.config_error_message) 162 | return False 163 | return True 164 | 165 | def process_request(self, request): 166 | """Resets logger stats between each request.""" 167 | mongo_command_logger.reset_tracker() 168 | 169 | def nav_title(self) -> str: 170 | """Debug toolbar in the bottom right corner.""" 171 | return self.name 172 | 173 | def nav_subtitle(self) -> str: 174 | """Count operations total time.""" 175 | if not self.is_properly_configured: 176 | self.has_content = False 177 | return self.config_error_message 178 | 179 | return ( 180 | f"{mongo_command_logger.started_operations_count} operations, " 181 | f"in {mongo_command_logger.total_time * 0.001: .2f}ms" 182 | ) 183 | 184 | def title(self) -> str: 185 | """Title for 'opened' debug panel window.""" 186 | return "MongoDB Operations" 187 | 188 | def url(self) -> str: 189 | """Placeholder for internal URLs.""" 190 | return "" 191 | 192 | def content(self): 193 | """Gathers all template required variables in one dict.""" 194 | return self.render("panels/mongo-panel.html", self._context) 195 | -------------------------------------------------------------------------------- /flask_mongoengine/sessions.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from datetime import datetime, timedelta 3 | 4 | from bson.tz_util import utc 5 | from flask.sessions import SessionInterface, SessionMixin 6 | from werkzeug.datastructures import CallbackDict 7 | 8 | __all__ = ("MongoEngineSession", "MongoEngineSessionInterface") 9 | 10 | 11 | class MongoEngineSession(CallbackDict, SessionMixin): 12 | def __init__(self, initial=None, sid=None): 13 | def on_update(self): 14 | self.modified = True 15 | 16 | CallbackDict.__init__(self, initial, on_update) 17 | self.sid = sid 18 | self.modified = False 19 | 20 | 21 | class MongoEngineSessionInterface(SessionInterface): 22 | """SessionInterface for mongoengine""" 23 | 24 | def __init__(self, db, collection="session"): 25 | """ 26 | The MongoSessionInterface 27 | 28 | :param db: The app's db eg: MongoEngine() 29 | :param collection: The session collection name defaults to "session" 30 | """ 31 | 32 | if not isinstance(collection, str): 33 | raise ValueError("Collection argument should be string") 34 | 35 | class DBSession(db.Document): 36 | sid = db.StringField(primary_key=True) 37 | data = db.DictField() 38 | expiration = db.DateTimeField() 39 | meta = { 40 | "allow_inheritance": False, 41 | "collection": collection, 42 | "indexes": [ 43 | { 44 | "fields": ["expiration"], 45 | "expireAfterSeconds": 60 * 60 * 24 * 7 * 31, 46 | } 47 | ], 48 | } 49 | 50 | self.cls = DBSession 51 | 52 | def get_expiration_time(self, app, session) -> timedelta: 53 | if session.permanent: 54 | return app.permanent_session_lifetime 55 | # Fallback to 1 day session ttl, if SESSION_TTL not set. 56 | return timedelta(**app.config.get("SESSION_TTL", {"days": 1})) 57 | 58 | def open_session(self, app, request): 59 | sid = request.cookies.get(app.session_cookie_name) 60 | if sid: 61 | stored_session = self.cls.objects(sid=sid).first() 62 | 63 | if stored_session: 64 | expiration = stored_session.expiration 65 | 66 | if not expiration.tzinfo: 67 | expiration = expiration.replace(tzinfo=utc) 68 | 69 | if expiration > datetime.utcnow().replace(tzinfo=utc): 70 | return MongoEngineSession( 71 | initial=stored_session.data, sid=stored_session.sid 72 | ) 73 | 74 | return MongoEngineSession(sid=str(uuid.uuid4())) 75 | 76 | def save_session(self, app, session, response): 77 | domain = self.get_cookie_domain(app) 78 | httponly = self.get_cookie_httponly(app) 79 | 80 | # If the session is modified to be empty, remove the cookie. 81 | # If the session is empty, return without setting the cookie. 82 | if not session: 83 | if session.modified: 84 | response.delete_cookie(app.session_cookie_name, domain=domain) 85 | return 86 | 87 | expiration = datetime.utcnow().replace(tzinfo=utc) + self.get_expiration_time( 88 | app, session 89 | ) 90 | 91 | if session.modified: 92 | self.cls(sid=session.sid, data=session, expiration=expiration).save() 93 | 94 | response.set_cookie( 95 | app.session_cookie_name, 96 | session.sid, 97 | expires=expiration, 98 | httponly=httponly, 99 | domain=domain, 100 | ) 101 | -------------------------------------------------------------------------------- /flask_mongoengine/templates/panels/mongo-panel.html: -------------------------------------------------------------------------------- 1 | 17 | 18 | {% macro render_stats(title, queries, slow_query_limit) %} 19 | 20 |

{{ title }}

21 | {% if queries %} 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | {% for query in queries %} 38 | 39 | {% set colspan = 9 %} 40 | 43 | 44 | 45 | 46 | 47 | 48 | 52 | 56 | 57 | 58 | 59 | 61 | 64 | 65 | 66 | 68 | 71 | 72 | 73 | {% endfor %} 74 | 75 |
Time (ms)SizeDatabaseCollectionCommand nameOperation idServer commandResponse dataRequest status
slow_query_limit %}style="color:red;" {% endif %}> 41 | {{ query.time|round(3) }} 42 | {{ query.size|round(2) }}Kb{{ query.database }}{{ query.collection }}{{ query.command_name }}{{ query.operation_id }} 49 | Toggle 51 | 53 | Toggle 55 | {{ query.request_status }}
62 |
{{ query.server_command|pprint }}
63 |
69 |
{{ query.server_response|pprint }}
70 |
76 | {% else %} 77 |

No {{ title|lower }} recorded

78 | {% endif %} 79 | {% endmacro %} 80 | 81 | {{ render_stats("Queries", queries, slow_query_limit) }} 82 | 83 | 94 | -------------------------------------------------------------------------------- /flask_mongoengine/wtf/__init__.py: -------------------------------------------------------------------------------- 1 | """WTFForms integration module init file.""" 2 | from flask_mongoengine.wtf.orm import model_fields, model_form # noqa 3 | -------------------------------------------------------------------------------- /flask_mongoengine/wtf/models.py: -------------------------------------------------------------------------------- 1 | from typing import Type, Union 2 | 3 | import mongoengine 4 | from flask_wtf import FlaskForm 5 | from flask_wtf.form import _Auto 6 | 7 | 8 | class ModelForm(FlaskForm): 9 | """A WTForms mongoengine model form""" 10 | 11 | model_class: Type[Union[mongoengine.Document, mongoengine.DynamicDocument]] 12 | 13 | def __init__(self, formdata=_Auto, **kwargs): 14 | self.instance = kwargs.pop("instance", None) or kwargs.get("obj") 15 | if self.instance and not formdata: 16 | kwargs["obj"] = self.instance 17 | self.formdata = formdata 18 | super(ModelForm, self).__init__(formdata, **kwargs) 19 | 20 | def save(self, commit=True, **kwargs): 21 | if not self.instance: 22 | self.instance = self.model_class() 23 | self.populate_obj(self.instance) 24 | 25 | if commit: 26 | self.instance.save(**kwargs) 27 | return self.instance 28 | -------------------------------------------------------------------------------- /flask_mongoengine/wtf/orm.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tools for generating forms based on mongoengine Document schemas. 3 | """ 4 | import decimal 5 | from collections import OrderedDict 6 | from typing import List, Optional, Type 7 | 8 | from bson import ObjectId 9 | from mongoengine import ReferenceField 10 | from mongoengine.base import BaseDocument, DocumentMetaclass 11 | from wtforms import fields as f 12 | from wtforms import validators 13 | 14 | from flask_mongoengine.decorators import orm_deprecated 15 | from flask_mongoengine.wtf.fields import ( 16 | BinaryField, 17 | DictField, 18 | ModelSelectField, 19 | ModelSelectMultipleField, 20 | NoneStringField, 21 | ) 22 | from flask_mongoengine.wtf.models import ModelForm 23 | 24 | __all__ = ( 25 | "model_fields", 26 | "model_form", 27 | ) 28 | 29 | 30 | @orm_deprecated 31 | def converts(*args): 32 | def _inner(func): 33 | func._converter_for = frozenset(args) 34 | return func 35 | 36 | return _inner 37 | 38 | 39 | class ModelConverter(object): 40 | @orm_deprecated 41 | def __init__(self, converters=None): 42 | if not converters: 43 | converters = {} 44 | 45 | for name in dir(self): 46 | obj = getattr(self, name) 47 | if hasattr(obj, "_converter_for"): 48 | for classname in obj._converter_for: 49 | converters[classname] = obj 50 | 51 | self.converters = converters 52 | 53 | @orm_deprecated 54 | def _generate_convert_base_kwargs(self, field, field_args) -> dict: 55 | kwargs: dict = { 56 | "label": getattr(field, "verbose_name", field.name), 57 | "description": getattr(field, "help_text", None) or "", 58 | "validators": getattr(field, "wtf_validators", None) 59 | or getattr(field, "validators", None) 60 | or [], 61 | "filters": getattr(field, "wtf_filters", None) 62 | or getattr(field, "filters", None) 63 | or [], 64 | "default": field.default, 65 | } 66 | if field_args: 67 | kwargs.update(field_args) 68 | 69 | # Create a copy of the lists since we will be modifying it, and if 70 | # validators set as shared list between fields - duplicates/conflicts may 71 | # be created. 72 | kwargs["validators"] = list(kwargs["validators"]) 73 | kwargs["filters"] = list(kwargs["filters"]) 74 | if field.required: 75 | kwargs["validators"].append(validators.InputRequired()) 76 | else: 77 | kwargs["validators"].append(validators.Optional()) 78 | 79 | return kwargs 80 | 81 | @orm_deprecated 82 | def _process_convert_for_choice_fields(self, field, field_class, kwargs): 83 | kwargs["choices"] = field.choices 84 | kwargs["coerce"] = self.coerce(field_class) 85 | if kwargs.pop("multiple", False): 86 | return f.SelectMultipleField(**kwargs) 87 | if kwargs.pop("radio", False): 88 | return f.RadioField(**kwargs) 89 | return f.SelectField(**kwargs) 90 | 91 | @orm_deprecated 92 | def convert(self, model, field, field_args): 93 | field_class = type(field).__name__ 94 | 95 | if field_class not in self.converters: 96 | return None 97 | 98 | kwargs = self._generate_convert_base_kwargs(field, field_args) 99 | 100 | if field.choices: 101 | return self._process_convert_for_choice_fields(field, field_class, kwargs) 102 | 103 | if hasattr(field, "field") and isinstance(field.field, ReferenceField): 104 | kwargs["label_modifier"] = getattr( 105 | model, f"{field.name}_label_modifier", None 106 | ) 107 | 108 | return self.converters[field_class](model, field, kwargs) 109 | 110 | @classmethod 111 | def _string_common(cls, model, field, kwargs): 112 | if field.max_length or field.min_length: 113 | kwargs["validators"].append( 114 | validators.Length( 115 | max=field.max_length or -1, min=field.min_length or -1 116 | ) 117 | ) 118 | 119 | @classmethod 120 | def _number_common(cls, model, field, kwargs): 121 | if field.max_value or field.min_value: 122 | kwargs["validators"].append( 123 | validators.NumberRange(max=field.max_value, min=field.min_value) 124 | ) 125 | 126 | @converts("StringField") 127 | def conv_String(self, model, field, kwargs): 128 | if field.regex: 129 | kwargs["validators"].append(validators.Regexp(regex=field.regex)) 130 | self._string_common(model, field, kwargs) 131 | password_field = kwargs.pop("password", False) 132 | textarea_field = kwargs.pop("textarea", False) or not field.max_length 133 | if password_field: 134 | return f.PasswordField(**kwargs) 135 | if textarea_field: 136 | return f.TextAreaField(**kwargs) 137 | return f.StringField(**kwargs) 138 | 139 | @converts("URLField") 140 | def conv_URL(self, model, field, kwargs): 141 | kwargs["validators"].append(validators.URL()) 142 | self._string_common(model, field, kwargs) 143 | return NoneStringField(**kwargs) 144 | 145 | @converts("EmailField") 146 | def conv_Email(self, model, field, kwargs): 147 | kwargs["validators"].append(validators.Email()) 148 | self._string_common(model, field, kwargs) 149 | return NoneStringField(**kwargs) 150 | 151 | @converts("IntField") 152 | def conv_Int(self, model, field, kwargs): 153 | self._number_common(model, field, kwargs) 154 | return f.IntegerField(**kwargs) 155 | 156 | @converts("FloatField") 157 | def conv_Float(self, model, field, kwargs): 158 | self._number_common(model, field, kwargs) 159 | return f.FloatField(**kwargs) 160 | 161 | @converts("DecimalField") 162 | def conv_Decimal(self, model, field, kwargs): 163 | self._number_common(model, field, kwargs) 164 | kwargs["places"] = getattr(field, "precision", None) 165 | return f.DecimalField(**kwargs) 166 | 167 | @converts("BooleanField") 168 | def conv_Boolean(self, model, field, kwargs): 169 | return f.BooleanField(**kwargs) 170 | 171 | @converts("DateTimeField") 172 | def conv_DateTime(self, model, field, kwargs): 173 | return f.DateTimeField(**kwargs) 174 | 175 | @converts("DateField") 176 | def conv_Date(self, model, field, kwargs): 177 | return f.DateField(**kwargs) 178 | 179 | @converts("BinaryField") 180 | def conv_Binary(self, model, field, kwargs): 181 | # TODO: may be set file field that will save file`s data to MongoDB 182 | if field.max_bytes: 183 | kwargs["validators"].append(validators.Length(max=field.max_bytes)) 184 | return BinaryField(**kwargs) 185 | 186 | @converts("DictField") 187 | def conv_Dict(self, model, field, kwargs): 188 | return DictField(**kwargs) 189 | 190 | @converts("ListField") 191 | def conv_List(self, model, field, kwargs): 192 | if isinstance(field.field, ReferenceField): 193 | return ModelSelectMultipleField(model=field.field.document_type, **kwargs) 194 | if field.field.choices: 195 | kwargs["multiple"] = True 196 | return self.convert(model, field.field, kwargs) 197 | field_args = kwargs.pop("field_args", {}) 198 | unbound_field = self.convert(model, field.field, field_args) 199 | unacceptable = { 200 | "validators": [], 201 | "filters": [], 202 | "min_entries": kwargs.get("min_entries", 0), 203 | } 204 | kwargs.update(unacceptable) 205 | return f.FieldList(unbound_field, **kwargs) 206 | 207 | @converts("SortedListField") 208 | def conv_SortedList(self, model, field, kwargs): 209 | # TODO: sort functionality, may be need sortable widget 210 | return self.conv_List(model, field, kwargs) 211 | 212 | @converts("GeoLocationField") 213 | def conv_GeoLocation(self, model, field, kwargs): 214 | # TODO: create geo field and widget (also GoogleMaps) 215 | return 216 | 217 | @converts("ObjectIdField") 218 | def conv_ObjectId(self, model, field, kwargs): 219 | return 220 | 221 | @converts("EmbeddedDocumentField") 222 | def conv_EmbeddedDocument(self, model, field, kwargs): 223 | kwargs = { 224 | "validators": [], 225 | "filters": [], 226 | "default": field.default or field.document_type_obj, 227 | } 228 | form_class = model_form(field.document_type_obj, field_args={}) 229 | return f.FormField(form_class, **kwargs) 230 | 231 | @converts("ReferenceField") 232 | def conv_Reference(self, model, field, kwargs): 233 | return ModelSelectField(model=field.document_type, **kwargs) 234 | 235 | @converts("GenericReferenceField") 236 | def conv_GenericReference(self, model, field, kwargs): 237 | return 238 | 239 | @converts("FileField") 240 | def conv_File(self, model, field, kwargs): 241 | return f.FileField(**kwargs) 242 | 243 | @orm_deprecated 244 | def coerce(self, field_type): 245 | coercions = { 246 | "IntField": int, 247 | "BooleanField": bool, 248 | "FloatField": float, 249 | "DecimalField": decimal.Decimal, 250 | "ObjectIdField": ObjectId, 251 | } 252 | return coercions.get(field_type, str) 253 | 254 | 255 | @orm_deprecated 256 | def _get_fields_names( 257 | model, 258 | only: Optional[List[str]], 259 | exclude: Optional[List[str]], 260 | ) -> List[str]: 261 | """ 262 | Filter fields names for further form generation. 263 | 264 | :param model: Source model class for fields list retrieval 265 | :param only: If provided, only these field names will have fields definition. 266 | :param exclude: If provided, field names will be excluded from fields definition. 267 | All other field names will have fields. 268 | """ 269 | field_names = model._fields_ordered 270 | 271 | if only: 272 | field_names = [field for field in only if field in field_names] 273 | elif exclude: 274 | field_names = [field for field in field_names if field not in set(exclude)] 275 | 276 | return field_names 277 | 278 | 279 | @orm_deprecated 280 | def model_fields( 281 | model: Type[BaseDocument], 282 | only: Optional[List[str]] = None, 283 | exclude: Optional[List[str]] = None, 284 | field_args=None, 285 | converter=None, 286 | ) -> OrderedDict: 287 | """ 288 | Generate a dictionary of fields for a given database model. 289 | 290 | See :func:`model_form` docstring for description of parameters. 291 | """ 292 | if not issubclass(model, (BaseDocument, DocumentMetaclass)): 293 | raise TypeError("model must be a mongoengine Document schema") 294 | 295 | converter = converter or ModelConverter() 296 | field_args = field_args or {} 297 | form_fields_dict = OrderedDict() 298 | # noinspection PyTypeChecker 299 | fields_names = _get_fields_names(model, only, exclude) 300 | 301 | for field_name in fields_names: 302 | # noinspection PyUnresolvedReferences 303 | model_field = model._fields[field_name] 304 | form_field = converter.convert(model, model_field, field_args.get(field_name)) 305 | if form_field is not None: 306 | form_fields_dict[field_name] = form_field 307 | 308 | return form_fields_dict 309 | 310 | 311 | @orm_deprecated 312 | def model_form( 313 | model: Type[BaseDocument], 314 | base_class: Type[ModelForm] = ModelForm, 315 | only: Optional[List[str]] = None, 316 | exclude: Optional[List[str]] = None, 317 | field_args=None, 318 | converter=None, 319 | ) -> Type[ModelForm]: 320 | """ 321 | Create a wtforms Form for a given mongoengine Document schema:: 322 | 323 | from flask_mongoengine.wtf import model_form 324 | from myproject.myapp.schemas import Article 325 | ArticleForm = model_form(Article) 326 | 327 | :param model: 328 | A mongoengine Document schema class 329 | :param base_class: 330 | Base form class to extend from. Must be a :class:`.ModelForm` subclass. 331 | :param only: 332 | An optional iterable with the property names that should be included in 333 | the form. Only these properties will have fields. 334 | Fields are always appear in provided order, this allows user to change form 335 | fields ordering, without changing database model. 336 | :param exclude: 337 | An optional iterable with the property names that should be excluded 338 | from the form. All other properties will have fields. 339 | Fields are appears in order, defined in model, excluding provided fields 340 | names. For adjusting fields ordering, use :attr:`only`. 341 | :param field_args: 342 | An optional dictionary of field names mapping to keyword arguments used 343 | to construct each field object. 344 | :param converter: 345 | A converter to generate the fields based on the model properties. If 346 | not set, :class:`.ModelConverter` is used. 347 | """ 348 | field_dict = model_fields(model, only, exclude, field_args, converter) 349 | field_dict["model_class"] = model 350 | # noinspection PyTypeChecker 351 | return type(f"{model.__name__}Form", (base_class,), field_dict) 352 | -------------------------------------------------------------------------------- /noxfile.py: -------------------------------------------------------------------------------- 1 | """Nox tool configuration file. 2 | 3 | Nox is Tox tool replacement. 4 | """ 5 | import shutil 6 | from pathlib import Path 7 | 8 | import nox 9 | 10 | nox.options.sessions = "latest", "lint", "documentation_tests" 11 | db_version = "5.0" 12 | 13 | 14 | def base_install(session, flask, mongoengine, toolbar, wtf): 15 | """Create basic environment setup for tests and linting.""" 16 | session.run("python", "-m", "pip", "install", "--upgrade", "pip") 17 | session.run("python", "-m", "pip", "install", "setuptools_scm[toml]>=6.3.1") 18 | 19 | if toolbar and wtf: 20 | extra = "wtf,toolbar," 21 | elif toolbar: 22 | extra = "toolbar," 23 | elif wtf: 24 | extra = "wtf," 25 | else: 26 | extra = "" 27 | 28 | if flask == "==1.1.4": 29 | session.install( 30 | f"Flask{flask}", 31 | f"mongoengine{mongoengine}", 32 | "-e", 33 | f".[{extra}legacy,legacy-dev]", 34 | ) 35 | else: 36 | session.install( 37 | f"Flask{flask}", 38 | f"mongoengine{mongoengine}", 39 | "-e", 40 | f".[{extra}dev]", 41 | ) 42 | return session 43 | 44 | 45 | @nox.session(python="3.7") 46 | def lint(session): 47 | """Run linting check locally.""" 48 | session.install("pre-commit") 49 | session.run("pre-commit", "run", "-a") 50 | 51 | 52 | @nox.session(python=["3.7", "3.8", "3.9", "3.10", "pypy3.7"]) 53 | @nox.parametrize("flask", ["==1.1.4", "==2.0.3", ">=2.1.2"]) 54 | @nox.parametrize("mongoengine", ["==0.21.0", "==0.22.1", "==0.23.1", ">=0.24.1"]) 55 | @nox.parametrize("toolbar", [True, False]) 56 | @nox.parametrize("wtf", [True, False]) 57 | def ci_cd_tests(session, flask, mongoengine, toolbar, wtf): 58 | """Run test suite with pytest into ci_cd (no docker).""" 59 | session = base_install(session, flask, mongoengine, toolbar, wtf) 60 | session.run("pytest", *session.posargs) 61 | 62 | 63 | def _run_in_docker(session): 64 | session.run( 65 | "docker", 66 | "run", 67 | "--name", 68 | "nox_docker_test", 69 | "-p", 70 | "27017:27017", 71 | "-d", 72 | f"mongo:{db_version}", 73 | external=True, 74 | ) 75 | try: 76 | session.run("pytest", *session.posargs) 77 | finally: 78 | session.run_always("docker", "rm", "-fv", "nox_docker_test", external=True) 79 | 80 | 81 | @nox.session(python=["3.7", "3.8", "3.9", "3.10", "pypy3.7"]) 82 | @nox.parametrize("flask", ["==1.1.4", "==2.0.3", ">=2.1.2"]) 83 | @nox.parametrize("mongoengine", ["==0.21.0", "==0.22.1", "==0.23.1", ">=0.24.1"]) 84 | @nox.parametrize("toolbar", [True, False]) 85 | @nox.parametrize("wtf", [True, False]) 86 | def full_tests(session, flask, mongoengine, toolbar, wtf): 87 | """Run tests locally with docker and complete support matrix.""" 88 | session = base_install(session, flask, mongoengine, toolbar, wtf) 89 | _run_in_docker(session) 90 | 91 | 92 | @nox.session(python=["3.7", "3.8", "3.9", "3.10", "pypy3.7"]) 93 | @nox.parametrize("toolbar", [True, False]) 94 | @nox.parametrize("wtf", [True, False]) 95 | def latest(session, toolbar, wtf): 96 | """Run minimum tests for checking minimum code quality.""" 97 | flask = ">=2.1.2" 98 | mongoengine = ">=0.24.1" 99 | session = base_install(session, flask, mongoengine, toolbar, wtf) 100 | if session.interactive: 101 | _run_in_docker(session) 102 | else: 103 | session.run("pytest", *session.posargs) 104 | 105 | 106 | @nox.session(python="3.10") 107 | def documentation_tests(session): 108 | """Run documentation tests.""" 109 | return docs(session, batch_run=True) 110 | 111 | 112 | @nox.session(python="3.10") 113 | def docs(session, batch_run: bool = False): 114 | """Build the documentation or serve documentation interactively.""" 115 | shutil.rmtree(Path("docs").joinpath("_build"), ignore_errors=True) 116 | session.install("-r", "docs/requirements.txt") 117 | session.install("-e", ".[wtf,toolbar]") 118 | session.cd("docs") 119 | sphinx_args = ["-b", "html", "-W", ".", "_build/html"] 120 | 121 | if not session.interactive or batch_run: 122 | sphinx_cmd = "sphinx-build" 123 | else: 124 | sphinx_cmd = "sphinx-autobuild" 125 | sphinx_args.extend( 126 | [ 127 | "--open-browser", 128 | "--port", 129 | "9812", 130 | "--watch", 131 | "../*.md", 132 | "--watch", 133 | "../*.rst", 134 | "--watch", 135 | "../*.py", 136 | "--watch", 137 | "../flask_mongoengine", 138 | ] 139 | ) 140 | 141 | session.run(sphinx_cmd, *sphinx_args) 142 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "flask-mongoengine" 3 | description = "Flask extension that provides integration with MongoEngine and WTF model forms." 4 | readme = "README.md" 5 | requires-python = ">=3.7" 6 | license = {text = "BSD 3-Clause License"} 7 | classifiers = [ 8 | "Development Status :: 4 - Beta", 9 | "Environment :: Web Environment", 10 | "Intended Audience :: Developers", 11 | "License :: OSI Approved :: BSD License", 12 | "Operating System :: OS Independent", 13 | "Programming Language :: Python", 14 | "Programming Language :: Python :: 3 :: Only", 15 | "Programming Language :: Python :: 3", 16 | "Programming Language :: Python :: 3.7", 17 | "Programming Language :: Python :: 3.8", 18 | "Programming Language :: Python :: 3.9", 19 | "Programming Language :: Python :: 3.10", 20 | "Programming Language :: Python :: Implementation :: PyPy", 21 | "Programming Language :: Python :: Implementation :: CPython", 22 | "Topic :: Internet :: WWW/HTTP :: Dynamic Content", 23 | "Topic :: Software Development :: Libraries :: Python Modules", 24 | "Framework :: Flask", 25 | ] 26 | dependencies = [ 27 | "Flask>=1.1.4", 28 | "mongoengine>=0.21", 29 | 'importlib-metadata; python_version<"3.8"', 30 | ] 31 | keywords = [ 32 | "flask", 33 | "wtf", 34 | "wtf-forms", 35 | "forms", 36 | "mongo", 37 | "pymongo", 38 | "mongoengine", 39 | "extension" 40 | ] 41 | authors = [ 42 | {name = "Ross Lawley", email = "ross.lawley@gmail.com"} 43 | ] 44 | maintainers = [ 45 | {name = "Andrey Shpak", email = "ashpak@ashpak.ru"} 46 | ] 47 | dynamic = ["version"] 48 | 49 | [project.optional-dependencies] 50 | wtf = ["WTForms[email]>=3.0.0", "Flask-WTF>=0.14.3"] 51 | toolbar = ["Flask-DebugToolbar>=0.11.0"] 52 | dev = [ 53 | "black==22.6.0", 54 | "pre-commit", 55 | "pytest", 56 | "pytest-cov", 57 | "pytest-mock", 58 | "nox", 59 | "Pillow>=7.0.0", 60 | "blinker", 61 | ] 62 | legacy = ["MarkupSafe==2.0.1"] 63 | legacy-dev = [ 64 | "pytest", 65 | "pytest-cov", 66 | "pytest-mock", 67 | "Pillow>=7.0.0", 68 | "blinker", 69 | ] 70 | 71 | [project.urls] 72 | Homepage = "https://github.com/MongoEngine/flask-mongoengine" 73 | Documentation = "http://docs.mongoengine.org/projects/flask-mongoengine/en/latest/" 74 | Repository = "https://github.com/MongoEngine/flask-mongoengine" 75 | Changelog = "https://github.com/MongoEngine/flask-mongoengine/releases" 76 | 77 | [build-system] 78 | requires = [ 79 | "setuptools>=45", 80 | "setuptools_scm[toml]>=6.3.1", 81 | "wheel" 82 | ] 83 | build-backend = "setuptools.build_meta" 84 | 85 | [tool.setuptools] 86 | zip-safe = false 87 | platforms = ["any"] 88 | packages=["flask_mongoengine", "flask_mongoengine.wtf"] 89 | 90 | [tool.setuptools.dynamic] 91 | version = {attr = "flask_mongoengine._version.version"} 92 | 93 | [tool.setuptools_scm] 94 | write_to = "flask_mongoengine/_version.py" 95 | 96 | [tool.black] 97 | line-length = 88 98 | target-version = ['py37'] 99 | exclude = ''' 100 | /( 101 | \.eggs 102 | | \.git 103 | | \.tox 104 | | \.venv 105 | | \.vscode 106 | | docs 107 | | _build 108 | | buck-out 109 | | build 110 | | dist 111 | )/ 112 | ''' 113 | 114 | [tool.isort] 115 | profile = "black" 116 | line_length = 88 117 | multi_line_output = 3 118 | include_trailing_comma = true 119 | force_grid_wrap = 0 120 | use_parentheses = true 121 | 122 | [tool.pytest.ini_options] 123 | addopts = "--cov=flask_mongoengine --cov-config=setup.cfg" 124 | testpaths = ["tests"] 125 | filterwarnings = [ 126 | "error", 127 | "ignore::ResourceWarning", 128 | "ignore::DeprecationWarning:flask_mongoengine", 129 | "ignore::DeprecationWarning:tests", 130 | ] 131 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore=E501,F403,F405,I201,W503,E203 3 | max-line-length=90 4 | exclude=build,dist,docs,examples,venv,.tox,.eggs,.nox 5 | max-complexity=17 6 | 7 | [report] 8 | sort = Cover 9 | # Regexes for lines to exclude from consideration 10 | exclude_lines = 11 | # Have to re-enable the standard pragma 12 | pragma: no cover 13 | 14 | # Don't complain about missing debug-only code: 15 | def __repr__ 16 | def __str__ 17 | if self\.debug 18 | if settings\.DEBUG: 19 | 20 | # Don't complain if tests don't hit defensive assertion code: 21 | raise AssertionError 22 | raise NotImplementedError 23 | 24 | # Don't complain if non-runnable code isn't run: 25 | if 0: 26 | if __name__ == .__main__.: 27 | 28 | ignore_errors = True 29 | show_missing = True 30 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """Setup file for backward compatibility with setuptools, not supported PEP 660.""" 2 | from setuptools import setup 3 | 4 | setup() 5 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MongoEngine/flask-mongoengine/d4526139cb1e2e94111ab7de96bb629d574c1690/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import NoReturn 3 | 4 | import mongoengine 5 | import pytest 6 | from flask import Flask 7 | from pymongo import MongoClient 8 | 9 | from flask_mongoengine import MongoEngine 10 | 11 | 12 | @pytest.fixture(autouse=True, scope="session") 13 | def session_clean_up() -> NoReturn: 14 | """Mandatory tests environment clean up before/after test session.""" 15 | client = MongoClient("localhost", 27017) 16 | client.drop_database("flask_mongoengine_test_db") 17 | client.drop_database("flask_mongoengine_test_db_1") 18 | client.drop_database("flask_mongoengine_test_db_2") 19 | 20 | yield 21 | 22 | client.drop_database("flask_mongoengine_test_db") 23 | client.drop_database("flask_mongoengine_test_db_1") 24 | client.drop_database("flask_mongoengine_test_db_2") 25 | 26 | 27 | @pytest.fixture() 28 | def app() -> Flask: 29 | app = Flask(__name__) 30 | app.config["TESTING"] = True 31 | app.config["WTF_CSRF_ENABLED"] = False 32 | 33 | with app.app_context(): 34 | yield app 35 | 36 | mongoengine.connection.disconnect_all() 37 | 38 | 39 | @pytest.fixture() 40 | def db(app) -> MongoEngine: 41 | app.config["MONGODB_SETTINGS"] = [ 42 | { 43 | "db": "flask_mongoengine_test_db", 44 | "host": "localhost", 45 | "port": 27017, 46 | "alias": "default", 47 | "uuidRepresentation": "standard", 48 | } 49 | ] 50 | test_db = MongoEngine(app) 51 | db_name = ( 52 | test_db.connection["default"].get_database("flask_mongoengine_test_db").name 53 | ) 54 | 55 | if not db_name.endswith("_test_db"): 56 | raise RuntimeError( 57 | f"DATABASE_URL must point to testing db, not to master db ({db_name})" 58 | ) 59 | 60 | # Clear database before tests, for cases when some test failed before. 61 | test_db.connection["default"].drop_database(db_name) 62 | 63 | yield test_db 64 | 65 | # Clear database after tests, for graceful exit. 66 | test_db.connection["default"].drop_database(db_name) 67 | 68 | 69 | @pytest.fixture() 70 | def todo(db): 71 | class Todo(db.Document): 72 | title = mongoengine.StringField(max_length=60) 73 | text = mongoengine.StringField() 74 | done = mongoengine.BooleanField(default=False) 75 | pub_date = mongoengine.DateTimeField(default=datetime.utcnow) 76 | comments = mongoengine.ListField(mongoengine.StringField()) 77 | comment_count = mongoengine.IntField() 78 | 79 | return Todo 80 | -------------------------------------------------------------------------------- /tests/test_base.py: -------------------------------------------------------------------------------- 1 | """Tests for base MongoEngine class.""" 2 | import pytest 3 | 4 | from flask_mongoengine import MongoEngine 5 | 6 | 7 | def test_mongoengine_class__should_raise_type_error__if_config_not_dict(): 8 | """MongoEngine will handle None values, but will pass anything else as app.""" 9 | input_value = "Not dict type" 10 | with pytest.raises(TypeError) as error: 11 | MongoEngine(input_value) 12 | assert str(error.value) == "Invalid Flask application instance" 13 | 14 | 15 | @pytest.mark.parametrize("input_value", [None, "Not dict type"]) 16 | def test_init_app__should_raise_type_error__if_config_not_dict(input_value): 17 | db = MongoEngine() 18 | with pytest.raises(TypeError) as error: 19 | db.init_app(input_value) 20 | assert str(error.value) == "Invalid Flask application instance" 21 | -------------------------------------------------------------------------------- /tests/test_basic_app.py: -------------------------------------------------------------------------------- 1 | import flask 2 | import pytest 3 | from bson import ObjectId 4 | 5 | 6 | @pytest.fixture(autouse=True) 7 | def setup_endpoints(app, todo): 8 | Todo = todo 9 | 10 | @app.route("/") 11 | def index(): 12 | return "\n".join(x.title for x in Todo.objects) 13 | 14 | @app.route("/add", methods=["POST"]) 15 | def add(): 16 | form = flask.request.form 17 | todo = Todo(title=form["title"], text=form["text"]) 18 | todo.save() 19 | return "added" 20 | 21 | @app.route("/show//") 22 | def show(id): 23 | todo = Todo.objects.get_or_404(id=id) 24 | return "\n".join([todo.title, todo.text]) 25 | 26 | 27 | def test_with_id(app, todo): 28 | Todo = todo 29 | client = app.test_client() 30 | response = client.get(f"/show/{ObjectId()}/") 31 | assert response.status_code == 404 32 | 33 | client.post("/add", data={"title": "First Item", "text": "The text"}) 34 | 35 | response = client.get(f"/show/{Todo.objects.first_or_404().id}/") 36 | assert response.status_code == 200 37 | assert response.data.decode("utf-8") == "First Item\nThe text" 38 | 39 | 40 | def test_basic_insert(app): 41 | client = app.test_client() 42 | client.post("/add", data={"title": "First Item", "text": "The text"}) 43 | client.post("/add", data={"title": "2nd Item", "text": "The text"}) 44 | response = client.get("/") 45 | assert response.data.decode("utf-8") == "First Item\n2nd Item" 46 | -------------------------------------------------------------------------------- /tests/test_connection.py: -------------------------------------------------------------------------------- 1 | import mongoengine 2 | import pytest 3 | from mongoengine.connection import ConnectionFailure 4 | from mongoengine.context_managers import switch_db 5 | from pymongo.database import Database 6 | from pymongo.errors import InvalidURI 7 | from pymongo.mongo_client import MongoClient 8 | from pymongo.read_preferences import ReadPreference 9 | 10 | from flask_mongoengine import MongoEngine, current_mongoengine_instance 11 | 12 | 13 | def is_mongo_mock_installed() -> bool: 14 | try: 15 | import mongomock.__version__ # noqa 16 | except ImportError: 17 | return False 18 | return True 19 | 20 | 21 | def test_connection__should_use_defaults__if_no_settings_provided(app): 22 | """Make sure a simple connection to a standalone MongoDB works.""" 23 | db = MongoEngine() 24 | 25 | # Verify no extension for Mongoengine yet created for app 26 | assert app.extensions == {} 27 | assert current_mongoengine_instance() is None 28 | 29 | # Create db connection. Should return None. 30 | assert db.init_app(app) is None 31 | 32 | # Verify db added to Flask extensions. 33 | assert current_mongoengine_instance() == db 34 | 35 | # Verify db settings passed to pymongo driver. 36 | # Default mongoengine db is 'default', default Flask-Mongoengine db is 'test'. 37 | connection = mongoengine.get_connection() 38 | mongo_engine_db = mongoengine.get_db() 39 | assert isinstance(mongo_engine_db, Database) 40 | assert isinstance(connection, MongoClient) 41 | assert mongo_engine_db.name == "test" 42 | assert connection.HOST == "localhost" 43 | assert connection.PORT == 27017 44 | 45 | 46 | @pytest.mark.parametrize( 47 | ("config_extension"), 48 | [ 49 | { 50 | "MONGODB_SETTINGS": { 51 | "ALIAS": "simple_conn", 52 | "HOST": "localhost", 53 | "PORT": 27017, 54 | "DB": "flask_mongoengine_test_db", 55 | } 56 | }, 57 | { 58 | "MONGODB_HOST": "localhost", 59 | "MONGODB_PORT": 27017, 60 | "MONGODB_DB": "flask_mongoengine_test_db", 61 | "MONGODB_ALIAS": "simple_conn", 62 | }, 63 | ], 64 | ids=("Dict format", "Config variable format"), 65 | ) 66 | def test_connection__should_pass_alias__if_provided(app, config_extension): 67 | """Make sure a simple connection pass ALIAS setting variable.""" 68 | db = MongoEngine() 69 | app.config.update(config_extension) 70 | 71 | # Verify no extension for Mongoengine yet created for app 72 | assert app.extensions == {} 73 | assert current_mongoengine_instance() is None 74 | 75 | # Create db connection. Should return None. 76 | assert db.init_app(app) is None 77 | 78 | # Verify db added to Flask extensions. 79 | assert current_mongoengine_instance() == db 80 | 81 | # Verify db settings passed to pymongo driver. 82 | # ALIAS is used to find correct connection. 83 | # As we do not use default alias, default call to mongoengine.get_connection 84 | # should raise. 85 | with pytest.raises(ConnectionFailure): 86 | mongoengine.get_connection() 87 | 88 | connection = mongoengine.get_connection("simple_conn") 89 | mongo_engine_db = mongoengine.get_db("simple_conn") 90 | assert isinstance(mongo_engine_db, Database) 91 | assert isinstance(connection, MongoClient) 92 | assert mongo_engine_db.name == "flask_mongoengine_test_db" 93 | assert connection.HOST == "localhost" 94 | assert connection.PORT == 27017 95 | 96 | 97 | @pytest.mark.parametrize( 98 | ("config_extension"), 99 | [ 100 | { 101 | "MONGODB_SETTINGS": { 102 | "HOST": "mongodb://localhost:27017/flask_mongoengine_test_db" 103 | } 104 | }, 105 | { 106 | "MONGODB_HOST": "mongodb://localhost:27017/flask_mongoengine_test_db", 107 | "MONGODB_PORT": 27017, 108 | "MONGODB_DB": "should_ignore_it", 109 | }, 110 | ], 111 | ids=("Dict format", "Config variable format"), 112 | ) 113 | def test_connection__should_parse_host_uri__if_host_formatted_as_uri( 114 | app, config_extension 115 | ): 116 | """Make sure a simple connection pass ALIAS setting variable.""" 117 | db = MongoEngine() 118 | app.config.update(config_extension) 119 | 120 | # Verify no extension for Mongoengine yet created for app 121 | assert app.extensions == {} 122 | assert current_mongoengine_instance() is None 123 | 124 | # Create db connection. Should return None. 125 | assert db.init_app(app) is None 126 | 127 | # Verify db added to Flask extensions. 128 | assert current_mongoengine_instance() == db 129 | 130 | connection = mongoengine.get_connection() 131 | mongo_engine_db = mongoengine.get_db() 132 | assert isinstance(mongo_engine_db, Database) 133 | assert isinstance(connection, MongoClient) 134 | assert mongo_engine_db.name == "flask_mongoengine_test_db" 135 | assert connection.HOST == "localhost" 136 | assert connection.PORT == 27017 137 | 138 | 139 | @pytest.mark.skipif( 140 | is_mongo_mock_installed(), reason="This test require mongomock not exist" 141 | ) 142 | @pytest.mark.parametrize( 143 | ("config_extension"), 144 | [ 145 | { 146 | "MONGODB_SETTINGS": { 147 | "HOST": "mongomock://localhost:27017/flask_mongoengine_test_db" 148 | } 149 | }, 150 | { 151 | "MONGODB_SETTINGS": { 152 | "ALIAS": "simple_conn", 153 | "HOST": "localhost", 154 | "PORT": 27017, 155 | "DB": "flask_mongoengine_test_db", 156 | "IS_MOCK": True, 157 | } 158 | }, 159 | {"MONGODB_HOST": "mongomock://localhost:27017/flask_mongoengine_test_db"}, 160 | ], 161 | ids=("Dict format as URI", "Dict format as Param", "Config variable format as URI"), 162 | ) 163 | def test_connection__should_parse_mongo_mock_uri__as_uri_and_as_settings( 164 | app, config_extension 165 | ): 166 | """Make sure a simple connection pass ALIAS setting variable.""" 167 | db = MongoEngine() 168 | app.config.update(config_extension) 169 | 170 | # Verify no extension for Mongoengine yet created for app 171 | assert app.extensions == {} 172 | assert current_mongoengine_instance() is None 173 | 174 | # Create db connection. Should return None. 175 | 176 | with pytest.raises(RuntimeError) as error: 177 | assert db.init_app(app) is None 178 | 179 | assert str(error.value) == "You need mongomock installed to mock MongoEngine." 180 | 181 | 182 | @pytest.mark.parametrize( 183 | ("config_extension"), 184 | [ 185 | { 186 | "MONGODB_SETTINGS": { 187 | "HOST": "postgre://localhost:27017/flask_mongoengine_test_db" 188 | } 189 | }, 190 | {"MONGODB_HOST": "mysql://localhost:27017/flask_mongoengine_test_db"}, 191 | ], 192 | ids=("Dict format as URI", "Config variable format as URI"), 193 | ) 194 | def test_connection__should_raise__if_uri_not_properly_formatted(app, config_extension): 195 | """Make sure a simple connection pass ALIAS setting variable.""" 196 | db = MongoEngine() 197 | app.config.update(config_extension) 198 | 199 | # Verify no extension for Mongoengine yet created for app 200 | assert app.extensions == {} 201 | assert current_mongoengine_instance() is None 202 | 203 | # Create db connection. Should return None. 204 | 205 | with pytest.raises(InvalidURI) as error: 206 | assert db.init_app(app) is None 207 | 208 | assert ( 209 | str(error.value) 210 | == "Invalid URI scheme: URI must begin with 'mongodb://' or 'mongodb+srv://'" 211 | ) 212 | 213 | 214 | def test_connection__should_accept_host_as_list(app): 215 | """Make sure MONGODB_HOST can be a list hosts.""" 216 | db = MongoEngine() 217 | app.config["MONGODB_SETTINGS"] = { 218 | "ALIAS": "host_list", 219 | "HOST": ["localhost:27017"], 220 | "DB": "flask_mongoengine_list_test_db", 221 | } 222 | db.init_app(app) 223 | 224 | connection = mongoengine.get_connection("host_list") 225 | mongo_engine_db = mongoengine.get_db("host_list") 226 | assert isinstance(mongo_engine_db, Database) 227 | assert isinstance(connection, MongoClient) 228 | assert mongo_engine_db.name == "flask_mongoengine_list_test_db" 229 | assert connection.HOST == "localhost" 230 | assert connection.PORT == 27017 231 | 232 | 233 | def test_multiple_connections(app): 234 | """Make sure establishing multiple connections to a standalone 235 | MongoDB and switching between them works. 236 | """ 237 | db = MongoEngine() 238 | app.config["MONGODB_SETTINGS"] = [ 239 | { 240 | "ALIAS": "default", 241 | "DB": "flask_mongoengine_test_db_1", 242 | "HOST": "localhost", 243 | "PORT": 27017, 244 | }, 245 | { 246 | "ALIAS": "alternative", 247 | "DB": "flask_mongoengine_test_db_2", 248 | "HOST": "localhost", 249 | "PORT": 27017, 250 | }, 251 | ] 252 | 253 | class Todo(db.Document): 254 | title = db.StringField(max_length=60) 255 | 256 | db.init_app(app) 257 | # Drop default collection from init 258 | Todo.drop_collection() 259 | Todo.meta = {"db_alias": "alternative"} 260 | # Drop 'alternative' collection initiated early. 261 | Todo.drop_collection() 262 | 263 | # Make sure init correct and both databases are clean 264 | with switch_db(Todo, "default") as Todo: 265 | doc = Todo.objects().first() 266 | assert doc is None 267 | 268 | with switch_db(Todo, "alternative") as Todo: 269 | doc = Todo.objects().first() 270 | assert doc is None 271 | 272 | # Test saving a doc via the default connection 273 | with switch_db(Todo, "default") as Todo: 274 | todo = Todo() 275 | todo.text = "Sample" 276 | todo.title = "Testing" 277 | todo.done = True 278 | s_todo = todo.save() 279 | 280 | f_to = Todo.objects().first() 281 | assert s_todo.title == f_to.title 282 | 283 | # Make sure the doc still doesn't exist in the alternative db 284 | with switch_db(Todo, "alternative") as Todo: 285 | doc = Todo.objects().first() 286 | assert doc is None 287 | 288 | # Make sure switching back to the default connection shows the doc 289 | with switch_db(Todo, "default") as Todo: 290 | doc = Todo.objects().first() 291 | assert doc is not None 292 | 293 | 294 | def test_incorrect_value_with_mongodb_prefix__should_trigger_mongoengine_raise(app): 295 | db = MongoEngine() 296 | app.config["MONGODB_HOST"] = "mongodb://localhost:27017/flask_mongoengine_test_db" 297 | # Invalid host, should trigger exception if used 298 | app.config["MONGODB_TEST_HOST"] = "dummy://localhost:27017/test" 299 | with pytest.raises(ConnectionFailure): 300 | db.init_app(app) 301 | 302 | 303 | def test_connection_kwargs(app): 304 | """Make sure additional connection kwargs work.""" 305 | 306 | app.config["MONGODB_SETTINGS"] = { 307 | "ALIAS": "tz_aware_true", 308 | "DB": "flask_mongoengine_test_db", 309 | "TZ_AWARE": True, 310 | "READ_PREFERENCE": ReadPreference.SECONDARY, 311 | "MAXPOOLSIZE": 10, 312 | } 313 | db = MongoEngine(app) 314 | 315 | assert db.connection["tz_aware_true"].codec_options.tz_aware 316 | assert db.connection["tz_aware_true"].read_preference == ReadPreference.SECONDARY 317 | -------------------------------------------------------------------------------- /tests/test_db_fields_import_protection.py: -------------------------------------------------------------------------------- 1 | """Testing independency from WTForms.""" 2 | import pytest 3 | 4 | try: 5 | import wtforms 6 | 7 | wtforms_not_installed = False 8 | except ImportError: 9 | wtforms = None 10 | wtforms_not_installed = True 11 | 12 | 13 | class TestImportProtection: 14 | def test__when_wtforms_available__import_use_its_data(self): 15 | from flask_mongoengine import db_fields 16 | 17 | assert db_fields is not None 18 | 19 | def test__core_class_imported_without_error(self): 20 | from flask_mongoengine import MongoEngine 21 | 22 | db = MongoEngine() 23 | assert db is not None 24 | 25 | @pytest.mark.skipif(condition=wtforms_not_installed, reason="No WTF CI/CD chain") 26 | def test__wtf_required_decorator__when_wtf_installed(self): 27 | from flask_mongoengine.decorators import wtf_required 28 | 29 | @wtf_required 30 | def foo(a, b=None): 31 | """Temp function.""" 32 | return a + b 33 | 34 | assert foo(1, 1) == 2 35 | 36 | def test__wtf_required_decorator__when_wtf_not_installed(self, caplog, monkeypatch): 37 | monkeypatch.setattr("flask_mongoengine.decorators.wtf_installed", False) 38 | from flask_mongoengine.decorators import wtf_required 39 | 40 | @wtf_required 41 | def foo(a, b=None): 42 | """Temp function.""" 43 | return a + b 44 | 45 | x = foo(1, 1) 46 | assert x is None 47 | assert caplog.messages[0] == "WTForms not installed. Function 'foo' aborted." 48 | -------------------------------------------------------------------------------- /tests/test_debug_panel.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for ``MongoDebugPanel`` and related mongo events listener. 3 | 4 | - Independent of global configuration by design. 5 | """ 6 | import contextlib 7 | 8 | import jinja2 9 | import pymongo 10 | import pytest 11 | from flask import Flask 12 | 13 | flask_debugtoolbar = pytest.importorskip("flask_debugtoolbar") 14 | 15 | from flask_debugtoolbar import DebugToolbarExtension # noqa 16 | from flask_debugtoolbar.panels import DebugPanel # noqa 17 | from jinja2 import ChoiceLoader, DictLoader # noqa 18 | from pymongo import monitoring # noqa 19 | from pymongo.errors import OperationFailure # noqa 20 | from pytest_mock import MockerFixture # noqa 21 | 22 | from flask_mongoengine.panels import ( # noqa 23 | MongoCommandLogger, 24 | MongoDebugPanel, 25 | _maybe_patch_jinja_loader, 26 | mongo_command_logger, 27 | ) 28 | 29 | 30 | @pytest.fixture() 31 | def app_no_mongo_monitoring() -> Flask: 32 | app = Flask(__name__) 33 | app.config["TESTING"] = True 34 | app.config["WTF_CSRF_ENABLED"] = False 35 | app.config["SECRET_KEY"] = "flask+mongoengine=<3" 36 | app.debug = True 37 | app.config["DEBUG_TB_PANELS"] = ("flask_mongoengine.panels.MongoDebugPanel",) 38 | app.config["DEBUG_TB_INTERCEPT_REDIRECTS"] = False 39 | DebugToolbarExtension(app) 40 | with app.app_context(): 41 | yield app 42 | 43 | 44 | @pytest.fixture(autouse=True) 45 | def registered_monitoring() -> MongoCommandLogger: 46 | """Register/Unregister mongo_command_logger in required tests""" 47 | monitoring.register(mongo_command_logger) 48 | mongo_command_logger.reset_tracker() 49 | yield mongo_command_logger 50 | # Unregister listener between independent tests 51 | mongo_command_logger.reset_tracker() 52 | with contextlib.suppress(ValueError): 53 | monitoring._LISTENERS.command_listeners.remove(mongo_command_logger) 54 | 55 | 56 | def test__maybe_patch_jinja_loader__replace_loader_when_initial_loader_is_not_choice_loader(): 57 | jinja2_env = jinja2.Environment() 58 | assert not isinstance(jinja2_env.loader, ChoiceLoader) 59 | _maybe_patch_jinja_loader(jinja2_env) 60 | assert isinstance(jinja2_env.loader, ChoiceLoader) 61 | 62 | 63 | def test__maybe_patch_jinja_loader__extend_loader_when_initial_loader_is_choice_loader(): 64 | jinja2_env = jinja2.Environment(loader=ChoiceLoader([DictLoader({"1": "1"})])) 65 | assert isinstance(jinja2_env.loader, ChoiceLoader) 66 | assert len(jinja2_env.loader.loaders) == 1 67 | _maybe_patch_jinja_loader(jinja2_env) 68 | assert len(jinja2_env.loader.loaders) == 2 69 | 70 | 71 | class TestMongoDebugPanel: 72 | """Trivial tests to highlight any unexpected changes in namings or code.""" 73 | 74 | @pytest.fixture 75 | def toolbar_with_no_flask(self) -> MongoDebugPanel: 76 | """Simple instance of MongoDebugPanel without flask application""" 77 | jinja2_env = jinja2.Environment() 78 | return MongoDebugPanel(jinja2_env) 79 | 80 | def test__panel_name_and_static_properties_not_modified( 81 | self, toolbar_with_no_flask 82 | ): 83 | assert toolbar_with_no_flask.name == "MongoDB" 84 | assert toolbar_with_no_flask.title() == "MongoDB Operations" 85 | assert toolbar_with_no_flask.nav_title() == "MongoDB" 86 | assert toolbar_with_no_flask.url() == "" 87 | 88 | def test__config_error_message_not_modified(self, toolbar_with_no_flask): 89 | assert toolbar_with_no_flask.config_error_message == ( 90 | "Pymongo monitoring configuration error. mongo_command_logger should be " 91 | "registered before database connection." 92 | ) 93 | 94 | def test__is_properly_configured__return_false__when_mongo_command_logger__not_registered( 95 | self, toolbar_with_no_flask, caplog 96 | ): 97 | """Also verify logging done.""" 98 | # Emulate not working state(against auto used registered_monitoring fixture) 99 | mongo_command_logger.reset_tracker() 100 | monitoring._LISTENERS.command_listeners.remove(mongo_command_logger) 101 | assert toolbar_with_no_flask.is_properly_configured is False 102 | assert caplog.messages[0] == toolbar_with_no_flask.config_error_message 103 | 104 | def test__is_properly_configured__return_true__when_mongo_command_logger_registered( 105 | self, 106 | toolbar_with_no_flask, 107 | ): 108 | assert toolbar_with_no_flask.is_properly_configured is True 109 | 110 | def test__nav_subtitle__return_config_error_message__when_toolbar_incorrectly_configured( 111 | self, toolbar_with_no_flask 112 | ): 113 | """Also check that content flag changed.""" 114 | # Emulate not working state(against auto used registered_monitoring fixture) 115 | mongo_command_logger.reset_tracker() 116 | monitoring._LISTENERS.command_listeners.remove(mongo_command_logger) 117 | # First line is before nav_subtitle() call 118 | assert toolbar_with_no_flask.has_content is True 119 | 120 | assert ( 121 | toolbar_with_no_flask.nav_subtitle() 122 | == toolbar_with_no_flask.config_error_message 123 | ) 124 | assert toolbar_with_no_flask.has_content is False 125 | 126 | def test__nav_subtitle__return_correct_message_when_toolbar_correctly_configured( 127 | self, 128 | toolbar_with_no_flask, 129 | ): 130 | assert toolbar_with_no_flask.nav_subtitle() == "0 operations, in 0.00ms" 131 | assert toolbar_with_no_flask.has_content is True 132 | 133 | def test__context__is_empty_by_default(self, app, toolbar_with_no_flask): 134 | assert toolbar_with_no_flask._context == { 135 | "queries": [], 136 | "slow_query_limit": 100, 137 | } 138 | 139 | def test__process_request__correctly_resets_monitoring__without_instance_replace( 140 | self, 141 | registered_monitoring, 142 | app, 143 | toolbar_with_no_flask, 144 | ): 145 | # sourcery skip: simplify-empty-collection-comparison 146 | # Test setup 147 | initial_id = id(registered_monitoring) 148 | # Inject some fakes to monitoring engine 149 | registered_monitoring.total_time = 1 150 | registered_monitoring.started_operations_count = 1 151 | registered_monitoring.succeeded_operations_count = 1 152 | registered_monitoring.failed_operations_count = 1 153 | registered_monitoring.queries = [1, 2] 154 | registered_monitoring.started_events = {1: 1, 2: 2} 155 | 156 | # Pre-test validation 157 | assert registered_monitoring.total_time == 1 158 | assert registered_monitoring.started_operations_count == 1 159 | assert registered_monitoring.succeeded_operations_count == 1 160 | assert registered_monitoring.failed_operations_count == 1 161 | assert registered_monitoring.queries == [1, 2] 162 | assert registered_monitoring.started_events == {1: 1, 2: 2} 163 | toolbar_with_no_flask.process_request(None) 164 | 165 | assert id(registered_monitoring) == initial_id 166 | assert registered_monitoring.total_time == 0 167 | assert registered_monitoring.started_operations_count == 0 168 | assert registered_monitoring.succeeded_operations_count == 0 169 | assert registered_monitoring.failed_operations_count == 0 170 | assert registered_monitoring.queries == [] 171 | assert registered_monitoring.started_events == {} 172 | 173 | def test__content__calls_parent__render__function( 174 | self, 175 | app, 176 | toolbar_with_no_flask, 177 | mocker: MockerFixture, 178 | ): 179 | spy = mocker.patch.object(DebugPanel, "render", autospec=True) 180 | toolbar_with_no_flask.content() 181 | spy.assert_called_with( 182 | toolbar_with_no_flask, 183 | "panels/mongo-panel.html", 184 | toolbar_with_no_flask._context, 185 | ) 186 | 187 | 188 | class TestMongoCommandLogger: 189 | """By design tested with raw pymongo.""" 190 | 191 | @pytest.fixture(autouse=True) 192 | def py_db(self, registered_monitoring) -> pymongo.MongoClient: 193 | """Clean up and returns special database for testing on pymongo driver level""" 194 | client = pymongo.MongoClient("localhost", 27017) 195 | db = client.pymongo_test_database 196 | client.drop_database(db) 197 | registered_monitoring.reset_tracker() 198 | yield db 199 | client.drop_database(db) 200 | 201 | def test__normal_command__logged(self, py_db, registered_monitoring): 202 | post = { 203 | "author": "Mike", 204 | "text": "My first blog post!", 205 | "tags": ["mongodb", "python", "pymongo"], 206 | } 207 | 208 | py_db.posts.insert_one(post) 209 | assert registered_monitoring.started_operations_count == 1 210 | assert registered_monitoring.succeeded_operations_count == 1 211 | assert registered_monitoring.queries[0].time >= 0 212 | assert registered_monitoring.queries[0].size >= 0 213 | assert registered_monitoring.queries[0].database == "pymongo_test_database" 214 | assert registered_monitoring.queries[0].collection == "posts" 215 | assert registered_monitoring.queries[0].command_name == "insert" 216 | assert isinstance(registered_monitoring.queries[0].operation_id, int) 217 | assert len(registered_monitoring.queries[0].server_command["documents"]) == 1 218 | assert registered_monitoring.queries[0].server_response == {"n": 1, "ok": 1.0} 219 | assert registered_monitoring.queries[0].request_status == "Succeed" 220 | 221 | def test__failed_command_logged__logged(self, py_db, registered_monitoring): 222 | """Failed command index 1 in provided test.""" 223 | pymongo.collection.Collection(py_db, "test", create=True) 224 | with contextlib.suppress(OperationFailure): 225 | pymongo.collection.Collection(py_db, "test", create=True) 226 | assert registered_monitoring.started_operations_count == 2 227 | assert registered_monitoring.succeeded_operations_count == 1 228 | assert registered_monitoring.failed_operations_count == 1 229 | assert registered_monitoring.queries[0].time >= 0 230 | assert registered_monitoring.queries[0].size >= 0 231 | assert registered_monitoring.queries[0].database == "pymongo_test_database" 232 | assert registered_monitoring.queries[0].collection == "test" 233 | assert registered_monitoring.queries[0].command_name == "create" 234 | assert isinstance(registered_monitoring.queries[0].operation_id, int) 235 | assert registered_monitoring.queries[0].server_command["create"] == "test" 236 | assert registered_monitoring.queries[0].server_response == {"ok": 1.0} 237 | assert registered_monitoring.queries[0].request_status == "Succeed" 238 | assert registered_monitoring.queries[1].time >= 0 239 | assert registered_monitoring.queries[1].size >= 0 240 | assert registered_monitoring.queries[1].database == "pymongo_test_database" 241 | assert registered_monitoring.queries[1].collection == "test" 242 | assert registered_monitoring.queries[1].command_name == "create" 243 | assert isinstance(registered_monitoring.queries[1].operation_id, int) 244 | assert registered_monitoring.queries[1].server_command["create"] == "test" 245 | assert ( 246 | "already exists" 247 | in registered_monitoring.queries[1].server_response["errmsg"] 248 | ) 249 | assert registered_monitoring.queries[1].request_status == "Failed" 250 | -------------------------------------------------------------------------------- /tests/test_decorators.py: -------------------------------------------------------------------------------- 1 | """Tests for project wide decorators.""" 2 | from flask_mongoengine.decorators import orm_deprecated 3 | 4 | 5 | def test__orm_deprecated(recwarn): 6 | @orm_deprecated 7 | def func(a, b): 8 | """Function example.""" 9 | return a + b 10 | 11 | assert func(1, 1) == 2 12 | assert str(recwarn.list[0].message) == ( 13 | "ORM module and function 'func' are deprecated and will be removed in version 3.0.0. " 14 | "Please switch to form generation from full model nesting. " 15 | "Support and bugfixes are not available for stalled code. " 16 | "Please read: " 17 | ) 18 | -------------------------------------------------------------------------------- /tests/test_forms_v2.py: -------------------------------------------------------------------------------- 1 | """Integration tests for new WTForms generation in Flask-Mongoengine 2.0.""" 2 | import json 3 | 4 | import pytest 5 | from bson.json_util import RELAXED_JSON_OPTIONS 6 | from markupsafe import Markup 7 | from werkzeug.datastructures import MultiDict 8 | 9 | wtforms = pytest.importorskip("wtforms") 10 | from wtforms import fields as wtf_fields # noqa 11 | from wtforms import widgets as wtf_widgets # noqa 12 | 13 | from flask_mongoengine.wtf import fields as mongo_fields # noqa 14 | 15 | 16 | @pytest.fixture() 17 | def local_app(app): 18 | """Helper fixture to minimize code indentation.""" 19 | with app.test_request_context("/"): 20 | yield app 21 | 22 | 23 | @pytest.mark.parametrize( 24 | ["value", "expected_value"], 25 | [ 26 | ("", None), 27 | ("none", None), 28 | ("nOne", None), 29 | ("None", None), 30 | ("null", None), 31 | (None, None), 32 | ("no", False), 33 | ("N", False), 34 | ("n", False), 35 | ("false", False), 36 | ("False", False), 37 | (False, False), 38 | ("yes", True), 39 | ("y", True), 40 | ("true", True), 41 | (True, True), 42 | ], 43 | ) 44 | def test_coerce_boolean__return_correct_value(value, expected_value): 45 | assert mongo_fields.coerce_boolean(value) == expected_value 46 | 47 | 48 | def test_coerce_boolean__raise_on_unexpected_value(): 49 | with pytest.raises(ValueError) as error: 50 | mongo_fields.coerce_boolean("some") 51 | assert str(error.value) == "Unexpected string value." 52 | 53 | 54 | def test__full_document_form__does_not_create_any_unexpected_data_in_database(db): 55 | """ 56 | Test to ensure that we are following own promise in documentation, read: 57 | http://docs.mongoengine.org/projects/flask-mongoengine/en/latest/migration_to_v2.html#empty-fields-are-not-created-in-database 58 | """ 59 | 60 | class Todo(db.Document): 61 | """Test model for AllFieldsModel.""" 62 | 63 | string = db.StringField() 64 | 65 | class Embedded(db.EmbeddedDocument): 66 | """Test embedded for AllFieldsModel.""" 67 | 68 | string = db.StringField() 69 | 70 | class AllFieldsModel(db.Document): 71 | """Meaningless Document with all field types.""" 72 | 73 | binary_field = db.BinaryField() 74 | boolean_field = db.BooleanField() 75 | date_field = db.DateField() 76 | date_time_field = db.DateTimeField() 77 | decimal_field = db.DecimalField() 78 | dict_field = db.DictField() 79 | email_field = db.EmailField() 80 | embedded_document_field = db.EmbeddedDocumentField(document_type=Embedded) 81 | file_field = db.FileField() 82 | float_field = db.FloatField() 83 | int_field = db.IntField() 84 | list_field = db.ListField(field=db.StringField) 85 | reference_field = db.ReferenceField(document_type=Todo) 86 | sorted_list_field = db.SortedListField(field=db.StringField) 87 | string_field = db.StringField() 88 | url_field = db.URLField() 89 | cached_reference_field = db.CachedReferenceField(document_type=Todo) 90 | complex_date_time_field = db.ComplexDateTimeField() 91 | dynamic_field = db.DynamicField() 92 | embedded_document_list_field = db.EmbeddedDocumentListField( 93 | document_type=Embedded 94 | ) 95 | enum_field = db.EnumField(enum=[1, 2]) 96 | generic_embedded_document_field = db.GenericEmbeddedDocumentField() 97 | generic_lazy_reference_field = db.GenericLazyReferenceField() 98 | geo_json_base_field = db.GeoJsonBaseField() 99 | geo_point_field = db.GeoPointField() 100 | image_field = db.ImageField() 101 | lazy_reference_field = db.LazyReferenceField(document_type=Todo) 102 | line_string_field = db.LineStringField() 103 | long_field = db.LongField() 104 | map_field = db.MapField(field=db.StringField()) 105 | multi_line_string_field = db.MultiLineStringField() 106 | multi_point_field = db.MultiPointField() 107 | multi_polygon_field = db.MultiPolygonField() 108 | point_field = db.PointField() 109 | polygon_field = db.PolygonField() 110 | sequence_field = db.SequenceField() 111 | uuid_field = db.UUIDField() 112 | generic_reference_field = db.GenericReferenceField(document_type=Todo) 113 | object_id_field = db.ObjectIdField() 114 | 115 | AllFieldsModelForm = AllFieldsModel.to_wtf_form() 116 | form = AllFieldsModelForm( 117 | MultiDict( 118 | { 119 | "binary_field": "", 120 | "boolean_field": "", 121 | "date_field": "", 122 | "date_time_field": "", 123 | "decimal_field": "", 124 | "dict_field": "", 125 | "email_field": "", 126 | "embedded_document_field": "", 127 | "file_field": "", 128 | "float_field": "", 129 | "int_field": "", 130 | "list_field": "", 131 | "reference_field": "", 132 | "sorted_list_field": "", 133 | "string_field": "", 134 | "url_field": "", 135 | "cached_reference_field": "", 136 | "complex_date_time_field": "", 137 | "dynamic_field": "", 138 | "embedded_document_list_field": "", 139 | "enum_field": "", 140 | "generic_embedded_document_field": "", 141 | "generic_lazy_reference_field": "", 142 | "geo_json_base_field": "", 143 | "geo_point_field": "", 144 | "image_field": "", 145 | "lazy_reference_field": "", 146 | "line_string_field": "", 147 | "long_field": "", 148 | "map_field": "", 149 | "multi_line_string_field": "", 150 | "multi_point_field": "", 151 | "multi_polygon_field": "", 152 | "point_field": "", 153 | "polygon_field": "", 154 | "sequence_field": "", 155 | "uuid_field": "", 156 | "generic_reference_field": "", 157 | "object_id_field": "", 158 | } 159 | ) 160 | ) 161 | assert form.validate() 162 | form.save() 163 | 164 | obj = AllFieldsModel.objects.get(id=form.instance.pk) 165 | object_dict = json.loads(obj.to_json(json_options=RELAXED_JSON_OPTIONS)) 166 | object_dict.pop("_id") 167 | 168 | assert object_dict == { 169 | "dict_field": {}, 170 | "list_field": [], 171 | "sorted_list_field": [], 172 | "embedded_document_list_field": [], 173 | "map_field": {}, 174 | "sequence_field": 1, 175 | } 176 | 177 | 178 | class TestEmptyStringIsNoneMixin: 179 | """Special mixin to ignore empty strings **before** parent class processing.""" 180 | 181 | class ParentClass: 182 | """MRO parent.""" 183 | 184 | def __init__(self): 185 | self.data = True 186 | 187 | def process_formdata(self, valuelist): 188 | """Status changer""" 189 | assert valuelist != "" 190 | self.data = False 191 | 192 | class FakeClass(mongo_fields.EmptyStringIsNoneMixin, ParentClass): 193 | """Just MRO setter.""" 194 | 195 | pass 196 | 197 | @pytest.mark.parametrize("value", ["", None, [], {}, (), "", ("",), [""]]) 198 | def test__process_formdata__does_not_call_parent_method_if_value_is_empty( 199 | self, value 200 | ): 201 | obj = self.FakeClass() 202 | assert obj.data is True 203 | obj.process_formdata(value) 204 | assert obj.data is None 205 | 206 | @pytest.mark.parametrize("value", [[None], [1], (1,), (" ",), [" "]]) 207 | def test__process_formdata__does_call_parent_method_if_value_is_not_empty( 208 | self, value 209 | ): 210 | obj = self.FakeClass() 211 | assert obj.data is True 212 | obj.process_formdata(value) 213 | assert obj.data is False 214 | 215 | 216 | class TestMongoDictField: 217 | def test_mongo_dict_field_mro_not_changed(self): 218 | field_mro = list(mongo_fields.MongoDictField.__mro__[:5]) 219 | assert field_mro == [ 220 | mongo_fields.MongoDictField, 221 | mongo_fields.MongoTextAreaField, 222 | mongo_fields.EmptyStringIsNoneMixin, 223 | wtf_fields.TextAreaField, 224 | wtf_fields.StringField, 225 | ] 226 | 227 | def test_mongo_dict_field_default_dict_in_form_object(self, db): 228 | class DefaultDictModel(db.Document): 229 | """Should populate form with {}.""" 230 | 231 | dict_field = db.DictField() 232 | 233 | DefaultDictForm = DefaultDictModel.to_wtf_form() 234 | 235 | form = DefaultDictForm() 236 | 237 | assert str(form.dict_field) == Markup( 238 | '' 239 | ) 240 | assert form.dict_field.data == {} # This is mongoengine default 241 | assert form.dict_field.null is False 242 | 243 | assert form.validate() 244 | form.save() 245 | 246 | obj = DefaultDictModel.objects.get(id=form.instance.pk) 247 | object_dict = json.loads(obj.to_json(json_options=RELAXED_JSON_OPTIONS)) 248 | object_dict.pop("_id") 249 | 250 | assert object_dict == {"dict_field": {}} 251 | 252 | @pytest.mark.parametrize( 253 | ["null", "expected_obj"], 254 | [ 255 | (True, {"placeholder_string": "1", "null_dict_field": None}), 256 | (False, {"placeholder_string": "1"}), 257 | ], 258 | ) 259 | def test_mongo_dict_field_default_null_dict_in_form_object( 260 | self, db, null, expected_obj 261 | ): 262 | class DefaultDictModel(db.Document): 263 | """Should populate form with empty form.""" 264 | 265 | placeholder_string = db.StringField(default="1") 266 | null_dict_field = db.DictField(default=None, null=null) 267 | 268 | DefaultDictForm = DefaultDictModel.to_wtf_form() 269 | 270 | form = DefaultDictForm() 271 | 272 | assert str(form.null_dict_field) == Markup( 273 | '' 274 | ) 275 | assert form.null_dict_field.data is None 276 | 277 | assert form.validate() 278 | form.save() 279 | 280 | obj = DefaultDictModel.objects.get(id=form.instance.pk) 281 | object_dict = json.loads(obj.to_json(json_options=RELAXED_JSON_OPTIONS)) 282 | object_dict.pop("_id") 283 | 284 | assert object_dict == expected_obj 285 | 286 | def test__parse_json_data__raise_error_when_input_is_incorrect_json(self, db): 287 | class DictModel(db.Document): 288 | """Should populate form with empty form.""" 289 | 290 | dict_field = db.DictField() 291 | 292 | FormClass = DictModel.to_wtf_form() 293 | form = FormClass(MultiDict({"dict_field": "foobar"})) 294 | assert not form.validate() 295 | assert "Cannot load data" in form.errors["dict_field"][0] 296 | 297 | def test__ensure_data_is_dict__raise_error_when_input_is_a_list(self, db): 298 | class DictModel(db.Document): 299 | """Should populate form with empty form.""" 300 | 301 | dict_field = db.DictField() 302 | 303 | FormClass = DictModel.to_wtf_form() 304 | form = FormClass(MultiDict({"dict_field": "[]"})) 305 | assert not form.validate() 306 | assert form.errors == { 307 | "dict_field": ["Not a valid dictionary (list input detected)."] 308 | } 309 | 310 | 311 | class TestMongoEmailField: 312 | def test_email_field_mro_not_changed(self): 313 | field_mro = list(mongo_fields.MongoEmailField.__mro__[:4]) 314 | assert field_mro == [ 315 | mongo_fields.MongoEmailField, 316 | mongo_fields.EmptyStringIsNoneMixin, 317 | wtf_fields.EmailField, 318 | wtf_fields.StringField, 319 | ] 320 | 321 | 322 | class TestMongoFloatField: 323 | def test_ensure_widget_not_accidentally_replaced(self): 324 | field = mongo_fields.MongoFloatField 325 | assert isinstance(field.widget, wtf_widgets.NumberInput) 326 | 327 | 328 | class TestMongoHiddenField: 329 | def test_hidden_field_mro_not_changed(self): 330 | field_mro = list(mongo_fields.MongoHiddenField.__mro__[:4]) 331 | assert field_mro == [ 332 | mongo_fields.MongoHiddenField, 333 | mongo_fields.EmptyStringIsNoneMixin, 334 | wtf_fields.HiddenField, 335 | wtf_fields.StringField, 336 | ] 337 | 338 | 339 | class TestMongoPasswordField: 340 | def test_password_field_mro_not_changed(self): 341 | field_mro = list(mongo_fields.MongoPasswordField.__mro__[:4]) 342 | assert field_mro == [ 343 | mongo_fields.MongoPasswordField, 344 | mongo_fields.EmptyStringIsNoneMixin, 345 | wtf_fields.PasswordField, 346 | wtf_fields.StringField, 347 | ] 348 | 349 | 350 | class TestMongoSearchField: 351 | def test_search_field_mro_not_changed(self): 352 | field_mro = list(mongo_fields.MongoSearchField.__mro__[:4]) 353 | assert field_mro == [ 354 | mongo_fields.MongoSearchField, 355 | mongo_fields.EmptyStringIsNoneMixin, 356 | wtf_fields.SearchField, 357 | wtf_fields.StringField, 358 | ] 359 | 360 | 361 | class TestMongoStringField: 362 | def test__parent__process_formdata__method_included_in_mro_chain(self, db, mocker): 363 | """Test to protect from accidental incorrect __init__ method overwrite.""" 364 | base_init_spy = mocker.spy(wtf_fields.StringField, "process_formdata") 365 | mixin_init_spy = mocker.spy( 366 | mongo_fields.EmptyStringIsNoneMixin, "process_formdata" 367 | ) 368 | 369 | class DocumentModel(db.Document): 370 | """Test DB model.""" 371 | 372 | string_field = db.StringField() 373 | 374 | DocumentForm = DocumentModel.to_wtf_form() 375 | form = DocumentForm(formdata=MultiDict({"string_field": "1"})) 376 | assert form.validate() 377 | obj = form.save() 378 | mixin_init_spy.assert_called_once() 379 | base_init_spy.assert_called_once() 380 | assert obj.string_field == "1" 381 | 382 | def test_string_field_mro_not_changed(self): 383 | field_mro = list(mongo_fields.MongoStringField.__mro__[:3]) 384 | assert field_mro == [ 385 | mongo_fields.MongoStringField, 386 | mongo_fields.EmptyStringIsNoneMixin, 387 | wtf_fields.StringField, 388 | ] 389 | 390 | 391 | class TestMongoTelField: 392 | def test_tel_field_mro_not_changed(self): 393 | field_mro = list(mongo_fields.MongoTelField.__mro__[:4]) 394 | assert field_mro == [ 395 | mongo_fields.MongoTelField, 396 | mongo_fields.EmptyStringIsNoneMixin, 397 | wtf_fields.TelField, 398 | wtf_fields.StringField, 399 | ] 400 | 401 | 402 | class TestMongoTextAreaField: 403 | def test_text_area_field_mro_not_changed(self): 404 | field_mro = list(mongo_fields.MongoTextAreaField.__mro__[:4]) 405 | assert field_mro == [ 406 | mongo_fields.MongoTextAreaField, 407 | mongo_fields.EmptyStringIsNoneMixin, 408 | wtf_fields.TextAreaField, 409 | wtf_fields.StringField, 410 | ] 411 | 412 | 413 | class TestMongoURLField: 414 | def test_url_field_mro_not_changed(self): 415 | field_mro = list(mongo_fields.MongoURLField.__mro__[:4]) 416 | assert field_mro == [ 417 | mongo_fields.MongoURLField, 418 | mongo_fields.EmptyStringIsNoneMixin, 419 | wtf_fields.URLField, 420 | wtf_fields.StringField, 421 | ] 422 | -------------------------------------------------------------------------------- /tests/test_json.py: -------------------------------------------------------------------------------- 1 | """Extension of app JSON capabilities.""" 2 | import flask 3 | import pytest 4 | 5 | from flask_mongoengine import MongoEngine 6 | from flask_mongoengine.json import use_json_provider 7 | 8 | 9 | @pytest.fixture() 10 | def extended_db(app): 11 | """Provider config fixture.""" 12 | if use_json_provider(): 13 | app.json_provider_class = DummyProvider 14 | else: 15 | app.json_encoder = DummyEncoder 16 | app.config["MONGODB_SETTINGS"] = [ 17 | { 18 | "db": "flask_mongoengine_test_db", 19 | "host": "localhost", 20 | "port": 27017, 21 | "alias": "default", 22 | "uuidRepresentation": "standard", 23 | } 24 | ] 25 | test_db = MongoEngine(app) 26 | db_name = ( 27 | test_db.connection["default"].get_database("flask_mongoengine_test_db").name 28 | ) 29 | 30 | if not db_name.endswith("_test_db"): 31 | raise RuntimeError( 32 | f"DATABASE_URL must point to testing db, not to master db ({db_name})" 33 | ) 34 | 35 | # Clear database before tests, for cases when some test failed before. 36 | test_db.connection["default"].drop_database(db_name) 37 | 38 | yield test_db 39 | 40 | # Clear database after tests, for graceful exit. 41 | test_db.connection["default"].drop_database(db_name) 42 | 43 | 44 | class DummyEncoder(flask.json.JSONEncoder): 45 | """ 46 | An example encoder which a user may create and override 47 | the apps json_encoder with. 48 | This class is a NO-OP, but used to test proper inheritance. 49 | """ 50 | 51 | 52 | DummyProvider = None 53 | if use_json_provider(): 54 | 55 | class DummyProvider(flask.json.provider.DefaultJSONProvider): 56 | """Dummy Provider, to test correct MRO in new flask versions.""" 57 | 58 | 59 | @pytest.mark.skipif(condition=use_json_provider(), reason="New flask use other test") 60 | @pytest.mark.usefixtures("extended_db") 61 | def test_inheritance_old_flask(app): 62 | assert issubclass(app.json_encoder, DummyEncoder) 63 | json_encoder_name = app.json_encoder.__name__ 64 | 65 | assert json_encoder_name == "MongoEngineJSONEncoder" 66 | 67 | 68 | @pytest.mark.skipif( 69 | condition=not use_json_provider(), reason="Old flask use other test" 70 | ) 71 | @pytest.mark.usefixtures("extended_db") 72 | def test_inheritance(app): 73 | assert issubclass(app.json_provider_class, DummyProvider) 74 | json_provider_class = app.json_provider_class.__name__ 75 | 76 | assert json_provider_class == "MongoEngineJSONProvider" 77 | assert isinstance(app.json, DummyProvider) 78 | -------------------------------------------------------------------------------- /tests/test_json_app.py: -------------------------------------------------------------------------------- 1 | import flask 2 | import pytest 3 | from bson import DBRef, ObjectId 4 | 5 | 6 | @pytest.fixture(autouse=True) 7 | def setup_endpoints(app, todo): 8 | Todo = todo 9 | 10 | @app.route("/") 11 | def index(): 12 | return flask.jsonify(result=Todo.objects()) 13 | 14 | @app.route("/as_pymongo") 15 | def as_pymongo(): 16 | return flask.jsonify(result=Todo.objects().as_pymongo()) 17 | 18 | @app.route("/aggregate") 19 | def aggregate(): 20 | return flask.jsonify( 21 | result=Todo.objects().aggregate([{"$match": {"title": {"$ne": "lksdjh"}}}]) 22 | ) 23 | 24 | @app.route("/add", methods=["POST"]) 25 | def add(): 26 | form = flask.request.form 27 | todo = Todo(title=form["title"], text=form["text"]) 28 | todo.save() 29 | return flask.jsonify(result=todo) 30 | 31 | @app.route("/show//") 32 | def show(id): 33 | return flask.jsonify(result=Todo.objects.get_or_404(id=id)) 34 | 35 | @app.route("/object_id") 36 | def object_id(): 37 | return flask.jsonify(result=ObjectId()) 38 | 39 | @app.route("/dbref") 40 | def dbref(): 41 | return flask.jsonify(result=DBRef("Todo", ObjectId())) 42 | 43 | 44 | def test_with_id(app, todo): 45 | Todo = todo 46 | client = app.test_client() 47 | response = client.get(f"/show/{ObjectId()}/") 48 | assert response.status_code == 404 49 | 50 | response = client.post("/add", data={"title": "First Item", "text": "The text"}) 51 | assert response.status_code == 200 52 | 53 | response = client.get("/dbref") 54 | assert response.status_code == 200 55 | 56 | response = client.get("/object_id") 57 | assert response.status_code == 200 58 | 59 | response = client.get(f"/show/{Todo.objects.first().id}/") 60 | assert response.status_code == 200 61 | 62 | result = flask.json.loads(response.data).get("result") 63 | assert ("title", "First Item") in result.items() 64 | 65 | 66 | def test_basic_insert(app): 67 | client = app.test_client() 68 | client.post("/add", data={"title": "First Item", "text": "The text"}) 69 | client.post("/add", data={"title": "2nd Item", "text": "The text"}) 70 | 71 | rv = client.get("/") 72 | result = flask.json.loads(rv.data).get("result") 73 | assert len(result) == 2 74 | for i in result: 75 | assert "title" in i 76 | assert "text" in i 77 | 78 | rv = client.get("/as_pymongo") 79 | result = flask.json.loads(rv.data).get("result") 80 | assert len(result) == 2 81 | for i in result: 82 | assert "title" in i 83 | assert "text" in i 84 | 85 | rv = client.get("/aggregate") 86 | result = flask.json.loads(rv.data).get("result") 87 | for i in result: 88 | assert "title" in i 89 | assert "text" in i 90 | assert len(result) == 2 91 | -------------------------------------------------------------------------------- /tests/test_pagination.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from werkzeug.exceptions import NotFound 3 | 4 | from flask_mongoengine import ListFieldPagination, Pagination 5 | 6 | 7 | def test_queryset_paginator(app, todo): 8 | Todo = todo 9 | for i in range(42): 10 | Todo(title=f"post: {i}").save() 11 | 12 | with pytest.raises(NotFound): 13 | Pagination(iterable=Todo.objects, page=0, per_page=10) 14 | 15 | with pytest.raises(NotFound): 16 | Pagination(iterable=Todo.objects, page=6, per_page=10) 17 | 18 | paginator = Pagination(Todo.objects, 1, 10) 19 | _test_paginator(paginator) 20 | 21 | for page in range(1, 10): 22 | for index, todo in enumerate( 23 | Todo.objects.paginate(page=page, per_page=5).items 24 | ): 25 | assert todo.title == f"post: {(page-1) * 5 + index}" 26 | 27 | 28 | def test_paginate_plain_list(): 29 | with pytest.raises(NotFound): 30 | Pagination(iterable=range(1, 42), page=0, per_page=10) 31 | 32 | with pytest.raises(NotFound): 33 | Pagination(iterable=range(1, 42), page=6, per_page=10) 34 | 35 | paginator = Pagination(range(1, 42), 1, 10) 36 | _test_paginator(paginator) 37 | 38 | 39 | def test_list_field_pagination(app, todo): 40 | Todo = todo 41 | 42 | comments = [f"comment: {i}" for i in range(42)] 43 | todo = Todo( 44 | title="todo has comments", 45 | comments=comments, 46 | comment_count=len(comments), 47 | ).save() 48 | 49 | # Check without providing a total 50 | paginator = ListFieldPagination(Todo.objects, todo.id, "comments", 1, 10) 51 | _test_paginator(paginator) 52 | 53 | # Check with providing a total (saves a query) 54 | paginator = ListFieldPagination( 55 | Todo.objects, todo.id, "comments", 1, 10, todo.comment_count 56 | ) 57 | _test_paginator(paginator) 58 | 59 | paginator = todo.paginate_field("comments", 1, 10) 60 | _test_paginator(paginator) 61 | 62 | 63 | def _test_paginator(paginator): 64 | assert 5 == paginator.pages 65 | assert [1, 2, 3, 4, 5] == list(paginator.iter_pages()) 66 | 67 | for i in [1, 2, 3, 4, 5]: 68 | 69 | if i == 1: 70 | assert not paginator.has_prev 71 | with pytest.raises(NotFound): 72 | paginator.prev() 73 | else: 74 | assert paginator.has_prev 75 | 76 | if i == 5: 77 | assert not paginator.has_next 78 | with pytest.raises(NotFound): 79 | paginator.next() 80 | else: 81 | assert paginator.has_next 82 | 83 | if i == 3: 84 | assert [None, 2, 3, 4, None] == list(paginator.iter_pages(0, 1, 1, 0)) 85 | 86 | assert i == paginator.page 87 | assert i - 1 == paginator.prev_num 88 | assert i + 1 == paginator.next_num 89 | 90 | # Paginate to the next page 91 | if i < 5: 92 | paginator = paginator.next() 93 | -------------------------------------------------------------------------------- /tests/test_session.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from flask import session 3 | from pytest_mock import MockerFixture 4 | 5 | from flask_mongoengine import MongoEngineSessionInterface 6 | 7 | 8 | @pytest.fixture(autouse=True) 9 | def setup_endpoints(app, db): 10 | 11 | app.session_interface = MongoEngineSessionInterface(db) 12 | 13 | @app.route("/") 14 | def index(): 15 | session["a"] = "hello session" 16 | return session["a"] 17 | 18 | @app.route("/check-session") 19 | def check_session(): 20 | return f'session: {session["a"]}' 21 | 22 | 23 | @pytest.fixture 24 | def permanent_session_app(app): 25 | @app.before_request 26 | def make_session_permanent(): 27 | session.permanent = True 28 | 29 | return app 30 | 31 | 32 | def test__save_session__called_on_session_set(app, mocker: MockerFixture): 33 | save_spy = mocker.spy(MongoEngineSessionInterface, "save_session") 34 | expiration_spy = mocker.spy(MongoEngineSessionInterface, "get_expiration_time") 35 | client = app.test_client() 36 | 37 | response = client.get("/") 38 | call_args, _ = expiration_spy.call_args_list[0] 39 | 40 | assert response.status_code == 200 41 | assert response.data.decode("utf-8") == "hello session" 42 | assert app.session_interface.cls.objects.count() == 1 43 | save_spy.assert_called_once() 44 | assert call_args[2].permanent is False # session object 45 | 46 | 47 | def test__save_session__called_on_session_set__should_respect_permanent_session_setting( 48 | permanent_session_app, mocker: MockerFixture 49 | ): 50 | expiration_spy = mocker.spy(MongoEngineSessionInterface, "get_expiration_time") 51 | client = permanent_session_app.test_client() 52 | client.get("/") 53 | 54 | call_args, _ = expiration_spy.call_args_list[0] 55 | assert call_args[2].permanent is True # session object 56 | 57 | 58 | def test__open_session_called_on_session_get(app, mocker: MockerFixture): 59 | client = app.test_client() 60 | open_spy = mocker.spy(MongoEngineSessionInterface, "open_session") 61 | client.get("/") 62 | open_spy.assert_called_once() # On init call with no session 63 | 64 | response = client.get("/check-session") 65 | 66 | assert response.status_code == 200 67 | assert response.data.decode("utf-8") == "session: hello session" 68 | assert open_spy.call_count == 2 # On init + get with sid 69 | 70 | 71 | @pytest.mark.parametrize("unsupported_value", (1, None, True, False, [], {})) 72 | def test_session_interface__should_raise_value_error_if_collection_name_not_string( 73 | db, unsupported_value 74 | ): 75 | with pytest.raises(ValueError) as error: 76 | MongoEngineSessionInterface(db, collection=unsupported_value) 77 | 78 | assert str(error.value) == "Collection argument should be string" 79 | --------------------------------------------------------------------------------