├── examples ├── biggerapp │ ├── __init__.py │ ├── models.py │ ├── templates │ │ ├── layout.html │ │ ├── index.html │ │ └── pagination.html │ ├── README.rst │ ├── views.py │ ├── static │ │ └── style.css │ └── app.py └── simpleapp │ ├── templates │ ├── index.html │ └── layout.html │ ├── README.rst │ ├── static │ └── style.css │ └── app.py ├── requirements.txt ├── docs ├── _static │ └── debugtoolbar.png ├── _themes │ ├── flask │ │ ├── theme.conf │ │ └── static │ │ │ └── flasky.css_t │ ├── flask_small │ │ ├── theme.conf │ │ ├── layout.html │ │ └── static │ │ │ └── flasky.css_t │ ├── README │ └── flask_theme_support.py ├── make.bat ├── changelog.rst ├── Makefile ├── conf.py └── index.rst ├── flask_mongoengine ├── wtf │ ├── __init__.py │ ├── models.py │ ├── base.py │ ├── fields.py │ └── orm.py ├── json.py ├── panels.py ├── sessions.py ├── connection.py ├── pagination.py ├── templates │ └── panels │ │ └── mongo-panel.html ├── __init__.py └── operation_tracker.py ├── .gitignore ├── MANIFEST.in ├── setup.cfg ├── tox.ini ├── tests ├── __init__.py ├── test_json.py ├── test_session.py ├── test_basic_app.py ├── test_json_app.py ├── test_pagination.py ├── test_connection.py └── test_forms.py ├── AUTHORS ├── LICENSE ├── .travis.yml ├── README.rst ├── CONTRIBUTING.rst └── setup.py /examples/biggerapp/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask>=0.8 2 | Flask-DebugToolbar>=0.8 3 | Flask-WTF>=0.8.3 4 | mongoengine>=0.8.0 5 | flake8 6 | six 7 | -------------------------------------------------------------------------------- /docs/_static/debugtoolbar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twodayslate/flask-mongoengine/master/docs/_static/debugtoolbar.png -------------------------------------------------------------------------------- /docs/_themes/flask/theme.conf: -------------------------------------------------------------------------------- 1 | [theme] 2 | inherit = basic 3 | stylesheet = flasky.css 4 | pygments_style = flask_theme_support.FlaskyStyle 5 | -------------------------------------------------------------------------------- /flask_mongoengine/wtf/__init__.py: -------------------------------------------------------------------------------- 1 | from flask_mongoengine.wtf.orm import model_fields, model_form # noqa 2 | from flask_mongoengine.wtf.base import WtfBaseField # noqa 3 | -------------------------------------------------------------------------------- /.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 | .eggs 18 | .idea 19 | -------------------------------------------------------------------------------- /docs/_themes/flask_small/theme.conf: -------------------------------------------------------------------------------- 1 | [theme] 2 | inherit = basic 3 | stylesheet = flasky.css 4 | nosidebar = true 5 | pygments_style = flask_theme_support.FlaskyStyle 6 | 7 | [options] 8 | index_logo = '' 9 | index_logo_height = 120px 10 | github_fork = '' 11 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include MANIFEST.in 2 | include README.rst 3 | include LICENSE 4 | include AUTHORS 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 -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [nosetests] 2 | rednose = 1 3 | verbosity = 2 4 | detailed-errors = 1 5 | cover-erase = 1 6 | cover-branches = 1 7 | cover-package = flask_mongoengine 8 | tests = tests 9 | 10 | [flake8] 11 | ignore=E501,F403,F405,I201 12 | exclude=build,dist,docs,examples,venv,.tox,.eggs 13 | max-complexity=17 14 | -------------------------------------------------------------------------------- /examples/biggerapp/models.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from flask_mongoengine import MongoEngine 3 | 4 | db = MongoEngine() 5 | 6 | class Todo(db.Document): 7 | title = db.StringField(max_length=60) 8 | text = db.StringField() 9 | done = db.BooleanField(default=False) 10 | pub_date = db.DateTimeField(default=datetime.datetime.now) 11 | -------------------------------------------------------------------------------- /examples/simpleapp/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 | {% for todo in todos %} 4 |
5 |

{{ todo.title }}

6 | {{ todo.text|safe }} 7 |
8 | {% else %} 9 | Unbelievable. No todos here so far Add one 10 | {% endfor %} 11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /examples/biggerapp/templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Flask MongoEngine 5 | 6 | 7 | 8 |
9 |

Toolbar example

10 |
11 | {% block body %}{% endblock %} 12 |
13 |
14 | 15 | -------------------------------------------------------------------------------- /examples/simpleapp/templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Flask MongoEngine 5 | 6 | 7 | 8 |
9 |

Toolbar example

10 |
11 | {% block body %}{% endblock %} 12 |
13 |
14 | 15 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = {py35,py36,py37,py38,pypy3}-{me_latest_pip,me_dev},flake8 3 | 4 | [testenv] 5 | commands = 6 | python -m pytest 7 | deps = 8 | me_latest_pip: mongoengine>=0.19.0 9 | me_dev: https://github.com/MongoEngine/mongoengine/tarball/master 10 | PyMongo>3.9.0 11 | pytest 12 | nose 13 | 14 | [testenv:flake8] 15 | deps = flake8 16 | commands = 17 | flake8 18 | -------------------------------------------------------------------------------- /examples/biggerapp/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 | {% for todo in todos %} 4 |
5 |

{{ todo.title }}

6 | {{ todo.text|safe }} 7 |
8 | {% else %} 9 | Unbelievable. No todos here so far Add one 10 | {% endfor %} 11 |
12 | See pagination 13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /examples/biggerapp/README.rst: -------------------------------------------------------------------------------- 1 | Sample app to test the toolbar 2 | ============================== 3 | 4 | A simple multi file app - to help get you started 5 | 6 | 1. Install all the requirements from: flask-mongoengine/requirements.txt:: 7 | 8 | pip install -r requirements.txt 9 | 10 | 2. From the root folder run the app eg:: 11 | 12 | python ./examples/biggerapp/app.py 13 | 14 | 3. Point your browser to localhost:4000/ 15 | 16 | -------------------------------------------------------------------------------- /examples/simpleapp/README.rst: -------------------------------------------------------------------------------- 1 | Sample app to test the toolbar 2 | ============================== 3 | 4 | A simple one file app - to help get you started 5 | 6 | 1. Install all the requirements from: flask-mongoengine/requirements.txt:: 7 | 8 | pip install -r requirements.txt 9 | 10 | 2. From the root folder run the app eg:: 11 | 12 | python ./examples/simpleapp/app.py 13 | 14 | 3. Point your browser to localhost:4000/ 15 | 16 | -------------------------------------------------------------------------------- /flask_mongoengine/wtf/models.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | 3 | 4 | class ModelForm(FlaskForm): 5 | """A WTForms mongoengine model form""" 6 | 7 | def __init__(self, formdata=None, **kwargs): 8 | self.instance = (kwargs.pop('instance', None) or kwargs.get('obj')) 9 | if self.instance and not formdata: 10 | kwargs['obj'] = self.instance 11 | self.formdata = formdata 12 | super(ModelForm, self).__init__(formdata, **kwargs) 13 | 14 | def save(self, commit=True, **kwargs): 15 | if self.instance: 16 | self.populate_obj(self.instance) 17 | else: 18 | self.instance = self.model_class(**self.data) 19 | 20 | if commit: 21 | self.instance.save(**kwargs) 22 | return self.instance 23 | -------------------------------------------------------------------------------- /docs/_themes/flask_small/layout.html: -------------------------------------------------------------------------------- 1 | {% extends "basic/layout.html" %} 2 | {% block header %} 3 | {{ super() }} 4 | {% if pagename == 'index' %} 5 |
6 | {% endif %} 7 | {% endblock %} 8 | {% block footer %} 9 | {% if pagename == 'index' %} 10 |
11 | {% endif %} 12 | {% endblock %} 13 | {# do not display relbars #} 14 | {% block relbar1 %}{% endblock %} 15 | {% block relbar2 %} 16 | {% if theme_github_fork %} 17 | Fork me on GitHub 19 | {% endif %} 20 | {% endblock %} 21 | {% block sidebar1 %}{% endblock %} 22 | {% block sidebar2 %}{% endblock %} 23 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import flask 3 | import mongoengine 4 | 5 | 6 | class FlaskMongoEngineTestCase(unittest.TestCase): 7 | """Parent class of all test cases""" 8 | 9 | def setUp(self): 10 | self.app = flask.Flask(__name__) 11 | self.app.config['MONGODB_DB'] = 'test_db' 12 | self.app.config['TESTING'] = True 13 | self.ctx = self.app.app_context() 14 | self.ctx.push() 15 | # Mongoengine keep a global state of the connections that must be 16 | # reset before each test. 17 | # Given it doesn't expose any method to get the list of registered 18 | # connections, we have to do the cleaning by hand... 19 | mongoengine.connection._connection_settings.clear() 20 | mongoengine.connection._connections.clear() 21 | mongoengine.connection._dbs.clear() 22 | 23 | def tearDown(self): 24 | self.ctx.pop() 25 | -------------------------------------------------------------------------------- /examples/biggerapp/views.py: -------------------------------------------------------------------------------- 1 | import flask 2 | from models import Todo 3 | 4 | def index(): 5 | # As a list to test debug toolbar 6 | Todo.objects().delete() # Removes 7 | Todo(title="Simple todo A", text="12345678910").save() # Insert 8 | Todo(title="Simple todo B", text="12345678910").save() # Insert 9 | Todo.objects(title__contains="B").update(set__text="Hello world") # Update 10 | todos = list(Todo.objects[:10]) 11 | todos = Todo.objects.all() 12 | return flask.render_template('index.html', todos=todos) 13 | 14 | def pagination(): 15 | Todo.objects().delete() 16 | for i in range(10): 17 | Todo(title='Simple todo {}'.format(i), text="12345678910").save() # Insert 18 | 19 | page_num = int(flask.request.args.get('page') or 1) 20 | todos_page = Todo.objects.paginate(page=page_num, per_page=3) 21 | 22 | return flask.render_template('pagination.html', todos_page=todos_page) 23 | -------------------------------------------------------------------------------- /examples/biggerapp/static/style.css: -------------------------------------------------------------------------------- 1 | body { font-family: sans-serif; background: #eee; } 2 | a, h1, h2 { color: #377BA8; } 3 | h1, h2 { font-family: 'Georgia', serif; margin: 0; } 4 | h1 { border-bottom: 2px solid #eee; } 5 | h2 { font-size: 1.2em; } 6 | .page { margin: 2em auto; width: 35em; border: 5px solid #ccc; 7 | padding: 0.8em; background: white; } 8 | .entries { list-style: none; margin: 0; padding: 0; } 9 | .entries li { margin: 0.8em 1.2em; } 10 | .entries li h2 { margin-left: -1em; } 11 | .add-entry { font-size: 0.9em; border-bottom: 1px solid #ccc; } 12 | .add-entry dl { font-weight: bold; } 13 | .metanav { text-align: right; font-size: 0.8em; padding: 0.3em; 14 | margin-bottom: 1em; background: #fafafa; } 15 | .flash { background: #CEE5F5; padding: 0.5em; 16 | border: 1px solid #AACBE2; } 17 | .error { background: #F0D6D6; padding: 0.5em; } 18 | -------------------------------------------------------------------------------- /examples/simpleapp/static/style.css: -------------------------------------------------------------------------------- 1 | body { font-family: sans-serif; background: #eee; } 2 | a, h1, h2 { color: #377BA8; } 3 | h1, h2 { font-family: 'Georgia', serif; margin: 0; } 4 | h1 { border-bottom: 2px solid #eee; } 5 | h2 { font-size: 1.2em; } 6 | 7 | .page { margin: 2em auto; width: 35em; border: 5px solid #ccc; 8 | padding: 0.8em; background: white; } 9 | .entries { list-style: none; margin: 0; padding: 0; } 10 | .entries li { margin: 0.8em 1.2em; } 11 | .entries li h2 { margin-left: -1em; } 12 | .add-entry { font-size: 0.9em; border-bottom: 1px solid #ccc; } 13 | .add-entry dl { font-weight: bold; } 14 | .metanav { text-align: right; font-size: 0.8em; padding: 0.3em; 15 | margin-bottom: 1em; background: #fafafa; } 16 | .flash { background: #CEE5F5; padding: 0.5em; 17 | border: 1px solid #AACBE2; } 18 | .error { background: #F0D6D6; padding: 0.5em; } 19 | -------------------------------------------------------------------------------- /examples/biggerapp/templates/pagination.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {# Macro for creating navigation links #} 4 | {% macro render_navigation(pagination, endpoint) %} 5 | 18 | {% endmacro %} 19 | 20 | {% block body %} 21 | 22 |
23 | 28 |
29 | 30 | 33 | 34 | 35 | {% endblock %} 36 | -------------------------------------------------------------------------------- /docs/_themes/README: -------------------------------------------------------------------------------- 1 | Flask Sphinx Styles 2 | =================== 3 | 4 | This repository contains sphinx styles for Flask and Flask related 5 | projects. To use this style in your Sphinx documentation, follow 6 | this guide: 7 | 8 | 1. put this folder as _themes into your docs folder. Alternatively 9 | you can also use git submodules to check out the contents there. 10 | 2. add this to your conf.py: 11 | 12 | sys.path.append(os.path.abspath('_themes')) 13 | html_theme_path = ['_themes'] 14 | html_theme = 'flask' 15 | 16 | The following themes exist: 17 | 18 | - 'flask' - the standard flask documentation theme for large 19 | projects 20 | - 'flask_small' - small one-page theme. Intended to be used by 21 | very small addon libraries for flask. 22 | 23 | The following options exist for the flask_small theme: 24 | 25 | [options] 26 | index_logo = '' filename of a picture in _static 27 | to be used as replacement for the 28 | h1 in the index.rst file. 29 | index_logo_height = 120px height of the index logo 30 | github_fork = '' repository name on github for the 31 | "fork me" badge 32 | -------------------------------------------------------------------------------- /examples/biggerapp/app.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import flask 4 | 5 | sys.path.insert(0, os.path.realpath(os.path.join(os.path.dirname(__file__), '../../'))) 6 | 7 | from flask_debugtoolbar import DebugToolbarExtension 8 | 9 | app = flask.Flask(__name__) 10 | app.config.from_object(__name__) 11 | app.config['MONGODB_SETTINGS'] = {'DB': 'testing'} 12 | app.config['TESTING'] = True 13 | app.config['SECRET_KEY'] = 'flask+mongoengine=<3' 14 | app.debug = True 15 | app.config['DEBUG_TB_PANELS'] = ( 16 | 'flask_debugtoolbar.panels.versions.VersionDebugPanel', 17 | 'flask_debugtoolbar.panels.timer.TimerDebugPanel', 18 | 'flask_debugtoolbar.panels.headers.HeaderDebugPanel', 19 | 'flask_debugtoolbar.panels.request_vars.RequestVarsDebugPanel', 20 | 'flask_debugtoolbar.panels.template.TemplateDebugPanel', 21 | 'flask_debugtoolbar.panels.logger.LoggingPanel', 22 | 'flask_mongoengine.panels.MongoDebugPanel' 23 | ) 24 | app.config['DEBUG_TB_INTERCEPT_REDIRECTS'] = False 25 | 26 | from models import db 27 | db.init_app(app) 28 | 29 | DebugToolbarExtension(app) 30 | 31 | from views import index, pagination 32 | app.add_url_rule('/', view_func=index) 33 | app.add_url_rule('/pagination', view_func=pagination) 34 | 35 | if __name__ == "__main__": 36 | app.run(host="0.0.0.0", port=4000) 37 | -------------------------------------------------------------------------------- /flask_mongoengine/json.py: -------------------------------------------------------------------------------- 1 | from bson import json_util 2 | from flask.json import JSONEncoder 3 | from mongoengine.base import BaseDocument 4 | from mongoengine.queryset import QuerySet 5 | 6 | 7 | def _make_encoder(superclass): 8 | class MongoEngineJSONEncoder(superclass): 9 | """ 10 | A JSONEncoder which provides serialization of MongoEngine 11 | documents and queryset objects. 12 | """ 13 | def default(self, obj): 14 | if isinstance(obj, BaseDocument): 15 | return json_util._json_convert(obj.to_mongo()) 16 | elif isinstance(obj, QuerySet): 17 | return json_util._json_convert(obj.as_pymongo()) 18 | return superclass.default(self, obj) 19 | return MongoEngineJSONEncoder 20 | 21 | 22 | MongoEngineJSONEncoder = _make_encoder(JSONEncoder) 23 | 24 | 25 | def override_json_encoder(app): 26 | """ 27 | A function to dynamically create a new MongoEngineJSONEncoder class 28 | based upon a custom base class. 29 | This function allows us to combine MongoEngine serialization with 30 | any changes to Flask's JSONEncoder which a user may have made 31 | prior to calling init_app. 32 | 33 | NOTE: This does not cover situations where users override 34 | an instance's json_encoder after calling init_app. 35 | """ 36 | app.json_encoder = _make_encoder(app.json_encoder) 37 | -------------------------------------------------------------------------------- /flask_mongoengine/wtf/base.py: -------------------------------------------------------------------------------- 1 | from mongoengine.base import BaseField 2 | 3 | __all__ = ('WtfBaseField') 4 | 5 | 6 | class WtfBaseField(BaseField): 7 | """ 8 | Extension wrapper class for mongoengine BaseField. 9 | 10 | This enables flask-mongoengine wtf to extend the 11 | number of field parameters, and settings on behalf 12 | of document model form generator for WTForm. 13 | 14 | @param validators: wtf model form field validators. 15 | @param filters: wtf model form field filters. 16 | """ 17 | 18 | def __init__(self, validators=None, filters=None, **kwargs): 19 | 20 | self.validators = \ 21 | self._ensure_callable_or_list(validators, 'validators') 22 | self.filters = self._ensure_callable_or_list(filters, 'filters') 23 | 24 | BaseField.__init__(self, **kwargs) 25 | 26 | def _ensure_callable_or_list(self, field, msg_flag): 27 | """ 28 | Ensure the value submitted via field is either 29 | a callable object to convert to list or it is 30 | in fact a valid list value. 31 | 32 | """ 33 | if field is not None: 34 | if callable(field): 35 | field = [field] 36 | else: 37 | msg = "Argument '%s' must be a list value" % msg_flag 38 | if not isinstance(field, list): 39 | raise TypeError(msg) 40 | 41 | return field 42 | -------------------------------------------------------------------------------- /tests/test_json.py: -------------------------------------------------------------------------------- 1 | import flask 2 | 3 | from flask_mongoengine import MongoEngine 4 | from tests import FlaskMongoEngineTestCase 5 | 6 | 7 | class DummyEncoder(flask.json.JSONEncoder): 8 | """ 9 | An example encoder which a user may create and override 10 | the apps json_encoder with. 11 | This class is a NO-OP, but used to test proper inheritance. 12 | """ 13 | 14 | 15 | class JSONAppTestCase(FlaskMongoEngineTestCase): 16 | 17 | def dictContains(self, superset, subset): 18 | for k, v in subset.items(): 19 | if not superset[k] == v: 20 | return False 21 | return True 22 | 23 | def assertDictContains(self, superset, subset): 24 | return self.assertTrue(self.dictContains(superset, subset)) 25 | 26 | def setUp(self): 27 | super(JSONAppTestCase, self).setUp() 28 | self.app.config['MONGODB_DB'] = 'test_db' 29 | self.app.config['TESTING'] = True 30 | self.app.json_encoder = DummyEncoder 31 | db = MongoEngine() 32 | db.init_app(self.app) 33 | self.db = db 34 | 35 | def test_inheritance(self): 36 | self.assertTrue(issubclass(self.app.json_encoder, DummyEncoder)) 37 | json_encoder_name = self.app.json_encoder.__name__ 38 | 39 | # Since the class is dynamically derrived, must compare class names 40 | # rather than class objects. 41 | self.assertEqual(json_encoder_name, 'MongoEngineJSONEncoder') 42 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | The PRIMARY AUTHORS are (and/or have been): 2 | 3 | Ross Lawley 4 | Bright Dadson 5 | Jorge Bastida 6 | Dan Jacob https://bitbucket.org/danjac 7 | Marat Khabibullin https://bitbucket.org/maratfm 8 | Streetlife.com 9 | atroche - https://github.com/atroche 10 | Rodrigue Cloutier 11 | Thomas Steinacher 12 | Anthony Nemitz 13 | Nauman Ahmad 14 | 15 | 16 | CONTRIBUTORS 17 | 18 | Dervived from the git logs, inevitably incomplete but all of whom and others 19 | have submitted patches, reported bugs and generally helped make MongoEngine 20 | that much better: 21 | 22 | * Insspb - https://github.com/insspb 23 | * Dragos - https://github.com/cdragos 24 | * IamFive - https://github.com/IamFive 25 | * mickey06 - https://github.com/mickey06 26 | * Serge S. Koval - https://github.com/mrjoes 27 | * Marcus Carlsson - https://github.com/xintron 28 | * RealJTG - https://github.com/RealJTG 29 | * Peter D. Gray 30 | * Massimo Santini 31 | * Len Buckens - https://github.com/buckensl 32 | * Garito - https://github.com/garito 33 | * Jérôme Lafréchoux - https://github.com/lafrech 34 | * Bruno Belarmino - https://github.com/brunobelarmino 35 | * Sibelius Seraphini - https://github.com/sibelius 36 | * Denny Huang - https://github.com/denny0223 37 | * Stefan Wojcik - https://github.com/wojcikstefan 38 | * John Cass - https://github.com/jcass77 39 | * Aly Sivji - https://github.com/alysivji 40 | * Buğra İşgüzar - https://github.com/bisguzar 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010-2016 See AUTHORS. 2 | 3 | Some rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are 7 | met: 8 | 9 | * Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above 13 | copyright notice, this list of conditions and the following 14 | disclaimer in the documentation and/or other materials provided 15 | with the distribution. 16 | 17 | * The names of the contributors may not be used to endorse or 18 | promote products derived from this software without specific 19 | prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 22 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 23 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 24 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 25 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 26 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 27 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 28 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 29 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | -------------------------------------------------------------------------------- /examples/simpleapp/app.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | import sys 4 | import datetime 5 | import flask 6 | 7 | sys.path.insert(0, os.path.realpath(os.path.join(os.path.dirname(__file__), '../../'))) 8 | 9 | from flask_mongoengine import MongoEngine 10 | from flask_debugtoolbar import DebugToolbarExtension 11 | 12 | app = flask.Flask(__name__) 13 | app.config.from_object(__name__) 14 | app.config['MONGODB_SETTINGS'] = {'DB': 'testing'} 15 | app.config['TESTING'] = True 16 | app.config['SECRET_KEY'] = 'flask+mongoengine=<3' 17 | app.debug = True 18 | app.config['DEBUG_TB_PANELS'] = ( 19 | 'flask_debugtoolbar.panels.versions.VersionDebugPanel', 20 | 'flask_debugtoolbar.panels.timer.TimerDebugPanel', 21 | 'flask_debugtoolbar.panels.headers.HeaderDebugPanel', 22 | 'flask_debugtoolbar.panels.request_vars.RequestVarsDebugPanel', 23 | 'flask_debugtoolbar.panels.template.TemplateDebugPanel', 24 | 'flask_debugtoolbar.panels.logger.LoggingPanel', 25 | 'flask_mongoengine.panels.MongoDebugPanel' 26 | ) 27 | 28 | app.config['DEBUG_TB_INTERCEPT_REDIRECTS'] = False 29 | 30 | db = MongoEngine() 31 | db.init_app(app) 32 | 33 | DebugToolbarExtension(app) 34 | 35 | class Todo(db.Document): 36 | title = db.StringField(max_length=60) 37 | text = db.StringField() 38 | done = db.BooleanField(default=False) 39 | pub_date = db.DateTimeField(default=datetime.datetime.now) 40 | 41 | @app.route('/') 42 | def index(): 43 | # As a list to test debug toolbar 44 | Todo.objects().delete() # Removes 45 | Todo(title="Simple todo A ПЫЩЬ!", text="12345678910").save() # Insert 46 | Todo(title="Simple todo B", text="12345678910").save() # Insert 47 | Todo.objects(title__contains="B").update(set__text="Hello world") # Update 48 | todos = Todo.objects.all() 49 | return flask.render_template('index.html', todos=todos) 50 | 51 | if __name__ == "__main__": 52 | app.run(host="0.0.0.0", port=4000) 53 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Use a container-based environment 2 | sudo: false 3 | 4 | language: python 5 | 6 | python: 7 | - '3.5' 8 | - '3.6' 9 | - '3.7' 10 | - '3.8' 11 | - pypy3 12 | 13 | env: 14 | - MONGOENGINE=latest_pip 15 | - MONGOENGINE=dev 16 | 17 | services: 18 | - mongodb 19 | 20 | install: 21 | - travis_retry pip install --upgrade pip 22 | - travis_retry pip install coveralls 23 | - travis_retry pip install flake8 24 | - travis_retry pip install tox>=3.14 25 | - travis_retry pip install virtualenv 26 | - travis_retry tox -e $(echo py$TRAVIS_PYTHON_VERSION-me_$MONGOENGINE | tr -d . | sed -e 's/pypypy/pypy/') -- -e test 27 | 28 | # Cache dependencies installed via pip 29 | cache: pip 30 | 31 | script: 32 | - tox -e $(echo py$TRAVIS_PYTHON_VERSION-me_$MONGOENGINE | tr -d . | sed -e 's/pypypy/pypy/') -- --with-coverage 33 | 34 | after_success: 35 | - coveralls --verbose 36 | 37 | notifications: 38 | irc: irc.freenode.org#flask-mongoengine 39 | 40 | # Only run builds on the master branch and GitHub releases (tagged as vX.Y.Z) 41 | branches: 42 | only: 43 | - master 44 | - /^v.*$/ 45 | 46 | # Whenever a new release is created via GitHub, publish it on PyPI. 47 | deploy: 48 | provider: pypi 49 | user: wojcikstefan 50 | password: 51 | secure: ZShEfSeu1pUHPGWtO5JAERp3nkG1omFIDxz/N+qNEJGwIRxXH6S0E17p9jaRAwrBY11F6ecajOgz1E1ICovYf8IitOP8I/CyieF6EPV7Bv4PZyKJdWGzT/Edjrt+wAjY7Kwlr2Gkh9Tu1lChZcWvhyMVPYCgqyA2yP0W2b9kC4Y= 52 | 53 | # create a source distribution and a pure python wheel for faster installs 54 | distributions: "sdist bdist_wheel" 55 | 56 | # only deploy on tagged commits (aka GitHub releases) and only for the 57 | # parent repo's builds running Python 2.7 along with dev MongoEngine (we run 58 | # Travis against many different Python and MongoEngine versions and we don't 59 | # want the deploy to occur multiple times). 60 | on: 61 | tags: true 62 | condition: "$MONGOENGINE = dev" 63 | python: 3.5 64 | repo: MongoEngine/flask-mongoengine 65 | -------------------------------------------------------------------------------- /tests/test_session.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from flask import session 4 | from flask_mongoengine import MongoEngine, MongoEngineSessionInterface 5 | from tests import FlaskMongoEngineTestCase 6 | 7 | 8 | class SessionTestCase(FlaskMongoEngineTestCase): 9 | 10 | def setUp(self): 11 | super(SessionTestCase, self).setUp() 12 | self.db_name = 'test_db' 13 | self.app.config['MONGODB_DB'] = self.db_name 14 | self.app.config['TESTING'] = True 15 | db = MongoEngine(self.app) 16 | self.app.session_interface = MongoEngineSessionInterface(db) 17 | 18 | @self.app.route('/') 19 | def index(): 20 | session["a"] = "hello session" 21 | return session["a"] 22 | 23 | @self.app.route('/check-session') 24 | def check_session(): 25 | return "session: %s" % session["a"] 26 | 27 | @self.app.route('/check-session-database') 28 | def check_session_database(): 29 | sessions = self.app.session_interface.cls.objects.count() 30 | return "sessions: %s" % sessions 31 | 32 | self.db = db 33 | 34 | def tearDown(self): 35 | try: 36 | self.db.connection.drop_database(self.db_name) 37 | except Exception: 38 | self.db.connection.client.drop_database(self.db_name) 39 | 40 | def test_setting_session(self): 41 | c = self.app.test_client() 42 | resp = c.get('/') 43 | self.assertEqual(resp.status_code, 200) 44 | self.assertEqual(resp.data.decode('utf-8'), 'hello session') 45 | 46 | resp = c.get('/check-session') 47 | self.assertEqual(resp.status_code, 200) 48 | self.assertEqual(resp.data.decode('utf-8'), 'session: hello session') 49 | 50 | resp = c.get('/check-session-database') 51 | self.assertEqual(resp.status_code, 200) 52 | self.assertEqual(resp.data.decode('utf-8'), 'sessions: 1') 53 | 54 | 55 | if __name__ == '__main__': 56 | unittest.main() 57 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ================= 2 | Flask-MongoEngine 3 | ================= 4 | :Info: MongoEngine for Flask web applications. 5 | :Repository: https://github.com/MongoEngine/flask-mongoengine 6 | 7 | .. image:: https://travis-ci.org/MongoEngine/flask-mongoengine.svg?branch=master 8 | :target: https://travis-ci.org/MongoEngine/flask-mongoengine 9 | 10 | .. image:: https://coveralls.io/repos/github/MongoEngine/flask-mongoengine/badge.svg?branch=master 11 | :target: https://coveralls.io/github/MongoEngine/flask-mongoengine?branch=master 12 | 13 | About 14 | ===== 15 | Flask-MongoEngine is a Flask extension that provides integration with MongoEngine. It handles connection management for your app. 16 | You can also use WTForms as model forms for your models. 17 | 18 | Documentation 19 | ============= 20 | You can find the documentation at https://flask-mongoengine.readthedocs.io 21 | 22 | Installation 23 | ============ 24 | You can install this package using pypi: ``pip install flask-mongoengine`` 25 | 26 | Tests 27 | ===== 28 | To run the test suite, ensure you are running a local copy of Flask-MongoEngine 29 | and run: ``python setup.py nosetests``. 30 | 31 | To run the test suite on every supported versions of Python, PyPy and MongoEngine you can use ``tox``. 32 | Ensure tox and each supported Python, PyPy versions are installed in your environment: 33 | 34 | .. code-block:: shell 35 | 36 | # Install tox 37 | $ pip install tox 38 | # Run the test suites 39 | $ tox 40 | 41 | To run a single or selected test suits, use the nosetest convention. E.g. 42 | 43 | .. code-block:: shell 44 | 45 | $ python setup.py nosetests --tests tests/example_test.py:ExampleTestClass.example_test_method 46 | 47 | Contributing 48 | ============ 49 | We welcome contributions! see the `Contribution guidelines `_ 50 | 51 | Community 52 | ========= 53 | - `#flask-mongoengine IRC channel `_ 54 | 55 | License 56 | ======= 57 | Flask-MongoEngine is distributed under MIT license, see LICENSE for more details. 58 | -------------------------------------------------------------------------------- /flask_mongoengine/panels.py: -------------------------------------------------------------------------------- 1 | from flask import current_app 2 | from flask_debugtoolbar.panels import DebugPanel 3 | from jinja2 import ChoiceLoader, PackageLoader 4 | 5 | from flask_mongoengine import operation_tracker 6 | 7 | 8 | package_loader = PackageLoader('flask_mongoengine', 'templates') 9 | 10 | 11 | def _maybe_patch_jinja_loader(jinja_env): 12 | """Patch the jinja_env loader to include flaskext.mongoengine 13 | templates folder if necessary. 14 | """ 15 | if not isinstance(jinja_env.loader, ChoiceLoader): 16 | jinja_env.loader = ChoiceLoader([jinja_env.loader, package_loader]) 17 | elif package_loader not in jinja_env.loader.loaders: 18 | jinja_env.loader.loaders.append(package_loader) 19 | 20 | 21 | class MongoDebugPanel(DebugPanel): 22 | """Panel that shows information about MongoDB operations (including stack) 23 | 24 | Adapted from https://github.com/hmarr/django-debug-toolbar-mongo 25 | """ 26 | name = 'MongoDB' 27 | has_content = True 28 | 29 | def __init__(self, *args, **kwargs): 30 | super(MongoDebugPanel, self).__init__(*args, **kwargs) 31 | _maybe_patch_jinja_loader(self.jinja_env) 32 | operation_tracker.install_tracker() 33 | 34 | def process_request(self, request): 35 | operation_tracker.reset() 36 | 37 | def nav_title(self): 38 | return 'MongoDB' 39 | 40 | def nav_subtitle(self): 41 | attrs = ['queries', 'inserts', 'updates', 'removes'] 42 | ops = sum(sum((1 for o in getattr(operation_tracker, a) 43 | if not o['internal'])) 44 | for a in attrs) 45 | total_time = sum(sum(o['time'] for o in getattr(operation_tracker, a)) 46 | for a in attrs) 47 | return '{0} operations in {1:.2f}ms'.format(ops, total_time) 48 | 49 | def title(self): 50 | return 'MongoDB Operations' 51 | 52 | def url(self): 53 | return '' 54 | 55 | def content(self): 56 | context = self.context.copy() 57 | context['queries'] = operation_tracker.queries 58 | context['inserts'] = operation_tracker.inserts 59 | context['updates'] = operation_tracker.updates 60 | context['removes'] = operation_tracker.removes 61 | context['slow_query_limit'] = current_app.config.get('MONGO_DEBUG_PANEL_SLOW_QUERY_LIMIT', 100) 62 | return self.render('panels/mongo-panel.html', context) 63 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | Contributing to Flask-MongoEngine 2 | ================================= 3 | 4 | MongoEngine has a large `community 5 | `_ and 6 | contributions are always encouraged. Contributions can be as simple as 7 | minor tweaks to the documentation. Please read these guidelines before 8 | sending a pull request. 9 | 10 | Bugfixes and New Features 11 | ------------------------- 12 | 13 | Before starting to write code, look for existing `tickets 14 | `_ or `create one 15 | `_ for your specific 16 | issue or feature request. That way you avoid working on something 17 | that might not be of interest or that has already been addressed. If in doubt 18 | post to the `user group ` 19 | 20 | Supported Interpreters 21 | ---------------------- 22 | 23 | Flask-MongoEngine supports CPython 3.5 and newer. Language 24 | features not supported by all interpreters can not be used. 25 | 26 | Style Guide 27 | ----------- 28 | 29 | MongoEngine aims to follow `PEP8 `_ 30 | including 4 space indents and 79 character line limits. 31 | 32 | Testing 33 | ------- 34 | 35 | All tests are run on `Travis `_ 36 | and any pull requests are automatically tested by Travis. Any pull requests 37 | without tests will take longer to be integrated and might be refused. 38 | 39 | General Guidelines 40 | ------------------ 41 | 42 | - Avoid backward breaking changes if at all possible. 43 | - Write inline documentation for new classes and methods. 44 | - Write tests and make sure they pass (make sure you have a mongod 45 | running on the default port, then execute ``python setup.py test`` 46 | from the cmd line to run the test suite). 47 | - Add yourself to AUTHORS :) 48 | 49 | Documentation 50 | ------------- 51 | 52 | To contribute to the `API documentation 53 | `_ 54 | just make your changes to the inline documentation of the appropriate 55 | `source code `_ or `rst file 56 | `_ in a 57 | branch and submit a `pull request `_. 58 | You might also use the github `Edit `_ 59 | button. 60 | -------------------------------------------------------------------------------- /tests/test_basic_app.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import flask 3 | from bson import ObjectId 4 | 5 | from flask_mongoengine import MongoEngine 6 | from tests import FlaskMongoEngineTestCase 7 | 8 | 9 | class BasicAppTestCase(FlaskMongoEngineTestCase): 10 | 11 | def setUp(self): 12 | super(BasicAppTestCase, self).setUp() 13 | db = MongoEngine() 14 | 15 | class Todo(db.Document): 16 | title = db.StringField(max_length=60) 17 | text = db.StringField() 18 | done = db.BooleanField(default=False) 19 | pub_date = db.DateTimeField(default=datetime.datetime.now) 20 | 21 | db.init_app(self.app) 22 | 23 | Todo.drop_collection() 24 | self.Todo = Todo 25 | 26 | @self.app.route('/') 27 | def index(): 28 | return '\n'.join(x.title for x in self.Todo.objects) 29 | 30 | @self.app.route('/add', methods=['POST']) 31 | def add(): 32 | form = flask.request.form 33 | todo = self.Todo(title=form['title'], 34 | text=form['text']) 35 | todo.save() 36 | return 'added' 37 | 38 | @self.app.route('/show//') 39 | def show(id): 40 | todo = self.Todo.objects.get_or_404(id=id) 41 | return '\n'.join([todo.title, todo.text]) 42 | 43 | self.db = db 44 | 45 | def test_connection_default(self): 46 | self.app.config['MONGODB_SETTINGS'] = {} 47 | self.app.config['TESTING'] = True 48 | 49 | db = MongoEngine() 50 | # Disconnect to drop connection from setup. 51 | db.disconnect() 52 | db.init_app(self.app) 53 | 54 | def test_with_id(self): 55 | c = self.app.test_client() 56 | resp = c.get('/show/%s/' % ObjectId()) 57 | self.assertEqual(resp.status_code, 404) 58 | 59 | c.post('/add', data={'title': 'First Item', 'text': 'The text'}) 60 | 61 | resp = c.get('/show/%s/' % self.Todo.objects.first_or_404().id) 62 | self.assertEqual(resp.status_code, 200) 63 | self.assertEqual(resp.data.decode('utf-8'), 'First Item\nThe text') 64 | 65 | def test_basic_insert(self): 66 | c = self.app.test_client() 67 | c.post('/add', data={'title': 'First Item', 'text': 'The text'}) 68 | c.post('/add', data={'title': '2nd Item', 'text': 'The text'}) 69 | rv = c.get('/') 70 | self.assertEqual(rv.data.decode('utf-8'), 'First Item\n2nd Item') 71 | 72 | def test_request_context(self): 73 | with self.app.test_request_context(): 74 | todo = self.Todo(title='Test', text='test') 75 | todo.save() 76 | self.assertEqual(self.Todo.objects.count(), 1) 77 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import io 2 | import os 3 | from setuptools import setup 4 | 5 | 6 | description = ('Flask-MongoEngine is a Flask extension ' 7 | 'that provides integration with MongoEngine and WTF model forms.') 8 | 9 | # Load index.rst as long_description 10 | doc_path = os.path.join(os.path.dirname(__file__), "docs", "index.rst") 11 | long_description = io.open(doc_path, encoding='utf-8').read() 12 | 13 | # Stops exit traceback on tests 14 | try: 15 | import multiprocessing # noqa 16 | except ImportError: 17 | pass 18 | 19 | 20 | def get_version(version_tuple): 21 | """Return the version tuple as a string, e.g. for (0, 10, 7), 22 | return '0.10.7'. 23 | """ 24 | return '.'.join(map(str, version_tuple)) 25 | 26 | 27 | # Dirty hack to get version number from flask_monogengine/__init__.py - we 28 | # can't import it as it depends on PyMongo and PyMongo isn't installed until 29 | # this file is read 30 | init = os.path.join(os.path.dirname(__file__), 'flask_mongoengine', '__init__.py') 31 | version_line = list(filter(lambda l: l.startswith('VERSION'), open(init)))[0] 32 | version = get_version(eval(version_line.split('=')[-1])) 33 | 34 | test_requirements = ['coverage', 'nose', 'rednose'] 35 | 36 | setup( 37 | name='flask-mongoengine', 38 | version=version, 39 | url='https://github.com/mongoengine/flask-mongoengine', 40 | license='BSD', 41 | author='Ross Lawley', 42 | author_email='ross.lawley@gmail.com', 43 | test_suite='nose.collector', 44 | zip_safe=False, 45 | platforms='any', 46 | install_requires=[ 47 | 'Flask>=1.1', 48 | 'Flask-WTF>=0.14', 49 | 'mongoengine>=0.19', 50 | 'six', 51 | ], 52 | packages=['flask_mongoengine', 53 | 'flask_mongoengine.wtf'], 54 | include_package_data=True, 55 | tests_require=test_requirements, 56 | setup_requires=test_requirements, # Allow proper nose usage with setuptools and tox 57 | description=description, 58 | long_description=long_description, 59 | classifiers=[ 60 | 'Development Status :: 4 - Beta', 61 | 'Environment :: Web Environment', 62 | 'Intended Audience :: Developers', 63 | 'License :: OSI Approved :: BSD License', 64 | 'Operating System :: OS Independent', 65 | 'Programming Language :: Python', 66 | 'Programming Language :: Python :: 3', 67 | 'Programming Language :: Python :: 3.5', 68 | 'Programming Language :: Python :: 3.6', 69 | 'Programming Language :: Python :: 3.7', 70 | 'Programming Language :: Python :: 3.8', 71 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 72 | 'Topic :: Software Development :: Libraries :: Python Modules' 73 | ], 74 | ) 75 | -------------------------------------------------------------------------------- /tests/test_json_app.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import flask 3 | from bson import ObjectId 4 | 5 | from flask_mongoengine import MongoEngine 6 | from tests import FlaskMongoEngineTestCase 7 | 8 | 9 | class JSONAppTestCase(FlaskMongoEngineTestCase): 10 | 11 | def dictContains(self, superset, subset): 12 | for k, v in subset.items(): 13 | if not superset[k] == v: 14 | return False 15 | return True 16 | 17 | def assertDictContains(self, superset, subset): 18 | return self.assertTrue(self.dictContains(superset, subset)) 19 | 20 | def setUp(self): 21 | super(JSONAppTestCase, self).setUp() 22 | self.app.config['MONGODB_DB'] = 'test_db' 23 | self.app.config['TESTING'] = True 24 | self.app.config['TEMP_DB'] = True 25 | db = MongoEngine() 26 | 27 | class Todo(db.Document): 28 | title = db.StringField(max_length=60) 29 | text = db.StringField() 30 | done = db.BooleanField(default=False) 31 | pub_date = db.DateTimeField(default=datetime.datetime.now) 32 | 33 | db.init_app(self.app) 34 | 35 | Todo.drop_collection() 36 | self.Todo = Todo 37 | 38 | @self.app.route('/') 39 | def index(): 40 | return flask.jsonify(result=self.Todo.objects()) 41 | 42 | @self.app.route('/add', methods=['POST']) 43 | def add(): 44 | form = flask.request.form 45 | todo = self.Todo(title=form['title'], 46 | text=form['text']) 47 | todo.save() 48 | return flask.jsonify(result=todo) 49 | 50 | @self.app.route('/show//') 51 | def show(id): 52 | return flask.jsonify(result=self.Todo.objects.get_or_404(id=id)) 53 | 54 | self.db = db 55 | 56 | def test_with_id(self): 57 | c = self.app.test_client() 58 | resp = c.get('/show/%s/' % ObjectId()) 59 | self.assertEqual(resp.status_code, 404) 60 | 61 | rv = c.post('/add', data={'title': 'First Item', 'text': 'The text'}) 62 | self.assertEqual(rv.status_code, 200) 63 | 64 | resp = c.get('/show/%s/' % self.Todo.objects.first().id) 65 | self.assertEqual(resp.status_code, 200) 66 | res = flask.json.loads(resp.data).get('result') 67 | self.assertDictContains(res, { 68 | 'title': 'First Item', 69 | 'text': 'The text' 70 | }) 71 | 72 | def test_basic_insert(self): 73 | c = self.app.test_client() 74 | d1 = {'title': 'First Item', 'text': 'The text'} 75 | d2 = {'title': '2nd Item', 'text': 'The text'} 76 | c.post('/add', data=d1) 77 | c.post('/add', data=d2) 78 | rv = c.get('/') 79 | result = flask.json.loads(rv.data).get('result') 80 | 81 | self.assertEqual(len(result), 2) 82 | 83 | # ensure each of the objects is one of the two we already 84 | # inserted 85 | for obj in result: 86 | self.assertTrue(any([ 87 | self.dictContains(obj, d1), 88 | self.dictContains(obj, d2) 89 | ])) 90 | -------------------------------------------------------------------------------- /flask_mongoengine/sessions.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import sys 3 | import uuid 4 | 5 | from bson.tz_util import utc 6 | from flask.sessions import SessionInterface, SessionMixin 7 | from werkzeug.datastructures import CallbackDict 8 | 9 | __all__ = ("MongoEngineSession", "MongoEngineSessionInterface") 10 | 11 | if sys.version_info >= (3, 0): 12 | basestring = str 13 | 14 | 15 | class MongoEngineSession(CallbackDict, SessionMixin): 16 | 17 | def __init__(self, initial=None, sid=None): 18 | def on_update(self): 19 | self.modified = True 20 | CallbackDict.__init__(self, initial, on_update) 21 | self.sid = sid 22 | self.modified = False 23 | 24 | 25 | class MongoEngineSessionInterface(SessionInterface): 26 | """SessionInterface for mongoengine""" 27 | 28 | def __init__(self, db, collection='session'): 29 | """ 30 | The MongoSessionInterface 31 | 32 | :param db: The app's db eg: MongoEngine() 33 | :param collection: The session collection name defaults to "session" 34 | """ 35 | 36 | if not isinstance(collection, basestring): 37 | raise ValueError('collection argument should be string or unicode') 38 | 39 | class DBSession(db.Document): 40 | sid = db.StringField(primary_key=True) 41 | data = db.DictField() 42 | expiration = db.DateTimeField() 43 | meta = { 44 | 'allow_inheritance': False, 45 | 'collection': collection, 46 | 'indexes': [{'fields': ['expiration'], 47 | 'expireAfterSeconds': 60 * 60 * 24 * 7 * 31}] 48 | } 49 | 50 | self.cls = DBSession 51 | 52 | def get_expiration_time(self, app, session): 53 | if session.permanent: 54 | return app.permanent_session_lifetime 55 | if 'SESSION_TTL' in app.config: 56 | return datetime.timedelta(**app.config['SESSION_TTL']) 57 | return datetime.timedelta(days=1) 58 | 59 | def open_session(self, app, request): 60 | sid = request.cookies.get(app.session_cookie_name) 61 | if sid: 62 | stored_session = self.cls.objects(sid=sid).first() 63 | 64 | if stored_session: 65 | expiration = stored_session.expiration 66 | 67 | if not expiration.tzinfo: 68 | expiration = expiration.replace(tzinfo=utc) 69 | 70 | if expiration > datetime.datetime.utcnow().replace(tzinfo=utc): 71 | return MongoEngineSession(initial=stored_session.data, sid=stored_session.sid) 72 | 73 | return MongoEngineSession(sid=str(uuid.uuid4())) 74 | 75 | def save_session(self, app, session, response): 76 | domain = self.get_cookie_domain(app) 77 | httponly = self.get_cookie_httponly(app) 78 | 79 | if not session: 80 | if session.modified: 81 | response.delete_cookie(app.session_cookie_name, domain=domain) 82 | return 83 | 84 | expiration = datetime.datetime.utcnow().replace(tzinfo=utc) + self.get_expiration_time(app, session) 85 | 86 | if session.modified: 87 | self.cls(sid=session.sid, data=session, expiration=expiration).save() 88 | 89 | response.set_cookie(app.session_cookie_name, session.sid, 90 | expires=expiration, httponly=httponly, domain=domain) 91 | -------------------------------------------------------------------------------- /tests/test_pagination.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from werkzeug.exceptions import NotFound 3 | 4 | from flask_mongoengine import ListFieldPagination, MongoEngine, Pagination 5 | from tests import FlaskMongoEngineTestCase 6 | 7 | 8 | class PaginationTestCase(FlaskMongoEngineTestCase): 9 | 10 | def setUp(self): 11 | super(PaginationTestCase, self).setUp() 12 | self.db_name = 'test_db' 13 | self.app.config['MONGODB_DB'] = self.db_name 14 | self.app.config['TESTING'] = True 15 | self.app.config['CSRF_ENABLED'] = False 16 | self.db = MongoEngine() 17 | self.db.init_app(self.app) 18 | 19 | def tearDown(self): 20 | try: 21 | self.db.connection.drop_database(self.db_name) 22 | except Exception: 23 | self.db.connection.client.drop_database(self.db_name) 24 | 25 | def test_queryset_paginator(self): 26 | with self.app.test_request_context('/'): 27 | db = self.db 28 | 29 | class Post(db.Document): 30 | title = db.StringField(required=True, max_length=200) 31 | 32 | for i in range(42): 33 | Post(title="post: %s" % i).save() 34 | 35 | self.assertRaises(NotFound, Pagination, Post.objects, 0, 10) 36 | self.assertRaises(NotFound, Pagination, Post.objects, 6, 10) 37 | 38 | paginator = Pagination(Post.objects, 1, 10) 39 | self._test_paginator(paginator) 40 | 41 | def test_paginate_plain_list(self): 42 | 43 | self.assertRaises(NotFound, Pagination, range(1, 42), 0, 10) 44 | self.assertRaises(NotFound, Pagination, range(1, 42), 6, 10) 45 | 46 | paginator = Pagination(range(1, 42), 1, 10) 47 | self._test_paginator(paginator) 48 | 49 | def test_list_field_pagination(self): 50 | 51 | with self.app.test_request_context('/'): 52 | db = self.db 53 | 54 | class Post(db.Document): 55 | title = db.StringField(required=True, max_length=200) 56 | comments = db.ListField(db.StringField()) 57 | comment_count = db.IntField() 58 | 59 | comments = ["comment: %s" % i for i in range(42)] 60 | post = Post(title="post has comments", comments=comments, 61 | comment_count=len(comments)).save() 62 | 63 | # Check without providing a total 64 | paginator = ListFieldPagination(Post.objects, post.id, "comments", 65 | 1, 10) 66 | self._test_paginator(paginator) 67 | 68 | # Check with providing a total (saves a query) 69 | paginator = ListFieldPagination(Post.objects, post.id, "comments", 70 | 1, 10, post.comment_count) 71 | self._test_paginator(paginator) 72 | 73 | paginator = post.paginate_field('comments', 1, 10) 74 | self._test_paginator(paginator) 75 | 76 | def _test_paginator(self, paginator): 77 | self.assertEqual(5, paginator.pages) 78 | self.assertEqual([1, 2, 3, 4, 5], list(paginator.iter_pages())) 79 | 80 | for i in [1, 2, 3, 4, 5]: 81 | 82 | if i == 1: 83 | self.assertRaises(NotFound, paginator.prev) 84 | self.assertFalse(paginator.has_prev) 85 | else: 86 | self.assertTrue(paginator.has_prev) 87 | 88 | if i == 5: 89 | self.assertRaises(NotFound, paginator.next) 90 | self.assertFalse(paginator.has_next) 91 | else: 92 | self.assertTrue(paginator.has_next) 93 | 94 | if i == 3: 95 | self.assertEqual([None, 2, 3, 4, None], 96 | list(paginator.iter_pages(0, 1, 1, 0))) 97 | 98 | self.assertEqual(i, paginator.page) 99 | self.assertEqual(i - 1, paginator.prev_num) 100 | self.assertEqual(i + 1, paginator.next_num) 101 | 102 | # Paginate to the next page 103 | if i < 5: 104 | paginator = paginator.next() 105 | 106 | 107 | if __name__ == '__main__': 108 | unittest.main() 109 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | if NOT "%PAPER%" == "" ( 11 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 12 | ) 13 | 14 | if "%1" == "" goto help 15 | 16 | if "%1" == "help" ( 17 | :help 18 | echo.Please use `make ^` where ^ is one of 19 | echo. html to make standalone HTML files 20 | echo. dirhtml to make HTML files named index.html in directories 21 | echo. singlehtml to make a single large HTML file 22 | echo. pickle to make pickle files 23 | echo. json to make JSON files 24 | echo. htmlhelp to make HTML files and a HTML help project 25 | echo. qthelp to make HTML files and a qthelp project 26 | echo. devhelp to make HTML files and a Devhelp project 27 | echo. epub to make an epub 28 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 29 | echo. text to make text files 30 | echo. man to make manual pages 31 | echo. changes to make an overview over all changed/added/deprecated items 32 | echo. linkcheck to check all external links for integrity 33 | echo. doctest to run all doctests embedded in the documentation if enabled 34 | goto end 35 | ) 36 | 37 | if "%1" == "clean" ( 38 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 39 | del /q /s %BUILDDIR%\* 40 | goto end 41 | ) 42 | 43 | if "%1" == "html" ( 44 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 45 | echo. 46 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 47 | goto end 48 | ) 49 | 50 | if "%1" == "dirhtml" ( 51 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 52 | echo. 53 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 54 | goto end 55 | ) 56 | 57 | if "%1" == "singlehtml" ( 58 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 59 | echo. 60 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 61 | goto end 62 | ) 63 | 64 | if "%1" == "pickle" ( 65 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 66 | echo. 67 | echo.Build finished; now you can process the pickle files. 68 | goto end 69 | ) 70 | 71 | if "%1" == "json" ( 72 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 73 | echo. 74 | echo.Build finished; now you can process the JSON files. 75 | goto end 76 | ) 77 | 78 | if "%1" == "htmlhelp" ( 79 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 80 | echo. 81 | echo.Build finished; now you can run HTML Help Workshop with the ^ 82 | .hhp project file in %BUILDDIR%/htmlhelp. 83 | goto end 84 | ) 85 | 86 | if "%1" == "qthelp" ( 87 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 88 | echo. 89 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 90 | .qhcp project file in %BUILDDIR%/qthelp, like this: 91 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\flask-unittest.qhcp 92 | echo.To view the help file: 93 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\flask-unittest.ghc 94 | goto end 95 | ) 96 | 97 | if "%1" == "devhelp" ( 98 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 99 | echo. 100 | echo.Build finished. 101 | goto end 102 | ) 103 | 104 | if "%1" == "epub" ( 105 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 106 | echo. 107 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 108 | goto end 109 | ) 110 | 111 | if "%1" == "latex" ( 112 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 113 | echo. 114 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 115 | goto end 116 | ) 117 | 118 | if "%1" == "text" ( 119 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 120 | echo. 121 | echo.Build finished. The text files are in %BUILDDIR%/text. 122 | goto end 123 | ) 124 | 125 | if "%1" == "man" ( 126 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 127 | echo. 128 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 129 | goto end 130 | ) 131 | 132 | if "%1" == "changes" ( 133 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 134 | echo. 135 | echo.The overview file is in %BUILDDIR%/changes. 136 | goto end 137 | ) 138 | 139 | if "%1" == "linkcheck" ( 140 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 141 | echo. 142 | echo.Link check complete; look for any errors in the above output ^ 143 | or in %BUILDDIR%/linkcheck/output.txt. 144 | goto end 145 | ) 146 | 147 | if "%1" == "doctest" ( 148 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 149 | echo. 150 | echo.Testing of doctests in the sources finished, look at the ^ 151 | results in %BUILDDIR%/doctest/output.txt. 152 | goto end 153 | ) 154 | 155 | :end 156 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | Changelog 3 | ========= 4 | 5 | Development 6 | =========== 7 | - BREAKING CHANGE: Dropped Python v2.6, v2.7, v3.2, v3.3, v3.4 (#355) 8 | - BREAKING CHANGE: Added tests support for python versions: v3.5, v3.6, v3.7, v3.8 (#355) 9 | - BREAKING CHANGE: Minimum Flask version set to v1.1 (#355) 10 | - BREAKING CHANGE: Minimum Flask-WTF version set to v0.14 (#355) 11 | - BREAKING CHANGE: Minimum mongoengine version set to v0.19 (#355) 12 | - BREAKING CHANGE: Minimum mongodb version set to v4.0 (#355) 13 | - CHANGED: Internal test engine switched from nose to pytest (#357) 14 | - DROPPED: Internal check with flake8-import-order dropped, as not 15 | compatible with modern editors (#358) 16 | - UPDATED: Functions `get_or_404`, `first_or_404` now accepts `message` 17 | argument, and will display custom message if specified. (#351) 18 | - UPDATED: `get_or_404` will raise 404 error only on `DoesNotExist` exception, 19 | other exceptions should be captured by user. (#360) 20 | 21 | Tests and development for old packages versions dropped to minimize tests footprint. 22 | 23 | Use version 0.9.5 if old dependencies required. 24 | 25 | Changes in 0.9.1 26 | ================ 27 | - Fixed setup.py for various platforms (#298). 28 | - Added Flask-WTF v0.14 support (#294). 29 | - MongoEngine instance now holds a reference to a particular Flask app it was initialized with (#261). 30 | 31 | Changes in 0.9.0 32 | ================ 33 | - BREAKING CHANGE: Dropped Python v2.6 support 34 | 35 | Changes in 0.8.2 36 | ================ 37 | - Fixed relying on mongoengine.python_support. 38 | - Fixed cleaning up empty connection settings #285 39 | 40 | Changes in 0.8.1 41 | ================ 42 | 43 | - Fixed connection issues introduced in 0.8 44 | - Removed support for MongoMock 45 | 46 | Changes in 0.8 47 | ============== 48 | 49 | - Dropped MongoEngine 0.7 support 50 | - Added MongoEngine 0.10 support 51 | - Added PyMongo 3 support 52 | - Added Python3 support up to 3.5 53 | - Allowed empying value list in SelectMultipleField 54 | - Fixed paginator issues 55 | - Use InputRequired validator to allow 0 in required field 56 | - Made help_text Field attribute optional 57 | - Added "radio" form_arg to convert field into RadioField 58 | - Added "textarea" form_arg to force conversion into TextAreaField 59 | - Added field parameters (validators, filters...) 60 | - Fixed 'False' connection settings ignored 61 | - Fixed bug to allow multiple instances of extension 62 | - Added MongoEngineSessionInterface support for PyMongo's tz_aware option 63 | - Support arbitrary primary key fields (not "id") 64 | - Configurable httponly flag for MongoEngineSessionInterface 65 | - Various bugfixes, code cleanup and documentation improvements 66 | - Move from deprecated flask.ext.* to flask_* syntax in imports 67 | - Added independent connection handler for FlaskMongoEngine 68 | - All MongoEngine connection calls are proxied via FlaskMongoEngine connection handler 69 | - Added backward compatibility for settings key names 70 | - Added support for MongoMock and temporary test DB 71 | - Fixed issue with multiple DB support 72 | - Various bugfixes 73 | 74 | Changes in 0.7 75 | ============== 76 | - Fixed only / exclude in model forms (#49) 77 | - Added automatic choices coerce for simple types (#34) 78 | - Fixed EmailField and URLField rendering and validation (#44, #9) 79 | - Use help_text for field description (#43) 80 | - Fixed Pagination and added Document.paginate_field() helper 81 | - Keep model_forms fields in order of creation 82 | - Added MongoEngineSessionInterface (#5) 83 | - Added customisation hooks for FieldList sub fields (#19) 84 | - Handle non ascii chars in the MongoDebugPanel (#22) 85 | - Fixed toolbar stacktrace if a html directory is in the path (#31) 86 | - ModelForms no longer patch Document.update (#32) 87 | - No longer wipe field kwargs in ListField (#20, #19) 88 | - Passthrough ModelField.save-arguments (#26) 89 | - QuerySetSelectMultipleField now supports initial value (#27) 90 | - Clarified configuration documentation (#33) 91 | - Fixed forms when EmbeddedDocument has no default (#36) 92 | - Fixed multiselect restore bug (#37) 93 | - Split out the examples into a single file app and a cross file app 94 | 95 | Changes in 0.6 96 | ============== 97 | - Support for JSON and DictFields 98 | - Speeding up QuerySetSelectField with big querysets 99 | 100 | Changes in 0.5 101 | ============== 102 | - Added support for all connection settings 103 | - Fixed extended DynamicDocument 104 | 105 | Changes in 0.4 106 | ============== 107 | - Added CSRF support and validate_on_save via flask.ext.WTF 108 | - Fixed DateTimeField not required 109 | 110 | Changes in 0.3 111 | =============== 112 | - Reverted mongopanel - got knocked out by a merge 113 | - Updated imports paths 114 | 115 | Changes in 0.2 116 | =============== 117 | - Added support for password StringField 118 | - Added ModelSelectMultiple 119 | 120 | Changes in 0.1 121 | =============== 122 | - Released to PyPi 123 | -------------------------------------------------------------------------------- /docs/_themes/flask_theme_support.py: -------------------------------------------------------------------------------- 1 | # flasky extensions. flasky pygments style based on tango style 2 | from pygments.style import Style 3 | from pygments.token import Keyword, Name, Comment, String, Error, \ 4 | Number, Operator, Generic, Whitespace, Punctuation, Other, Literal 5 | 6 | 7 | class FlaskyStyle(Style): 8 | background_color = "#f8f8f8" 9 | default_style = "" 10 | 11 | styles = { 12 | # No corresponding class for the following: 13 | #Text: "", # class: '' 14 | Whitespace: "underline #f8f8f8", # class: 'w' 15 | Error: "#a40000 border:#ef2929", # class: 'err' 16 | Other: "#000000", # class 'x' 17 | 18 | Comment: "italic #8f5902", # class: 'c' 19 | Comment.Preproc: "noitalic", # class: 'cp' 20 | 21 | Keyword: "bold #004461", # class: 'k' 22 | Keyword.Constant: "bold #004461", # class: 'kc' 23 | Keyword.Declaration: "bold #004461", # class: 'kd' 24 | Keyword.Namespace: "bold #004461", # class: 'kn' 25 | Keyword.Pseudo: "bold #004461", # class: 'kp' 26 | Keyword.Reserved: "bold #004461", # class: 'kr' 27 | Keyword.Type: "bold #004461", # class: 'kt' 28 | 29 | Operator: "#582800", # class: 'o' 30 | Operator.Word: "bold #004461", # class: 'ow' - like keywords 31 | 32 | Punctuation: "bold #000000", # class: 'p' 33 | 34 | # because special names such as Name.Class, Name.Function, etc. 35 | # are not recognized as such later in the parsing, we choose them 36 | # to look the same as ordinary variables. 37 | Name: "#000000", # class: 'n' 38 | Name.Attribute: "#c4a000", # class: 'na' - to be revised 39 | Name.Builtin: "#004461", # class: 'nb' 40 | Name.Builtin.Pseudo: "#3465a4", # class: 'bp' 41 | Name.Class: "#000000", # class: 'nc' - to be revised 42 | Name.Constant: "#000000", # class: 'no' - to be revised 43 | Name.Decorator: "#888", # class: 'nd' - to be revised 44 | Name.Entity: "#ce5c00", # class: 'ni' 45 | Name.Exception: "bold #cc0000", # class: 'ne' 46 | Name.Function: "#000000", # class: 'nf' 47 | Name.Property: "#000000", # class: 'py' 48 | Name.Label: "#f57900", # class: 'nl' 49 | Name.Namespace: "#000000", # class: 'nn' - to be revised 50 | Name.Other: "#000000", # class: 'nx' 51 | Name.Tag: "bold #004461", # class: 'nt' - like a keyword 52 | Name.Variable: "#000000", # class: 'nv' - to be revised 53 | Name.Variable.Class: "#000000", # class: 'vc' - to be revised 54 | Name.Variable.Global: "#000000", # class: 'vg' - to be revised 55 | Name.Variable.Instance: "#000000", # class: 'vi' - to be revised 56 | 57 | Number: "#990000", # class: 'm' 58 | 59 | Literal: "#000000", # class: 'l' 60 | Literal.Date: "#000000", # class: 'ld' 61 | 62 | String: "#4e9a06", # class: 's' 63 | String.Backtick: "#4e9a06", # class: 'sb' 64 | String.Char: "#4e9a06", # class: 'sc' 65 | String.Doc: "italic #8f5902", # class: 'sd' - like a comment 66 | String.Double: "#4e9a06", # class: 's2' 67 | String.Escape: "#4e9a06", # class: 'se' 68 | String.Heredoc: "#4e9a06", # class: 'sh' 69 | String.Interpol: "#4e9a06", # class: 'si' 70 | String.Other: "#4e9a06", # class: 'sx' 71 | String.Regex: "#4e9a06", # class: 'sr' 72 | String.Single: "#4e9a06", # class: 's1' 73 | String.Symbol: "#4e9a06", # class: 'ss' 74 | 75 | Generic: "#000000", # class: 'g' 76 | Generic.Deleted: "#a40000", # class: 'gd' 77 | Generic.Emph: "italic #000000", # class: 'ge' 78 | Generic.Error: "#ef2929", # class: 'gr' 79 | Generic.Heading: "bold #000080", # class: 'gh' 80 | Generic.Inserted: "#00A000", # class: 'gi' 81 | Generic.Output: "#888", # class: 'go' 82 | Generic.Prompt: "#745334", # class: 'gp' 83 | Generic.Strong: "bold #000000", # class: 'gs' 84 | Generic.Subheading: "bold #800080", # class: 'gu' 85 | Generic.Traceback: "bold #a40000", # class: 'gt' 86 | } 87 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | 15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " singlehtml to make a single large HTML file" 22 | @echo " pickle to make pickle files" 23 | @echo " json to make JSON files" 24 | @echo " htmlhelp to make HTML files and a HTML help project" 25 | @echo " qthelp to make HTML files and a qthelp project" 26 | @echo " devhelp to make HTML files and a Devhelp project" 27 | @echo " epub to make an epub" 28 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 29 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 30 | @echo " text to make text files" 31 | @echo " man to make manual pages" 32 | @echo " changes to make an overview of all changed/added/deprecated items" 33 | @echo " linkcheck to check all external links for integrity" 34 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 35 | 36 | clean: 37 | -rm -rf $(BUILDDIR)/* 38 | 39 | html: 40 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 41 | @echo 42 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 43 | 44 | dirhtml: 45 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 48 | 49 | singlehtml: 50 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 51 | @echo 52 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 53 | 54 | pickle: 55 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 56 | @echo 57 | @echo "Build finished; now you can process the pickle files." 58 | 59 | json: 60 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 61 | @echo 62 | @echo "Build finished; now you can process the JSON files." 63 | 64 | htmlhelp: 65 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 66 | @echo 67 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 68 | ".hhp project file in $(BUILDDIR)/htmlhelp." 69 | 70 | qthelp: 71 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 72 | @echo 73 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 74 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 75 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/flask-unittest.qhcp" 76 | @echo "To view the help file:" 77 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/flask-unittest.qhc" 78 | 79 | devhelp: 80 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 81 | @echo 82 | @echo "Build finished." 83 | @echo "To view the help file:" 84 | @echo "# mkdir -p $$HOME/.local/share/devhelp/flask-unittest" 85 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/flask-unittest" 86 | @echo "# devhelp" 87 | 88 | epub: 89 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 90 | @echo 91 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 92 | 93 | latex: 94 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 95 | @echo 96 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 97 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 98 | "(use \`make latexpdf' here to do that automatically)." 99 | 100 | latexpdf: 101 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 102 | @echo "Running LaTeX files through pdflatex..." 103 | make -C $(BUILDDIR)/latex all-pdf 104 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 105 | 106 | text: 107 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 108 | @echo 109 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 110 | 111 | man: 112 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 113 | @echo 114 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 115 | 116 | changes: 117 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 118 | @echo 119 | @echo "The overview file is in $(BUILDDIR)/changes." 120 | 121 | linkcheck: 122 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 123 | @echo 124 | @echo "Link check complete; look for any errors in the above output " \ 125 | "or in $(BUILDDIR)/linkcheck/output.txt." 126 | 127 | doctest: 128 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 129 | @echo "Testing of doctests in the sources finished, look at the " \ 130 | "results in $(BUILDDIR)/doctest/output.txt." 131 | -------------------------------------------------------------------------------- /flask_mongoengine/connection.py: -------------------------------------------------------------------------------- 1 | import mongoengine 2 | from pymongo import ReadPreference, uri_parser 3 | 4 | 5 | __all__ = ( 6 | 'create_connections', 'get_connection_settings', 'InvalidSettingsError', 7 | ) 8 | 9 | 10 | MONGODB_CONF_VARS = ('MONGODB_ALIAS', 'MONGODB_DB', 'MONGODB_HOST', 'MONGODB_IS_MOCK', 11 | 'MONGODB_PASSWORD', 'MONGODB_PORT', 'MONGODB_USERNAME', 12 | 'MONGODB_CONNECT', 'MONGODB_TZ_AWARE') 13 | 14 | 15 | class InvalidSettingsError(Exception): 16 | pass 17 | 18 | 19 | def _sanitize_settings(settings): 20 | """Given a dict of connection settings, sanitize the keys and fall 21 | back to some sane defaults. 22 | """ 23 | # Remove the "MONGODB_" prefix and make all settings keys lower case. 24 | resolved_settings = {} 25 | for k, v in settings.items(): 26 | if k.startswith('MONGODB_'): 27 | k = k[len('MONGODB_'):] 28 | k = k.lower() 29 | resolved_settings[k] = v 30 | 31 | # Handle uri style connections 32 | if "://" in resolved_settings.get('host', ''): 33 | # this section pulls the database name from the URI 34 | # PyMongo requires URI to start with mongodb:// to parse 35 | # this workaround allows mongomock to work 36 | uri_to_check = resolved_settings['host'] 37 | 38 | if uri_to_check.startswith('mongomock://'): 39 | uri_to_check = uri_to_check.replace('mongomock://', 'mongodb://') 40 | 41 | uri_dict = uri_parser.parse_uri(uri_to_check) 42 | resolved_settings['db'] = uri_dict['database'] 43 | 44 | # Add a default name param or use the "db" key if exists 45 | if resolved_settings.get('db'): 46 | resolved_settings['name'] = resolved_settings.pop('db') 47 | else: 48 | resolved_settings['name'] = 'test' 49 | 50 | # Add various default values. 51 | resolved_settings['alias'] = resolved_settings.get('alias', mongoengine.DEFAULT_CONNECTION_NAME) # TODO do we have to specify it here? MongoEngine should take care of that 52 | resolved_settings['host'] = resolved_settings.get('host', 'localhost') # TODO this is the default host in pymongo.mongo_client.MongoClient, we may not need to explicitly set a default here 53 | resolved_settings['port'] = resolved_settings.get('port', 27017) # TODO this is the default port in pymongo.mongo_client.MongoClient, we may not need to explicitly set a default here 54 | 55 | # Default to ReadPreference.PRIMARY if no read_preference is supplied 56 | resolved_settings['read_preference'] = resolved_settings.get('read_preference', ReadPreference.PRIMARY) 57 | 58 | # Clean up empty values 59 | for k, v in list(resolved_settings.items()): 60 | if v is None: 61 | del resolved_settings[k] 62 | 63 | return resolved_settings 64 | 65 | 66 | def get_connection_settings(config): 67 | """ 68 | Given a config dict, return a sanitized dict of MongoDB connection 69 | settings that we can then use to establish connections. For new 70 | applications, settings should exist in a "MONGODB_SETTINGS" key, but 71 | for backward compactibility we also support several config keys 72 | prefixed by "MONGODB_", e.g. "MONGODB_HOST", "MONGODB_PORT", etc. 73 | """ 74 | # Sanitize all the settings living under a "MONGODB_SETTINGS" config var 75 | if 'MONGODB_SETTINGS' in config: 76 | settings = config['MONGODB_SETTINGS'] 77 | 78 | # If MONGODB_SETTINGS is a list of settings dicts, sanitize each 79 | # dict separately. 80 | if isinstance(settings, list): 81 | # List of connection settings. 82 | settings_list = [] 83 | for setting in settings: 84 | settings_list.append(_sanitize_settings(setting)) 85 | return settings_list 86 | 87 | # Otherwise, it should be a single dict describing a single connection. 88 | else: 89 | return _sanitize_settings(settings) 90 | 91 | # If "MONGODB_SETTINGS" doesn't exist, sanitize the "MONGODB_" keys 92 | # as if they all describe a single connection. 93 | else: 94 | config = dict((k, v) for k, v in config.items() if k in MONGODB_CONF_VARS) # ugly dict comprehention in order to support python 2.6 95 | return _sanitize_settings(config) 96 | 97 | 98 | def create_connections(config): 99 | """ 100 | Given Flask application's config dict, extract relevant config vars 101 | out of it and establish MongoEngine connection(s) based on them. 102 | """ 103 | # Validate that the config is a dict 104 | if config is None or not isinstance(config, dict): 105 | raise InvalidSettingsError('Invalid application configuration') 106 | 107 | # Get sanitized connection settings based on the config 108 | conn_settings = get_connection_settings(config) 109 | 110 | # If conn_settings is a list, set up each item as a separate connection 111 | # and return a dict of connection aliases and their connections. 112 | if isinstance(conn_settings, list): 113 | connections = {} 114 | for each in conn_settings: 115 | alias = each['alias'] 116 | connections[alias] = _connect(each) 117 | return connections 118 | 119 | # Otherwise, return a single connection 120 | return _connect(conn_settings) 121 | 122 | 123 | def _connect(conn_settings): 124 | """Given a dict of connection settings, create a connection to 125 | MongoDB by calling mongoengine.connect and return its result. 126 | """ 127 | db_name = conn_settings.pop('name') 128 | return mongoengine.connect(db_name, **conn_settings) 129 | -------------------------------------------------------------------------------- /docs/_themes/flask_small/static/flasky.css_t: -------------------------------------------------------------------------------- 1 | /* 2 | * flasky.css_t 3 | * ~~~~~~~~~~~~ 4 | * 5 | * Sphinx stylesheet -- flasky theme based on nature theme. 6 | * 7 | * :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. 8 | * :license: BSD, see LICENSE for details. 9 | * 10 | */ 11 | 12 | @import url("basic.css"); 13 | 14 | /* -- page layout ----------------------------------------------------------- */ 15 | 16 | body { 17 | font-family: 'Georgia', serif; 18 | font-size: 17px; 19 | color: #000; 20 | background: white; 21 | margin: 0; 22 | padding: 0; 23 | } 24 | 25 | div.documentwrapper { 26 | float: left; 27 | width: 100%; 28 | } 29 | 30 | div.bodywrapper { 31 | margin: 40px auto 0 auto; 32 | width: 700px; 33 | } 34 | 35 | hr { 36 | border: 1px solid #B1B4B6; 37 | } 38 | 39 | div.body { 40 | background-color: #ffffff; 41 | color: #3E4349; 42 | padding: 0 30px 30px 30px; 43 | } 44 | 45 | img.floatingflask { 46 | padding: 0 0 10px 10px; 47 | float: right; 48 | } 49 | 50 | div.footer { 51 | text-align: right; 52 | color: #888; 53 | padding: 10px; 54 | font-size: 14px; 55 | width: 650px; 56 | margin: 0 auto 40px auto; 57 | } 58 | 59 | div.footer a { 60 | color: #888; 61 | text-decoration: underline; 62 | } 63 | 64 | div.related { 65 | line-height: 32px; 66 | color: #888; 67 | } 68 | 69 | div.related ul { 70 | padding: 0 0 0 10px; 71 | } 72 | 73 | div.related a { 74 | color: #444; 75 | } 76 | 77 | /* -- body styles ----------------------------------------------------------- */ 78 | 79 | a { 80 | color: #004B6B; 81 | text-decoration: underline; 82 | } 83 | 84 | a:hover { 85 | color: #6D4100; 86 | text-decoration: underline; 87 | } 88 | 89 | div.body { 90 | padding-bottom: 40px; /* saved for footer */ 91 | } 92 | 93 | div.body h1, 94 | div.body h2, 95 | div.body h3, 96 | div.body h4, 97 | div.body h5, 98 | div.body h6 { 99 | font-family: 'Garamond', 'Georgia', serif; 100 | font-weight: normal; 101 | margin: 30px 0px 10px 0px; 102 | padding: 0; 103 | } 104 | 105 | {% if theme_index_logo %} 106 | div.indexwrapper h1 { 107 | text-indent: -999999px; 108 | background: url({{ theme_index_logo }}) no-repeat center center; 109 | height: {{ theme_index_logo_height }}; 110 | } 111 | {% endif %} 112 | 113 | div.body h2 { font-size: 180%; } 114 | div.body h3 { font-size: 150%; } 115 | div.body h4 { font-size: 130%; } 116 | div.body h5 { font-size: 100%; } 117 | div.body h6 { font-size: 100%; } 118 | 119 | a.headerlink { 120 | color: white; 121 | padding: 0 4px; 122 | text-decoration: none; 123 | } 124 | 125 | a.headerlink:hover { 126 | color: #444; 127 | background: #eaeaea; 128 | } 129 | 130 | div.body p, div.body dd, div.body li { 131 | line-height: 1.4em; 132 | } 133 | 134 | div.admonition { 135 | background: #fafafa; 136 | margin: 20px -30px; 137 | padding: 10px 30px; 138 | border-top: 1px solid #ccc; 139 | border-bottom: 1px solid #ccc; 140 | } 141 | 142 | div.admonition p.admonition-title { 143 | font-family: 'Garamond', 'Georgia', serif; 144 | font-weight: normal; 145 | font-size: 24px; 146 | margin: 0 0 10px 0; 147 | padding: 0; 148 | line-height: 1; 149 | } 150 | 151 | div.admonition p.last { 152 | margin-bottom: 0; 153 | } 154 | 155 | div.highlight{ 156 | background-color: white; 157 | } 158 | 159 | dt:target, .highlight { 160 | background: #FAF3E8; 161 | } 162 | 163 | div.note { 164 | background-color: #eee; 165 | border: 1px solid #ccc; 166 | } 167 | 168 | div.seealso { 169 | background-color: #ffc; 170 | border: 1px solid #ff6; 171 | } 172 | 173 | div.topic { 174 | background-color: #eee; 175 | } 176 | 177 | div.warning { 178 | background-color: #ffe4e4; 179 | border: 1px solid #f66; 180 | } 181 | 182 | p.admonition-title { 183 | display: inline; 184 | } 185 | 186 | p.admonition-title:after { 187 | content: ":"; 188 | } 189 | 190 | pre, tt { 191 | font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; 192 | font-size: 0.85em; 193 | } 194 | 195 | img.screenshot { 196 | } 197 | 198 | tt.descname, tt.descclassname { 199 | font-size: 0.95em; 200 | } 201 | 202 | tt.descname { 203 | padding-right: 0.08em; 204 | } 205 | 206 | img.screenshot { 207 | -moz-box-shadow: 2px 2px 4px #eee; 208 | -webkit-box-shadow: 2px 2px 4px #eee; 209 | box-shadow: 2px 2px 4px #eee; 210 | } 211 | 212 | table.docutils { 213 | border: 1px solid #888; 214 | -moz-box-shadow: 2px 2px 4px #eee; 215 | -webkit-box-shadow: 2px 2px 4px #eee; 216 | box-shadow: 2px 2px 4px #eee; 217 | } 218 | 219 | table.docutils td, table.docutils th { 220 | border: 1px solid #888; 221 | padding: 0.25em 0.7em; 222 | } 223 | 224 | table.field-list, table.footnote { 225 | border: none; 226 | -moz-box-shadow: none; 227 | -webkit-box-shadow: none; 228 | box-shadow: none; 229 | } 230 | 231 | table.footnote { 232 | margin: 15px 0; 233 | width: 100%; 234 | border: 1px solid #eee; 235 | } 236 | 237 | table.field-list th { 238 | padding: 0 0.8em 0 0; 239 | } 240 | 241 | table.field-list td { 242 | padding: 0; 243 | } 244 | 245 | table.footnote td { 246 | padding: 0.5em; 247 | } 248 | 249 | dl { 250 | margin: 0; 251 | padding: 0; 252 | } 253 | 254 | dl dd { 255 | margin-left: 30px; 256 | } 257 | 258 | pre { 259 | padding: 0; 260 | margin: 15px -30px; 261 | padding: 8px; 262 | line-height: 1.3em; 263 | padding: 7px 30px; 264 | background: #eee; 265 | border-radius: 2px; 266 | -moz-border-radius: 2px; 267 | -webkit-border-radius: 2px; 268 | } 269 | 270 | dl pre { 271 | margin-left: -60px; 272 | padding-left: 60px; 273 | } 274 | 275 | tt { 276 | background-color: #ecf0f3; 277 | color: #222; 278 | /* padding: 1px 2px; */ 279 | } 280 | 281 | tt.xref, a tt { 282 | background-color: #FBFBFB; 283 | } 284 | 285 | a:hover tt { 286 | background: #EEE; 287 | } 288 | -------------------------------------------------------------------------------- /flask_mongoengine/pagination.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import math 3 | from flask import abort 4 | from mongoengine.queryset import QuerySet 5 | 6 | __all__ = ("Pagination", "ListFieldPagination") 7 | 8 | 9 | class Pagination(object): 10 | 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(iterable, QuerySet): 21 | self.total = iterable.count() 22 | else: 23 | self.total = len(iterable) 24 | 25 | start_index = (page - 1) * per_page 26 | end_index = page * per_page 27 | 28 | self.items = iterable[start_index:end_index] 29 | if isinstance(self.items, QuerySet): 30 | self.items = self.items.select_related() 31 | if not self.items and page != 1: 32 | abort(404) 33 | 34 | @property 35 | def pages(self): 36 | """The total number of pages""" 37 | return int(math.ceil(self.total / float(self.per_page))) 38 | 39 | def prev(self, error_out=False): 40 | """Returns a :class:`Pagination` object for the previous page.""" 41 | assert self.iterable is not None, ('an object is required ' 42 | 'for this method to work') 43 | iterable = self.iterable 44 | if isinstance(iterable, QuerySet): 45 | iterable._skip = None 46 | iterable._limit = None 47 | return self.__class__(iterable, self.page - 1, self.per_page) 48 | 49 | @property 50 | def prev_num(self): 51 | """Number of the previous page.""" 52 | return self.page - 1 53 | 54 | @property 55 | def has_prev(self): 56 | """True if a previous page exists""" 57 | return self.page > 1 58 | 59 | def next(self, error_out=False): 60 | """Returns a :class:`Pagination` object for the next page.""" 61 | assert self.iterable is not None, ('an object is required ' 62 | 'for this method to work') 63 | iterable = self.iterable 64 | if isinstance(iterable, QuerySet): 65 | iterable._skip = None 66 | iterable._limit = None 67 | return self.__class__(iterable, self.page + 1, self.per_page) 68 | 69 | @property 70 | def has_next(self): 71 | """True if a next page exists.""" 72 | return self.page < self.pages 73 | 74 | @property 75 | def next_num(self): 76 | """Number of the next page""" 77 | return self.page + 1 78 | 79 | def iter_pages(self, left_edge=2, left_current=2, 80 | right_current=5, right_edge=2): 81 | """Iterates over the page numbers in the pagination. The four 82 | parameters control the thresholds how many numbers should be produced 83 | from the sides. Skipped page numbers are represented as `None`. 84 | This is how you could render such a pagination in the templates: 85 | 86 | .. sourcecode:: html+jinja 87 | 88 | {% macro render_pagination(pagination, endpoint) %} 89 | 102 | {% endmacro %} 103 | """ 104 | last = 0 105 | for num in range(1, self.pages + 1): 106 | if ( 107 | num <= left_edge 108 | or num > self.pages - right_edge 109 | or (num >= self.page - left_current 110 | and num <= self.page + right_current) 111 | ): 112 | if last + 1 != num: 113 | yield None 114 | yield num 115 | last = num 116 | if last != self.pages: 117 | yield None 118 | 119 | 120 | class ListFieldPagination(Pagination): 121 | 122 | def __init__(self, queryset, doc_id, field_name, page, per_page, 123 | total=None): 124 | """Allows an array within a document to be paginated. 125 | 126 | Queryset must contain the document which has the array we're 127 | paginating, and doc_id should be it's _id. 128 | Field name is the name of the array we're paginating. 129 | Page and per_page work just like in Pagination. 130 | Total is an argument because it can be computed more efficiently 131 | elsewhere, but we still use array.length as a fallback. 132 | """ 133 | if page < 1: 134 | abort(404) 135 | 136 | self.page = page 137 | self.per_page = per_page 138 | 139 | self.queryset = queryset 140 | self.doc_id = doc_id 141 | self.field_name = field_name 142 | 143 | start_index = (page - 1) * per_page 144 | 145 | field_attrs = {field_name: {"$slice": [start_index, per_page]}} 146 | 147 | qs = queryset(pk=doc_id) 148 | self.items = getattr(qs.fields(**field_attrs).first(), field_name) 149 | self.total = total or len(getattr(qs.fields(**{field_name: 1}).first(), 150 | field_name)) 151 | 152 | if not self.items and page != 1: 153 | abort(404) 154 | 155 | def prev(self, error_out=False): 156 | """Returns a :class:`Pagination` object for the previous page.""" 157 | assert self.items is not None, ('a query object is required ' 158 | 'for this method to work') 159 | return self.__class__(self.queryset, self.doc_id, self.field_name, 160 | self.page - 1, self.per_page, self.total) 161 | 162 | def next(self, error_out=False): 163 | """Returns a :class:`Pagination` object for the next page.""" 164 | assert self.items is not None, ('a query object is required ' 165 | 'for this method to work') 166 | return self.__class__(self.queryset, self.doc_id, self.field_name, 167 | self.page + 1, self.per_page, self.total) 168 | -------------------------------------------------------------------------------- /flask_mongoengine/wtf/fields.py: -------------------------------------------------------------------------------- 1 | """ 2 | Useful form fields for use with the mongoengine. 3 | """ 4 | import json 5 | import sys 6 | from gettext import gettext as _ 7 | 8 | from mongoengine.queryset import DoesNotExist 9 | import six 10 | from wtforms import widgets 11 | from wtforms.fields import SelectFieldBase, StringField, TextAreaField 12 | from wtforms.validators import ValidationError 13 | 14 | __all__ = ( 15 | 'ModelSelectField', 'QuerySetSelectField', 16 | ) 17 | 18 | if sys.version_info >= (3, 0): 19 | unicode = str 20 | 21 | 22 | class QuerySetSelectField(SelectFieldBase): 23 | """ 24 | Given a QuerySet either at initialization or inside a view, will display a 25 | select drop-down field of choices. The `data` property actually will 26 | store/keep an ORM model instance, not the ID. Submitting a choice which is 27 | not in the queryset will result in a validation error. 28 | 29 | Specifying `label_attr` in the constructor will use that property of the 30 | model instance for display in the list, else the model object's `__str__` 31 | or `__unicode__` will be used. 32 | 33 | If `allow_blank` is set to `True`, then a blank choice will be added to the 34 | top of the list. Selecting this choice will result in the `data` property 35 | being `None`. The label for the blank choice can be set by specifying the 36 | `blank_text` parameter. 37 | """ 38 | widget = widgets.Select() 39 | 40 | def __init__(self, label=u'', validators=None, queryset=None, 41 | label_attr='', allow_blank=False, blank_text=u'---', 42 | **kwargs): 43 | 44 | super(QuerySetSelectField, self).__init__(label, validators, **kwargs) 45 | self.label_attr = label_attr 46 | self.allow_blank = allow_blank 47 | self.blank_text = blank_text 48 | self.queryset = queryset 49 | 50 | def iter_choices(self): 51 | if self.allow_blank: 52 | yield (u'__None', self.blank_text, self.data is None) 53 | 54 | if self.queryset is None: 55 | return 56 | 57 | self.queryset.rewind() 58 | for obj in self.queryset: 59 | label = self.label_attr and getattr(obj, self.label_attr) or obj 60 | if isinstance(self.data, list): 61 | selected = obj in self.data 62 | else: 63 | selected = self._is_selected(obj) 64 | yield (obj.id, label, selected) 65 | 66 | def process_formdata(self, valuelist): 67 | if valuelist: 68 | if valuelist[0] == '__None': 69 | self.data = None 70 | else: 71 | if self.queryset is None: 72 | self.data = None 73 | return 74 | 75 | try: 76 | obj = self.queryset.get(pk=valuelist[0]) 77 | self.data = obj 78 | except DoesNotExist: 79 | self.data = None 80 | 81 | def pre_validate(self, form): 82 | if not self.allow_blank or self.data is not None: 83 | if not self.data: 84 | raise ValidationError(_(u'Not a valid choice')) 85 | 86 | def _is_selected(self, item): 87 | return item == self.data 88 | 89 | 90 | class QuerySetSelectMultipleField(QuerySetSelectField): 91 | 92 | widget = widgets.Select(multiple=True) 93 | 94 | def __init__(self, label=u'', validators=None, queryset=None, 95 | label_attr='', allow_blank=False, blank_text=u'---', 96 | **kwargs): 97 | 98 | super(QuerySetSelectMultipleField, self).__init__( 99 | label, validators, queryset, label_attr, allow_blank, blank_text, 100 | **kwargs) 101 | 102 | def process_formdata(self, valuelist): 103 | 104 | if valuelist: 105 | if valuelist[0] == '__None': 106 | self.data = None 107 | else: 108 | if not self.queryset: 109 | self.data = None 110 | return 111 | 112 | self.queryset.rewind() 113 | self.data = list(self.queryset(pk__in=valuelist)) 114 | if not len(self.data): 115 | self.data = None 116 | 117 | # If no value passed, empty the list 118 | else: 119 | self.data = None 120 | 121 | def _is_selected(self, item): 122 | return item in self.data if self.data else False 123 | 124 | 125 | class ModelSelectField(QuerySetSelectField): 126 | """ 127 | Like a QuerySetSelectField, except takes a model class instead of a 128 | queryset and lists everything in it. 129 | """ 130 | def __init__(self, label=u'', validators=None, model=None, **kwargs): 131 | queryset = kwargs.pop('queryset', model.objects) 132 | super(ModelSelectField, self).__init__(label, validators, queryset=queryset, **kwargs) 133 | 134 | 135 | class ModelSelectMultipleField(QuerySetSelectMultipleField): 136 | """ 137 | Allows multiple select 138 | """ 139 | def __init__(self, label=u'', validators=None, model=None, **kwargs): 140 | queryset = kwargs.pop('queryset', model.objects) 141 | super(ModelSelectMultipleField, self).__init__(label, validators, queryset=queryset, **kwargs) 142 | 143 | 144 | class JSONField(TextAreaField): 145 | def _value(self): 146 | if self.raw_data: 147 | return self.raw_data[0] 148 | else: 149 | return self.data and unicode(json.dumps(self.data)) or u'' 150 | 151 | def process_formdata(self, value): 152 | if value: 153 | try: 154 | self.data = json.loads(value[0]) 155 | except ValueError: 156 | raise ValueError(self.gettext(u'Invalid JSON data.')) 157 | 158 | 159 | class DictField(JSONField): 160 | def process_formdata(self, value): 161 | super(DictField, self).process_formdata(value) 162 | if value and not isinstance(self.data, dict): 163 | raise ValueError(self.gettext(u'Not a valid dictionary.')) 164 | 165 | 166 | class NoneStringField(StringField): 167 | """ 168 | Custom StringField that counts "" as None 169 | """ 170 | def process_formdata(self, valuelist): 171 | if valuelist: 172 | self.data = valuelist[0] 173 | if self.data == "": 174 | self.data = None 175 | 176 | 177 | class BinaryField(TextAreaField): 178 | """ 179 | Custom TextAreaField that converts its value with binary_type. 180 | """ 181 | def process_formdata(self, valuelist): 182 | if valuelist: 183 | if six.PY3: 184 | self.data = six.binary_type(valuelist[0], 'utf-8') 185 | else: 186 | self.data = six.binary_type(valuelist[0]) 187 | -------------------------------------------------------------------------------- /docs/_themes/flask/static/flasky.css_t: -------------------------------------------------------------------------------- 1 | /* 2 | * flasky.css_t 3 | * ~~~~~~~~~~~~ 4 | * 5 | * Sphinx stylesheet -- flasky theme based on nature theme. 6 | * 7 | * :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. 8 | * :license: BSD, see LICENSE for details. 9 | * 10 | */ 11 | 12 | @import url("basic.css"); 13 | 14 | /* -- page layout ----------------------------------------------------------- */ 15 | 16 | body { 17 | font-family: 'Georgia', serif; 18 | font-size: 17px; 19 | background-color: #ddd; 20 | color: #000; 21 | margin: 0; 22 | padding: 0; 23 | } 24 | 25 | div.document { 26 | background: #fafafa; 27 | } 28 | 29 | div.documentwrapper { 30 | float: left; 31 | width: 100%; 32 | } 33 | 34 | div.bodywrapper { 35 | margin: 0 0 0 230px; 36 | } 37 | 38 | hr { 39 | border: 1px solid #B1B4B6; 40 | } 41 | 42 | div.body { 43 | background-color: #ffffff; 44 | color: #3E4349; 45 | padding: 0 30px 30px 30px; 46 | min-height: 34em; 47 | } 48 | 49 | img.floatingflask { 50 | padding: 0 0 10px 10px; 51 | float: right; 52 | } 53 | 54 | div.footer { 55 | position: absolute; 56 | right: 0; 57 | margin-top: -70px; 58 | text-align: right; 59 | color: #888; 60 | padding: 10px; 61 | font-size: 14px; 62 | } 63 | 64 | div.footer a { 65 | color: #888; 66 | text-decoration: underline; 67 | } 68 | 69 | div.related { 70 | line-height: 32px; 71 | color: #888; 72 | } 73 | 74 | div.related ul { 75 | padding: 0 0 0 10px; 76 | } 77 | 78 | div.related a { 79 | color: #444; 80 | } 81 | 82 | div.sphinxsidebar { 83 | font-size: 14px; 84 | line-height: 1.5; 85 | } 86 | 87 | div.sphinxsidebarwrapper { 88 | padding: 0 20px; 89 | } 90 | 91 | div.sphinxsidebarwrapper p.logo { 92 | padding: 20px 0 10px 0; 93 | margin: 0; 94 | text-align: center; 95 | } 96 | 97 | div.sphinxsidebar h3, 98 | div.sphinxsidebar h4 { 99 | font-family: 'Garamond', 'Georgia', serif; 100 | color: #222; 101 | font-size: 24px; 102 | font-weight: normal; 103 | margin: 20px 0 5px 0; 104 | padding: 0; 105 | } 106 | 107 | div.sphinxsidebar h4 { 108 | font-size: 20px; 109 | } 110 | 111 | div.sphinxsidebar h3 a { 112 | color: #444; 113 | } 114 | 115 | div.sphinxsidebar p { 116 | color: #555; 117 | margin: 10px 0; 118 | } 119 | 120 | div.sphinxsidebar ul { 121 | margin: 10px 0; 122 | padding: 0; 123 | color: #000; 124 | } 125 | 126 | div.sphinxsidebar a { 127 | color: #444; 128 | text-decoration: none; 129 | } 130 | 131 | div.sphinxsidebar a:hover { 132 | text-decoration: underline; 133 | } 134 | 135 | div.sphinxsidebar input { 136 | border: 1px solid #ccc; 137 | font-family: 'Georgia', serif; 138 | font-size: 1em; 139 | } 140 | 141 | /* -- body styles ----------------------------------------------------------- */ 142 | 143 | a { 144 | color: #004B6B; 145 | text-decoration: underline; 146 | } 147 | 148 | a:hover { 149 | color: #6D4100; 150 | text-decoration: underline; 151 | } 152 | 153 | div.body { 154 | padding-bottom: 40px; /* saved for footer */ 155 | } 156 | 157 | div.body h1, 158 | div.body h2, 159 | div.body h3, 160 | div.body h4, 161 | div.body h5, 162 | div.body h6 { 163 | font-family: 'Garamond', 'Georgia', serif; 164 | font-weight: normal; 165 | margin: 30px 0px 10px 0px; 166 | padding: 0; 167 | } 168 | 169 | div.body h1 { margin-top: 0; padding-top: 20px; font-size: 240%; } 170 | div.body h2 { font-size: 180%; } 171 | div.body h3 { font-size: 150%; } 172 | div.body h4 { font-size: 130%; } 173 | div.body h5 { font-size: 100%; } 174 | div.body h6 { font-size: 100%; } 175 | 176 | a.headerlink { 177 | color: white; 178 | padding: 0 4px; 179 | text-decoration: none; 180 | } 181 | 182 | a.headerlink:hover { 183 | color: #444; 184 | background: #eaeaea; 185 | } 186 | 187 | div.body p, div.body dd, div.body li { 188 | line-height: 1.4em; 189 | } 190 | 191 | div.admonition { 192 | background: #fafafa; 193 | margin: 20px -30px; 194 | padding: 10px 30px; 195 | border-top: 1px solid #ccc; 196 | border-bottom: 1px solid #ccc; 197 | } 198 | 199 | div.admonition p.admonition-title { 200 | font-family: 'Garamond', 'Georgia', serif; 201 | font-weight: normal; 202 | font-size: 24px; 203 | margin: 0 0 10px 0; 204 | padding: 0; 205 | line-height: 1; 206 | } 207 | 208 | div.admonition p.last { 209 | margin-bottom: 0; 210 | } 211 | 212 | div.highlight{ 213 | background-color: white; 214 | } 215 | 216 | dt:target, .highlight { 217 | background: #FAF3E8; 218 | } 219 | 220 | div.note { 221 | background-color: #eee; 222 | border: 1px solid #ccc; 223 | } 224 | 225 | div.seealso { 226 | background-color: #ffc; 227 | border: 1px solid #ff6; 228 | } 229 | 230 | div.topic { 231 | background-color: #eee; 232 | } 233 | 234 | div.warning { 235 | background-color: #ffe4e4; 236 | border: 1px solid #f66; 237 | } 238 | 239 | p.admonition-title { 240 | display: inline; 241 | } 242 | 243 | p.admonition-title:after { 244 | content: ":"; 245 | } 246 | 247 | pre, tt { 248 | font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; 249 | font-size: 0.9em; 250 | } 251 | 252 | img.screenshot { 253 | } 254 | 255 | tt.descname, tt.descclassname { 256 | font-size: 0.95em; 257 | } 258 | 259 | tt.descname { 260 | padding-right: 0.08em; 261 | } 262 | 263 | img.screenshot { 264 | -moz-box-shadow: 2px 2px 4px #eee; 265 | -webkit-box-shadow: 2px 2px 4px #eee; 266 | box-shadow: 2px 2px 4px #eee; 267 | } 268 | 269 | table.docutils { 270 | border: 1px solid #888; 271 | -moz-box-shadow: 2px 2px 4px #eee; 272 | -webkit-box-shadow: 2px 2px 4px #eee; 273 | box-shadow: 2px 2px 4px #eee; 274 | } 275 | 276 | table.docutils td, table.docutils th { 277 | border: 1px solid #888; 278 | padding: 0.25em 0.7em; 279 | } 280 | 281 | table.field-list, table.footnote { 282 | border: none; 283 | -moz-box-shadow: none; 284 | -webkit-box-shadow: none; 285 | box-shadow: none; 286 | } 287 | 288 | table.footnote { 289 | margin: 15px 0; 290 | width: 100%; 291 | border: 1px solid #eee; 292 | } 293 | 294 | table.field-list th { 295 | padding: 0 0.8em 0 0; 296 | } 297 | 298 | table.field-list td { 299 | padding: 0; 300 | } 301 | 302 | table.footnote td { 303 | padding: 0.5em; 304 | } 305 | 306 | dl { 307 | margin: 0; 308 | padding: 0; 309 | } 310 | 311 | dl dd { 312 | margin-left: 30px; 313 | } 314 | 315 | pre { 316 | background: #eee; 317 | padding: 7px 30px; 318 | margin: 15px -30px; 319 | line-height: 1.3em; 320 | } 321 | 322 | dl pre { 323 | margin-left: -60px; 324 | padding-left: 60px; 325 | } 326 | 327 | dl dl pre { 328 | margin-left: -90px; 329 | padding-left: 90px; 330 | } 331 | 332 | tt { 333 | background-color: #ecf0f3; 334 | color: #222; 335 | /* padding: 1px 2px; */ 336 | } 337 | 338 | tt.xref, a tt { 339 | background-color: #FBFBFB; 340 | text-decoration: none!important; 341 | } 342 | 343 | a:hover tt { 344 | background: #EEE; 345 | } 346 | -------------------------------------------------------------------------------- /flask_mongoengine/templates/panels/mongo-panel.html: -------------------------------------------------------------------------------- 1 | 31 | 32 | {% macro render_stats(title, queries, slow_query_limit=100) %} 33 | 34 |

{{ title }}

35 | {% if queries %} 36 | 37 | 38 | 39 | 40 | 41 | {% if title == 'Queries' %} 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | {% elif title == 'Inserts' %} 50 | 51 | 52 | {% elif title == 'Removes' %} 53 | 54 | 55 | {% elif title == 'Updates' %} 56 | 57 | 58 | 59 | 60 | 61 | {% endif %} 62 | 63 | 64 | 65 | 66 | {% for query in queries %} 67 | 68 | 70 | 71 | {% if title == "Queries" %} 72 | {% set colspan = 10 %} 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | {% elif title == "Inserts" %} 81 | {% set colspan = 5 %} 82 | 84 | 85 | {% elif title == 'Removes' %} 86 | {% set colspan = 5 %} 87 | 90 | 91 | {% elif title == 'Updates' %} 92 | {% set colspan = 8 %} 93 | 96 | 99 | 100 | 101 | 102 | {% endif %} 103 | 104 | 105 | {% if title == "Queries" %} 106 | 107 | 110 | 111 | {% endif %} 112 | 113 | 142 | 143 | {% endfor %} 144 | 145 |
Time (ms)SizeOperationCollectionQueryOrderingSkipLimitDataDocumentSafeQuery / IdSafeQueryUpdateSafeMultiUpsertStack Trace
slow_query_limit %}style="color:red;" {% endif %}> 69 | {{ query.time|round(3) }}{{ query.size|round(2) }}Kb{{ query.operation|title }}{{ query.collection }}{% if query.query %}{{ query.query|safe }}{% endif %}{% if query.ordering %}{{ query.ordering }}{% endif %}{% if query.skip %}{{ query.skip }}{% endif %}{% if query.limit %}{{ query.limit }}{% endif %}Toggle{{ query.document|safe }} 83 | {{ query.safe }} 88 |
{{ query.spec_or_id|safe }}
89 |
{{ query.safe }}
94 |
{{ query.spec|safe }}
95 |
97 |
{{ query.document|safe }}
98 |
{{ query.safe }}{{ query.multi }}{{ query.upsert }}Toggle
108 |
{{ query.data|pprint }}
109 |
114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | {% for line in query.stack_trace %} 125 | 126 | 127 | 128 | 129 | 130 | 131 | {% endfor %} 132 | 133 | 134 | 135 | 138 | 139 | 140 |
LineFileFunctionCode
{{ line.1 }}{{ line.0 }}{{ line.2 }}{{ line.3|safe }}
136 | Toggle full trace 137 |
141 |
146 | {% else %} 147 |

No {{ title|lower }} recorded

148 | {% endif %} 149 | {% endmacro %} 150 | 151 | {{ render_stats("Queries", queries, slow_query_limit)}} 152 | {{ render_stats("Removes", removes, slow_query_limit)}} 153 | {{ render_stats("Inserts", inserts, slow_query_limit)}} 154 | {{ render_stats("Updates", updates, slow_query_limit)}} 155 | 156 | 172 | -------------------------------------------------------------------------------- /flask_mongoengine/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import 3 | 4 | import inspect 5 | 6 | import mongoengine 7 | from flask import Flask, abort, current_app 8 | from mongoengine.base.fields import BaseField 9 | from mongoengine.errors import ( 10 | DoesNotExist, 11 | MultipleObjectsReturned, 12 | ValidationError, 13 | ) 14 | from mongoengine.queryset import QuerySet 15 | 16 | from .connection import * 17 | from .json import override_json_encoder 18 | from .pagination import * 19 | from .sessions import * 20 | from .wtf import WtfBaseField 21 | 22 | VERSION = (0, 9, 5) 23 | 24 | 25 | def get_version(): 26 | """Return the VERSION as a string, e.g. for VERSION == (0, 9, 2), 27 | return '0.9.2'. 28 | """ 29 | return '.'.join(map(str, VERSION)) 30 | 31 | 32 | __version__ = get_version() 33 | 34 | 35 | def _patch_base_field(obj, name): 36 | """ 37 | If the object submitted has a class whose base class is 38 | mongoengine.base.fields.BaseField, then monkey patch to 39 | replace it with flask_mongoengine.wtf.WtfBaseField. 40 | 41 | @note: WtfBaseField is an instance of BaseField - but 42 | gives us the flexibility to extend field parameters 43 | and settings required of WTForm via model form generator. 44 | 45 | @see: flask_mongoengine.wtf.base.WtfBaseField. 46 | @see: model_form in flask_mongoengine.wtf.orm 47 | 48 | @param obj: MongoEngine instance in which we should locate the class. 49 | @param name: Name of an attribute which may or may not be a BaseField. 50 | """ 51 | # TODO is there a less hacky way to accomplish the same level of 52 | # extensibility/control? 53 | 54 | # get an attribute of the MongoEngine class and return if it's not 55 | # a class 56 | cls = getattr(obj, name) 57 | if not inspect.isclass(cls): 58 | return 59 | 60 | # if it is a class, inspect all of its parent classes 61 | cls_bases = list(cls.__bases__) 62 | 63 | # if any of them is a BaseField, replace it with WtfBaseField 64 | for index, base in enumerate(cls_bases): 65 | if base == BaseField: 66 | cls_bases[index] = WtfBaseField 67 | cls.__bases__ = tuple(cls_bases) 68 | break 69 | 70 | # re-assign the class back to the MongoEngine instance 71 | delattr(obj, name) 72 | setattr(obj, name, cls) 73 | 74 | 75 | def _include_mongoengine(obj): 76 | """ 77 | Copy all of the attributes from mongoengine and mongoengine.fields 78 | onto obj (which should be an instance of the MongoEngine class). 79 | """ 80 | # TODO why do we need this? What's wrong with importing from the 81 | # original modules? 82 | for module in (mongoengine, mongoengine.fields): 83 | for attr_name in module.__all__: 84 | if not hasattr(obj, attr_name): 85 | setattr(obj, attr_name, getattr(module, attr_name)) 86 | 87 | # patch BaseField if available 88 | _patch_base_field(obj, attr_name) 89 | 90 | 91 | def current_mongoengine_instance(): 92 | """Return a MongoEngine instance associated with current Flask app.""" 93 | me = current_app.extensions.get('mongoengine', {}) 94 | for k, v in me.items(): 95 | if isinstance(k, MongoEngine): 96 | return k 97 | 98 | 99 | class MongoEngine(object): 100 | """Main class used for initialization of Flask-MongoEngine.""" 101 | 102 | def __init__(self, app=None, config=None): 103 | _include_mongoengine(self) 104 | 105 | self.app = None 106 | self.Document = Document 107 | self.DynamicDocument = DynamicDocument 108 | 109 | if app is not None: 110 | self.init_app(app, config) 111 | 112 | def init_app(self, app, config=None): 113 | if not app or not isinstance(app, Flask): 114 | raise Exception('Invalid Flask application instance') 115 | 116 | self.app = app 117 | 118 | app.extensions = getattr(app, 'extensions', {}) 119 | 120 | # Make documents JSON serializable 121 | override_json_encoder(app) 122 | 123 | if 'mongoengine' not in app.extensions: 124 | app.extensions['mongoengine'] = {} 125 | 126 | if self in app.extensions['mongoengine']: 127 | # Raise an exception if extension already initialized as 128 | # potentially new configuration would not be loaded. 129 | raise Exception('Extension already initialized') 130 | 131 | if not config: 132 | # If not passed a config then we read the connection settings 133 | # from the app config. 134 | config = app.config 135 | 136 | # Obtain db connection(s) 137 | connections = create_connections(config) 138 | 139 | # Store objects in application instance so that multiple apps do not 140 | # end up accessing the same objects. 141 | s = {'app': app, 'conn': connections} 142 | app.extensions['mongoengine'][self] = s 143 | 144 | @property 145 | def connection(self): 146 | """ 147 | Return MongoDB connection(s) associated with this MongoEngine 148 | instance. 149 | """ 150 | return current_app.extensions['mongoengine'][self]['conn'] 151 | 152 | 153 | class BaseQuerySet(QuerySet): 154 | """Mongoengine's queryset extended with handy extras.""" 155 | 156 | def get_or_404(self, *args, **kwargs): 157 | """ 158 | Get a document and raise a 404 Not Found error if it doesn't 159 | exist. 160 | """ 161 | try: 162 | return self.get(*args, **kwargs) 163 | except DoesNotExist: 164 | message = kwargs.get('message', None) 165 | abort(404, message) if message else abort(404) 166 | 167 | def first_or_404(self, message=None): 168 | """Same as get_or_404, but uses .first, not .get.""" 169 | obj = self.first() 170 | return obj if obj else abort(404, message) if message else abort(404) 171 | 172 | def paginate(self, page, per_page, **kwargs): 173 | """ 174 | Paginate the QuerySet with a certain number of docs per page 175 | and return docs for a given page. 176 | """ 177 | return Pagination(self, page, per_page) 178 | 179 | def paginate_field(self, field_name, doc_id, page, per_page, total=None): 180 | """ 181 | Paginate items within a list field from one document in the 182 | QuerySet. 183 | """ 184 | # TODO this doesn't sound useful at all - remove in next release? 185 | item = self.get(id=doc_id) 186 | count = getattr(item, field_name + "_count", '') 187 | total = total or count or len(getattr(item, field_name)) 188 | return ListFieldPagination(self, doc_id, field_name, page, per_page, 189 | total=total) 190 | 191 | 192 | class Document(mongoengine.Document): 193 | """Abstract document with extra helpers in the queryset class""" 194 | 195 | meta = {'abstract': True, 196 | 'queryset_class': BaseQuerySet} 197 | 198 | def paginate_field(self, field_name, page, per_page, total=None): 199 | """Paginate items within a list field.""" 200 | # TODO this doesn't sound useful at all - remove in next release? 201 | count = getattr(self, field_name + "_count", '') 202 | total = total or count or len(getattr(self, field_name)) 203 | return ListFieldPagination(self.__class__.objects, self.pk, field_name, 204 | page, per_page, total=total) 205 | 206 | 207 | class DynamicDocument(mongoengine.DynamicDocument): 208 | """Abstract Dynamic document with extra helpers in the queryset class""" 209 | 210 | meta = {'abstract': True, 211 | 'queryset_class': BaseQuerySet} 212 | -------------------------------------------------------------------------------- /tests/test_connection.py: -------------------------------------------------------------------------------- 1 | import mongoengine 2 | from mongoengine.context_managers import switch_db 3 | from nose import SkipTest 4 | from nose.tools import assert_raises 5 | import pymongo 6 | from pymongo.errors import InvalidURI 7 | from pymongo.read_preferences import ReadPreference 8 | 9 | from flask_mongoengine import MongoEngine 10 | 11 | from tests import FlaskMongoEngineTestCase 12 | 13 | 14 | class ConnectionTestCase(FlaskMongoEngineTestCase): 15 | 16 | def _do_persist(self, db, alias=None): 17 | """Initialize a test Flask application and persist some data in 18 | MongoDB, ultimately asserting that the connection works. 19 | """ 20 | if alias: 21 | class Todo(db.Document): 22 | meta = {'db_alias': alias} 23 | title = db.StringField(max_length=60) 24 | text = db.StringField() 25 | done = db.BooleanField(default=False) 26 | else: 27 | class Todo(db.Document): 28 | title = db.StringField(max_length=60) 29 | text = db.StringField() 30 | done = db.BooleanField(default=False) 31 | 32 | db.init_app(self.app) 33 | Todo.drop_collection() 34 | 35 | # Test persist 36 | todo = Todo() 37 | todo.text = "Sample" 38 | todo.title = "Testing" 39 | todo.done = True 40 | s_todo = todo.save() 41 | 42 | f_to = Todo.objects().first() 43 | self.assertEqual(s_todo.title, f_to.title) 44 | 45 | def test_simple_connection(self): 46 | """Make sure a simple connection to a standalone MongoDB works.""" 47 | db = MongoEngine() 48 | self.app.config['MONGODB_SETTINGS'] = { 49 | 'ALIAS': 'simple_conn', 50 | 'HOST': 'localhost', 51 | 'PORT': 27017, 52 | 'DB': 'flask_mongoengine_test_db' 53 | } 54 | self._do_persist(db, alias='simple_conn') 55 | 56 | def test_host_as_uri_string(self): 57 | """Make sure we can connect to a standalone MongoDB if we specify 58 | the host as a MongoDB URI. 59 | """ 60 | db = MongoEngine() 61 | self.app.config['MONGODB_HOST'] = 'mongodb://localhost:27017/flask_mongoengine_test_db' 62 | self._do_persist(db) 63 | 64 | def test_mongomock_host_as_uri_string(self): 65 | """Make sure we switch to mongomock if we specify the host as a mongomock URI. 66 | """ 67 | if mongoengine.VERSION < (0, 9, 0): 68 | raise SkipTest('Mongomock not supported for mongoengine < 0.9.0') 69 | db = MongoEngine() 70 | self.app.config['MONGODB_HOST'] = 'mongomock://localhost:27017/flask_mongoengine_test_db' 71 | with assert_raises(RuntimeError) as exc: 72 | self._do_persist(db) 73 | assert str(exc.exception) == 'You need mongomock installed to mock MongoEngine.' 74 | 75 | def test_mongomock_as_param(self): 76 | """Make sure we switch to mongomock when providing IS_MOCK option. 77 | """ 78 | if mongoengine.VERSION < (0, 9, 0): 79 | raise SkipTest('Mongomock not supported for mongoengine < 0.9.0') 80 | db = MongoEngine() 81 | self.app.config['MONGODB_SETTINGS'] = { 82 | 'ALIAS': 'simple_conn', 83 | 'HOST': 'localhost', 84 | 'PORT': 27017, 85 | 'DB': 'flask_mongoengine_test_db', 86 | 'IS_MOCK': True 87 | } 88 | with assert_raises(RuntimeError) as exc: 89 | self._do_persist(db, alias='simple_conn') 90 | assert str(exc.exception) == 'You need mongomock installed to mock MongoEngine.' 91 | 92 | def test_host_as_list(self): 93 | """Make sure MONGODB_HOST can be a list hosts.""" 94 | db = MongoEngine() 95 | self.app.config['MONGODB_SETTINGS'] = { 96 | 'ALIAS': 'host_list', 97 | 'HOST': ['localhost:27017'], 98 | 'DB': 'flask_mongoengine_test_db' 99 | } 100 | self._do_persist(db, alias='host_list') 101 | 102 | def test_multiple_connections(self): 103 | """Make sure establishing multiple connections to a standalone 104 | MongoDB and switching between them works. 105 | """ 106 | db = MongoEngine() 107 | self.app.config['MONGODB_SETTINGS'] = [ 108 | { 109 | 'ALIAS': 'default', 110 | 'DB': 'flask_mongoengine_test_db_1', 111 | 'HOST': 'localhost', 112 | 'PORT': 27017 113 | }, 114 | { 115 | 'ALIAS': 'alternative', 116 | 'DB': 'flask_mongoengine_test_db_2', 117 | 'HOST': 'localhost', 118 | 'PORT': 27017 119 | }, 120 | ] 121 | 122 | class Todo(db.Document): 123 | title = db.StringField(max_length=60) 124 | text = db.StringField() 125 | done = db.BooleanField(default=False) 126 | meta = {'db_alias': 'alternative'} 127 | 128 | db.init_app(self.app) 129 | Todo.drop_collection() 130 | 131 | # Test saving a doc via the default connection 132 | with switch_db(Todo, 'default') as Todo: 133 | todo = Todo() 134 | todo.text = "Sample" 135 | todo.title = "Testing" 136 | todo.done = True 137 | s_todo = todo.save() 138 | 139 | f_to = Todo.objects().first() 140 | self.assertEqual(s_todo.title, f_to.title) 141 | 142 | # Make sure the doc doesn't exist in the alternative db 143 | with switch_db(Todo, 'alternative') as Todo: 144 | doc = Todo.objects().first() 145 | self.assertEqual(doc, None) 146 | 147 | # Make sure switching back to the default connection shows the doc 148 | with switch_db(Todo, 'default') as Todo: 149 | doc = Todo.objects().first() 150 | self.assertNotEqual(doc, None) 151 | 152 | def test_connection_with_invalid_uri(self): 153 | """Make sure connecting via an invalid URI raises an InvalidURI 154 | exception. 155 | """ 156 | self.app.config['MONGODB_HOST'] = 'mongo://localhost' 157 | self.assertRaises(InvalidURI, MongoEngine, self.app) 158 | 159 | def test_ingnored_mongodb_prefix_config(self): 160 | """Config starting by MONGODB_ but not used by flask-mongoengine 161 | should be ignored. 162 | """ 163 | db = MongoEngine() 164 | self.app.config['MONGODB_HOST'] = 'mongodb://localhost:27017/flask_mongoengine_test_db_prod' 165 | # Invalid host, should trigger exception if used 166 | self.app.config['MONGODB_TEST_HOST'] = 'dummy://localhost:27017/test' 167 | self._do_persist(db) 168 | 169 | def test_connection_kwargs(self): 170 | """Make sure additional connection kwargs work.""" 171 | 172 | # Figure out whether to use "MAX_POOL_SIZE" or "MAXPOOLSIZE" based 173 | # on PyMongo version (former was changed to the latter as described 174 | # in https://jira.mongodb.org/browse/PYTHON-854) 175 | # TODO remove once PyMongo < 3.0 support is dropped 176 | if pymongo.version_tuple[0] >= 3: 177 | MAX_POOL_SIZE_KEY = 'MAXPOOLSIZE' 178 | else: 179 | MAX_POOL_SIZE_KEY = 'MAX_POOL_SIZE' 180 | 181 | self.app.config['MONGODB_SETTINGS'] = { 182 | 'ALIAS': 'tz_aware_true', 183 | 'DB': 'flask_mongoengine_testing_tz_aware', 184 | 'TZ_AWARE': True, 185 | 'READ_PREFERENCE': ReadPreference.SECONDARY, 186 | MAX_POOL_SIZE_KEY: 10, 187 | } 188 | db = MongoEngine() 189 | db.init_app(self.app) 190 | self.assertTrue(db.connection.codec_options.tz_aware) 191 | self.assertEqual(db.connection.max_pool_size, 10) 192 | self.assertEqual( 193 | db.connection.read_preference, 194 | ReadPreference.SECONDARY 195 | ) 196 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # flask-script documentation build configuration file, created by 4 | # sphinx-quickstart on Wed Jun 23 08:26:41 2010. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | sys.path.append(os.path.abspath('_themes')) 20 | 21 | # -- General configuration ----------------------------------------------------- 22 | 23 | # 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 = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx'] 29 | 30 | # Add any paths that contain templates here, relative to this directory. 31 | templates_path = ['_templates'] 32 | 33 | # The suffix of source filenames. 34 | source_suffix = '.rst' 35 | 36 | # The encoding of source files. 37 | #source_encoding = 'utf-8-sig' 38 | 39 | # The master toctree document. 40 | master_doc = 'index' 41 | 42 | # General information about the project. 43 | project = u'Flask-MongoEngine' 44 | copyright = u'2010-2020, Streetlife and others' 45 | 46 | # The version info for the project you're documenting, acts as replacement for 47 | # |version| and |release|, also used in various other places throughout the 48 | # built documents. 49 | # 50 | import flask_mongoengine 51 | # The short X.Y version. 52 | version = flask_mongoengine.__version__ 53 | # The full version, including alpha/beta/rc tags. 54 | release = flask_mongoengine.__version__ 55 | 56 | # The language for content autogenerated by Sphinx. Refer to documentation 57 | # for a list of supported languages. 58 | #language = None 59 | 60 | # There are two options for replacing |today|: either, you set today to some 61 | # non-false value, then it is used: 62 | #today = '' 63 | # Else, today_fmt is used as the format for a strftime call. 64 | #today_fmt = '%B %d, %Y' 65 | 66 | # List of patterns, relative to source directory, that match files and 67 | # directories to ignore when looking for source files. 68 | exclude_patterns = ['_build'] 69 | 70 | # The reST default role (used for this markup: `text`) to use for all documents. 71 | #default_role = None 72 | 73 | # If true, '()' will be appended to :func: etc. cross-reference text. 74 | #add_function_parentheses = True 75 | 76 | # If true, the current module name will be prepended to all description 77 | # unit titles (such as .. function::). 78 | #add_module_names = True 79 | 80 | # If true, sectionauthor and moduleauthor directives will be shown in the 81 | # output. They are ignored by default. 82 | #show_authors = False 83 | 84 | # The name of the Pygments (syntax highlighting) style to use. 85 | #pygments_style = 'sphinx' 86 | 87 | # A list of ignored prefixes for module index sorting. 88 | #modindex_common_prefix = [] 89 | 90 | 91 | # -- Options for HTML output --------------------------------------------------- 92 | 93 | # The theme to use for HTML and HTML Help pages. See the documentation for 94 | # a list of builtin themes. 95 | html_theme = 'flask_small' 96 | 97 | html_theme_options = { 98 | 'index_logo': '', 99 | 'github_fork': None 100 | } 101 | # Theme options are theme-specific and customize the look and feel of a theme 102 | # further. For a list of options available for each theme, see the 103 | # documentation. 104 | #html_theme_options = {} 105 | 106 | # Add any paths that contain custom themes here, relative to this directory. 107 | html_theme_path = ['_themes'] 108 | 109 | # The name for this set of Sphinx documents. If None, it defaults to 110 | # " v documentation". 111 | #html_title = None 112 | 113 | # A shorter title for the navigation bar. Default is the same as html_title. 114 | #html_short_title = None 115 | 116 | # The name of an image file (relative to this directory) to place at the top 117 | # of the sidebar. 118 | #html_logo = None 119 | 120 | # The name of an image file (within the static path) to use as favicon of the 121 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 122 | # pixels large. 123 | #html_favicon = None 124 | 125 | # Add any paths that contain custom static files (such as style sheets) here, 126 | # relative to this directory. They are copied after the builtin static files, 127 | # so a file named "default.css" will overwrite the builtin "default.css". 128 | html_static_path = ['_static'] 129 | 130 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 131 | # using the given strftime format. 132 | #html_last_updated_fmt = '%b %d, %Y' 133 | 134 | # If true, SmartyPants will be used to convert quotes and dashes to 135 | # typographically correct entities. 136 | #html_use_smartypants = True 137 | 138 | # Custom sidebar templates, maps document names to template names. 139 | #html_sidebars = {} 140 | 141 | # Additional templates that should be rendered to pages, maps page names to 142 | # template names. 143 | #html_additional_pages = {} 144 | 145 | # If false, no module index is generated. 146 | #html_domain_indices = True 147 | 148 | # If false, no index is generated. 149 | #html_use_index = True 150 | 151 | # If true, the index is split into individual pages for each letter. 152 | #html_split_index = False 153 | 154 | # If true, links to the reST sources are added to the pages. 155 | #html_show_sourcelink = True 156 | 157 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 158 | #html_show_sphinx = True 159 | 160 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 161 | #html_show_copyright = True 162 | 163 | # If true, an OpenSearch description file will be output, and all pages will 164 | # contain a tag referring to it. The value of this option must be the 165 | # base URL from which the finished HTML is served. 166 | #html_use_opensearch = '' 167 | 168 | # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). 169 | #html_file_suffix = '' 170 | 171 | # Output file base name for HTML help builder. 172 | htmlhelp_basename = 'flask-mongoenginedoc' 173 | 174 | 175 | # -- Options for LaTeX output -------------------------------------------------- 176 | 177 | # The paper size ('letter' or 'a4'). 178 | #latex_paper_size = 'letter' 179 | 180 | # The font size ('10pt', '11pt' or '12pt'). 181 | #latex_font_size = '10pt' 182 | 183 | # Grouping the document tree into LaTeX files. List of tuples 184 | # (source start file, target name, title, author, documentclass [howto/manual]). 185 | latex_documents = [ 186 | ('index', 'flask-mongoengine.tex', u'Flask-MongoEngine Documentation', 187 | u'Ross Lawley', 'manual'), 188 | ] 189 | 190 | # The name of an image file (relative to this directory) to place at the top of 191 | # the title page. 192 | #latex_logo = None 193 | 194 | # For "manual" documents, if this is true, then toplevel headings are parts, 195 | # not chapters. 196 | #latex_use_parts = False 197 | 198 | # If true, show page references after internal links. 199 | #latex_show_pagerefs = False 200 | 201 | # If true, show URL addresses after external links. 202 | #latex_show_urls = False 203 | 204 | # Additional stuff for the LaTeX preamble. 205 | #latex_preamble = '' 206 | 207 | # Documents to append as an appendix to all manuals. 208 | #latex_appendices = [] 209 | 210 | # If false, no module index is generated. 211 | #latex_domain_indices = True 212 | 213 | 214 | # -- Options for manual page output -------------------------------------------- 215 | 216 | # One entry per manual page. List of tuples 217 | # (source start file, name, description, authors, manual section). 218 | man_pages = [ 219 | ('index', 'flask-mongoengine', u'Flask-MongoEngine Documentation', 220 | [u'Ross Lawley', u'Dan Jacob', u'Marat Khabibullin'], 1) 221 | ] 222 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Flask-MongoEngine 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 | Installing Flask-MongoEngine 11 | ============================ 12 | 13 | Install with **pip**:: 14 | 15 | pip install flask-mongoengine 16 | 17 | Configuration 18 | ============= 19 | 20 | Basic setup is easy, just fetch the extension:: 21 | 22 | from flask import Flask 23 | from flask_mongoengine import MongoEngine 24 | 25 | app = Flask(__name__) 26 | app.config.from_pyfile('the-config.cfg') 27 | db = MongoEngine(app) 28 | 29 | Or, if you are setting up your database before your app is initialized, as is the case with application factories:: 30 | 31 | from flask import Flask 32 | from flask_mongoengine import MongoEngine 33 | db = MongoEngine() 34 | ... 35 | app = Flask(__name__) 36 | app.config.from_pyfile('the-config.cfg') 37 | db.init_app(app) 38 | 39 | 40 | By default, Flask-MongoEngine assumes that the :program:`mongod` instance is running 41 | on **localhost** on port **27017**, and you wish to connect to the database named **test**. 42 | 43 | If MongoDB is running elsewhere, you should provide the :attr:`host` and :attr:`port` settings 44 | in the `'MONGODB_SETTINGS'` dictionary wih `app.config`.:: 45 | 46 | app.config['MONGODB_SETTINGS'] = { 47 | 'db': 'project1', 48 | 'host': '192.168.1.35', 49 | 'port': 12345 50 | } 51 | 52 | If the database requires authentication, the :attr:`username` and :attr:`password` 53 | arguments should be provided `'MONGODB_SETTINGS'` dictionary wih `app.config`.:: 54 | 55 | app.config['MONGODB_SETTINGS'] = { 56 | 'db': 'project1', 57 | 'username':'webapp', 58 | 'password':'pwd123' 59 | } 60 | 61 | Uri style connections are also supported, just supply the uri as the :attr:`host` 62 | in the `'MONGODB_SETTINGS'` dictionary with `app.config`. **Note that database name from uri has priority over name.** :: 63 | 64 | app.config['MONGODB_SETTINGS'] = { 65 | 'db': 'project1', 66 | 'host': 'mongodb://localhost/database_name' 67 | } 68 | 69 | Connection settings may also be provided individually by prefixing the setting with `'MONGODB_'` in the `app.config`.:: 70 | 71 | app.config['MONGODB_DB'] = 'project1' 72 | app.config['MONGODB_HOST'] = '192.168.1.35' 73 | app.config['MONGODB_PORT'] = 12345 74 | app.config['MONGODB_USERNAME'] = 'webapp' 75 | app.config['MONGODB_PASSWORD'] = 'pwd123' 76 | 77 | By default flask-mongoengine open the connection when extension is instanciated but you can configure it 78 | to open connection only on first database access by setting the ``MONGODB_SETTINGS['connect']`` parameter 79 | or its ``MONGODB_CONNECT`` flat equivalent to ``False``:: 80 | 81 | app.config['MONGODB_SETTINGS'] = { 82 | 'host': 'mongodb://localhost/database_name', 83 | 'connect': False, 84 | } 85 | # or 86 | app.config['MONGODB_CONNECT'] = False 87 | 88 | Custom Queryset 89 | =============== 90 | 91 | flask-mongoengine attaches the following methods to Mongoengine's default QuerySet: 92 | 93 | * **get_or_404**: works like .get(), but calls abort(404) if the object DoesNotExist. 94 | Optional arguments: *message* - custom message to display. 95 | * **first_or_404**: same as above, except for .first(). 96 | Optional arguments: *message* - custom message to display. 97 | * **paginate**: paginates the QuerySet. Takes two arguments, *page* and *per_page*. 98 | * **paginate_field**: paginates a field from one document in the QuerySet. 99 | Arguments: *field_name*, *doc_id*, *page*, *per_page*. 100 | 101 | Examples:: 102 | 103 | # 404 if object doesn't exist 104 | def view_todo(todo_id): 105 | todo = Todo.objects.get_or_404(_id=todo_id) 106 | .. 107 | 108 | # Paginate through todo 109 | def view_todos(page=1): 110 | paginated_todos = Todo.objects.paginate(page=page, per_page=10) 111 | 112 | # Paginate through tags of todo 113 | def view_todo_tags(todo_id, page=1): 114 | todo = Todo.objects.get_or_404(_id=todo_id) 115 | paginated_tags = todo.paginate_field('tags', page, per_page=10) 116 | 117 | Properties of the pagination object include: iter_pages, next, prev, has_next, 118 | has_prev, next_num, prev_num. 119 | 120 | In the template:: 121 | 122 | {# Display a page of todos #} 123 |
    124 | {% for todo in paginated_todos.items %} 125 |
  • {{ todo.title }}
  • 126 | {% endfor %} 127 |
128 | 129 | {# Macro for creating navigation links #} 130 | {% macro render_navigation(pagination, endpoint) %} 131 | 144 | {% endmacro %} 145 | 146 | {{ render_navigation(paginated_todos, 'view_todos') }} 147 | 148 | 149 | MongoEngine and WTForms 150 | ======================= 151 | 152 | flask-mongoengine automatically generates WTForms from MongoEngine models:: 153 | 154 | from flask_mongoengine.wtf import model_form 155 | 156 | class User(db.Document): 157 | email = db.StringField(required=True) 158 | first_name = db.StringField(max_length=50) 159 | last_name = db.StringField(max_length=50) 160 | 161 | class Content(db.EmbeddedDocument): 162 | text = db.StringField() 163 | lang = db.StringField(max_length=3) 164 | 165 | class Post(db.Document): 166 | title = db.StringField(max_length=120, required=True, validators=[validators.InputRequired(message=u'Missing title.'),]) 167 | author = db.ReferenceField(User) 168 | tags = db.ListField(db.StringField(max_length=30)) 169 | content = db.EmbeddedDocumentField(Content) 170 | 171 | PostForm = model_form(Post) 172 | 173 | def add_post(request): 174 | form = PostForm(request.POST) 175 | if request.method == 'POST' and form.validate(): 176 | # do something 177 | redirect('done') 178 | return render_template('add_post.html', form=form) 179 | 180 | For each MongoEngine field, the most appropriate WTForm field is used. 181 | Parameters allow the user to provide hints if the conversion is not implicit:: 182 | 183 | PostForm = model_form(Post, field_args={'title': {'textarea': True}}) 184 | 185 | Supported parameters: 186 | 187 | For fields with `choices`: 188 | 189 | - `multiple` to use a SelectMultipleField 190 | - `radio` to use a RadioField 191 | 192 | For `StringField`: 193 | 194 | - `password` to use a PasswordField 195 | - `textarea` to use a TextAreaField 196 | 197 | (By default, a StringField is converted into a TextAreaField if and only if it has no max_length.) 198 | 199 | 200 | Supported fields 201 | ---------------- 202 | 203 | * StringField 204 | * BinaryField 205 | * URLField 206 | * EmailField 207 | * IntField 208 | * FloatField 209 | * DecimalField 210 | * BooleanField 211 | * DateTimeField 212 | * **ListField** (using wtforms.fields.FieldList ) 213 | * SortedListField (duplicate ListField) 214 | * **EmbeddedDocumentField** (using wtforms.fields.FormField and generating inline Form) 215 | * **ReferenceField** (using wtforms.fields.SelectFieldBase with options loaded from QuerySet or Document) 216 | * DictField 217 | 218 | Not currently supported field types: 219 | ------------------------------------ 220 | 221 | * ObjectIdField 222 | * GeoLocationField 223 | * GenericReferenceField 224 | 225 | Session Interface 226 | ================= 227 | 228 | To use MongoEngine as your session store simple configure the session interface:: 229 | 230 | from flask_mongoengine import MongoEngine, MongoEngineSessionInterface 231 | 232 | app = Flask(__name__) 233 | db = MongoEngine(app) 234 | app.session_interface = MongoEngineSessionInterface(db) 235 | 236 | 237 | Debug Toolbar Panel 238 | =================== 239 | 240 | .. image:: _static/debugtoolbar.png 241 | :target: #debug-toolbar-panel 242 | 243 | If you use the Flask-DebugToolbar you can add 244 | `'flask_mongoengine.panels.MongoDebugPanel'` to the `DEBUG_TB_PANELS` config 245 | list and then it will automatically track your queries:: 246 | 247 | from flask import Flask 248 | from flask_debugtoolbar import DebugToolbarExtension 249 | 250 | app = Flask(__name__) 251 | app.config['DEBUG_TB_PANELS'] = ['flask_mongoengine.panels.MongoDebugPanel'] 252 | db = MongoEngine(app) 253 | toolbar = DebugToolbarExtension(app) 254 | 255 | 256 | 257 | Upgrading 258 | ========= 259 | 260 | 0.6 to 0.7 261 | ---------- 262 | 263 | `ListFieldPagination` order of arguments have been changed to be more logical:: 264 | 265 | # Old order 266 | ListFieldPagination(self, queryset, field_name, doc_id, page, per_page, total) 267 | 268 | # New order 269 | ListFieldPagination(self, queryset, doc_id, field_name, page, per_page, total) 270 | 271 | 272 | Credits 273 | ======= 274 | 275 | Inspired by two repos: 276 | 277 | `danjac `_ 278 | `maratfm `_ 279 | -------------------------------------------------------------------------------- /flask_mongoengine/operation_tracker.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import functools 3 | import inspect 4 | import os 5 | import sys 6 | import time 7 | try: 8 | import SocketServer 9 | except ImportError: 10 | import socketserver as SocketServer 11 | 12 | import bson 13 | import pymongo.collection 14 | import pymongo.cursor 15 | import pymongo.helpers 16 | 17 | 18 | __all__ = ['queries', 'inserts', 'updates', 'removes', 'install_tracker', 19 | 'uninstall_tracker', 'reset', 'response_sizes'] 20 | 21 | _original_methods = { 22 | 'insert': pymongo.collection.Collection.insert, 23 | 'update': pymongo.collection.Collection.update, 24 | 'remove': pymongo.collection.Collection.remove, 25 | 'refresh': pymongo.cursor.Cursor._refresh, 26 | '_unpack_response': pymongo.helpers._unpack_response, 27 | } 28 | 29 | queries = [] 30 | inserts = [] 31 | updates = [] 32 | removes = [] 33 | response_sizes = [] 34 | 35 | if sys.version_info >= (3, 0): 36 | unicode = str 37 | 38 | 39 | # Wrap helpers._unpack_response for getting response size 40 | @functools.wraps(_original_methods['_unpack_response']) 41 | def _unpack_response(response, *args, **kwargs): 42 | 43 | result = _original_methods['_unpack_response']( 44 | response, 45 | *args, 46 | **kwargs 47 | ) 48 | response_sizes.append(sys.getsizeof(response, len(response)) / 1024.0) 49 | return result 50 | 51 | 52 | # Wrap Cursor.insert for getting queries 53 | @functools.wraps(_original_methods['insert']) 54 | def _insert(collection_self, doc_or_docs, manipulate=True, 55 | safe=None, check_keys=True, **kwargs): 56 | start_time = time.time() 57 | result = _original_methods['insert']( 58 | collection_self, 59 | doc_or_docs, 60 | check_keys=check_keys, 61 | **kwargs 62 | ) 63 | total_time = (time.time() - start_time) * 1000 64 | 65 | __traceback_hide__ = True # noqa 66 | stack_trace, internal = _tidy_stacktrace() 67 | inserts.append({ 68 | 'document': doc_or_docs, 69 | 'time': total_time, 70 | 'stack_trace': stack_trace, 71 | 'size': response_sizes[-1] if response_sizes else 0, 72 | 'internal': internal 73 | }) 74 | return result 75 | 76 | 77 | # Wrap Cursor.update for getting queries 78 | @functools.wraps(_original_methods['update']) 79 | def _update(collection_self, spec, document, upsert=False, 80 | maniuplate=False, safe=None, multi=False, **kwargs): 81 | start_time = time.time() 82 | result = _original_methods['update']( 83 | collection_self, 84 | spec, 85 | document, 86 | upsert=upsert, 87 | multi=multi, 88 | **kwargs 89 | ) 90 | total_time = (time.time() - start_time) * 1000 91 | 92 | __traceback_hide__ = True # noqa 93 | stack_trace, internal = _tidy_stacktrace() 94 | updates.append({ 95 | 'document': document, 96 | 'upsert': upsert, 97 | 'multi': multi, 98 | 'spec': spec, 99 | 'time': total_time, 100 | 'stack_trace': stack_trace, 101 | 'size': response_sizes[-1] if response_sizes else 0, 102 | 'internal': internal 103 | }) 104 | return result 105 | 106 | 107 | # Wrap Cursor.remove for getting queries 108 | @functools.wraps(_original_methods['remove']) 109 | def _remove(collection_self, spec_or_id, safe=None, **kwargs): 110 | start_time = time.time() 111 | result = _original_methods['remove']( 112 | collection_self, 113 | spec_or_id, 114 | **kwargs 115 | ) 116 | total_time = (time.time() - start_time) * 1000 117 | 118 | __traceback_hide__ = True # noqa 119 | stack_trace, internal = _tidy_stacktrace() 120 | removes.append({ 121 | 'spec_or_id': spec_or_id, 122 | 'time': total_time, 123 | ' ': stack_trace, 124 | 'size': response_sizes[-1] if response_sizes else 0, 125 | 'internal': internal 126 | }) 127 | return result 128 | 129 | 130 | # Wrap Cursor._refresh for getting queries 131 | @functools.wraps(_original_methods['refresh']) 132 | def _cursor_refresh(cursor_self): 133 | # Look up __ private instance variables 134 | def privar(name): 135 | return getattr(cursor_self, '_Cursor__{0}'.format(name), None) 136 | 137 | if privar('id') is not None: 138 | # getMore not query - move on 139 | return _original_methods['refresh'](cursor_self) 140 | 141 | # NOTE: See pymongo/cursor.py+557 [_refresh()] and 142 | # pymongo/message.py for where information is stored 143 | 144 | # Time the actual query 145 | start_time = time.time() 146 | result = _original_methods['refresh'](cursor_self) 147 | total_time = (time.time() - start_time) * 1000 148 | 149 | query_son = privar('query_spec')() 150 | if not isinstance(query_son, bson.SON): 151 | 152 | if "$query" not in query_son: 153 | query_son = {"$query": query_son} 154 | 155 | data = privar("data") 156 | if data: 157 | query_son["data"] = data 158 | 159 | orderby = privar("ordering") 160 | if orderby: 161 | query_son["$orderby"] = orderby 162 | 163 | hint = privar("hint") 164 | if hint: 165 | query_son["$hint"] = hint 166 | 167 | snapshot = privar("snapshot") 168 | if snapshot: 169 | query_son["$snapshot"] = snapshot 170 | 171 | maxScan = privar("max_scan") 172 | if maxScan: 173 | query_son["$maxScan"] = maxScan 174 | 175 | __traceback_hide__ = True # noqa 176 | stack_trace, internal = _tidy_stacktrace() 177 | query_data = { 178 | 'time': total_time, 179 | 'operation': 'query', 180 | 'stack_trace': stack_trace, 181 | 'size': response_sizes[-1] if response_sizes else 0, 182 | 'data': copy.copy(privar('data')), 183 | 'internal': internal 184 | } 185 | 186 | # Collection in format . 187 | collection_name = privar('collection') 188 | query_data['collection'] = collection_name.full_name.split('.')[1] 189 | 190 | if query_data['collection'] == '$cmd': 191 | query_data['operation'] = 'command' 192 | # Handle count as a special case 193 | if 'count' in query_son: 194 | # Information is in a different format to a standar query 195 | query_data['collection'] = query_son['count'] 196 | query_data['operation'] = 'count' 197 | query_data['skip'] = query_son.get('skip') 198 | query_data['limit'] = query_son.get('limit') 199 | query_data['query'] = query_son['query'] 200 | else: 201 | # Normal Query 202 | query_data['skip'] = privar('skip') 203 | query_data['limit'] = privar('limit') 204 | query_data['query'] = query_son['$query'] 205 | query_data['ordering'] = _get_ordering(query_son) 206 | 207 | queries.append(query_data) 208 | 209 | return result 210 | 211 | 212 | def install_tracker(): 213 | if pymongo.collection.Collection.insert != _insert: 214 | pymongo.collection.Collection.insert = _insert 215 | if pymongo.collection.Collection.update != _update: 216 | pymongo.collection.Collection.update = _update 217 | if pymongo.collection.Collection.remove != _remove: 218 | pymongo.collection.Collection.remove = _remove 219 | if pymongo.cursor.Cursor._refresh != _cursor_refresh: 220 | pymongo.cursor.Cursor._refresh = _cursor_refresh 221 | if pymongo.helpers._unpack_response != _unpack_response: 222 | pymongo.helpers._unpack_response = _unpack_response 223 | 224 | 225 | def uninstall_tracker(): 226 | if pymongo.collection.Collection.insert == _insert: 227 | pymongo.collection.Collection.insert = _original_methods['insert'] 228 | if pymongo.collection.Collection.update == _update: 229 | pymongo.collection.Collection.update = _original_methods['update'] 230 | if pymongo.collection.Collection.remove == _remove: 231 | pymongo.collection.Collection.remove = _original_methods['remove'] 232 | if pymongo.cursor.Cursor._refresh == _cursor_refresh: 233 | pymongo.cursor.Cursor._refresh = _original_methods['cursor_refresh'] 234 | if pymongo.helpers._unpack_response == _unpack_response: 235 | pymongo.helpers._unpack_response = _original_methods['_unpack_response'] 236 | 237 | 238 | def reset(): 239 | global queries, inserts, updates, removes, response_sizes 240 | queries = [] 241 | inserts = [] 242 | updates = [] 243 | removes = [] 244 | response_sizes = [] 245 | 246 | 247 | def _get_ordering(son): 248 | """Helper function to extract formatted ordering from dict. 249 | """ 250 | def fmt(field, direction): 251 | return '{0}{1}'.format({-1: '-', 1: '+'}[direction], field) 252 | 253 | if '$orderby' in son: 254 | return ', '.join(fmt(f, d) for f, d in son['$orderby'].items()) 255 | 256 | 257 | def _tidy_stacktrace(): 258 | """ 259 | Tidy the stack_trace 260 | """ 261 | socketserver_path = os.path.realpath(os.path.dirname(SocketServer.__file__)) 262 | pymongo_path = os.path.realpath(os.path.dirname(pymongo.__file__)) 263 | paths = ['/site-packages/', '/flaskext/', socketserver_path, pymongo_path] 264 | internal = False 265 | 266 | # Check html templates 267 | fnames = [] 268 | for i in range(100): 269 | try: 270 | fname = sys._getframe(i).f_code.co_filename 271 | if '.html' in fname: 272 | fnames.append(fname) 273 | except Exception: 274 | break 275 | fnames = list(set(fnames)) 276 | trace = [] 277 | 278 | for path in fnames: 279 | if 'flask_debugtoolbar' in path: 280 | internal = True 281 | trace.append((path, '?', '?', '?', False)) 282 | 283 | if trace: 284 | return trace, internal 285 | 286 | stack = inspect.stack() 287 | reversed(stack) 288 | 289 | trace = [] 290 | for frame, path, line_no, func_name, text in (f[:5] for f in stack): 291 | s_path = os.path.realpath(path) 292 | # Support hiding of frames -- used in various utilities that provide 293 | # inspection. 294 | if '__traceback_hide__' in frame.f_locals: 295 | continue 296 | hidden = False 297 | if func_name == "": 298 | hidden = True 299 | if any([p for p in paths if p in s_path]): 300 | hidden = True 301 | if not text: 302 | text = '' 303 | else: 304 | if sys.version_info >= (3, 0): 305 | text = ''.join(text).strip() 306 | else: 307 | try: 308 | text = unicode(''.join(text).strip()) 309 | except Exception: 310 | pass 311 | trace.append((path, line_no, func_name, text, hidden)) 312 | return trace, internal 313 | -------------------------------------------------------------------------------- /flask_mongoengine/wtf/orm.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tools for generating forms based on mongoengine Document schemas. 3 | """ 4 | import decimal 5 | import sys 6 | 7 | from bson import ObjectId 8 | 9 | try: 10 | from collections import OrderedDict 11 | except ImportError: 12 | # Use bson's SON implementation instead 13 | from bson import SON as OrderedDict 14 | 15 | from mongoengine import ReferenceField 16 | from wtforms import fields as f, validators 17 | 18 | from flask_mongoengine.wtf.fields import (BinaryField, DictField, 19 | ModelSelectField, 20 | ModelSelectMultipleField, 21 | NoneStringField) 22 | from flask_mongoengine.wtf.models import ModelForm 23 | 24 | __all__ = ( 25 | 'model_fields', 'model_form', 26 | ) 27 | 28 | 29 | if sys.version_info >= (3, 0): 30 | unicode = str 31 | 32 | 33 | def converts(*args): 34 | def _inner(func): 35 | func._converter_for = frozenset(args) 36 | return func 37 | return _inner 38 | 39 | 40 | class ModelConverter(object): 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 | def convert(self, model, field, field_args): 54 | kwargs = { 55 | 'label': getattr(field, 'verbose_name', field.name), 56 | 'description': getattr(field, 'help_text', None) or '', 57 | 'validators': getattr(field, 'validators', None) or [], 58 | 'filters': getattr(field, 'filters', None) or [], 59 | 'default': field.default, 60 | } 61 | if field_args: 62 | kwargs.update(field_args) 63 | 64 | if kwargs['validators']: 65 | # Create a copy of the list since we will be modifying it. 66 | kwargs['validators'] = list(kwargs['validators']) 67 | 68 | if field.required: 69 | kwargs['validators'].append(validators.InputRequired()) 70 | else: 71 | kwargs['validators'].append(validators.Optional()) 72 | 73 | ftype = type(field).__name__ 74 | 75 | if field.choices: 76 | kwargs['choices'] = field.choices 77 | 78 | if ftype in self.converters: 79 | kwargs["coerce"] = self.coerce(ftype) 80 | multiple_field = kwargs.pop('multiple', False) 81 | radio_field = kwargs.pop('radio', False) 82 | if multiple_field: 83 | return f.SelectMultipleField(**kwargs) 84 | if radio_field: 85 | return f.RadioField(**kwargs) 86 | return f.SelectField(**kwargs) 87 | 88 | ftype = type(field).__name__ 89 | 90 | if hasattr(field, 'to_form_field'): 91 | return field.to_form_field(model, kwargs) 92 | 93 | if ftype in self.converters: 94 | return self.converters[ftype](model, field, kwargs) 95 | 96 | @classmethod 97 | def _string_common(cls, model, field, kwargs): 98 | if field.max_length or field.min_length: 99 | kwargs['validators'].append( 100 | validators.Length(max=field.max_length or -1, 101 | min=field.min_length or -1)) 102 | 103 | @classmethod 104 | def _number_common(cls, model, field, kwargs): 105 | if field.max_value or field.min_value: 106 | kwargs['validators'].append( 107 | validators.NumberRange(max=field.max_value, 108 | min=field.min_value)) 109 | 110 | @converts('StringField') 111 | def conv_String(self, model, field, kwargs): 112 | if field.regex: 113 | kwargs['validators'].append(validators.Regexp(regex=field.regex)) 114 | self._string_common(model, field, kwargs) 115 | password_field = kwargs.pop('password', False) 116 | textarea_field = kwargs.pop('textarea', False) or not field.max_length 117 | if password_field: 118 | return f.PasswordField(**kwargs) 119 | if textarea_field: 120 | return f.TextAreaField(**kwargs) 121 | return f.StringField(**kwargs) 122 | 123 | @converts('URLField') 124 | def conv_URL(self, model, field, kwargs): 125 | kwargs['validators'].append(validators.URL()) 126 | self._string_common(model, field, kwargs) 127 | return NoneStringField(**kwargs) 128 | 129 | @converts('EmailField') 130 | def conv_Email(self, model, field, kwargs): 131 | kwargs['validators'].append(validators.Email()) 132 | self._string_common(model, field, kwargs) 133 | return NoneStringField(**kwargs) 134 | 135 | @converts('IntField') 136 | def conv_Int(self, model, field, kwargs): 137 | self._number_common(model, field, kwargs) 138 | return f.IntegerField(**kwargs) 139 | 140 | @converts('FloatField') 141 | def conv_Float(self, model, field, kwargs): 142 | self._number_common(model, field, kwargs) 143 | return f.FloatField(**kwargs) 144 | 145 | @converts('DecimalField') 146 | def conv_Decimal(self, model, field, kwargs): 147 | self._number_common(model, field, kwargs) 148 | return f.DecimalField(**kwargs) 149 | 150 | @converts('BooleanField') 151 | def conv_Boolean(self, model, field, kwargs): 152 | return f.BooleanField(**kwargs) 153 | 154 | @converts('DateTimeField') 155 | def conv_DateTime(self, model, field, kwargs): 156 | return f.DateTimeField(**kwargs) 157 | 158 | @converts('BinaryField') 159 | def conv_Binary(self, model, field, kwargs): 160 | # TODO: may be set file field that will save file`s data to MongoDB 161 | if field.max_bytes: 162 | kwargs['validators'].append(validators.Length(max=field.max_bytes)) 163 | return BinaryField(**kwargs) 164 | 165 | @converts('DictField') 166 | def conv_Dict(self, model, field, kwargs): 167 | return DictField(**kwargs) 168 | 169 | @converts('ListField') 170 | def conv_List(self, model, field, kwargs): 171 | if isinstance(field.field, ReferenceField): 172 | return ModelSelectMultipleField(model=field.field.document_type, **kwargs) 173 | if field.field.choices: 174 | kwargs['multiple'] = True 175 | return self.convert(model, field.field, kwargs) 176 | field_args = kwargs.pop("field_args", {}) 177 | unbound_field = self.convert(model, field.field, field_args) 178 | unacceptable = { 179 | 'validators': [], 180 | 'filters': [], 181 | 'min_entries': kwargs.get('min_entries', 0) 182 | } 183 | kwargs.update(unacceptable) 184 | return f.FieldList(unbound_field, **kwargs) 185 | 186 | @converts('SortedListField') 187 | def conv_SortedList(self, model, field, kwargs): 188 | # TODO: sort functionality, may be need sortable widget 189 | return self.conv_List(model, field, kwargs) 190 | 191 | @converts('GeoLocationField') 192 | def conv_GeoLocation(self, model, field, kwargs): 193 | # TODO: create geo field and widget (also GoogleMaps) 194 | return 195 | 196 | @converts('ObjectIdField') 197 | def conv_ObjectId(self, model, field, kwargs): 198 | return 199 | 200 | @converts('EmbeddedDocumentField') 201 | def conv_EmbeddedDocument(self, model, field, kwargs): 202 | kwargs = { 203 | 'validators': [], 204 | 'filters': [], 205 | 'default': field.default or field.document_type_obj, 206 | } 207 | form_class = model_form(field.document_type_obj, field_args={}) 208 | return f.FormField(form_class, **kwargs) 209 | 210 | @converts('ReferenceField') 211 | def conv_Reference(self, model, field, kwargs): 212 | return ModelSelectField(model=field.document_type, **kwargs) 213 | 214 | @converts('GenericReferenceField') 215 | def conv_GenericReference(self, model, field, kwargs): 216 | return 217 | 218 | def coerce(self, field_type): 219 | coercions = { 220 | "IntField": int, 221 | "BooleanField": bool, 222 | "FloatField": float, 223 | "DecimalField": decimal.Decimal, 224 | "ObjectIdField": ObjectId 225 | } 226 | return coercions.get(field_type, unicode) 227 | 228 | 229 | def model_fields(model, only=None, exclude=None, field_args=None, converter=None): 230 | """ 231 | Generate a dictionary of fields for a given database model. 232 | 233 | See `model_form` docstring for description of parameters. 234 | """ 235 | from mongoengine.base import BaseDocument, DocumentMetaclass 236 | if not isinstance(model, (BaseDocument, DocumentMetaclass)): 237 | raise TypeError('model must be a mongoengine Document schema') 238 | 239 | converter = converter or ModelConverter() 240 | field_args = field_args or {} 241 | 242 | names = ((k, v.creation_counter) for k, v in model._fields.items()) 243 | field_names = [n[0] for n in sorted(names, key=lambda n: n[1])] 244 | 245 | if only: 246 | field_names = [x for x in only if x in set(field_names)] 247 | elif exclude: 248 | field_names = [x for x in field_names if x not in set(exclude)] 249 | 250 | field_dict = OrderedDict() 251 | for name in field_names: 252 | model_field = model._fields[name] 253 | field = converter.convert(model, model_field, field_args.get(name)) 254 | if field is not None: 255 | field_dict[name] = field 256 | 257 | return field_dict 258 | 259 | 260 | def model_form(model, base_class=ModelForm, only=None, exclude=None, field_args=None, converter=None): 261 | """ 262 | Create a wtforms Form for a given mongoengine Document schema:: 263 | 264 | from flask_mongoengine.wtf import model_form 265 | from myproject.myapp.schemas import Article 266 | ArticleForm = model_form(Article) 267 | 268 | :param model: 269 | A mongoengine Document schema class 270 | :param base_class: 271 | Base form class to extend from. Must be a ``wtforms.Form`` subclass. 272 | :param only: 273 | An optional iterable with the property names that should be included in 274 | the form. Only these properties will have fields. 275 | :param exclude: 276 | An optional iterable with the property names that should be excluded 277 | from the form. All other properties will have fields. 278 | :param field_args: 279 | An optional dictionary of field names mapping to keyword arguments used 280 | to construct each field object. 281 | :param converter: 282 | A converter to generate the fields based on the model properties. If 283 | not set, ``ModelConverter`` is used. 284 | """ 285 | field_dict = model_fields(model, only, exclude, field_args, converter) 286 | field_dict['model_class'] = model 287 | return type(model.__name__ + 'Form', (base_class,), field_dict) 288 | -------------------------------------------------------------------------------- /tests/test_forms.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import re 3 | import unittest 4 | 5 | import bson 6 | import flask 7 | from mongoengine import queryset_manager 8 | from werkzeug.datastructures import MultiDict 9 | import wtforms 10 | 11 | from flask_mongoengine import MongoEngine 12 | from flask_mongoengine.wtf import model_form 13 | from tests import FlaskMongoEngineTestCase 14 | 15 | 16 | class WTFormsAppTestCase(FlaskMongoEngineTestCase): 17 | 18 | def setUp(self): 19 | super(WTFormsAppTestCase, self).setUp() 20 | self.db_name = 'test_db' 21 | self.app.config['MONGODB_DB'] = self.db_name 22 | self.app.config['TESTING'] = True 23 | # For Flask-WTF < 0.9 24 | self.app.config['CSRF_ENABLED'] = False 25 | # For Flask-WTF >= 0.9 26 | self.app.config['WTF_CSRF_ENABLED'] = False 27 | self.db = MongoEngine() 28 | self.db.init_app(self.app) 29 | 30 | def tearDown(self): 31 | try: 32 | self.db.connection.drop_database(self.db_name) 33 | except Exception: 34 | self.db.connection.client.drop_database(self.db_name) 35 | 36 | def test_binaryfield(self): 37 | 38 | with self.app.test_request_context('/'): 39 | db = self.db 40 | 41 | class Binary(db.Document): 42 | binary = db.BinaryField() 43 | 44 | BinaryForm = model_form(Binary) 45 | form = BinaryForm(MultiDict({'binary': '1'})) 46 | self.assertTrue(form.validate()) 47 | form.save() 48 | 49 | def test_choices_coerce(self): 50 | 51 | with self.app.test_request_context('/'): 52 | db = self.db 53 | 54 | CHOICES = ((1, "blue"), (2, "red")) 55 | 56 | class MyChoices(db.Document): 57 | pill = db.IntField(choices=CHOICES) 58 | 59 | MyChoicesForm = model_form(MyChoices) 60 | form = MyChoicesForm(MultiDict({"pill": "1"})) 61 | self.assertTrue(form.validate()) 62 | form.save() 63 | self.assertEqual(MyChoices.objects.first().pill, 1) 64 | 65 | def test_list_choices_coerce(self): 66 | 67 | with self.app.test_request_context('/'): 68 | db = self.db 69 | 70 | CHOICES = ((1, "blue"), (2, "red")) 71 | 72 | class MyChoices(db.Document): 73 | pill = db.ListField(db.IntField(choices=CHOICES)) 74 | 75 | MyChoicesForm = model_form(MyChoices) 76 | form = MyChoicesForm(MultiDict({"pill": "1"})) 77 | self.assertTrue(form.validate()) 78 | form.save() 79 | self.assertEqual(MyChoices.objects.first().pill[0], 1) 80 | 81 | def test_emailfield(self): 82 | 83 | with self.app.test_request_context('/'): 84 | db = self.db 85 | 86 | class Email(db.Document): 87 | email = db.EmailField(required=False) 88 | 89 | EmailForm = model_form(Email) 90 | form = EmailForm(instance=Email()) 91 | self.assertFalse("None" in "%s" % form.email) 92 | self.assertTrue(form.validate()) 93 | 94 | form = EmailForm(MultiDict({"email": ""})) 95 | self.assertFalse("None" in "%s" % form.email) 96 | self.assertTrue(form.validate()) 97 | 98 | # Ensure required works 99 | 100 | class Email(db.Document): 101 | email = db.EmailField(required=True) 102 | 103 | EmailForm = model_form(Email) 104 | form = EmailForm(MultiDict({"email": ""})) 105 | self.assertFalse("None" in "%s" % form.email) 106 | self.assertFalse(form.validate()) 107 | 108 | def test_model_form(self): 109 | with self.app.test_request_context('/'): 110 | db = self.db 111 | 112 | class BlogPost(db.Document): 113 | meta = {'allow_inheritance': True} 114 | title = db.StringField(required=True, max_length=200) 115 | posted = db.DateTimeField(default=datetime.datetime.now) 116 | tags = db.ListField(db.StringField()) 117 | 118 | class TextPost(BlogPost): 119 | email = db.EmailField(required=False) 120 | lead_paragraph = db.StringField(max_length=200) 121 | content = db.StringField(required=True) 122 | 123 | class LinkPost(BlogPost): 124 | url = db.StringField(required=True, max_length=200) 125 | interest = db.DecimalField(required=True) 126 | 127 | # Create a text-based post 128 | TextPostForm = model_form( 129 | TextPost, 130 | field_args={'lead_paragraph': {'textarea': True}}) 131 | 132 | form = TextPostForm(MultiDict({ 133 | 'title': 'Using MongoEngine', 134 | 'tags': ['mongodb', 'mongoengine']})) 135 | 136 | self.assertFalse(form.validate()) 137 | 138 | form = TextPostForm(MultiDict({ 139 | 'title': 'Using MongoEngine', 140 | 'content': 'See the tutorial', 141 | 'tags': ['mongodb', 'mongoengine']})) 142 | 143 | self.assertTrue(form.validate()) 144 | form.save() 145 | 146 | self.assertEqual(form.title.type, 'StringField') 147 | self.assertEqual(form.content.type, 'TextAreaField') 148 | self.assertEqual(form.lead_paragraph.type, 'TextAreaField') 149 | 150 | self.assertEqual(BlogPost.objects.first().title, 'Using MongoEngine') 151 | self.assertEqual(BlogPost.objects.count(), 1) 152 | 153 | form = TextPostForm(MultiDict({ 154 | 'title': 'Using Flask-MongoEngine', 155 | 'content': 'See the tutorial', 156 | 'tags': ['flask', 'mongodb', 'mongoengine']})) 157 | 158 | self.assertTrue(form.validate()) 159 | form.save() 160 | self.assertEqual(BlogPost.objects.count(), 2) 161 | 162 | post = BlogPost.objects(title="Using Flask-MongoEngine").get() 163 | 164 | form = TextPostForm(MultiDict({ 165 | 'title': 'Using Flask-MongoEngine', 166 | 'content': 'See the tutorial', 167 | 'tags-0': 'flask', 168 | 'tags-1': 'mongodb', 169 | 'tags-2': 'mongoengine', 170 | 'tags-3': 'flask-mongoengine', 171 | }), instance=post) 172 | self.assertTrue(form.validate()) 173 | form.save() 174 | post = post.reload() 175 | 176 | self.assertEqual(post.tags, ['flask', 'mongodb', 'mongoengine', 'flask-mongoengine']) 177 | 178 | # Create a link post 179 | LinkPostForm = model_form(LinkPost) 180 | 181 | form = LinkPostForm(MultiDict({ 182 | 'title': 'Using Flask-MongoEngine', 183 | 'url': 'http://flask-mongoengine.org', 184 | 'interest': '0', 185 | })) 186 | form.validate() 187 | self.assertTrue(form.validate()) 188 | 189 | def test_model_form_only(self): 190 | with self.app.test_request_context('/'): 191 | db = self.db 192 | 193 | class BlogPost(db.Document): 194 | title = db.StringField(required=True, max_length=200) 195 | posted = db.DateTimeField(default=datetime.datetime.now) 196 | tags = db.ListField(db.StringField()) 197 | 198 | BlogPost.drop_collection() 199 | 200 | BlogPostForm = model_form(BlogPost, only=['tags']) 201 | form = BlogPostForm() 202 | self.assertTrue(hasattr(form, 'tags')) 203 | self.assertFalse(hasattr(form, 'posted')) 204 | 205 | BlogPostForm = model_form(BlogPost, exclude=['posted']) 206 | form = BlogPostForm() 207 | self.assertTrue(hasattr(form, 'tags')) 208 | self.assertFalse(hasattr(form, 'posted')) 209 | 210 | def test_model_form_with_custom_query_set(self): 211 | with self.app.test_request_context('/'): 212 | db = self.db 213 | 214 | class Dog(db.Document): 215 | breed = db.StringField() 216 | 217 | @queryset_manager 218 | def large_objects(cls, queryset): 219 | return queryset(breed__in=['german sheppard', 'wolfhound']) 220 | 221 | class DogOwner(db.Document): 222 | dog = db.ReferenceField(Dog) 223 | 224 | big_dogs = [Dog(breed="german sheppard"), Dog(breed="wolfhound")] 225 | dogs = [Dog(breed="poodle")] + big_dogs 226 | for dog in dogs: 227 | dog.save() 228 | 229 | BigDogForm = model_form(DogOwner, field_args={'dog': {'queryset': Dog.large_objects}}) 230 | 231 | form = BigDogForm(dog=big_dogs[0]) 232 | self.assertTrue(form.validate()) 233 | self.assertEqual(big_dogs, [d[1] for d in form.dog.iter_choices()]) 234 | 235 | def test_modelselectfield(self): 236 | with self.app.test_request_context('/'): 237 | db = self.db 238 | 239 | class Dog(db.Document): 240 | name = db.StringField() 241 | 242 | class DogOwner(db.Document): 243 | dog = db.ReferenceField(Dog) 244 | 245 | DogOwnerForm = model_form(DogOwner, field_args={ 246 | 'dog': {'allow_blank': True} 247 | }) 248 | 249 | dog = Dog(name="fido") 250 | dog.save() 251 | 252 | form = DogOwnerForm(dog=dog) 253 | self.assertTrue(form.validate()) 254 | 255 | self.assertEqual(wtforms.widgets.Select, type(form.dog.widget)) 256 | self.assertFalse(form.dog.widget.multiple) 257 | 258 | # Validate the options - should contain a dog (selected) and a 259 | # blank option there should be an extra blank option. 260 | choices = list(form.dog) 261 | self.assertEqual(len(choices), 2) 262 | self.assertFalse(choices[0].checked) 263 | self.assertEqual(choices[0].data, '__None') 264 | self.assertTrue(choices[1].checked) 265 | self.assertEqual(choices[1].data, dog.pk) 266 | 267 | # Validate selecting one item 268 | form = DogOwnerForm(MultiDict({ 269 | 'dog': dog.id, 270 | })) 271 | self.assertEqual(form.dog.data, dog) 272 | 273 | # Validate selecting no item 274 | form = DogOwnerForm(MultiDict({ 275 | 'dog': u'__None', 276 | }), dog=dog) 277 | self.assertEqual(form.dog.data, None) 278 | 279 | def test_modelselectfield_multiple(self): 280 | with self.app.test_request_context('/'): 281 | db = self.db 282 | 283 | class Dog(db.Document): 284 | name = db.StringField() 285 | 286 | class DogOwner(db.Document): 287 | dogs = db.ListField(db.ReferenceField(Dog)) 288 | 289 | DogOwnerForm = model_form(DogOwner, field_args={ 290 | 'dogs': {'allow_blank': True} 291 | }) 292 | 293 | dogs = [Dog(name="fido"), Dog(name="rex")] 294 | for dog in dogs: 295 | dog.save() 296 | 297 | form = DogOwnerForm(dogs=dogs) 298 | self.assertTrue(form.validate()) 299 | 300 | self.assertEqual(wtforms.widgets.Select, type(form.dogs.widget)) 301 | self.assertTrue(form.dogs.widget.multiple) 302 | 303 | # Validate the options - both dogs should be selected and 304 | # there should be an extra blank option. 305 | choices = list(form.dogs) 306 | self.assertEqual(len(choices), 3) 307 | self.assertFalse(choices[0].checked) 308 | self.assertEqual(choices[0].data, '__None') 309 | self.assertTrue(choices[1].checked) 310 | self.assertEqual(choices[1].data, dogs[0].pk) 311 | self.assertTrue(choices[2].checked) 312 | self.assertEqual(choices[2].data, dogs[1].pk) 313 | 314 | # Validate selecting two items 315 | form = DogOwnerForm(MultiDict({ 316 | 'dogs': [dog.id for dog in dogs], 317 | })) 318 | self.assertEqual(form.dogs.data, dogs) 319 | 320 | # Validate selecting none actually empties the list 321 | form = DogOwnerForm(MultiDict({ 322 | 'dogs': '__None', 323 | }), dogs=dogs) 324 | self.assertEqual(form.dogs.data, None) 325 | 326 | def test_modelselectfield_multiple_initalvalue_None(self): 327 | with self.app.test_request_context('/'): 328 | db = self.db 329 | 330 | class Dog(db.Document): 331 | name = db.StringField() 332 | 333 | class DogOwner(db.Document): 334 | dogs = db.ListField(db.ReferenceField(Dog)) 335 | 336 | DogOwnerForm = model_form(DogOwner) 337 | 338 | dogs = [Dog(name="fido"), Dog(name="rex")] 339 | for dog in dogs: 340 | dog.save() 341 | 342 | form = DogOwnerForm(dogs=None) 343 | self.assertTrue(form.validate()) 344 | 345 | self.assertEqual(wtforms.widgets.Select, type(form.dogs.widget)) 346 | self.assertTrue(form.dogs.widget.multiple) 347 | 348 | # Validate if both dogs are selected 349 | choices = list(form.dogs) 350 | self.assertEqual(len(choices), 2) 351 | self.assertFalse(choices[0].checked) 352 | self.assertFalse(choices[1].checked) 353 | 354 | def test_modelradiofield(self): 355 | with self.app.test_request_context('/'): 356 | db = self.db 357 | 358 | choices = (('male', 'Male'), ('female', 'Female'), ('other', 'Other')) 359 | 360 | class Poll(db.Document): 361 | answer = db.StringField(choices=choices) 362 | 363 | PollForm = model_form(Poll, field_args={'answer': {'radio': True}}) 364 | 365 | form = PollForm(answer=None) 366 | self.assertTrue(form.validate()) 367 | 368 | self.assertEqual(form.answer.type, 'RadioField') 369 | self.assertEqual(form.answer.choices, choices) 370 | 371 | def test_passwordfield(self): 372 | with self.app.test_request_context('/'): 373 | db = self.db 374 | 375 | class User(db.Document): 376 | password = db.StringField() 377 | 378 | UserForm = model_form(User, field_args={'password': {'password': True}}) 379 | form = UserForm(password='12345') 380 | self.assertEqual(wtforms.widgets.PasswordInput, type(form.password.widget)) 381 | 382 | def test_unique_with(self): 383 | 384 | with self.app.test_request_context('/'): 385 | db = self.db 386 | 387 | class Item (db.Document): 388 | owner_id = db.ObjectIdField(required=True) 389 | owner_item_id = db.StringField(required=True, unique_with='owner_id') 390 | 391 | Item.drop_collection() 392 | 393 | object_id = bson.ObjectId() 394 | Item(owner_id=object_id, owner_item_id="1").save() 395 | 396 | try: 397 | Item(owner_id=object_id, owner_item_id="1").save() 398 | self.fail("Should have raised duplicate key error") 399 | except Exception: 400 | pass 401 | 402 | self.assertEqual(1, Item.objects.count()) 403 | 404 | def test_sub_field_args(self): 405 | with self.app.test_request_context('/'): 406 | db = self.db 407 | 408 | class TestModel(db.Document): 409 | lst = db.ListField(db.StringField()) 410 | 411 | field_args = {'lst': {'label': 'Custom Label', 412 | 'field_args': {'widget': wtforms.widgets.HiddenInput(), 413 | 'label': "Hidden Input"}}} 414 | CustomForm = model_form(TestModel, field_args=field_args) 415 | 416 | custom_form = CustomForm(obj=TestModel(lst=["Foo"])) 417 | list_label = flask.render_template_string("{{ custom_form.lst.label }}", custom_form=custom_form) 418 | self.assertTrue("Custom Label" in list_label) 419 | self.assertTrue("Hidden Input" not in list_label) 420 | 421 | sub_label = flask.render_template_string("{{ custom_form.lst }}", custom_form=custom_form) 422 | self.assertTrue("Hidden Input" in sub_label) 423 | 424 | def test_modelselectfield_multiple_selected_elements_must_be_retained(self): 425 | with self.app.test_request_context('/'): 426 | db = self.db 427 | 428 | class Dog(db.Document): 429 | name = db.StringField() 430 | 431 | def __unicode__(self): 432 | return self.name 433 | 434 | class DogOwner(db.Document): 435 | dogs = db.ListField(db.ReferenceField(Dog)) 436 | 437 | DogOwnerForm = model_form(DogOwner) 438 | 439 | fido = Dog(name="fido").save() 440 | Dog(name="rex").save() 441 | 442 | dogOwner = DogOwner(dogs=[fido]) 443 | form = DogOwnerForm(obj=dogOwner) 444 | html = form.dogs() 445 | 446 | m = re.search("", html) 447 | self.assertTrue(m is not None, "Should have one selected option") 448 | self.assertEqual("fido", m.group(1)) 449 | 450 | def test_model_form_help_text(self): 451 | with self.app.test_request_context('/'): 452 | db = self.db 453 | 454 | class BlogPost(db.Document): 455 | title = db.StringField(required=True, help_text="Some imaginative title to set the world on fire") 456 | 457 | post = BlogPost(title="hello world").save() 458 | 459 | BlogPostForm = model_form(BlogPost) 460 | form = BlogPostForm(instance=post) 461 | 462 | self.assertEqual(form.title.description, "Some imaginative title to set the world on fire") 463 | 464 | def test_shared_field_args(self): 465 | with self.app.test_request_context('/'): 466 | db = self.db 467 | 468 | class BlogPost(db.Document): 469 | title = db.StringField(required=True) 470 | content = db.StringField(required=False) 471 | 472 | shared_field_args = {'title': {'validators': [ 473 | wtforms.validators.Regexp('test') 474 | ]}} 475 | 476 | TitleOnlyForm = model_form(BlogPost, field_args=shared_field_args, 477 | exclude=['content']) 478 | BlogPostForm = model_form(BlogPost, field_args=shared_field_args) 479 | 480 | # ensure shared field_args don't create duplicate validators 481 | title_only_form = TitleOnlyForm() 482 | self.assertEqual(len(title_only_form.title.validators), 2) 483 | 484 | blog_post_form = BlogPostForm() 485 | self.assertEqual(len(blog_post_form.title.validators), 2) 486 | 487 | def test_embedded_model_form(self): 488 | with self.app.test_request_context('/'): 489 | db = self.db 490 | 491 | class Content(db.EmbeddedDocument): 492 | text = db.StringField() 493 | lang = db.StringField(max_length=3) 494 | 495 | class Post(db.Document): 496 | title = db.StringField(max_length=120, required=True) 497 | tags = db.ListField(db.StringField(max_length=30)) 498 | content = db.EmbeddedDocumentField("Content") 499 | 500 | PostForm = model_form(Post) 501 | form = PostForm() 502 | self.assertTrue("content-text" in "%s" % form.content.text) 503 | 504 | 505 | if __name__ == '__main__': 506 | unittest.main() 507 | --------------------------------------------------------------------------------