├── tests ├── dbindexes.py ├── contrib │ ├── views.py │ ├── __init__.py │ ├── utils.py │ ├── models.py │ └── tests.py ├── embedded │ ├── views.py │ ├── __init__.py │ ├── utils.py │ ├── tests.py │ └── models.py ├── query │ ├── __init__.py │ ├── utils.py │ └── models.py ├── storage │ ├── models.py │ ├── __init__.py │ ├── utils.py │ └── tests.py ├── aggregations │ ├── views.py │ ├── __init__.py │ ├── utils.py │ ├── models.py │ └── tests.py ├── lookup │ ├── __init__.py │ └── models.py ├── mongodb │ ├── __init__.py │ ├── utils.py │ └── models.py ├── router │ ├── __init__.py │ ├── models.py │ └── tests.py ├── multiple_database │ ├── utils.py │ ├── __init__.py │ ├── fixtures │ │ ├── multidb-common.json │ │ ├── pets.json │ │ ├── multidb.other.json │ │ └── multidb.default.json │ └── models.py ├── settings │ ├── slow_tests.py │ ├── dbindexer.py │ ├── sqlite.py │ ├── router.py │ ├── __init__.py │ ├── settings_base.py │ ├── debug.py │ └── serializer.py ├── manage.py ├── utils.py └── runtests.py ├── docs ├── source │ ├── code │ │ ├── mapreduce │ │ │ ├── __init__.py │ │ │ ├── mr │ │ │ │ ├── __init__.py │ │ │ │ ├── views.py │ │ │ │ ├── models.py │ │ │ │ └── tests.py │ │ │ ├── settings.py │ │ │ ├── manage.py │ │ │ └── urls.py │ │ └── tutorial │ │ │ ├── v1 │ │ │ ├── __init__.py │ │ │ ├── nonrelblog │ │ │ │ ├── __init__.py │ │ │ │ ├── views.py │ │ │ │ ├── models.py │ │ │ │ └── tests.py │ │ │ ├── settings.py │ │ │ ├── manage.py │ │ │ └── urls.py │ │ │ ├── v2 │ │ │ ├── __init__.py │ │ │ ├── nonrelblog │ │ │ │ ├── __init__.py │ │ │ │ ├── views.py │ │ │ │ ├── tests.py │ │ │ │ └── models.py │ │ │ ├── settings.py │ │ │ ├── manage.py │ │ │ └── urls.py │ │ │ ├── v3 │ │ │ ├── __init__.py │ │ │ ├── nonrelblog │ │ │ │ ├── __init__.py │ │ │ │ ├── templates │ │ │ │ │ └── nonrelblog │ │ │ │ │ │ ├── post_detail.html │ │ │ │ │ │ └── post_list.html │ │ │ │ ├── urls.py │ │ │ │ └── models.py │ │ │ ├── settings.py │ │ │ ├── urls.py │ │ │ └── manage.py │ │ │ ├── v4 │ │ │ ├── __init__.py │ │ │ ├── nonrelblog │ │ │ │ ├── __init__.py │ │ │ │ ├── templates │ │ │ │ │ └── nonrelblog │ │ │ │ │ │ ├── post_detail.html │ │ │ │ │ │ └── post_list.html │ │ │ │ ├── urls.py │ │ │ │ └── models.py │ │ │ ├── upload.png │ │ │ ├── itworks.png │ │ │ ├── gridfsuploads │ │ │ │ ├── __init__.py │ │ │ │ ├── admin.py │ │ │ │ ├── urls.py │ │ │ │ ├── models.py │ │ │ │ └── views.py │ │ │ ├── settings.py │ │ │ ├── manage.py │ │ │ └── urls.py │ │ │ └── v7 │ │ │ ├── __init__.py │ │ │ ├── nonrelblog │ │ │ ├── __init__.py │ │ │ ├── templates │ │ │ │ └── nonrelblog │ │ │ │ │ ├── post_detail.html │ │ │ │ │ └── post_list.html │ │ │ ├── urls.py │ │ │ ├── models.py │ │ │ └── tests.py │ │ │ ├── settings.py │ │ │ ├── urls.py │ │ │ └── manage.py │ ├── mongodbtheme │ │ ├── relations.html │ │ ├── sourcelink.html │ │ ├── theme.conf │ │ ├── localtoc.html │ │ ├── static │ │ │ ├── Qlassik_TB.otf │ │ │ ├── QlassikBold_TB.otf │ │ │ └── mongodb.css │ │ ├── sidebar.html │ │ └── layout.html │ ├── meta │ │ ├── authors.rst │ │ ├── changelog.rst │ │ ├── index.rst │ │ └── contributing.rst │ ├── reference │ │ ├── storage.rst │ │ ├── index.rst │ │ ├── mapreduce.rst │ │ ├── lowerlevel.rst │ │ ├── fields.rst │ │ ├── settings.rst │ │ └── model-options.rst │ ├── topics │ │ ├── index.rst │ │ ├── atomic-updates.rst │ │ ├── aggregations.rst │ │ ├── cache.rst │ │ ├── setup.rst │ │ ├── gridfs.rst │ │ ├── lists-and-dicts.rst │ │ ├── embedded-models.rst │ │ ├── mapreduce.rst │ │ └── lowerlevel.rst │ ├── utils.py │ ├── index.rst │ ├── doc-organization.rst │ ├── conf.py │ └── troubleshooting.rst ├── make.bat └── Makefile ├── django_mongodb_engine ├── contrib │ ├── search │ │ ├── __init__.py │ │ ├── tokenizer.py │ │ └── fields.py │ └── __init__.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── tellsiteid.py ├── models.py ├── query.py ├── aggregations.py ├── south.py ├── router.py ├── __init__.py ├── utils.py ├── south_adapter.py ├── storage.py ├── fields.py ├── creation.py └── base.py ├── MANIFEST.in ├── requirements.txt ├── .gitignore ├── AUTHORS.rst ├── README.rst ├── .travis.yml ├── setup.py └── CHANGELOG.rst /tests/dbindexes.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/contrib/views.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/embedded/views.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/query/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/storage/models.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/aggregations/views.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/contrib/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/embedded/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/lookup/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/mongodb/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/router/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/storage/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/aggregations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/contrib/utils.py: -------------------------------------------------------------------------------- 1 | ../utils.py -------------------------------------------------------------------------------- /tests/mongodb/utils.py: -------------------------------------------------------------------------------- 1 | ../utils.py -------------------------------------------------------------------------------- /tests/query/utils.py: -------------------------------------------------------------------------------- 1 | ../utils.py -------------------------------------------------------------------------------- /tests/storage/utils.py: -------------------------------------------------------------------------------- 1 | ../utils.py -------------------------------------------------------------------------------- /docs/source/code/mapreduce/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/aggregations/utils.py: -------------------------------------------------------------------------------- 1 | ../utils.py -------------------------------------------------------------------------------- /tests/embedded/utils.py: -------------------------------------------------------------------------------- 1 | ../utils.py -------------------------------------------------------------------------------- /docs/source/code/mapreduce/mr/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/source/code/tutorial/v1/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/source/code/tutorial/v2/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/source/code/tutorial/v3/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/source/code/tutorial/v4/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/source/code/tutorial/v7/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/source/mongodbtheme/relations.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/source/mongodbtheme/sourcelink.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/multiple_database/utils.py: -------------------------------------------------------------------------------- 1 | ../utils.py -------------------------------------------------------------------------------- /django_mongodb_engine/contrib/search/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /django_mongodb_engine/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/source/meta/authors.rst: -------------------------------------------------------------------------------- 1 | ../../../AUTHORS.rst -------------------------------------------------------------------------------- /django_mongodb_engine/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/source/code/tutorial/v1/nonrelblog/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/source/code/tutorial/v2/nonrelblog/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/source/code/tutorial/v3/nonrelblog/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/source/code/tutorial/v4/nonrelblog/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/source/code/tutorial/v7/nonrelblog/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/source/meta/changelog.rst: -------------------------------------------------------------------------------- 1 | ../../../CHANGELOG.rst -------------------------------------------------------------------------------- /docs/source/code/mapreduce/mr/views.py: -------------------------------------------------------------------------------- 1 | # Create your views here. 2 | -------------------------------------------------------------------------------- /docs/source/code/tutorial/v1/nonrelblog/views.py: -------------------------------------------------------------------------------- 1 | # Create your views here. 2 | -------------------------------------------------------------------------------- /docs/source/code/tutorial/v2/nonrelblog/views.py: -------------------------------------------------------------------------------- 1 | # Create your views here. 2 | -------------------------------------------------------------------------------- /docs/source/mongodbtheme/theme.conf: -------------------------------------------------------------------------------- 1 | [theme] 2 | inherit = basic 3 | stylesheet = mongodb.css 4 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include MANIFEST.in 2 | include README.rst 3 | include AUTHORS.rst 4 | include CHANGELOG.rst 5 | -------------------------------------------------------------------------------- /tests/multiple_database/__init__.py: -------------------------------------------------------------------------------- 1 | """Stolen from the Django testsuite, shaked down for m2m tests.""" 2 | -------------------------------------------------------------------------------- /docs/source/code/tutorial/v4/upload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-nonrel/mongodb-engine/HEAD/docs/source/code/tutorial/v4/upload.png -------------------------------------------------------------------------------- /docs/source/mongodbtheme/localtoc.html: -------------------------------------------------------------------------------- 1 | {%- if display_toc %} 2 |

{{ _('Table Of Contents') }}

3 | {{ toc }} 4 | {%- endif %} 5 | -------------------------------------------------------------------------------- /docs/source/reference/storage.rst: -------------------------------------------------------------------------------- 1 | GridFS Storage 2 | ============== 3 | 4 | .. autoclass:: django_mongodb_engine.storage.GridFSStorage 5 | -------------------------------------------------------------------------------- /docs/source/code/tutorial/v4/itworks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-nonrel/mongodb-engine/HEAD/docs/source/code/tutorial/v4/itworks.png -------------------------------------------------------------------------------- /docs/source/meta/index.rst: -------------------------------------------------------------------------------- 1 | Meta 2 | ==== 3 | 4 | .. toctree:: 5 | :maxdepth: 1 6 | 7 | changelog 8 | contributing 9 | authors 10 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pymongo>=2.8 2 | djangotoolbox>=1.6.0 3 | django-dbindexer>=1.6.0 4 | https://github.com/django-nonrel/django/tarball/nonrel-1.6 5 | -------------------------------------------------------------------------------- /docs/source/mongodbtheme/static/Qlassik_TB.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-nonrel/mongodb-engine/HEAD/docs/source/mongodbtheme/static/Qlassik_TB.otf -------------------------------------------------------------------------------- /docs/source/code/tutorial/v4/gridfsuploads/__init__.py: -------------------------------------------------------------------------------- 1 | from django_mongodb_engine.storage import GridFSStorage 2 | 3 | 4 | gridfs_storage = GridFSStorage() 5 | -------------------------------------------------------------------------------- /docs/source/mongodbtheme/static/QlassikBold_TB.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-nonrel/mongodb-engine/HEAD/docs/source/mongodbtheme/static/QlassikBold_TB.otf -------------------------------------------------------------------------------- /tests/aggregations/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Person(models.Model): 5 | age = models.IntegerField() 6 | birthday = models.DateTimeField() 7 | -------------------------------------------------------------------------------- /docs/source/code/tutorial/v4/gridfsuploads/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib.admin import site 2 | 3 | from gridfsuploads.models import FileUpload 4 | 5 | 6 | site.register(FileUpload) 7 | -------------------------------------------------------------------------------- /docs/source/code/mapreduce/settings.py: -------------------------------------------------------------------------------- 1 | DATABASES = { 2 | 'default': { 3 | 'ENGINE': 'django_mongodb_engine', 4 | 'NAME': 'mapreduce', 5 | }, 6 | } 7 | 8 | INSTALLED_APPS = ['mr'] 9 | -------------------------------------------------------------------------------- /docs/source/code/tutorial/v1/settings.py: -------------------------------------------------------------------------------- 1 | DATABASES = { 2 | 'default': { 3 | 'ENGINE': 'django_mongodb_engine', 4 | 'NAME': 'tutorial', 5 | }, 6 | } 7 | 8 | INSTALLED_APPS = ['nonrelblog'] 9 | -------------------------------------------------------------------------------- /docs/source/code/tutorial/v2/settings.py: -------------------------------------------------------------------------------- 1 | DATABASES = { 2 | 'default': { 3 | 'ENGINE': 'django_mongodb_engine', 4 | 'NAME': 'tutorial', 5 | }, 6 | } 7 | 8 | INSTALLED_APPS = ['nonrelblog'] 9 | -------------------------------------------------------------------------------- /docs/source/code/tutorial/v4/gridfsuploads/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import patterns, url 2 | 3 | 4 | urlpatterns = patterns('gridfsuploads.views', 5 | ('^(?P.+)', 'serve_from_gridfs'), 6 | ) 7 | -------------------------------------------------------------------------------- /docs/source/reference/index.rst: -------------------------------------------------------------------------------- 1 | Reference 2 | ========= 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | fields 8 | storage 9 | mapreduce 10 | settings 11 | model-options 12 | lowerlevel 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *~ 3 | .db* 4 | .pydevproject 5 | .settings 6 | .project 7 | django_mongodb_engine.egg-info 8 | build 9 | .build 10 | docs/.* 11 | dist/ 12 | .tox 13 | 14 | MANIFEST 15 | 16 | 17 | .env 18 | .idea -------------------------------------------------------------------------------- /tests/settings/slow_tests.py: -------------------------------------------------------------------------------- 1 | from settings import * 2 | 3 | 4 | INSTALLED_APPS = DEFAULT_APPS + ['django.contrib.contenttypes', 'multiple_database'] 5 | ROOT_URLCONF = '' 6 | TEST_RUNNER = 'djangotoolbox.test.NonrelTestSuiteRunner' 7 | -------------------------------------------------------------------------------- /docs/source/topics/index.rst: -------------------------------------------------------------------------------- 1 | Topics 2 | ====== 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | lists-and-dicts 8 | embedded-models 9 | atomic-updates 10 | gridfs 11 | mapreduce 12 | cache 13 | aggregations 14 | lowerlevel 15 | -------------------------------------------------------------------------------- /tests/multiple_database/fixtures/multidb-common.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pk": 1, 4 | "model": "multiple_database.book", 5 | "fields": { 6 | "title": "The Definitive Guide to Django", 7 | "published": "2009-7-8" 8 | } 9 | } 10 | ] -------------------------------------------------------------------------------- /docs/source/code/tutorial/v3/settings.py: -------------------------------------------------------------------------------- 1 | DATABASES = { 2 | 'default': { 3 | 'ENGINE': 'django_mongodb_engine', 4 | 'NAME': 'tutorial', 5 | } 6 | }, 7 | 8 | INSTALLED_APPS = ['nonrelblog'] 9 | 10 | ROOT_URLCONF = 'urls' 11 | 12 | DEBUG = TEMPLATE_DEBUG = True 13 | -------------------------------------------------------------------------------- /tests/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /docs/source/code/tutorial/v1/nonrelblog/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from djangotoolbox.fields import ListField 4 | 5 | 6 | class Post(models.Model): 7 | title = models.CharField() 8 | text = models.TextField() 9 | tags = ListField() 10 | comments = ListField() 11 | -------------------------------------------------------------------------------- /docs/source/code/tutorial/v4/gridfsuploads/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from gridfsuploads import gridfs_storage 4 | 5 | 6 | class FileUpload(models.Model): 7 | created_on = models.DateTimeField(auto_now_add=True) 8 | file = models.FileField(storage=gridfs_storage, upload_to='/') 9 | -------------------------------------------------------------------------------- /tests/settings/dbindexer.py: -------------------------------------------------------------------------------- 1 | from settings import * 2 | 3 | 4 | DATABASES['mongodb'] = DATABASES['default'] 5 | DATABASES['default'] = {'ENGINE': 'dbindexer', 'TARGET': 'mongodb'} 6 | 7 | ROOT_URLCONF = '' 8 | DBINDEXER_SITECONF = 'dbindexes' 9 | 10 | INSTALLED_APPS = INSTALLED_APPS + ['dbindexer'] 11 | -------------------------------------------------------------------------------- /tests/router/models.py: -------------------------------------------------------------------------------- 1 | from django.db.models import Model 2 | 3 | from djangotoolbox.fields import RawField 4 | 5 | 6 | class SQLiteModel(Model): 7 | pass 8 | 9 | 10 | class MongoDBModel(Model): 11 | # Wnsure this goes to MongoDB on syncdb: SQLite can't 12 | # handle RawFields. 13 | o = RawField() 14 | -------------------------------------------------------------------------------- /docs/source/code/tutorial/v3/nonrelblog/templates/nonrelblog/post_detail.html: -------------------------------------------------------------------------------- 1 |

{{ post.title }}

2 |

{{ post.created_on }}

3 |

{{ post.text }}

4 |

Comments

5 | {% for comment in post.comments %} 6 |

{{ comment.author.name }} on {{ comment.created_on }}

7 | {{ comment.text }} 8 | {% endfor %} 9 | -------------------------------------------------------------------------------- /docs/source/code/tutorial/v4/nonrelblog/templates/nonrelblog/post_detail.html: -------------------------------------------------------------------------------- 1 |

{{ post.title }}

2 |

{{ post.created_on }}

3 |

{{ post.text }}

4 |

Comments

5 | {% for comment in post.comments %} 6 |

{{ comment.author.name }} on {{ comment.created_on }}

7 | {{ comment.text }} 8 | {% endfor %} 9 | -------------------------------------------------------------------------------- /docs/source/code/tutorial/v7/nonrelblog/templates/nonrelblog/post_detail.html: -------------------------------------------------------------------------------- 1 |

{{ post.title }}

2 |

{{ post.created_on }}

3 |

{{ post.text }}

4 |

Comments

5 | {% for comment in post.comments %} 6 |

{{ comment.author.name }} on {{ comment.created_on }}

7 | {{ comment.text }} 8 | {% endfor %} 9 | -------------------------------------------------------------------------------- /tests/settings/sqlite.py: -------------------------------------------------------------------------------- 1 | from settings import * 2 | 3 | 4 | DATABASES = { 5 | 'default': { 6 | 'NAME': 'test', 7 | 'ENGINE': 'sqlite3', 8 | }, 9 | } 10 | for app in ['embedded', 'storage']: 11 | INSTALLED_APPS.remove(app) 12 | 13 | DATABASES['mongodb'] = {'NAME': 'mongodb', 'ENGINE': 'django_mongodb_engine'} 14 | -------------------------------------------------------------------------------- /docs/source/code/mapreduce/mr/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from django_mongodb_engine.contrib import MongoDBManager 4 | 5 | 6 | class Article(models.Model): 7 | author = models.ForeignKey('Author') 8 | text = models.TextField() 9 | 10 | objects = MongoDBManager() 11 | 12 | 13 | class Author(models.Model): 14 | pass 15 | -------------------------------------------------------------------------------- /docs/source/code/tutorial/v3/nonrelblog/templates/nonrelblog/post_list.html: -------------------------------------------------------------------------------- 1 |

Post Overview

2 | {% for post in post_list %} 3 |

{{ post.title }}

4 |

5 | {{ post.created_on }} | 6 | {{ post.comments|length }} comments | 7 | tagged {{ post.tags|join:', ' }} 8 |

9 | {% endfor %} 10 | -------------------------------------------------------------------------------- /docs/source/code/tutorial/v4/nonrelblog/templates/nonrelblog/post_list.html: -------------------------------------------------------------------------------- 1 |

Post Overview

2 | {% for post in post_list %} 3 |

{{ post.title }}

4 |

5 | {{ post.created_on }} | 6 | {{ post.comments|length }} comments | 7 | tagged {{ post.tags|join:', ' }} 8 |

9 | {% endfor %} 10 | -------------------------------------------------------------------------------- /docs/source/code/tutorial/v7/nonrelblog/templates/nonrelblog/post_list.html: -------------------------------------------------------------------------------- 1 |

Post Overview

2 | {% for post in post_list %} 3 |

{{ post.title }}

4 |

5 | {{ post.created_on }} | 6 | {{ post.comments|length }} comments | 7 | tagged {{ post.tags|join:', ' }} 8 |

9 | {% endfor %} 10 | -------------------------------------------------------------------------------- /tests/settings/router.py: -------------------------------------------------------------------------------- 1 | from settings import * 2 | 3 | 4 | INSTALLED_APPS = ['query', 'router'] 5 | 6 | DATABASES['default'] = {'ENGINE': 'django.db.backends.sqlite3', 'NAME': '/tmp/db.sql'} 7 | 8 | DATABASE_ROUTERS = ['django_mongodb_engine.router.MongoDBRouter'] 9 | MONGODB_MANAGED_APPS = ['query'] 10 | MONGODB_MANAGED_MODELS = ['router.MongoDBModel'] 11 | -------------------------------------------------------------------------------- /docs/source/mongodbtheme/sidebar.html: -------------------------------------------------------------------------------- 1 |

⚠ Documentation version

2 |

3 | These are the Git version docs. 4 | Docs for 0.4 (PyPI) are here. 5 |

6 | 7 |

Help out!

8 |

9 | To make this documentation even better, we'd love to receive your 10 | feedback and suggestions for improvement! 11 |

12 | -------------------------------------------------------------------------------- /docs/source/code/tutorial/v7/settings.py: -------------------------------------------------------------------------------- 1 | DATABASES = { 2 | 'default': { 3 | 'ENGINE': 'django_mongodb_engine', 4 | 'NAME': 'tutorial', 5 | }, 6 | } 7 | 8 | INSTALLED_APPS = [ 9 | 'django.contrib.contenttypes', 10 | 'django.contrib.auth', 11 | 'django.contrib.admin', 12 | 13 | 'nonrelblog', 14 | ] 15 | 16 | ROOT_URLCONF = 'urls' 17 | 18 | DEBUG = TEMPLATE_DEBUG = True 19 | -------------------------------------------------------------------------------- /tests/multiple_database/fixtures/pets.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pk": 1, 4 | "model": "multiple_database.pet", 5 | "fields": { 6 | "name": "Mr Bigglesworth", 7 | "owner": 1 8 | } 9 | }, 10 | { 11 | "pk": 2, 12 | "model": "multiple_database.pet", 13 | "fields": { 14 | "name": "Spot", 15 | "owner": 2 16 | } 17 | } 18 | ] -------------------------------------------------------------------------------- /tests/settings/__init__.py: -------------------------------------------------------------------------------- 1 | from settings_base import * 2 | 3 | 4 | DEFAULT_APPS = [ 5 | 'django.contrib.auth', 6 | 'django.contrib.sessions', 7 | 'django.contrib.messages', 8 | ] 9 | 10 | INSTALLED_APPS = [ 11 | 'django_mongodb_engine', 12 | 'djangotoolbox', 13 | 'query', 14 | 'lookup', 15 | 'embedded', 16 | 'mongodb', 17 | 'aggregations', 18 | 'contrib', 19 | 'storage', 20 | ] 21 | -------------------------------------------------------------------------------- /docs/source/code/tutorial/v4/settings.py: -------------------------------------------------------------------------------- 1 | DATABASES = { 2 | 'default': { 3 | 'ENGINE': 'django_mongodb_engine', 4 | 'NAME': 'tutorial', 5 | }, 6 | } 7 | 8 | INSTALLED_APPS = [ 9 | 'django.contrib.contenttypes', 10 | 'django.contrib.auth', 11 | 'django.contrib.admin', 12 | 13 | 'nonrelblog', 14 | 'gridfsuploads', 15 | ] 16 | 17 | ROOT_URLCONF = 'urls' 18 | 19 | DEBUG = TEMPLATE_DEBUG = True 20 | -------------------------------------------------------------------------------- /docs/source/utils.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from subprocess import CalledProcessError, check_output 3 | 4 | 5 | def get_current_year(): 6 | return datetime.utcnow().year 7 | 8 | 9 | def get_git_head(): 10 | try: 11 | return check_output(['git', 'rev-parse', 'HEAD']) 12 | except CalledProcessError: 13 | pass 14 | except OSError, exc: 15 | if exc.errno != 2: 16 | raise 17 | -------------------------------------------------------------------------------- /docs/source/code/tutorial/v7/nonrelblog/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import patterns, url 2 | from django.views.generic import ListView, DetailView 3 | from models import Post 4 | 5 | post_detail = DetailView.as_view(model=Post) 6 | post_list = ListView.as_view(model=Post) 7 | 8 | urlpatterns = patterns('', 9 | url(r'^post/(?P[a-z\d]+)/$', post_detail, name='post_detail'), 10 | url(r'^$', post_list, name='post_list'), 11 | ) 12 | -------------------------------------------------------------------------------- /docs/source/code/tutorial/v3/nonrelblog/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import patterns, url 2 | from django.views.generic import ListView, DetailView 3 | 4 | from models import Post 5 | 6 | 7 | post_detail = DetailView.as_view(model=Post) 8 | post_list = ListView.as_view(model=Post) 9 | 10 | urlpatterns = patterns('', 11 | url(r'^post/(?P[a-z\d]+)/$', post_detail, name='post_detail'), 12 | url(r'^$', post_list, name='post_list'), 13 | ) 14 | -------------------------------------------------------------------------------- /docs/source/code/tutorial/v4/nonrelblog/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import patterns, url 2 | from django.views.generic import ListView, DetailView 3 | 4 | from models import Post 5 | 6 | 7 | post_detail = DetailView.as_view(model=Post) 8 | post_list = ListView.as_view(model=Post) 9 | 10 | urlpatterns = patterns('', 11 | url(r'^post/(?P[a-z\d]+)/$', post_detail, name='post_detail'), 12 | url(r'^$', post_list, name='post_list'), 13 | ) 14 | -------------------------------------------------------------------------------- /docs/source/code/tutorial/v7/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import patterns, include, url 2 | from django.contrib import admin 3 | 4 | 5 | admin.autodiscover() 6 | 7 | urlpatterns = patterns('', 8 | # Uncomment the admin/doc line below to enable admin documentation: 9 | # url(r'^admin/doc/', include('django.contrib.admindocs.urls')), 10 | 11 | url(r'^admin/', include(admin.site.urls)), 12 | 13 | url('', include('nonrelblog.urls')), 14 | ) 15 | -------------------------------------------------------------------------------- /tests/settings/settings_base.py: -------------------------------------------------------------------------------- 1 | DATABASES = { 2 | 'default': { 3 | 'ENGINE': 'django_mongodb_engine', 4 | 'NAME': 'test', 5 | 'OPTIONS': {'OPERATIONS': {}} 6 | }, 7 | 'other': { 8 | 'ENGINE': 'django_mongodb_engine', 9 | 'NAME': 'test2' 10 | }, 11 | } 12 | 13 | SERIALIZATION_MODULES = {'json': 'settings.serializer'} 14 | 15 | SECRET_KEY = 'super secret' 16 | 17 | try: 18 | from local_settings import * 19 | except ImportError: 20 | pass 21 | -------------------------------------------------------------------------------- /tests/settings/debug.py: -------------------------------------------------------------------------------- 1 | from . import * 2 | 3 | 4 | TEST_DEBUG = True 5 | 6 | LOGGING = { 7 | 'version': 1, 8 | 'formatters': {'simple': {'format': '%(levelname)s %(message)s'}}, 9 | 'handlers': { 10 | 'console': { 11 | 'level': 'DEBUG', 12 | 'class': 'logging.StreamHandler', 13 | 'formatter': 'simple', 14 | } 15 | }, 16 | 'loggers': { 17 | 'django.db.backends': { 18 | 'level': 'DEBUG', 19 | 'handlers': ['console'], 20 | }, 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /docs/source/code/tutorial/v3/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import patterns, include, url 2 | 3 | # Uncomment the next two lines to enable the admin: 4 | # from django.contrib import admin 5 | # admin.autodiscover() 6 | 7 | urlpatterns = patterns('', 8 | # Uncomment the admin/doc line below to enable admin documentation: 9 | # url(r'^admin/doc/', include('django.contrib.admindocs.urls')), 10 | 11 | # Uncomment the next line to enable the admin: 12 | # url(r'^admin/', include(admin.site.urls)), 13 | 14 | url('', include('nonrelblog.urls')), 15 | ) 16 | -------------------------------------------------------------------------------- /tests/embedded/tests.py: -------------------------------------------------------------------------------- 1 | from django_mongodb_engine.query import A 2 | 3 | from models import * 4 | from utils import TestCase, get_collection 5 | 6 | 7 | class EmbeddedModelFieldTestCase(TestCase): 8 | 9 | def test_query_embedded(self): 10 | Model(x=3, em=EmbeddedModel(charfield='foo')).save() 11 | obj = Model(x=3, em=EmbeddedModel(charfield='blurg')) 12 | obj.save() 13 | Model(x=3, em=EmbeddedModel(charfield='bar')).save() 14 | obj_from_db = Model.objects.get(em=A('charfield', 'blurg')) 15 | self.assertEqual(obj, obj_from_db) 16 | -------------------------------------------------------------------------------- /docs/source/code/mapreduce/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from django.core.management import execute_manager 3 | import imp 4 | try: 5 | imp.find_module('settings') # Assumed to be in the same directory. 6 | except ImportError: 7 | import sys 8 | sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n" % __file__) 9 | sys.exit(1) 10 | 11 | import settings 12 | 13 | if __name__ == "__main__": 14 | execute_manager(settings) 15 | -------------------------------------------------------------------------------- /docs/source/code/tutorial/v1/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from django.core.management import execute_manager 3 | import imp 4 | try: 5 | imp.find_module('settings') # Assumed to be in the same directory. 6 | except ImportError: 7 | import sys 8 | sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n" % __file__) 9 | sys.exit(1) 10 | 11 | import settings 12 | 13 | if __name__ == "__main__": 14 | execute_manager(settings) 15 | -------------------------------------------------------------------------------- /docs/source/code/tutorial/v2/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from django.core.management import execute_manager 3 | import imp 4 | try: 5 | imp.find_module('settings') # Assumed to be in the same directory. 6 | except ImportError: 7 | import sys 8 | sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n" % __file__) 9 | sys.exit(1) 10 | 11 | import settings 12 | 13 | if __name__ == "__main__": 14 | execute_manager(settings) 15 | -------------------------------------------------------------------------------- /docs/source/code/tutorial/v3/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from django.core.management import execute_manager 3 | import imp 4 | try: 5 | imp.find_module('settings') # Assumed to be in the same directory. 6 | except ImportError: 7 | import sys 8 | sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n" % __file__) 9 | sys.exit(1) 10 | 11 | import settings 12 | 13 | if __name__ == "__main__": 14 | execute_manager(settings) 15 | -------------------------------------------------------------------------------- /docs/source/code/tutorial/v4/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from django.core.management import execute_manager 3 | import imp 4 | try: 5 | imp.find_module('settings') # Assumed to be in the same directory. 6 | except ImportError: 7 | import sys 8 | sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n" % __file__) 9 | sys.exit(1) 10 | 11 | import settings 12 | 13 | if __name__ == "__main__": 14 | execute_manager(settings) 15 | -------------------------------------------------------------------------------- /docs/source/code/tutorial/v7/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from django.core.management import execute_manager 3 | import imp 4 | try: 5 | imp.find_module('settings') # Assumed to be in the same directory. 6 | except ImportError: 7 | import sys 8 | sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n" % __file__) 9 | sys.exit(1) 10 | 11 | import settings 12 | 13 | if __name__ == "__main__": 14 | execute_manager(settings) 15 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | Welcome 2 | ======= 3 | 4 | Django MongoDB Engine is a MongoDB_ backend for Django_, 5 | the Python Web framework for perfectionists with deadlines. 6 | 7 | This documentation is split into several sections: 8 | 9 | .. include:: doc-organization.rst 10 | 11 | Contents 12 | -------- 13 | .. toctree:: 14 | :maxdepth: 2 15 | :titlesonly: 16 | 17 | Intro 18 | tutorial 19 | topics/setup 20 | topics/index 21 | reference/index 22 | troubleshooting 23 | meta/index 24 | 25 | .. _MongoDB: http://mongodb.org 26 | .. _Django: http://djangoproject.com 27 | -------------------------------------------------------------------------------- /docs/source/reference/mapreduce.rst: -------------------------------------------------------------------------------- 1 | Map/Reduce 2 | ========== 3 | :: 4 | 5 | from django_mongodb_engine.contrib import MongoDBManager 6 | 7 | class MapReduceableModel(models.Model): 8 | ... 9 | objects = MongoDBManager() 10 | 11 | :: 12 | 13 | >>> MapReduceableModel.objects.filter(...).map_reduce(...) 14 | 15 | .. currentmodule:: django_mongodb_engine.contrib 16 | 17 | .. TODO Should be documented as MongoDBManager.map_reduce 18 | .. automethod:: MongoDBQuerySet.map_reduce(...) 19 | 20 | .. automethod:: MongoDBQuerySet.inline_map_reduce(...) 21 | 22 | .. autoclass:: MapReduceResult 23 | -------------------------------------------------------------------------------- /docs/source/code/tutorial/v1/nonrelblog/tests.py: -------------------------------------------------------------------------------- 1 | __test__ = {'v1': """ 2 | >>> from nonrelblog.models import Post 3 | >>> post = Post.objects.create( 4 | ... title='Hello MongoDB!', 5 | ... text='Just wanted to drop a note from Django. Cya!', 6 | ... tags=['mongodb', 'django'] 7 | ... ) 8 | 9 | Surely we want to add some comments. 10 | 11 | >>> post.comments 12 | [] 13 | >>> post.comments.extend(['Great post!', 'Please, do more of these!']) 14 | >>> post.save() 15 | 16 | Look and see, it has actually been saved! 17 | 18 | >>> Post.objects.get().comments 19 | [u'Great post!', u'Please, do more of these!'] 20 | """} 21 | -------------------------------------------------------------------------------- /docs/source/code/tutorial/v4/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import patterns, include, url 2 | 3 | # Uncomment the next two lines to enable the admin: 4 | from django.contrib import admin 5 | admin.autodiscover() 6 | 7 | urlpatterns = patterns('', 8 | # Uncomment the admin/doc line below to enable admin documentation: 9 | # url(r'^admin/doc/', include('django.contrib.admindocs.urls')), 10 | 11 | # Uncomment the next line to enable the admin: 12 | url(r'^admin/', include(admin.site.urls)), 13 | 14 | url('^uploads/', include('gridfsuploads.urls')), 15 | url('', include('nonrelblog.urls')), 16 | ) 17 | -------------------------------------------------------------------------------- /tests/multiple_database/fixtures/multidb.other.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pk": 1, 4 | "model": "multiple_database.person", 5 | "fields": { 6 | "name": "Mark Pilgrim" 7 | } 8 | }, 9 | { 10 | "pk": 2, 11 | "model": "multiple_database.person", 12 | "fields": { 13 | "name": "Chris Mills" 14 | } 15 | }, 16 | { 17 | "pk": 2, 18 | "model": "multiple_database.book", 19 | "fields": { 20 | "title": "Dive into Python", 21 | "published": "2009-5-4", 22 | "editor": ["Chris Mills"] 23 | } 24 | } 25 | ] 26 | -------------------------------------------------------------------------------- /docs/source/code/mapreduce/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import patterns, include, url 2 | 3 | 4 | # Uncomment the next two lines to enable the admin: 5 | # from django.contrib import admin 6 | # admin.autodiscover() 7 | 8 | urlpatterns = patterns('', 9 | # Examples: 10 | # url(r'^$', 'v1.views.home', name='home'), 11 | # url(r'^v1/', include('v1.foo.urls')), 12 | 13 | # Uncomment the admin/doc line below to enable admin documentation: 14 | # url(r'^admin/doc/', include('django.contrib.admindocs.urls')), 15 | 16 | # Uncomment the next line to enable the admin: 17 | # url(r'^admin/', include(admin.site.urls)), 18 | ) 19 | -------------------------------------------------------------------------------- /docs/source/code/tutorial/v1/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import patterns, include, url 2 | 3 | 4 | # Uncomment the next two lines to enable the admin: 5 | # from django.contrib import admin 6 | # admin.autodiscover() 7 | 8 | urlpatterns = patterns('', 9 | # Examples: 10 | # url(r'^$', 'v1.views.home', name='home'), 11 | # url(r'^v1/', include('v1.foo.urls')), 12 | 13 | # Uncomment the admin/doc line below to enable admin documentation: 14 | # url(r'^admin/doc/', include('django.contrib.admindocs.urls')), 15 | 16 | # Uncomment the next line to enable the admin: 17 | # url(r'^admin/', include(admin.site.urls)), 18 | ) 19 | -------------------------------------------------------------------------------- /docs/source/code/tutorial/v2/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import patterns, include, url 2 | 3 | 4 | # Uncomment the next two lines to enable the admin: 5 | # from django.contrib import admin 6 | # admin.autodiscover() 7 | 8 | urlpatterns = patterns('', 9 | # Examples: 10 | # url(r'^$', 'v1.views.home', name='home'), 11 | # url(r'^v1/', include('v1.foo.urls')), 12 | 13 | # Uncomment the admin/doc line below to enable admin documentation: 14 | # url(r'^admin/doc/', include('django.contrib.admindocs.urls')), 15 | 16 | # Uncomment the next line to enable the admin: 17 | # url(r'^admin/', include(admin.site.urls)), 18 | ) 19 | -------------------------------------------------------------------------------- /tests/multiple_database/fixtures/multidb.default.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pk": 1, 4 | "model": "multiple_database.person", 5 | "fields": { 6 | "name": "Marty Alchin" 7 | } 8 | }, 9 | { 10 | "pk": 2, 11 | "model": "multiple_database.person", 12 | "fields": { 13 | "name": "George Vilches" 14 | } 15 | }, 16 | { 17 | "pk": 2, 18 | "model": "multiple_database.book", 19 | "fields": { 20 | "title": "Pro Django", 21 | "published": "2008-12-16", 22 | "editor": ["George Vilches"] 23 | } 24 | } 25 | ] 26 | -------------------------------------------------------------------------------- /docs/source/code/tutorial/v2/nonrelblog/tests.py: -------------------------------------------------------------------------------- 1 | __test__ = { 2 | 'v4': """ 3 | >>> from nonrelblog.models import Post 4 | >>> from nonrelblog.models import Comment, Author 5 | >>> Comment( 6 | ... author=Author(name='Bob', email='bob@example.org'), 7 | ... text='The cake is a lie' 8 | ... ).save() 9 | >>> comment = Comment.objects.get() 10 | >>> comment.author 11 | 12 | >>> Post( 13 | ... title='I like cake', 14 | ... comments=[comment] 15 | ... ).save() 16 | >>> post = Post.objects.get(title='I like cake') 17 | >>> post.comments 18 | [] 19 | >>> post.comments[0].author.email 20 | u'bob@example.org' 21 | """} 22 | -------------------------------------------------------------------------------- /django_mongodb_engine/models.py: -------------------------------------------------------------------------------- 1 | # If you wonder what this file is about please head over to '__init__.py' :-) 2 | 3 | from django.db.models import signals 4 | 5 | 6 | def class_prepared_mongodb_signal(sender, *args, **kwargs): 7 | mongo_meta = getattr(sender, 'MongoMeta', None) 8 | if mongo_meta is not None: 9 | for attr in dir(mongo_meta): 10 | if not attr.startswith('_'): 11 | if attr == 'index_together': 12 | attr_name = 'mongo_index_together' 13 | else: 14 | attr_name = attr 15 | setattr(sender._meta, attr_name, getattr(mongo_meta, attr)) 16 | 17 | signals.class_prepared.connect(class_prepared_mongodb_signal) 18 | -------------------------------------------------------------------------------- /docs/source/code/tutorial/v4/gridfsuploads/views.py: -------------------------------------------------------------------------------- 1 | from mimetypes import guess_type 2 | 3 | from django.conf import settings 4 | from django.http import HttpResponse, Http404 5 | 6 | from gridfs.errors import NoFile 7 | from gridfsuploads import gridfs_storage 8 | from gridfsuploads.models import FileUpload 9 | 10 | 11 | if settings.DEBUG: 12 | 13 | def serve_from_gridfs(request, path): 14 | # Serving GridFS files through Django is inefficient and 15 | # insecure. NEVER USE IN PRODUCTION! 16 | try: 17 | gridfile = gridfs_storage.open(path) 18 | except NoFile: 19 | raise Http404 20 | else: 21 | return HttpResponse(gridfile, mimetype=guess_type(path)[0]) 22 | -------------------------------------------------------------------------------- /docs/source/doc-organization.rst: -------------------------------------------------------------------------------- 1 | * :doc:`The tutorial ` introduces you to non-relational schema design 2 | and tools provided by Django MongoDB Engine to put that design into action. 3 | * Our :doc:`topical guides ` explain every feature of 4 | Django MongoDB Engine in great detail -- they'll provide you with usage guides, 5 | API overviews, code samples etc. 6 | * The :doc:`API reference ` has all the lower-level details on 7 | functions, methods, classes, settings, modules and whatnot. 8 | * In the :doc:`Meta section `, we collect information about the 9 | project itself: :doc:`What has changed between versions `, 10 | how development happens and :doc:`how you can get involved `. 11 | -------------------------------------------------------------------------------- /django_mongodb_engine/management/commands/tellsiteid.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import NoArgsCommand 2 | 3 | 4 | class Command(NoArgsCommand): 5 | help = "Tells the ID of the default Site object." 6 | 7 | def handle_noargs(self, **options): 8 | verbosity = int(options.get('verbosity', 1)) 9 | site_id = self._get_site_id() 10 | if verbosity >= 1: 11 | self.stdout.write( 12 | "The default site's ID is %r. To use the sites framework, " 13 | "add this line to settings.py:\nSITE_ID=%r" % 14 | (site_id, site_id)) 15 | else: 16 | self.stdout.write(site_id) 17 | 18 | def _get_site_id(self): 19 | from django.contrib.sites.models import Site 20 | return Site.objects.get().id 21 | -------------------------------------------------------------------------------- /docs/source/code/tutorial/v2/nonrelblog/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from djangotoolbox.fields import ListField, EmbeddedModelField 4 | 5 | 6 | class Post(models.Model): 7 | created_on = models.DateTimeField(auto_now_add=True, null=True) 8 | title = models.CharField() 9 | text = models.TextField() 10 | tags = ListField() 11 | comments = ListField(EmbeddedModelField('Comment')) # <--- 12 | 13 | 14 | class Comment(models.Model): 15 | created_on = models.DateTimeField(auto_now_add=True) 16 | author = EmbeddedModelField('Author') 17 | text = models.TextField() 18 | 19 | 20 | class Author(models.Model): 21 | name = models.CharField() 22 | email = models.EmailField() 23 | 24 | def __unicode__(self): 25 | return '%s (%s)' % (self.name, self.email) 26 | -------------------------------------------------------------------------------- /django_mongodb_engine/query.py: -------------------------------------------------------------------------------- 1 | from warnings import warn 2 | 3 | from djangotoolbox.fields import RawField, AbstractIterableField, \ 4 | EmbeddedModelField 5 | 6 | 7 | __all__ = ['A'] 8 | 9 | 10 | DJANGOTOOLBOX_FIELDS = (RawField, AbstractIterableField, EmbeddedModelField) 11 | 12 | 13 | class A(object): 14 | 15 | def __init__(self, op, value): 16 | warn("A() queries are deprecated as of 0.5 and will be removed in 0.6.", DeprecationWarning) 17 | 18 | self.op = op 19 | self.val = value 20 | 21 | def as_q(self, field): 22 | if isinstance(field, DJANGOTOOLBOX_FIELDS): 23 | return '%s.%s' % (field.column, self.op), self.val 24 | else: 25 | raise TypeError("Can not use A() queries on %s." % 26 | field.__class__.__name__) 27 | -------------------------------------------------------------------------------- /tests/contrib/models.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.db import models 3 | from django.utils.translation import ugettext_lazy as _ 4 | 5 | from django_mongodb_engine.contrib import MongoDBManager 6 | from django_mongodb_engine.contrib.search.fields import TokenizedField 7 | 8 | 9 | class MapReduceModel(models.Model): 10 | n = models.IntegerField() 11 | m = models.IntegerField() 12 | 13 | objects = MongoDBManager() 14 | 15 | 16 | class MapReduceModelWithCustomPrimaryKey(models.Model): 17 | primarykey = models.CharField(max_length=100, primary_key=True) 18 | data = models.CharField(max_length=100) 19 | objects = MongoDBManager() 20 | 21 | 22 | class Post(models.Model): 23 | content = TokenizedField(max_length=255) 24 | 25 | def __unicode__(self): 26 | return "Post" 27 | -------------------------------------------------------------------------------- /docs/source/code/tutorial/v3/nonrelblog/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from djangotoolbox.fields import ListField, EmbeddedModelField 4 | 5 | 6 | class Post(models.Model): 7 | created_on = models.DateTimeField(auto_now_add=True, null=True) 8 | title = models.CharField(max_length=100) 9 | text = models.TextField() 10 | tags = ListField() 11 | comments = ListField(EmbeddedModelField('Comment')) # <--- 12 | 13 | 14 | class Comment(models.Model): 15 | created_on = models.DateTimeField(auto_now_add=True) 16 | author = EmbeddedModelField('Author') 17 | text = models.TextField() 18 | 19 | 20 | class Author(models.Model): 21 | name = models.CharField(max_length=50) 22 | email = models.EmailField() 23 | 24 | def __unicode__(self): 25 | return '%s (%s)' % (self.name, self.email) 26 | -------------------------------------------------------------------------------- /docs/source/code/tutorial/v4/nonrelblog/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from djangotoolbox.fields import ListField, EmbeddedModelField 4 | 5 | 6 | class Post(models.Model): 7 | created_on = models.DateTimeField(auto_now_add=True, null=True) 8 | title = models.CharField(max_length=100) 9 | text = models.TextField() 10 | tags = ListField() 11 | comments = ListField(EmbeddedModelField('Comment')) # <--- 12 | 13 | 14 | class Comment(models.Model): 15 | created_on = models.DateTimeField(auto_now_add=True) 16 | author = EmbeddedModelField('Author') 17 | text = models.TextField() 18 | 19 | 20 | class Author(models.Model): 21 | name = models.CharField(max_length=50) 22 | email = models.EmailField() 23 | 24 | def __unicode__(self): 25 | return '%s (%s)' % (self.name, self.email) 26 | -------------------------------------------------------------------------------- /docs/source/mongodbtheme/layout.html: -------------------------------------------------------------------------------- 1 | {# Hack to get rid of jQuery and friends #} 2 | {% set script_files = [] %} 3 | 4 | {% extends 'basic/layout.html' %} 5 | 6 | {# No relbars please #} 7 | {% block relbar1 %}{% endblock %} 8 | {% block relbar2 %}{% endblock %} 9 | 10 | {%- block header %} 11 | 18 | {%- endblock %} 19 | 20 | {# Sidebar before content #} 21 | {%- block sidebar1 %} 22 |
23 | {{ sidebar() }} 24 | {% endblock %} 25 | {%- block sidebar2 %}{% endblock %} 26 | 27 | {# Wrap all content to make special-casing certain pages possible #} 28 | {%- block content %} 29 |
30 | {{ super() }} 31 |
32 | {%- endblock %} 33 | -------------------------------------------------------------------------------- /tests/lookup/models.py: -------------------------------------------------------------------------------- 1 | """ 2 | 7. The lookup API 3 | 4 | This demonstrates features of the database API. 5 | """ 6 | 7 | from django.conf import settings 8 | from django.db import models, DEFAULT_DB_ALIAS, connection 9 | 10 | 11 | class Author(models.Model): 12 | name = models.CharField(max_length=100) 13 | 14 | class Meta: 15 | ordering = ('name', ) 16 | 17 | 18 | class Article(models.Model): 19 | headline = models.CharField(max_length=100) 20 | pub_date = models.DateTimeField() 21 | author = models.ForeignKey(Author, blank=True, null=True) 22 | 23 | class Meta: 24 | ordering = ('-pub_date', 'headline') 25 | 26 | def __unicode__(self): 27 | return self.headline 28 | 29 | 30 | class Tag(models.Model): 31 | articles = models.ManyToManyField(Article) 32 | name = models.CharField(max_length=100) 33 | 34 | class Meta: 35 | ordering = ('name', ) 36 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | Authors 2 | ======= 3 | 4 | Current Primary Authors 5 | ----------------------- 6 | * Jonas Haag 7 | * Flavio Percoco Premoli 8 | * Wojtek Ruszczewski 9 | 10 | Past Primary Authors 11 | -------------------- 12 | * Alberto Paro 13 | * George Karpenkov 14 | 15 | Contributions by 16 | ---------------- 17 | * Ellie Frost (https://github.com/stillinbeta) 18 | * Simon Charette (https://github.com/charettes) 19 | * Dag Stockstad (https://github.com/dstockstad) -- found tons of bugs in our query generator 20 | * Shane R. Spencer (https://github.com/whardier) -- Website Review 21 | * Sabin Iacob (https://github.com/m0n5t3r) 22 | * kryton (https://github.com/kryton) 23 | * Brandon Pedersen (https://github.com/bpedman) 24 | 25 | (For an up-to-date list of contributors, see 26 | https://github.com/django-nonrel/mongodb-engine/contributors.) 27 | -------------------------------------------------------------------------------- /docs/source/code/tutorial/v7/nonrelblog/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from djangotoolbox.fields import ListField, EmbeddedModelField 4 | 5 | from django_mongodb_engine.contrib import MongoDBManager 6 | 7 | 8 | class Post(models.Model): 9 | created_on = models.DateTimeField(auto_now_add=True, null=True) 10 | title = models.CharField(max_length=100) 11 | text = models.TextField() 12 | tags = ListField() 13 | comments = ListField(EmbeddedModelField('Comment')) # <--- 14 | 15 | objects = MongoDBManager() 16 | 17 | 18 | class Comment(models.Model): 19 | created_on = models.DateTimeField(auto_now_add=True) 20 | author = EmbeddedModelField('Author') 21 | text = models.TextField() 22 | 23 | 24 | class Author(models.Model): 25 | name = models.CharField(max_length=50) 26 | email = models.EmailField() 27 | 28 | def __unicode__(self): 29 | return '%s (%s)' % (self.name, self.email) 30 | -------------------------------------------------------------------------------- /docs/source/reference/lowerlevel.rst: -------------------------------------------------------------------------------- 1 | .. the following warning is a 1:1 copy from topics/lowerlevel.rst 2 | .. warning:: 3 | 4 | These APIs are available for MongoDB only, so using any of these features 5 | breaks portability to other non-relational databases (Google App Engine, 6 | Cassandra, Redis, ...). For the sake of portability you should try to avoid 7 | database-specific features whenever possible. 8 | 9 | Lower-Level API 10 | =============== 11 | :: 12 | 13 | from django_mongodb_engine.contrib import MongoDBManager 14 | 15 | class FooModel(models.Model): 16 | ... 17 | objects = MongoDBManager() 18 | 19 | :: 20 | 21 | >>> FooModel.objects.raw_query(...) 22 | >>> FooModel.objects.raw_update(...) 23 | 24 | .. currentmodule:: django_mongodb_engine.contrib 25 | 26 | .. automethod:: MongoDBManager.raw_query 27 | 28 | .. automethod:: MongoDBManager.raw_update 29 | 30 | .. automethod:: MongoDBManager.distinct 31 | -------------------------------------------------------------------------------- /tests/settings/serializer.py: -------------------------------------------------------------------------------- 1 | from django_mongodb_engine.creation import DatabaseCreation 2 | from django.core.serializers.json import \ 3 | Serializer, Deserializer as JSONDeserializer 4 | 5 | 6 | def get_objectid_fields(modelopts, typemap=DatabaseCreation.data_types): 7 | return [field for field in modelopts.fields if 8 | typemap.get(field.__class__.__name__) == 'key'] 9 | 10 | 11 | def Deserializer(*args, **kwargs): 12 | for objwrapper in JSONDeserializer(*args, **kwargs): 13 | obj = objwrapper.object 14 | for field in get_objectid_fields(obj._meta): 15 | value = getattr(obj, field.attname) 16 | try: 17 | int(value) 18 | except (TypeError, ValueError): 19 | pass 20 | else: 21 | setattr(obj, field.attname, int_to_objectid(value)) 22 | yield objwrapper 23 | 24 | 25 | def int_to_objectid(i): 26 | return str(i).rjust(24, '0') 27 | -------------------------------------------------------------------------------- /tests/embedded/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from djangotoolbox.fields import DictField, EmbeddedModelField 4 | 5 | 6 | class EmbeddedModel(models.Model): 7 | charfield = models.CharField(max_length=3, blank=False) 8 | datetime = models.DateTimeField(null=True) 9 | datetime_auto_now_add = models.DateTimeField(auto_now_add=True) 10 | datetime_auto_now = models.DateTimeField(auto_now=True) 11 | 12 | 13 | class Model(models.Model): 14 | x = models.IntegerField() 15 | em = EmbeddedModelField(EmbeddedModel) 16 | dict_emb = DictField(EmbeddedModelField(EmbeddedModel)) 17 | 18 | 19 | # Docstring example copy. 20 | class Address(models.Model): 21 | street = models.CharField(max_length=200) 22 | postal_code = models.IntegerField() 23 | city = models.CharField(max_length=100) 24 | 25 | 26 | class Customer(models.Model): 27 | name = models.CharField(max_length=100) 28 | last_name = models.CharField(max_length=100) 29 | address = EmbeddedModelField(Address) 30 | -------------------------------------------------------------------------------- /docs/source/topics/atomic-updates.rst: -------------------------------------------------------------------------------- 1 | Atomic Updates 2 | ============== 3 | 4 | Django's support for updates_ 5 | (using the :meth:`~django.db.models.query.QuerySet.update` method) 6 | can be used to run atomic updates against a single or multiple documents:: 7 | 8 | Post.objects.filter(...).update(title='Everything is the same') 9 | 10 | results in a :meth:`~pymongo.collection.Collection.update` query that uses the 11 | atomic ``$set`` operator to update the `title` field:: 12 | 13 | .update(..., {'$set': {'title': 'Everything is the same'}}) 14 | 15 | It's also possible to use `F()`_ objects which are 16 | translated into ``$inc`` operations. For example, :: 17 | 18 | Post.objects.filter(...).update(visits=F('visits')+1) 19 | 20 | is translated to:: 21 | 22 | .update(..., {'$inc': {'visits': 1}}) 23 | 24 | .. _updates: https://docs.djangoproject.com/en/dev/topics/db/queries/#updating-multiple-objects-at-once 25 | .. _F(): https://docs.djangoproject.com/en/dev/topics/db/queries/#filters-can-reference-fields-on-the-model 26 | -------------------------------------------------------------------------------- /docs/source/reference/fields.rst: -------------------------------------------------------------------------------- 1 | Fields 2 | ====== 3 | 4 | This is a reference of both fields that are implemented in djangotoolbox_ and 5 | fields specific to MongoDB. 6 | 7 | (In signatures, ``...`` represents arbitrary positional and keyword arguments 8 | that are passed to :class:`django.db.models.Field`.) 9 | 10 | in :mod:`djangotoolbox` 11 | --------------------------- 12 | .. currentmodule:: djangotoolbox.fields 13 | 14 | .. autoclass:: ListField(item_field=None, ...) 15 | 16 | .. autoclass:: SetField(item_field=None, ...) 17 | 18 | .. autoclass:: DictField(item_field=None, ...) 19 | 20 | .. autoclass:: EmbeddedModelField(embedded_model=None, ...) 21 | 22 | .. autoclass:: BlobField(...) 23 | 24 | in :mod:`django_mongodb_engine` 25 | ----------------------------------- 26 | .. currentmodule:: django_mongodb_engine.fields 27 | 28 | .. autoclass:: GridFSField(delete=True, versioning=False, ...) 29 | 30 | .. autoclass:: GridFSString(delete=True, versioning=False, ...) 31 | 32 | 33 | .. _djangotoolbox: http://allbuttonspressed.com/projects/djangotoolbox 34 | -------------------------------------------------------------------------------- /docs/source/code/mapreduce/mr/tests.py: -------------------------------------------------------------------------------- 1 | mapfunc = """ 2 | function() { 3 | this.text.split(' ').forEach( 4 | function(word) { emit(word, 1) } 5 | ) 6 | } 7 | """ 8 | 9 | reducefunc = """ 10 | function reduce(key, values) { 11 | return values.length; /* == sum(values) */ 12 | } 13 | """ 14 | 15 | __test__ = { 16 | 'mr': """ 17 | >>> from models import Author, Article 18 | 19 | >>> bob = Author.objects.create() 20 | >>> ann = Author.objects.create() 21 | 22 | >>> bobs_article = Article.objects.create(author=bob, text="A B C") 23 | >>> anns_article = Article.objects.create(author=ann, text="A B C D E") 24 | 25 | Map/Reduce over all articles: 26 | >>> for pair in Article.objects.map_reduce(mapfunc, reducefunc, 'wordcount'): 27 | ... print pair.key, pair.value 28 | A 2.0 29 | B 2.0 30 | C 2.0 31 | D 1.0 32 | E 1.0 33 | 34 | Map/Reduce over Bob's articles: 35 | >>> for pair in Article.objects.filter(author=bob).map_reduce( 36 | mapfunc, reducefunc, 'wordcount'): 37 | ... print pair.key, pair.value 38 | A 1.0 39 | B 1.0 40 | C 1.0 41 | """} 42 | -------------------------------------------------------------------------------- /docs/source/topics/aggregations.rst: -------------------------------------------------------------------------------- 1 | Aggregations 2 | ============ 3 | 4 | Django has `out-of-the-box support for aggregation`__. 5 | The following aggregations are currently supported by Django MongoDB Engine: 6 | 7 | * :class:`~django.db.models.Count` 8 | * :class:`~django.db.models.Avg` 9 | * :class:`~django.db.models.Min` 10 | * :class:`~django.db.models.Max` 11 | * :class:`~django.db.models.Sum` 12 | 13 | MongoDB's group_ command is used to perform aggregations using generated 14 | Javascript code that implements the aggregation functions. 15 | 16 | While being more flexible than :doc:`mapreduce`, a ``group`` command can not be 17 | processed in parallel, for which reason you should prefer Map/Reduce to process 18 | big data sets. 19 | 20 | .. warning:: 21 | 22 | Needless to say, you shouldn't use these aggregations on a regular basis 23 | (i.e. in your views or business logic) but regard them as a powerful tool for 24 | one-time operations. 25 | 26 | .. _group: http://docs.mongodb.org/manual/reference/command/group/#dbcmd.group 27 | .. __: https://docs.djangoproject.com/en/dev/topics/db/aggregation/ 28 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.db import connections 3 | from django.db.models import Model 4 | from django.test import TestCase 5 | from django.utils.unittest import skip 6 | 7 | class TestCase(TestCase): 8 | 9 | def setUp(self): 10 | super(TestCase, self).setUp() 11 | if getattr(settings, 'TEST_DEBUG', False): 12 | settings.DEBUG = True 13 | 14 | def assertEqualLists(self, a, b): 15 | self.assertEqual(list(a), list(b)) 16 | 17 | 18 | def skip_all_except(*tests): 19 | 20 | class meta(type): 21 | 22 | def __new__(cls, name, bases, dict): 23 | for attr in dict.keys(): 24 | if attr.startswith('test_') and attr not in tests: 25 | del dict[attr] 26 | return type.__new__(cls, name, bases, dict) 27 | 28 | return meta 29 | 30 | 31 | def get_collection(model_or_name): 32 | if isinstance(model_or_name, type) and issubclass(model_or_name, Model): 33 | model_or_name = model_or_name._meta.db_table 34 | return connections['default'].get_collection(model_or_name) 35 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Django MongoDB Engine, the MongoDB backend for Django 2 | ===================================================== 3 | 4 | Documentation on http://django-mongodb-engine.readthedocs.org/ 5 | 6 | *Use Django's ORM (including Aggregations, Atomic Updates, Embedded Objects, 7 | Map/Reduce and GridFS), admin site, authentication, site, session and caching 8 | frameworks with MongoDB.* 9 | 10 | :master branch: 11 | .. image:: https://secure.travis-ci.org/django-nonrel/mongodb-engine.png?branch=master 12 | :target: https://travis-ci.org/django-nonrel/mongodb-engine 13 | 14 | 15 | Contributing 16 | ------------ 17 | You are highly encouraged to participate in the development, simply use 18 | GitHub's fork/pull request system. 19 | If you don't like GitHub (for some reason) you're welcome 20 | to send regular patches to the mailing list. 21 | 22 | :Web site: http://www.django-nonrel.org/ 23 | :Mailing list: http://groups.google.com/group/django-non-relational 24 | :Bug tracker: https://github.com/django-nonrel/mongodb-engine/issues/ 25 | :PyPI: http://pypi.python.org/pypi/django-mongodb-engine/ 26 | :License: 2-clause BSD 27 | :Keywords: django, mongodb, orm, nosql, database, python -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | branches: 2 | only: 3 | - master 4 | 5 | language: python 6 | 7 | services: 8 | - mongodb 9 | 10 | python: 11 | - 2.6 12 | - 2.7 13 | 14 | env: 15 | - DJANGO_VERSION=1.4 PYMONGO_VERSION=2.8.1 16 | - DJANGO_VERSION=1.4 PYMONGO_VERSION=3.0.2 17 | - DJANGO_VERSION=1.5 PYMONGO_VERSION=2.8.1 18 | - DJANGO_VERSION=1.5 PYMONGO_VERSION=3.0.2 19 | - DJANGO_VERSION=1.6 PYMONGO_VERSION=2.8.1 20 | - DJANGO_VERSION=1.6 PYMONGO_VERSION=3.0.2 21 | - DJANGO_VERSION=1.7 PYMONGO_VERSION=2.8.1 22 | - DJANGO_VERSION=1.7 PYMONGO_VERSION=3.0.2 23 | 24 | 25 | matrix: 26 | allow_failures: 27 | - env: DJANGO_VERSION=1.4 PYMONGO_VERSION=2.8.1 28 | - env: DJANGO_VERSION=1.4 PYMONGO_VERSION=3.0.2 29 | - env: DJANGO_VERSION=1.7 PYMONGO_VERSION=2.8.1 30 | - env: DJANGO_VERSION=1.7 PYMONGO_VERSION=3.0.2 31 | 32 | install: 33 | - pip install git+http://github.com/django-nonrel/django@nonrel-$DJANGO_VERSION 34 | - pip install git+http://github.com/django-nonrel/django-dbindexer@master 35 | - pip install git+http://github.com/django-nonrel/djangotoolbox@master 36 | - pip install pymongo==$PYMONGO_VERSION --upgrade 37 | - python setup.py install 38 | 39 | script: cd tests && python runtests.py 40 | -------------------------------------------------------------------------------- /tests/aggregations/tests.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from django.db.models.aggregates import Count, Sum, Max, Min, Avg 4 | 5 | from .utils import TestCase 6 | from models import Person 7 | 8 | 9 | class SimpleTest(TestCase): 10 | 11 | def test_aggregations(self): 12 | for age, birthday in ( 13 | [4, (2007, 12, 25)], 14 | [4, (2006, 1, 1)], 15 | [1, (2008, 12, 1)], 16 | [4, (2006, 6, 1)], 17 | [12, (1998, 9, 1)], 18 | ): 19 | Person.objects.create(age=age, birthday=datetime(*birthday)) 20 | 21 | aggregates = Person.objects.aggregate(Min('age'), Max('age'), 22 | avgage=Avg('age')) 23 | self.assertEqual(aggregates, {'age__min': 1, 'age__max': 12, 24 | 'avgage': 5.0}) 25 | 26 | # With filters and testing the sqlaggregates->mongoaggregate 27 | # conversion. 28 | aggregates = Person.objects.filter(age__gte=4).aggregate( 29 | Min('birthday'), Max('birthday'), Avg('age'), Count('id')) 30 | self.assertEqual(aggregates, { 31 | 'birthday__max': datetime(2007, 12, 25, 0, 0), 32 | 'birthday__min': datetime(1998, 9, 1, 0, 0), 33 | 'age__avg': 6.0, 34 | 'id__count': 4, 35 | }) 36 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import sys; sys.path.append('.') 3 | from .utils import get_current_year, get_git_head 4 | 5 | project = 'Django MongoDB Engine' 6 | copyright = '2010-%d, Jonas Haag, Flavio Percoco Premoli and contributors' % \ 7 | get_current_year() 8 | 9 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx'] 10 | 11 | master_doc = 'index' 12 | exclude_patterns = ['_build'] 13 | 14 | pygments_style = 'friendly' 15 | 16 | intersphinx_mapping = { 17 | 'python': ('http://docs.python.org', None), 18 | 'pymongo': ('http://api.mongodb.org/python/current/', None), 19 | 'django': ('http://docs.djangoproject.com/en/dev/', 20 | 'http://docs.djangoproject.com/en/dev/_objects/'), 21 | } 22 | 23 | # -- Options for HTML output --------------------------------------------------- 24 | 25 | html_title = project 26 | 27 | html_last_updated_fmt = '%b %d, %Y' 28 | git_head = get_git_head() 29 | if git_head: 30 | html_last_updated_fmt += ' (%s)' % git_head[:7] 31 | 32 | html_theme = 'mongodbtheme' 33 | html_theme_path = ['mongodbtheme', '.'] 34 | html_show_copyright = False 35 | 36 | # Custom sidebar templates, maps document names to template names. 37 | html_sidebars = {'**': ['localtoc.html', 'sidebar.html']} 38 | 39 | # If false, no module index is generated. 40 | html_domain_indices = False 41 | 42 | # If false, no index is generated. 43 | html_use_index = False 44 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | import django_mongodb_engine as distmeta 4 | 5 | DESCRIPTION = 'MongoDB backend for Django-nonrel' 6 | LONG_DESCRIPTION = None 7 | try: 8 | LONG_DESCRIPTION = open('README.rst').read() 9 | except: 10 | pass 11 | 12 | setup(name='django-mongodb-engine', 13 | version='.'.join(map(str, distmeta.__version__)), 14 | author=distmeta.__author__, 15 | author_email=distmeta.__contact__, 16 | url=distmeta.__homepage__, 17 | license='2-clause BSD', 18 | description=DESCRIPTION, 19 | long_description=LONG_DESCRIPTION, 20 | 21 | install_requires=['pymongo>=2.8', 'djangotoolbox>=1.6.0'], 22 | 23 | packages=find_packages(exclude=['tests', 'tests.*']), 24 | zip_safe=False, 25 | classifiers=[ 26 | 'Development Status :: 5 - Production/Stable', 27 | 'Environment :: Web Environment', 28 | 'Framework :: Django', 29 | 'Intended Audience :: Developers', 30 | 'License :: OSI Approved :: BSD License', 31 | 'Operating System :: OS Independent', 32 | 'Programming Language :: Python', 33 | 'Programming Language :: Python :: 2.5', 34 | 'Programming Language :: Python :: 2.6', 35 | 'Programming Language :: Python :: 2.7', 36 | 'Topic :: Database', 37 | 'Topic :: Internet', 38 | 'Topic :: Software Development :: Libraries :: Python Modules', 39 | ], 40 | ) 41 | -------------------------------------------------------------------------------- /django_mongodb_engine/contrib/search/tokenizer.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | class BaseTokenizer(object): 5 | """ 6 | Really simple tokenizer. 7 | """ 8 | 9 | @staticmethod 10 | def tokenize(text): 11 | """ 12 | Splits text into a list of words removing any symbol and 13 | converts it into lowercase. 14 | """ 15 | tokens = [] 16 | text = text.lower() 17 | for dot_item in BaseTokenizer.regex_split('\.(?=[a-zA-Z\s])', text): 18 | for comman_item in BaseTokenizer.regex_split(',(?=[a-zA-Z\s])', 19 | dot_item): 20 | for item in comman_item.split(' '): 21 | item = BaseTokenizer.tokenize_item(item) 22 | if item: 23 | tokens.append(item) 24 | return tokens 25 | 26 | @staticmethod 27 | def regex_split(regex, text): 28 | for item in re.split(regex, text, re.I): 29 | yield item 30 | 31 | @staticmethod 32 | def tokenize_item(item): 33 | """ 34 | If it is an int/float it returns the item (there's no need to 35 | remove , or .). 36 | """ 37 | item = item.strip() 38 | try: 39 | float(item) 40 | return item 41 | except ValueError: 42 | pass 43 | 44 | # This will keep underscores. 45 | return re.sub(r'[^\w]', '', item) 46 | -------------------------------------------------------------------------------- /tests/router/tests.py: -------------------------------------------------------------------------------- 1 | from django.db.utils import DatabaseError 2 | from django.test import TestCase 3 | 4 | 5 | class RouterTest(TestCase): 6 | 7 | def test_managed_apps(self): 8 | # MONGODB_MANAGED_APPS = ['query'] : Any 'query' model resides 9 | # in the MongoDB 'other'. 10 | from query.models import Blog 11 | Blog.objects.create() 12 | self.assertEqual(Blog.objects.using('other').count(), 1) 13 | self.assertRaisesRegexp(DatabaseError, "no such table", 14 | Blog.objects.using('default').count) 15 | 16 | def test_managed_models(self): 17 | # MONGODB_MANAGED_MODELS = ['router.MongoDBModel']: 18 | # router.models.MongoDBModel resides in MongoDB, 19 | # .SQLiteModel in SQLite. 20 | from router.models import MongoDBModel, SQLiteModel 21 | mongo_obj = MongoDBModel.objects.create() 22 | sql_obj = SQLiteModel.objects.create() 23 | 24 | self.assertEqual(MongoDBModel.objects.get(), mongo_obj) 25 | self.assertEqual(SQLiteModel.objects.get(), sql_obj) 26 | 27 | self.assertEqual(MongoDBModel.objects.using('other').get(), mongo_obj) 28 | self.assertEqual(SQLiteModel.objects.using('default').get(), sql_obj) 29 | 30 | self.assertEqual(SQLiteModel.objects.using('other').count(), 0) 31 | self.assertRaisesRegexp(DatabaseError, "no such table", 32 | MongoDBModel.objects.using('default').count) 33 | -------------------------------------------------------------------------------- /docs/source/code/tutorial/v7/nonrelblog/tests.py: -------------------------------------------------------------------------------- 1 | mapfunc = """ 2 | function map() { 3 | /* `this` refers to the current document */ 4 | this.comments.forEach(function(comment) { 5 | emit(comment.author.name, 1); 6 | }); 7 | } 8 | """ 9 | 10 | reducefunc = """ 11 | function reduce(id, values) { 12 | /* [1, 1, ..., 1].length is the same as sum([1, 1, ..., 1]) */ 13 | return values.length; 14 | } 15 | """ 16 | 17 | __test__ = {'mapreduce': """ 18 | >>> from nonrelblog.models import * 19 | 20 | Add some data so we can actually mapreduce anything. 21 | Bob: 3 comments 22 | Ann: 6 comments 23 | Alice: 9 comments 24 | >>> authors = [Author(name='Bob', email='bob@example.org'), 25 | ... Author(name='Ann', email='ann@example.org'), 26 | ... Author(name='Alice', email='alice@example.org')] 27 | >>> for distribution in [(0, 1, 2), (1, 2, 3), (2, 3, 4)]: 28 | ... comments = [] 29 | ... for author, ncomments in zip(authors, distribution): 30 | ... comments.extend([Comment(author=author) 31 | ... for i in xrange(ncomments)]) 32 | ... Post(comments=comments).save() 33 | 34 | ------------------------ 35 | Kick off the Map/Reduce: 36 | ------------------------ 37 | >>> pairs = Post.objects.map_reduce(mapfunc, reducefunc, out='temp', 38 | ... delete_collection=True) 39 | >>> for pair in pairs: 40 | ... print pair.key, pair.value 41 | Alice 9.0 42 | Ann 6.0 43 | Bob 3.0 44 | """} 45 | -------------------------------------------------------------------------------- /tests/query/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.conf import settings 3 | 4 | from djangotoolbox.fields import ListField, RawField 5 | 6 | 7 | class RawModel(models.Model): 8 | raw = RawField() 9 | 10 | 11 | class Empty(models.Model): 12 | pass 13 | 14 | 15 | class IntegerModel(models.Model): 16 | integer = models.IntegerField() 17 | 18 | 19 | class Blog(models.Model): 20 | title = models.CharField(max_length=200, db_index=True) 21 | 22 | class Meta: 23 | ordering = ['id'] 24 | 25 | 26 | class Post(models.Model): 27 | title = models.CharField(max_length=200, db_index=True, unique=True) 28 | content = models.CharField(max_length=1000, db_column='text') 29 | date_published = models.DateTimeField(null=True, blank=True) 30 | blog = models.ForeignKey(Blog, null=True, blank=True) 31 | 32 | 33 | # TODO: Get rid of this model. 34 | class Person(models.Model): 35 | name = models.CharField(max_length=20) 36 | surname = models.CharField(max_length=20) 37 | age = models.IntegerField(null=True, blank=True) 38 | another_age = models.IntegerField(null=True, blank=True, db_column='age2') 39 | 40 | class Meta: 41 | unique_together = ('name', 'surname') 42 | 43 | 44 | class DateModel(models.Model): 45 | datetime = models.DateTimeField(auto_now_add=True) 46 | time = models.TimeField(null=True) 47 | date = models.DateField(null=True) 48 | _datelist_default = [] 49 | datelist = ListField(models.DateField(), default=_datelist_default) 50 | 51 | 52 | class Article(models.Model): 53 | headline = models.CharField(max_length=50) 54 | pub_date = models.DateTimeField() 55 | -------------------------------------------------------------------------------- /django_mongodb_engine/contrib/search/fields.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from .tokenizer import BaseTokenizer 4 | 5 | 6 | __all__ = ['TokenizedField'] 7 | 8 | 9 | class TokenizedField(models.Field): 10 | 11 | def __init__(self, *args, **kwargs): 12 | super(TokenizedField, self).__init__(*args, **kwargs) 13 | as_textfield = kwargs.pop('as_textfield', False) 14 | self._tokenizer = kwargs.pop('tokenizer', BaseTokenizer)() 15 | self.parent_field = models.CharField(*args, **kwargs) 16 | 17 | def contribute_to_class(self, cls, name): 18 | super(TokenizedField, self).contribute_to_class( 19 | cls, '%s_tokenized' % name) 20 | setattr(self, 'parent_field_name', name) 21 | cls.add_to_class(name, self.parent_field) 22 | 23 | def get_db_prep_lookup(self, lookup_type, value, connection, 24 | prepared=False): 25 | # If for some reason value is being converted to list by some 26 | # internal processing we'll convert it back to string. 27 | # For Example: When using the 'in' lookup type. 28 | if isinstance(value, list): 29 | value = ''.join(value) 30 | 31 | # When 'exact' is used we'll perform an exact_phrase query 32 | # using the $all operator otherwhise we'll just tokenized 33 | # the value. Djangotoolbox will do the remaining checks. 34 | if lookup_type == 'exact': 35 | return {'$all': self._tokenizer.tokenize(value)} 36 | return self._tokenizer.tokenize(value) 37 | 38 | def pre_save(self, model_instance, add): 39 | return self._tokenizer.tokenize(getattr(model_instance, 40 | self.parent_field_name)) 41 | -------------------------------------------------------------------------------- /django_mongodb_engine/aggregations.py: -------------------------------------------------------------------------------- 1 | class MongoAggregate(object): 2 | is_ordinal = False 3 | is_computed = False 4 | reduce_template = NotImplemented 5 | finalize_template = '' 6 | 7 | def __init__(self, alias, lookup, source): 8 | self.alias = alias 9 | self.lookup = lookup 10 | self.field = self.source = source 11 | 12 | def format(self, template): 13 | alias = 'out.%s' % self.alias 14 | lookup = 'doc.%s' % self.lookup 15 | return template.format(alias=alias, lookup=lookup) 16 | 17 | def initial(self): 18 | return {self.alias: self.initial_value} 19 | 20 | def reduce(self): 21 | return self.format(self.reduce_template) 22 | 23 | def finalize(self): 24 | return self.format(self.finalize_template) 25 | 26 | def as_sql(self): 27 | raise NotImplementedError 28 | 29 | 30 | class Count(MongoAggregate): 31 | is_ordinal = True 32 | initial_value = 0 33 | reduce_template = '{alias}++' 34 | 35 | 36 | class Min(MongoAggregate): 37 | initial_value = float('inf') 38 | reduce_template = '{alias} = ({lookup} < {alias}) ? {lookup}: {alias}' 39 | 40 | 41 | class Max(MongoAggregate): 42 | initial_value = float('-inf') 43 | reduce_template = '{alias} = ({lookup} > {alias}) ? {lookup}: {alias}' 44 | 45 | 46 | class Avg(MongoAggregate): 47 | is_computed = True 48 | 49 | def initial(self): 50 | return {'%s__count' % self.alias: 0, '%s__total' % self.alias: 0} 51 | 52 | reduce_template = '{alias}__count++; {alias}__total += {lookup}' 53 | finalize_template = '{alias} = {alias}__total / {alias}__count' 54 | 55 | 56 | class Sum(MongoAggregate): 57 | is_computed = True 58 | initial_value = 0 59 | 60 | reduce_template = '{alias} += {lookup}' 61 | 62 | 63 | _AGGREGATION_CLASSES = dict((cls.__name__, cls) 64 | for cls in MongoAggregate.__subclasses__()) 65 | 66 | def get_aggregation_class_by_name(name): 67 | return _AGGREGATION_CLASSES[name] 68 | -------------------------------------------------------------------------------- /docs/source/topics/cache.rst: -------------------------------------------------------------------------------- 1 | Caching 2 | ======= 3 | 4 | .. note:: 5 | 6 | This document assumes that you're already familiar with 7 | `Django's caching framework`_ (database caching in particular). 8 | 9 | `Django MongoDB Cache`_ is a Django database cache backend similar to the one 10 | built into Django (which only works with SQL databases). 11 | 12 | Cache entries are structured like this: 13 | 14 | .. code-block:: js 15 | 16 | { 17 | "_id" : , 18 | "v" : , 19 | "e" : 20 | } 21 | 22 | Thanks to MongoDB's ``_id`` lookups being very fast, MongoDB caching may be used 23 | as a drop-in replacement for "real" cache systems such as Memcached in many cases. 24 | (Memcached is still way faster and does a better caching job in general, but the 25 | performance you get out of MongoDB should be enough for most mid-sized Web sites.) 26 | 27 | Installation 28 | ------------ 29 | .. code-block:: bash 30 | 31 | git clone https://github.com/django-nonrel/mongodb-cache 32 | cd mongodb-cache 33 | python setup.py install 34 | 35 | Setup 36 | ----- 37 | Please follow the instructions in the `Django db cache setup docs`_ for details 38 | on how to configure a database cache. Skip the ``createcachetable`` step since 39 | there's no need to create databases in MongoDB. Also, instead of the default db 40 | cache backend name, use ``"django_mongodb_cache.MongoDBCache"`` as ``BACKEND``:: 41 | 42 | CACHES = { 43 | 'default' : { 44 | 'BACKEND' : 'django_mongodb_cache.MongoDBCache', 45 | 'LOCATION' : 'my_cache_collection' 46 | } 47 | } 48 | 49 | Django MongoDB Cache will also honor all optional settings the default database 50 | cache backend takes care of (``TIMEOUT``, ``OPTIONS``, etc). 51 | 52 | .. _Django's caching framework: https://docs.djangoproject.com/en/dev/topics/cache/ 53 | .. _Django MongoDB Cache: https://github.com/django-nonrel/mongodb-cache 54 | .. _Django db cache setup docs: https://docs.djangoproject.com/en/dev/topics/cache/#database-caching 55 | -------------------------------------------------------------------------------- /docs/source/troubleshooting.rst: -------------------------------------------------------------------------------- 1 | Troubleshooting 2 | =============== 3 | 4 | This page is going to be a collection of common issues Django MongoDB Engine 5 | users faced. Please help grow this collection -- 6 | :doc:`tell us about your troubles `! 7 | 8 | 9 | .. _troubleshooting/SITE_ID: 10 | 11 | ``SITE_ID`` issues 12 | ------------------ 13 | .. code-block:: none 14 | 15 | AutoField (default primary key) values must be strings representing an ObjectId on MongoDB (got u'1' instead). Please make sure your SITE_ID contains a valid ObjectId string. 16 | 17 | This means that your ``SITE_ID`` setting (`What's SITE_ID?!`_) is incorrect -- 18 | it is set to "1" but the site object that has automatically been created has an 19 | ObjectId primary key. 20 | 21 | If you add ``'django_mongodb_engine'`` to your list of ``INSTALLED_APPS``, you 22 | can use the ``tellsiteid`` command to get the default site's ObjectId and update 23 | your ``SITE_ID`` setting accordingly: 24 | 25 | .. code-block:: none 26 | 27 | $ ./manage.py tellsiteid 28 | The default site's ID is u'deafbeefdeadbeef00000000'. To use the sites framework, add this line to settings.py: 29 | SITE_ID=u'deafbeefdeadbeef00000000' 30 | 31 | .. _What's SITE_ID?!: http://docs.djangoproject.com/en/dev/ref/settings/#std:setting-SITE_ID 32 | 33 | 34 | Creating/editing user in admin causes ``DatabaseError`` 35 | ------------------------------------------------------- 36 | .. code-block:: none 37 | 38 | DatabaseError at /admin/auth/user/deafbeefdeadbeef00000000/ 39 | [...] This query is not supported by the database. 40 | 41 | This happens because Django tries to execute JOINs in order to display a list of 42 | groups/permissions in the user edit form. 43 | 44 | To workaround this problem, add ``'djangotoolbox'`` to your ``INSTALLED_APPS`` 45 | which makes the Django admin skip the groups and permissions widgets. 46 | 47 | No form field implemented for 48 | ---------------------------------------------------------------------- 49 | See https://gist.github.com/1200165 50 | -------------------------------------------------------------------------------- /docs/source/meta/contributing.rst: -------------------------------------------------------------------------------- 1 | How to Contribute 2 | ================= 3 | 4 | We'd love to see you getting involved in Django MongoDB Engine's development! 5 | 6 | Here are some ideas on how you can help evolve this project: 7 | 8 | * :ref:`Send us feedback `! Tell us about your good or bad 9 | experiences with Django MongoDB Engine -- what did you like and what should be improved? 10 | * Blog/write about using Django with MongoDB and 11 | :ref:`let us know about your work `. 12 | * Help solving other people's problems on :ref:`the mailing list `. 13 | * Fix and improve this documentation. Since none of the Django MongoDB Engine 14 | developers are native speakers, these documents are probably full of typos and 15 | weird, ungrammatical phrasings. Also, if you're missing something from this 16 | documentation, please tell us! 17 | * :ref:`Report bugs and feature requests `. 18 | * :ref:`Send patches or pull requests ` containing bug 19 | fixes, new features or code improvement. 20 | 21 | .. _contributing/mailinglist: 22 | 23 | Mailing List 24 | ------------ 25 | Our `mailing list`_, ``django-non-relational@googlegroups.com``, is the right 26 | place for general feedback, discussion and support. 27 | 28 | Development 29 | ----------- 30 | Django MongoDB Engine is being developed `on GitHub`_. 31 | 32 | .. _contributing/bugreports: 33 | 34 | Bug Reports 35 | ........... 36 | Bugs can be reported to our `ticket tracker on GitHub`_. 37 | 38 | .. _contributing/patches: 39 | 40 | Patches 41 | ....... 42 | The most comfortable way to get your changes into Django MongoDB Engine is to 43 | use `GitHub's pull requests`_. It's perfectly fine, however, to send regular 44 | patches to :ref:`the mailing list `. 45 | 46 | .. _mailing list: http://groups.google.com/group/django-non-relational 47 | .. _on Github: https://github.com/django-nonrel 48 | .. _ticket tracker on GitHub: https://github.com/django-nonrel/mongodb-engine/issues/ 49 | .. _GitHub's pull requests: http://help.github.com/pull-requests/ 50 | -------------------------------------------------------------------------------- /django_mongodb_engine/south.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | 4 | warnings.warn( 5 | '`django_mongodb_engine.south.DatabaseOperations` south database backend ' 6 | 'is actually a dummy backend that does nothing at all. It will be ' 7 | 'removed in favor of the `django_mongodb_engine.south_adapter.DatabaseOperations` ' 8 | 'that provides the correct behavior.', 9 | DeprecationWarning 10 | ) 11 | 12 | class DatabaseOperations(object): 13 | """ 14 | MongoDB implementation of database operations. 15 | """ 16 | 17 | backend_name = 'django_mongodb_engine' 18 | 19 | supports_foreign_keys = False 20 | has_check_constraints = False 21 | has_ddl_transactions = False 22 | 23 | def __init__(self, db_alias): 24 | pass 25 | 26 | def add_column(self, table_name, name, field, *args, **kwds): 27 | pass 28 | 29 | def alter_column(self, table_name, name, field, explicit_name=True): 30 | pass 31 | 32 | def delete_column(self, table_name, column_name): 33 | pass 34 | 35 | def rename_column(self, table_name, old, new): 36 | pass 37 | 38 | def create_unique(self, table_name, columns): 39 | pass 40 | 41 | def delete_unique(self, table_name, columns): 42 | pass 43 | 44 | def delete_primary_key(self, table_name): 45 | pass 46 | 47 | def delete_table(self, table_name, cascade=True): 48 | pass 49 | 50 | def connection_init(self): 51 | pass 52 | 53 | def send_pending_create_signals(self, verbosity=False, interactive=False): 54 | pass 55 | 56 | def get_pending_creates(self): 57 | pass 58 | 59 | def start_transaction(self): 60 | pass 61 | 62 | def rollback_transaction(self): 63 | pass 64 | 65 | def rollback_transactions_dry_run(self): 66 | pass 67 | 68 | def clear_run_data(self, pending_creates): 69 | pass 70 | 71 | def create_table(self, unique=True, null=True, blank=True): 72 | pass 73 | 74 | def send_create_signal(self, verbosity=False, interactive=False): 75 | pass 76 | 77 | def execute_deferred_sql(self): 78 | pass 79 | 80 | def commit_transaction(self): 81 | pass 82 | -------------------------------------------------------------------------------- /docs/source/topics/setup.rst: -------------------------------------------------------------------------------- 1 | Setup 2 | ===== 3 | 4 | This page explains how to install and configure a Django/MongoDB setup. 5 | 6 | Installation 7 | ------------ 8 | Django MongoDB Engine depends on 9 | 10 | * Django-nonrel_, a fork of Django that adds support for non-relational databases 11 | * djangotoolbox_, a bunch of utilities for non-relational Django applications and backends 12 | 13 | It's highly recommended (although not required) to use a virtualenv_ for your 14 | project to not mess up other Django setups. 15 | 16 | virtualenv 17 | .......... 18 | If not already installed, grab a copy from the Cheeseshop:: 19 | 20 | pip install virtualenv 21 | 22 | To set up a virtual environment for your project, use :: 23 | 24 | virtualenv myproject 25 | 26 | To join the environment, use (in Bash):: 27 | 28 | source myproject/bin/activate 29 | 30 | Django-nonrel 31 | ............. 32 | :: 33 | 34 | pip install git+https://github.com/django-nonrel/django@nonrel-1.5 35 | 36 | djangotoolbox 37 | ............. 38 | :: 39 | 40 | pip install git+https://github.com/django-nonrel/djangotoolbox 41 | 42 | Django MongoDB Engine 43 | ..................... 44 | You should use the latest Git revision. :: 45 | 46 | pip install git+https://github.com/django-nonrel/mongodb-engine 47 | 48 | 49 | Configuration 50 | ------------- 51 | Database setup is easy (see also the `Django database setup docs`_):: 52 | 53 | DATABASES = { 54 | 'default' : { 55 | 'ENGINE' : 'django_mongodb_engine', 56 | 'NAME' : 'my_database' 57 | } 58 | } 59 | 60 | Django MongoDB Engine also takes into account the ``HOST``, ``PORT``, ``USER``, 61 | ``PASSWORD`` and ``OPTIONS`` settings. 62 | 63 | Possible values of ``OPTIONS`` are described in the 64 | :doc:`settings reference
`. 65 | 66 | Done! 67 | ----- 68 | That's it! You can now go straight ahead developing your Django application as 69 | you would do with any other database. 70 | 71 | 72 | .. _virtualenv: http://www.virtualenv.org/ 73 | .. _Django database setup docs: https://docs.djangoproject.com/en/dev/ref/settings/#databases 74 | .. _djangotoolbox: https://github.com/django-nonrel/djangotoolbox 75 | .. _Django-nonrel: http://django-nonrel.org/ 76 | -------------------------------------------------------------------------------- /tests/multiple_database/models.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.contrib.auth.models import User 3 | from django.db import models 4 | 5 | class Review(models.Model): 6 | source = models.CharField(max_length=100) 7 | object_id = models.PositiveIntegerField() 8 | 9 | def __unicode__(self): 10 | return self.source 11 | 12 | class Meta: 13 | ordering = ('source',) 14 | 15 | class PersonManager(models.Manager): 16 | def get_by_natural_key(self, name): 17 | return self.get(name=name) 18 | 19 | class Person(models.Model): 20 | objects = PersonManager() 21 | name = models.CharField(max_length=100) 22 | 23 | def __unicode__(self): 24 | return self.name 25 | 26 | class Meta: 27 | ordering = ('name',) 28 | 29 | # This book manager doesn't do anything interesting; it just 30 | # exists to strip out the 'extra_arg' argument to certain 31 | # calls. This argument is used to establish that the BookManager 32 | # is actually getting used when it should be. 33 | class BookManager(models.Manager): 34 | def create(self, *args, **kwargs): 35 | kwargs.pop('extra_arg', None) 36 | return super(BookManager, self).create(*args, **kwargs) 37 | 38 | def get_or_create(self, *args, **kwargs): 39 | kwargs.pop('extra_arg', None) 40 | return super(BookManager, self).get_or_create(*args, **kwargs) 41 | 42 | class Book(models.Model): 43 | objects = BookManager() 44 | title = models.CharField(max_length=100) 45 | published = models.DateField() 46 | editor = models.ForeignKey(Person, null=True, related_name='edited') 47 | pages = models.IntegerField(default=100) 48 | 49 | def __unicode__(self): 50 | return self.title 51 | 52 | class Meta: 53 | ordering = ('title',) 54 | 55 | class Pet(models.Model): 56 | name = models.CharField(max_length=100) 57 | owner = models.ForeignKey(Person) 58 | 59 | def __unicode__(self): 60 | return self.name 61 | 62 | class Meta: 63 | ordering = ('name',) 64 | 65 | class UserProfile(models.Model): 66 | user = models.OneToOneField(User, null=True) 67 | flavor = models.CharField(max_length=100) 68 | 69 | class Meta: 70 | ordering = ('flavor',) 71 | -------------------------------------------------------------------------------- /docs/source/reference/settings.rst: -------------------------------------------------------------------------------- 1 | Settings 2 | ======== 3 | 4 | .. TODO fix highlighting 5 | 6 | Client Settings 7 | ------------------- 8 | Additional flags may be passed to :class:`pymongo.MongoClient` using the 9 | ``OPTIONS`` dictionary:: 10 | 11 | DATABASES = { 12 | 'default' : { 13 | 'ENGINE' : 'django_mongodb_engine', 14 | 'NAME' : 'my_database', 15 | ... 16 | 'OPTIONS' : { 17 | 'socketTimeoutMS' : 500, 18 | ... 19 | } 20 | } 21 | } 22 | 23 | All of these settings directly mirror PyMongo settings. In fact, all Django 24 | MongoDB Engine does is lower-casing the names before passing the flags to 25 | :class:`~pymongo.MongoClient`. For a list of possible options head over to the 26 | `PyMongo documentation on client options`_. 27 | 28 | .. _operations-setting: 29 | 30 | Acknowledged Operations 31 | ----------------------- 32 | Use the ``OPERATIONS`` dict to specify extra flags passed to 33 | :meth:`Collection.save `, 34 | :meth:`~pymongo.collection.Collection.update` or 35 | :meth:`~pymongo.collection.Collection.remove` (and thus included in the write concern): 36 | 37 | .. code-block:: python 38 | 39 | 'OPTIONS' : { 40 | 'OPERATIONS' : {'w' : 3}, 41 | ... 42 | } 43 | 44 | 45 | 46 | Get a more fine-grained setup by introducing another layer to this dict: 47 | 48 | .. code-block:: python 49 | 50 | 'OPTIONS' : { 51 | 'OPERATIONS' : { 52 | 'save' : {'w' : 3}, 53 | 'update' : {}, 54 | 'delete' : {'j' : True} 55 | }, 56 | ... 57 | } 58 | 59 | .. note:: 60 | 61 | This operations map to the **Django** operations `save`, `update` and `delete` 62 | (**not** to MongoDB operations). This is because Django abstracts 63 | "`insert vs. update`" into `save`. 64 | 65 | 66 | A full list of write concern flags may be found in the 67 | `MongoDB documentation `_. 68 | 69 | .. _Similar to Django's built-in backends: 70 | http://docs.djangoproject.com/en/dev/ref/settings/#std:setting-OPTIONS 71 | .. _PyMongo documentation on client options: 72 | http://api.mongodb.org/python/current/api/pymongo/mongo_client.html 73 | -------------------------------------------------------------------------------- /docs/source/reference/model-options.rst: -------------------------------------------------------------------------------- 1 | Model Options 2 | ============= 3 | 4 | In addition to Django's `default Meta options`_, Django MongoDB Engine supports 5 | various options specific to MongoDB through a special ``class MongoMeta``. :: 6 | 7 | class FooModel(models.Model): 8 | ... 9 | class MongoMeta: 10 | # Mongo options here 11 | ... 12 | 13 | Indexes 14 | ------- 15 | Django MongoDB Engine already understands the standard 16 | :attr:`~django.db.models.Field.db_index` and 17 | :attr:`~django.db.models.Options.unique_together` options and generates the 18 | corresponding MongoDB indexes on ``syncdb``. 19 | 20 | To make use of other index features, like multi-key indexes and Geospatial 21 | Indexing, additional indexes can be specified using the ``indexes`` setting. :: 22 | 23 | class Club(models.Model): 24 | location = ListField() 25 | rating = models.FloatField() 26 | admission = models.IntegerField() 27 | ... 28 | class MongoMeta: 29 | indexes = [ 30 | [('rating', -1)], 31 | [('rating', -1), ('admission', 1)], 32 | {'fields': [('location', '2d')], 'min': -42, 'max': 42}, 33 | ] 34 | 35 | ``indexes`` can be specified in two ways: 36 | 37 | * The simple "without options" form is a list of ``(field, direction)`` pairs. 38 | For example, a single ascending index (the same thing you get using ``db_index``) 39 | is expressed as ``[(field, 1)]``. A multi-key, descending index can be written 40 | as ``[(field1, -1), (field2, -1), ...]``. 41 | * The second form is slightly more verbose but takes additional MongoDB index 42 | options. A descending, sparse index for instance may be expressed as 43 | ``{'fields': [(field, -1)], 'sparse': True}``. 44 | 45 | 46 | Capped Collections 47 | ------------------ 48 | Use the ``capped`` option and ``collection_size`` (and/or ``collection_max``) 49 | to limit a collection in size (and/or document count), new documents replacing 50 | old ones after reaching one of the limit sets. 51 | 52 | For example, a logging collection fixed to 50MiB could be defined as follows:: 53 | 54 | class LogEntry(models.Model): 55 | timestamp = models.DateTimeField() 56 | message = models.TextField() 57 | ... 58 | class MongoMeta: 59 | capped = True 60 | collection_size = 50*1024*1024 61 | 62 | .. _default Meta options: http://docs.djangoproject.com/en/dev/topics/db/models/#meta-options 63 | .. _sparse: http://www.mongodb.org/display/DOCS/Indexes#Indexes-SparseIndexes 64 | -------------------------------------------------------------------------------- /django_mongodb_engine/router.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.core.exceptions import ImproperlyConfigured 3 | 4 | 5 | _mongodbs = [] 6 | 7 | def _init_mongodbs(): 8 | for name, options in settings.DATABASES.iteritems(): 9 | if options['ENGINE'] != 'django_mongodb_engine': 10 | continue 11 | if options.get('IS_DEFAULT'): 12 | _mongodbs.insert(0, name) 13 | else: 14 | _mongodbs.append(name) 15 | 16 | if not _mongodbs: 17 | raise ImproperlyConfigured("No MongoDB database found in " 18 | "settings.DATABASES.") 19 | 20 | 21 | class MongoDBRouter(object): 22 | """ 23 | A Django router to manage models that should be stored in MongoDB. 24 | 25 | MongoDBRouter uses the MONGODB_MANAGED_APPS and MONGODB_MANAGED_MODELS 26 | settings to know which models/apps should be stored inside MongoDB. 27 | 28 | See: http://docs.djangoproject.com/en/dev/topics/db/multi-db/#topics-db-multi-db-routing 29 | """ 30 | 31 | def __init__(self): 32 | if not _mongodbs: 33 | _init_mongodbs() 34 | self.managed_apps = [app.split('.')[-1] for app in 35 | getattr(settings, 'MONGODB_MANAGED_APPS', [])] 36 | self.managed_models = getattr(settings, 'MONGODB_MANAGED_MODELS', []) 37 | 38 | def is_managed(self, model): 39 | """ 40 | Returns True if the model passed is managed by Django MongoDB 41 | Engine. 42 | """ 43 | if model._meta.app_label in self.managed_apps: 44 | return True 45 | full_name = '%s.%s' % (model._meta.app_label, model._meta.object_name) 46 | return full_name in self.managed_models 47 | 48 | def db_for_read(self, model, **hints): 49 | """ 50 | Points all operations on MongoDB models to a MongoDB database. 51 | """ 52 | if self.is_managed(model): 53 | return _mongodbs[0] 54 | 55 | db_for_write = db_for_read # Same algorithm. 56 | 57 | def allow_relation(self, obj1, obj2, **hints): 58 | """ 59 | Allows any relation if a model in myapp is involved. 60 | """ 61 | return self.is_managed(obj2) or None 62 | 63 | def allow_syncdb(self, db, model): 64 | """ 65 | Makes sure that MongoDB models only appear on MongoDB databases. 66 | """ 67 | if db in _mongodbs: 68 | return self.is_managed(model) 69 | elif self.is_managed(model): 70 | return db in _mongodbs 71 | return None 72 | -------------------------------------------------------------------------------- /docs/source/topics/gridfs.rst: -------------------------------------------------------------------------------- 1 | GridFS 2 | ====== 3 | 4 | MongoDB's built-in distributed file system, GridFS_, can be used in Django 5 | applications in two different ways. 6 | 7 | In most cases, you should use the GridFS :ref:`storage backend ` 8 | provided by Django MongoDB Engine. 9 | 10 | .. _gridfs/storage: 11 | 12 | Storage 13 | ------- 14 | :class:`~django_mongodb_engine.storage.GridFSStorage` is a `Django storage`_ 15 | that stores files in GridFS. That means it can be used with whatever component 16 | makes use of storages -- most importantly, 17 | :class:`~django.db.models.fields.files.FileField`. 18 | 19 | It uses a special collection for storing files, by default named "storage". :: 20 | 21 | from django_mongodb_engine.storage import GridFSStorage 22 | 23 | gridfs = GridFSStorage() 24 | uploads = GridFSStorage(location='/uploads') 25 | 26 | .. warning:: 27 | 28 | To serve files out of GridFS, use tools like 29 | `nginx-gridfs `_. 30 | **Never** serve files through Django in production! 31 | 32 | 33 | Model Field 34 | ----------- 35 | (You should probably be using the :ref:`GridFS storage backend `.) 36 | 37 | Use :class:`~django_mongodb_engine.fields.GridFSField` to store "nameless" blobs 38 | besides documents that would normally go into the document itself. 39 | 40 | All that's kept in the document is a reference (an ObjectId) to the GridFS blobs 41 | which are retrieved on demand. 42 | 43 | Assuming you want to store a 10MiB blob "in" each document, this is what you 44 | *shouldn't* do:: 45 | 46 | # DON'T DO THIS 47 | class Bad(models.Model): 48 | blob = models.TextField() 49 | 50 | # NEITHER THIS 51 | class EventWorse(models.Model): 52 | blob = models.CharField(max_length=10*1024*1024) 53 | 54 | Instead, use :class:`~django_mongodb_engine.fields.GridFSField`:: 55 | 56 | class Better(models.Model): 57 | blob = GridFSField() 58 | 59 | A GridFSField may be fed with anything that PyMongo can handle, that is, 60 | (preferably) file-like objects and strings. 61 | 62 | You'll always get a :class:`~gridfs.grid_file.GridOut` for documents from the 63 | database. :: 64 | 65 | >>> doc = Better() 66 | 67 | GridFSField takes file-likes (and strings)... 68 | >>> doc.blob = file_like 69 | >>> doc.save() 70 | 71 | ... and always returns GridOuts. 72 | >>> samedoc = Better.objects.get(...) 73 | >>> samedoc.blob 74 | 75 | 76 | .. _GridFS: http://docs.mongodb.org/manual/core/gridfs/ 77 | .. _Django storage: https://docs.djangoproject.com/en/dev/topics/files/#file-storage 78 | -------------------------------------------------------------------------------- /tests/runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | from types import ModuleType 5 | 6 | 7 | def runtests(foo, settings='settings', extra=[], test_builtin=False): 8 | if isinstance(foo, ModuleType): 9 | settings = foo.__name__ 10 | apps = foo.INSTALLED_APPS 11 | else: 12 | apps = foo 13 | if not test_builtin: 14 | apps = filter(lambda name: not name.startswith('django.contrib.'), 15 | apps) 16 | # pre-1.6 test runners don't understand full module names 17 | import django 18 | if django.VERSION < (1, 6): 19 | apps = [app.replace('django.contrib.', '') for app in apps] 20 | execute(['./manage.py', 'test', '--settings', settings] + extra + apps) 21 | 22 | 23 | def execute_python(lines): 24 | from textwrap import dedent 25 | return execute( 26 | [sys.executable, '-c', dedent(lines)], 27 | env=dict(os.environ, DJANGO_SETTINGS_MODULE='settings', 28 | PYTHONPATH='..')) 29 | 30 | 31 | def main(short): 32 | # Run some basic tests outside Django's test environment. 33 | execute_python(''' 34 | from mongodb.models import RawModel 35 | RawModel.objects.create(raw=41) 36 | RawModel.objects.update(raw=42) 37 | RawModel.objects.all().delete() 38 | RawModel.objects.create(raw=42) 39 | ''') 40 | 41 | import settings 42 | import settings.dbindexer 43 | import settings.slow_tests 44 | 45 | runtests(settings, extra=['--failfast'] if short else []) 46 | 47 | # Assert we didn't touch the production database. 48 | execute_python(''' 49 | from mongodb.models import RawModel 50 | assert RawModel.objects.get().raw == 42 51 | ''') 52 | 53 | if short: 54 | exit() 55 | 56 | # Make sure we can syncdb. 57 | execute(['./manage.py', 'syncdb', '--noinput']) 58 | 59 | runtests(settings.dbindexer) 60 | runtests(['router'], 'settings.router') 61 | runtests(settings.INSTALLED_APPS, 'settings.debug') 62 | runtests(settings.slow_tests, test_builtin=True) 63 | 64 | 65 | if __name__ == '__main__': 66 | import sys 67 | if 'ignorefailures' in sys.argv: 68 | from subprocess import call as execute 69 | else: 70 | from subprocess import check_call as execute 71 | if 'coverage' in sys.argv: 72 | 73 | def _new_check_call_closure(old_check_call): 74 | 75 | def _new_check_call(cmd, **kwargs): 76 | if not cmd[0].endswith('python'): 77 | cmd = ['coverage', 'run', '-a', '--source', 78 | '../django_mongodb_engine'] + cmd 79 | return old_check_call(cmd, **kwargs) 80 | 81 | return _new_check_call 82 | 83 | execute = _new_check_call_closure(execute) 84 | main('short' in sys.argv) 85 | -------------------------------------------------------------------------------- /django_mongodb_engine/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | __version__ = (0, 6, 0) 5 | __author__ = "Flavio Percoco Premoli, Alberto Paro, " + \ 6 | "Jonas Haag and contributors" 7 | __contact__ = "django-non-relational@googlegroups.com" 8 | __homepage__ = "http://django-nonrel.org/" 9 | __docformat__ = "restructuredtext" 10 | 11 | try: 12 | from django.conf import settings 13 | from django.core.exceptions import ImproperlyConfigured 14 | except ImportError as exc: 15 | # setup.py imports this file in order to read version/author/... metadata 16 | # but does not necessarily have a Django context. 17 | import logging 18 | logging.error('Error while trying to get django' 19 | ' settings module.\nError was: {0}'.format(str(exc))) 20 | else: 21 | try: 22 | # It might be irritating that django-mongodb-engine registers itself as 23 | # an app, and I think this is worth an explanation - so here you go: 24 | # django-mongodb-engine provides a way to set MongoDB-specific options 25 | # for a certain model via the 'MongoMeta' class/attribute (similar to 26 | # the Django-style 'Meta' class). We want those options to be copied 27 | # into the model's '_meta' object, right after the class has been 28 | # defined. For this, we have to listen to the 'class_prepared' signal 29 | # from 'django.db.models.signals'. Because the 'django_mongodb_engine' 30 | # module gets imported as part of the initialization process of 31 | # Django's ORM ('django.db'), we can *not* import anything from 32 | # 'django.db' in this file (or any other submodule that is imported 33 | # during the ORM initialization) because that would get us into 34 | # recursive import hell which the Python interpreter doesn't allow. The 35 | # only way to make sure certain code is executed after Django's ORM has 36 | # been initialized is registering an app. After initializing itself, 37 | # Django imports all apps defined in the project's 'settings.py' in the 38 | # order implied by iterating over the INSTALLED_APPS list. As we have 39 | # to make sure that django-mongodb-engine is loaded very first, we 40 | # prepend it to the list and gracefully handle when it's a tuple. 41 | if isinstance(settings.INSTALLED_APPS, tuple): 42 | settings.INSTALLED_APPS = ('django_mongodb_engine',) + settings.INSTALLED_APPS 43 | else: 44 | settings.INSTALLED_APPS.insert(0, 'django_mongodb_engine') 45 | except (ImportError, ImproperlyConfigured) as exc: 46 | # setup.py imports this file in order to read version/author/... metadata 47 | # but does not necessarily have a Django context. 48 | import logging 49 | logging.error('Error while trying to get django' 50 | ' settings module.\nError was: {0}'.format(str(exc))) 51 | -------------------------------------------------------------------------------- /docs/source/topics/lists-and-dicts.rst: -------------------------------------------------------------------------------- 1 | Lists & Dicts 2 | ============= 3 | 4 | Django MongoDB Engine provides two fields for storing arbitrary (BSON-compatible) 5 | Python :class:`list` and :class:`dict` objects in Django model objects, 6 | :ref:`ListField ` and :ref:`DictField `, 7 | which can be used to store information that is not worth a separate model or 8 | that should be queryable in efficient manner (using an index). 9 | 10 | Both fields may optionally be provided with type information. That restricts 11 | their usage to one single type but has the advantage of automatic type checks 12 | and conversions. 13 | 14 | .. _topics/listfield: 15 | 16 | ListField 17 | --------- 18 | Stores Python lists (or any other iterable), represented in BSON as arrays. :: 19 | 20 | from djangotoolbox.fields import ListField 21 | 22 | class Post(models.Model): 23 | ... 24 | tags = ListField() 25 | 26 | :: 27 | 28 | >>> Post(tags=['django', 'mongodb'], ...).save() 29 | >>> Post.objects.get(...).tags 30 | ['django', 'mongodb'] 31 | 32 | The typed variant automatically does type conversions according to the given type:: 33 | 34 | class Post(models.Model): 35 | ... 36 | edited_on = ListField(models.DateTimeField()) 37 | 38 | :: 39 | 40 | >>> post = Post(edited_on=['1010-10-10 10:10:10']) 41 | >>> post.save() 42 | >>> Post.objects.get(...).edited_on 43 | [datetime.datetime([1010, 10, 10, 10, 10, 10])] 44 | 45 | As described :ref:`in the tutorial `, ListFields are 46 | very useful when used together with :doc:`embedded-models` to store lists of 47 | sub-entities to model 1-to-n relationships:: 48 | 49 | from djangotoolbox.fields import EmbeddedModelField, ListField 50 | 51 | class Post(models.Model): 52 | ... 53 | comments = ListField(EmbeddedModelField('Comment')) 54 | 55 | class Comment(models.Model): 56 | ... 57 | text = models.TextField() 58 | 59 | Please head over to the :doc:`embedded-models` topic for more about embedded models. 60 | 61 | SetField 62 | -------- 63 | Much like a :ref:`ListField ` except that it's represented as 64 | a :class:`set` on Python side (but stored as a list on MongoDB due to the lack 65 | of a separate set type in BSON). 66 | 67 | .. _topics/dictfield: 68 | 69 | DictField 70 | --------- 71 | Stores Python dicts (or any dict-like iterable), represented in BSON as subobjects. :: 72 | 73 | from djangotoolbox.fields import DictField 74 | 75 | class Image(models.Model): 76 | ... 77 | exif = DictField() 78 | 79 | :: 80 | 81 | >>> Image(exif=get_exif_data(...), ...).save() 82 | >>> Image.objects.get(...).exif 83 | {u'camera_model' : 'Spamcams 4242', 'exposure_time' : 0.3, ...} 84 | 85 | The typed variant automatically does type conversion on values. (Not on keys 86 | as the are required to be strings on MongoDB.) :: 87 | 88 | class Poll(models.Model): 89 | ... 90 | votes = DictField(models.IntegerField()) 91 | 92 | :: 93 | 94 | >>> Poll(votes={'bob' : 3.14, 'alice' : '42'}, ...).save() 95 | >>> Poll.objects.get(...).votes 96 | {u'bob' : 3, u'alice' : 42} 97 | 98 | DictFields are useful mainly for storing objects of varying shape, i.e. objects 99 | whose structure is unknow at coding time. If all your objects have the same 100 | structure, you should consider using :doc:`embedded-models`. 101 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | .. currentmodule:: djangotoolbox.fields 5 | 6 | Version 0.6 (Jul 12, 2015) 7 | ----------------- 8 | * Add support for PyMongo 3 (Thanks @markunsworth & @ajdavis) 9 | 10 | 11 | Version 0.5.2 (Jun 19, 2015) 12 | ----------------- 13 | * Add support for Replica Sets (Thanks @r4fek) 14 | * Make safe writes the default (Thanks @markunsworth) 15 | 16 | 17 | Version 0.5.1 (Nov 2013) 18 | ----------------- 19 | * Fixed packaging issues 20 | 21 | 22 | Version 0.5 (Nov 2013) 23 | ----------------- 24 | Major changes 25 | ~~~~~~~~~~~~~ 26 | * Added support for Django 1.4-1.6, requires djangotoolbox >= 1.6.0 27 | * PyPy support 28 | * MongoDB 2.0 support 29 | * We're now on Travis_ 30 | * New custom primary key behavior (to be documented) 31 | * New ``MongoMeta.indexes`` system (see :doc:`/reference/model-options`), 32 | deprecation of ``MongoMeta.{index_together,descending_indexes,sparse_indexes}`` 33 | 34 | Minor changes/fixes 35 | ~~~~~~~~~~~~~~~~~~~ 36 | * Support for MongoDB :meth:`~django_mongodb_engine.contrib.MongoDBManager.distinct` queries 37 | * Support for reversed-``$natural`` ordering using :meth:`~django.db.query.QuerySet.reverse` 38 | * Dropped ``LegacyEmbeddedModelField`` 39 | * ``url()`` support for the :doc:`GridFS Storage ` 40 | * Deprecation of ``A()`` queries 41 | * Deprecation of the ``GridFSField.versioning`` feature 42 | * Numerous query generator fixes 43 | * Fixed ``DecimalField`` values sorting 44 | * Other bug fixes, cleanup, new tests etc. 45 | 46 | 47 | Version 0.4 (May 2011) 48 | ---------------------- 49 | * :doc:`GridFS storage backend ` 50 | * Fulltext search 51 | * Query logging support 52 | * Support for sparse indexes (see :doc:`/reference/model-options`) 53 | * Database settings specific to MongoDB were moved into the ``OPTIONS`` dict. 54 | (see :doc:`/reference/settings`) 55 | Furthermore, the `SAFE_INSERTS` and `WAIT_FOR_SLAVES` flags are now deprecated 56 | in favor of the new ``OPERATIONS`` setting (see :ref:`operations-setting`) 57 | * Added the :ref:`tellsiteid command ` 58 | * Defined a stable :ref:`lower-level database API ` 59 | * Numerous bug fixes, new tests, code improvements and deprecations 60 | 61 | 62 | Version 0.3 (Jan 2011) 63 | ---------------------- 64 | * *OR* query support 65 | * Support for :class:`~django.db.models.DateTimeField` and friends 66 | * Support for atomic updates using F_ 67 | * :class:`EmbeddedModelField` has been `merged into djangotoolbox`_. 68 | For legacy data records in your setup, you can use the ``LegacyEmbeddedModelField``. 69 | * Support for :ref:`raw queries and raw updates ` 70 | 71 | .. * Added a flag to enable :ref:`model-referencing` 72 | 73 | 74 | Version 0.2 (Oct 2010) 75 | ---------------------- 76 | * :doc:`Aggregation support ` 77 | * :doc:`Map/Reduce support ` 78 | * :class:`ListField`, :class:`SetListField`, :class:`DictField` and 79 | :class:`GenericField` have been `merged into djangotoolbox`_ 80 | * Added an :class:`EmbeddedModelField` to store arbitrary model instances as 81 | MongoDB embedded objects/subobjects. 82 | * Internal Refactorings 83 | 84 | .. * Support for queries and updates on embedded models (see :ref:`embedded-object-queries`) 85 | 86 | .. _merged into djangotoolbox: https://github.com/django-nonrel/djangotoolbox/blob/master/djangotoolbox/fields.py 87 | .. _F: http://docs.djangoproject.com/en/dev/topics/db/queries/#query-expressions 88 | .. _Travis: http://travis-ci.org/django-nonrel/mongodb-engine 89 | -------------------------------------------------------------------------------- /django_mongodb_engine/utils.py: -------------------------------------------------------------------------------- 1 | import re 2 | import time 3 | 4 | from django.conf import settings 5 | from django.db.backends.util import logger 6 | 7 | from pymongo import ASCENDING 8 | from pymongo.cursor import Cursor 9 | 10 | 11 | def first(test_func, iterable): 12 | for item in iterable: 13 | if test_func(item): 14 | return item 15 | 16 | 17 | def safe_regex(regex, *re_args, **re_kwargs): 18 | 19 | def wrapper(value): 20 | return re.compile(regex % re.escape(value), *re_args, **re_kwargs) 21 | wrapper.__name__ = 'safe_regex (%r)' % regex 22 | 23 | return wrapper 24 | 25 | 26 | def make_struct(*attrs): 27 | 28 | class _Struct(object): 29 | __slots__ = attrs 30 | 31 | def __init__(self, *args): 32 | for attr, arg in zip(self.__slots__, args): 33 | setattr(self, attr, arg) 34 | 35 | return _Struct 36 | 37 | 38 | def make_index_list(indexes): 39 | if isinstance(indexes, basestring): 40 | indexes = [indexes] 41 | for index in indexes: 42 | if not isinstance(index, tuple): 43 | index = index, ASCENDING 44 | yield index 45 | 46 | 47 | class CollectionDebugWrapper(object): 48 | 49 | def __init__(self, collection, db_alias): 50 | self.collection = collection 51 | self.alias = db_alias 52 | 53 | def __getattr__(self, attr): 54 | return getattr(self.collection, attr) 55 | 56 | def profile_call(self, func, args=(), kwargs={}): 57 | start = time.time() 58 | retval = func(*args, **kwargs) 59 | duration = time.time() - start 60 | return duration, retval 61 | 62 | def log(self, op, duration, args, kwargs=None): 63 | args = ' '.join(str(arg) for arg in args) 64 | msg = '%s.%s (%.2f) %s' % (self.collection.name, op, duration, args) 65 | kwargs = dict((k, v) for k, v in kwargs.iteritems() if v) 66 | if kwargs: 67 | msg += ' %s' % kwargs 68 | if len(settings.DATABASES) > 1: 69 | msg = self.alias + '.' + msg 70 | logger.debug(msg, extra={'duration': duration}) 71 | 72 | def find(self, *args, **kwargs): 73 | return DebugCursor(self, self.collection, *args, **kwargs) 74 | 75 | def logging_wrapper(method): 76 | 77 | def wrapper(self, *args, **kwargs): 78 | func = getattr(self.collection, method) 79 | duration, retval = self.profile_call(func, args, kwargs) 80 | self.log(method, duration, args, kwargs) 81 | return retval 82 | 83 | return wrapper 84 | 85 | save = logging_wrapper('save') 86 | remove = logging_wrapper('remove') 87 | update = logging_wrapper('update') 88 | map_reduce = logging_wrapper('map_reduce') 89 | inline_map_reduce = logging_wrapper('inline_map_reduce') 90 | 91 | del logging_wrapper 92 | 93 | 94 | class DebugCursor(Cursor): 95 | 96 | def __init__(self, collection_wrapper, *args, **kwargs): 97 | self.collection_wrapper = collection_wrapper 98 | super(DebugCursor, self).__init__(*args, **kwargs) 99 | 100 | def _refresh(self): 101 | super_meth = super(DebugCursor, self)._refresh 102 | if self._Cursor__id is not None: 103 | return super_meth() 104 | # self.__id is None: first time the .find() iterator is 105 | # entered. find() profiling happens here. 106 | duration, retval = self.collection_wrapper.profile_call(super_meth) 107 | kwargs = {'limit': self._Cursor__limit, 'skip': self._Cursor__skip, 108 | 'sort': self._Cursor__ordering} 109 | self.collection_wrapper.log('find', duration, [self._Cursor__spec], 110 | kwargs) 111 | return retval 112 | -------------------------------------------------------------------------------- /docs/source/topics/embedded-models.rst: -------------------------------------------------------------------------------- 1 | Embedded Models 2 | =============== 3 | 4 | Django MongoDB Engine supports `MongoDB's subobjects`_ which can be used 5 | to embed an object into another. 6 | 7 | Using :ref:`topics/listfield` and :ref:`topics/dictfield` it's already possible 8 | to embed objects (:class:`dicts `) of arbitrary shape. 9 | 10 | However, EmbeddedModelField (described beneath) is a much more comfortable tool 11 | for many use cases, ensuring the data you store actually matches the structure 12 | and types you want it to be in. 13 | 14 | The Basics 15 | ---------- 16 | Let's consider this example:: 17 | 18 | from djangotoolbox.fields import EmbeddedModelField 19 | 20 | class Customer(models.Model): 21 | name = models.CharField(...) 22 | address = EmbeddedModelField('Address') 23 | ... 24 | 25 | class Address(models.Model): 26 | ... 27 | city = models.CharField(...) 28 | 29 | The API feels very natural and is similar to that of Django's relation fields. :: 30 | 31 | >>> Customer(name='Bob', address=Address(city='New York', ...), ...).save() 32 | >>> bob = Customer.objects.get(...) 33 | >>> bob.address 34 | 35 | >>> bob.address.city 36 | 'New York' 37 | 38 | Represented in BSON, Bob's structure looks like this: 39 | 40 | .. code-block:: js 41 | 42 | { 43 | "_id": ObjectId(...), 44 | "name": "Bob", 45 | "address": { 46 | ... 47 | "city": "New York" 48 | }, 49 | ... 50 | } 51 | 52 | While such "flat" embedding is useful if you want to bundle multiple related 53 | fields into one common namespace -- for instance, in the example above we 54 | bundled all information about a customers' address into the `address` namespace 55 | -- there's a much more common usecase for embedded objects: one-to-many relations. 56 | 57 | .. _topics/list-of-subobjects: 58 | 59 | Lists of Subobjects (One-to-Many Relations) 60 | ------------------------------------------- 61 | Often, lists of subobjects are superior to relations (in terms of simplicity and 62 | performance) for modeling one-to-many relationships between models. 63 | 64 | Consider this elegant way to implement the Post ⇔ Comments relationship:: 65 | 66 | from djangotoolbox.fields import ListField, EmbeddedModelField 67 | 68 | class Post(models.Model): 69 | ... 70 | comments = ListField(EmbeddedModelField('Comment')) 71 | 72 | class Comment(models.Model): 73 | text = models.TextField() 74 | 75 | Embedded objects are represented as subobjects on MongoDB:: 76 | 77 | >>> comments = [Comment(text='foo'), Comment(text='bar')] 78 | >>> Post(comments=comments, ...).save() 79 | >>> Post.objects.get(...).comments 80 | [, ] 81 | 82 | .. code-block:: js 83 | 84 | { 85 | "_id": ObjectId(...), 86 | ... 87 | "comments" : [ 88 | {"text": "foo", }, 89 | {"text": "bar"} 90 | ] 91 | } 92 | 93 | Generic Embedding 94 | ----------------- 95 | Similar to Django's `generic relations`_, it's possible to embed objects of any 96 | type (sometimes referred to as "polymorphic" relationships). This works by 97 | adding the model's name and module to each subobject, accompanying the actual 98 | data with type information: 99 | 100 | .. code-block:: js 101 | 102 | { 103 | "_id" : ObjectId(...), 104 | "stuff" : [ 105 | {"foo" : 42, "_module" : "demoapp.models", "_model" : "FooModel"}, 106 | {"bar" : "spam", "_module" : "demoapp.models", "_model" : "FooModel"} 107 | ] 108 | } 109 | 110 | As you can see, generic embedded models add a lot of overhead that bloats up 111 | your data records. If you want to use them anyway, here's how you'd do it:: 112 | 113 | class Container(models.Model): 114 | stuff = ListField(EmbeddedModelField()) 115 | 116 | class FooModel(models.Model): 117 | foo = models.IntegerField() 118 | 119 | class BarModel(models.Model): 120 | bar = models.CharField(max_length=255) 121 | 122 | :: 123 | 124 | Container.objects.create( 125 | stuff=[FooModel(foo=42), BarModel(bar='spam')] 126 | ) 127 | 128 | .. _MongoDB's subobjects: http://docs.mongodb.org/manual/core/data-model-design/ 129 | .. _generic relations: https://docs.djangoproject.com/en/dev/ref/contrib/contenttypes/ 130 | -------------------------------------------------------------------------------- /django_mongodb_engine/south_adapter.py: -------------------------------------------------------------------------------- 1 | # This is needed until the sibling south module is removed 2 | from __future__ import absolute_import 3 | 4 | from django.core.exceptions import ImproperlyConfigured 5 | from django.db.models.fields import NOT_PROVIDED 6 | from django.db.utils import IntegrityError 7 | from pymongo.errors import DuplicateKeyError 8 | 9 | from .utils import make_index_list 10 | 11 | 12 | try: 13 | from south.db.generic import DatabaseOperations 14 | except ImportError: 15 | raise ImproperlyConfigured('Make sure to install south before trying to ' 16 | 'import this module.') 17 | 18 | 19 | class DatabaseOperations(DatabaseOperations): 20 | """ 21 | MongoDB implementation of database operations. 22 | """ 23 | 24 | backend_name = 'django_mongodb_engine' 25 | 26 | supports_foreign_keys = False 27 | has_check_constraints = False 28 | has_ddl_transactions = False 29 | 30 | def _get_collection(self, name): 31 | return self._get_connection().get_collection(name) 32 | 33 | def add_column(self, table_name, field_name, field, keep_default=True): 34 | # Make sure the field is correctly prepared 35 | field.set_attributes_from_name(field_name) 36 | if field.has_default(): 37 | default = field.get_default() 38 | if default is not None: 39 | connection = self._get_connection() 40 | collection = self._get_collection(table_name) 41 | name = field.column 42 | db_prep_save = field.get_db_prep_save(default, connection=connection) 43 | default = connection.ops.value_for_db(db_prep_save, field) 44 | # Update all the documents that haven't got this field yet 45 | collection.update({name: {'$exists': False}}, 46 | {'$set': {name: default}}) 47 | if not keep_default: 48 | field.default = NOT_PROVIDED 49 | 50 | def alter_column(self, table_name, column_name, field, explicit_name=True): 51 | # Since MongoDB is schemaless there's no way to coerce field datatype 52 | pass 53 | 54 | def delete_column(self, table_name, name): 55 | collection = self._get_collection(table_name) 56 | collection.update({}, {'$unset': {name: 1}}) 57 | 58 | def rename_column(self, table_name, old, new): 59 | collection = self._get_collection(table_name) 60 | collection.update({}, {'$rename': {old: new}}) 61 | 62 | def create_unique(self, table_name, columns, drop_dups=False): 63 | collection = self._get_collection(table_name) 64 | try: 65 | index_list = list(make_index_list(columns)) 66 | collection.create_index(index_list, unique=True, drop_dups=drop_dups) 67 | except DuplicateKeyError as e: 68 | raise IntegrityError(e) 69 | 70 | def delete_unique(self, table_name, columns): 71 | collection = self._get_collection(table_name) 72 | index_list = list(make_index_list(columns)) 73 | collection.drop_index(index_list) 74 | 75 | def delete_primary_key(self, table_name): 76 | # MongoDB doesn't support primary key deletion 77 | pass 78 | 79 | def create_table(self, table_name, fields, **kwargs): 80 | # Collection creation is automatic but code calling this might expect 81 | # it to exist, thus we create it here. i.e. Calls to `rename_table` will 82 | # fail if the collection doesn't already exist. 83 | connection = self._get_connection() 84 | connection.database.create_collection(table_name, **kwargs) 85 | 86 | def rename_table(self, table_name, new_table_name): 87 | collection = self._get_collection(table_name) 88 | collection.rename(new_table_name) 89 | 90 | def delete_table(self, table_name, cascade=True): 91 | connection = self._get_connection() 92 | connection.database.drop_collection(table_name) 93 | 94 | def start_transaction(self): 95 | # MongoDB doesn't support transactions 96 | pass 97 | 98 | def rollback_transaction(self): 99 | # MongoDB doesn't support transactions 100 | pass 101 | 102 | def commit_transaction(self): 103 | # MongoDB doesn't support transactions 104 | pass 105 | 106 | def rollback_transactions_dry_run(self): 107 | # MongoDB doesn't support transactions 108 | pass 109 | -------------------------------------------------------------------------------- /tests/mongodb/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from djangotoolbox.fields import RawField, ListField, DictField, \ 4 | EmbeddedModelField 5 | 6 | from django_mongodb_engine.fields import GridFSField, GridFSString 7 | 8 | # ensures class_prepared signal handler is installed 9 | from django_mongodb_engine import models as mongo_models 10 | 11 | from query.models import Post 12 | 13 | 14 | class DescendingIndexModel(models.Model): 15 | desc = models.IntegerField() 16 | 17 | class MongoMeta: 18 | descending_indexes = ['desc'] 19 | 20 | 21 | class DateModel(models.Model): 22 | date = models.DateField() 23 | 24 | 25 | class DateTimeModel(models.Model): 26 | datetime = models.DateTimeField() 27 | 28 | 29 | class RawModel(models.Model): 30 | raw = RawField() 31 | 32 | 33 | class IndexTestModel(models.Model): 34 | regular_index = models.IntegerField(db_index=True) 35 | custom_column = models.IntegerField(db_column='foo', db_index=True) 36 | descending_index = models.IntegerField(db_index=True) 37 | descending_index_custom_column = models.IntegerField(db_column='bar', 38 | db_index=True) 39 | foreignkey_index = models.ForeignKey(RawModel, db_index=True, on_delete=models.DO_NOTHING) 40 | foreignkey_custom_column = models.ForeignKey('DateModel', 41 | db_column='spam') 42 | sparse_index = models.IntegerField(db_index=True) 43 | sparse_index_unique = models.IntegerField(db_index=True, unique=True) 44 | sparse_index_cmp_1 = models.IntegerField(db_index=True) 45 | sparse_index_cmp_2 = models.IntegerField(db_index=True) 46 | 47 | class MongoMeta: 48 | sparse_indexes = ['sparse_index', 'sparse_index_unique', 49 | ('sparse_index_cmp_1', 'sparse_index_cmp_2')] 50 | descending_indexes = ['descending_index', 51 | 'descending_index_custom_column'] 52 | index_together = [ 53 | {'fields': ['regular_index', 'custom_column']}, 54 | {'fields': ('sparse_index_cmp_1', 'sparse_index_cmp_2')}] 55 | 56 | 57 | class IndexTestModel2(models.Model): 58 | a = models.IntegerField(db_index=True) 59 | b = models.IntegerField(db_index=True) 60 | 61 | class MongoMeta: 62 | index_together = ['a', ('b', -1)] 63 | 64 | 65 | class CustomColumnEmbeddedModel(models.Model): 66 | a = models.IntegerField(db_column='a2') 67 | 68 | 69 | class NewStyleIndexesTestModel(models.Model): 70 | f1 = models.IntegerField() 71 | f2 = models.IntegerField() 72 | f3 = models.IntegerField() 73 | 74 | db_index = models.IntegerField(db_index=True) 75 | unique = models.IntegerField(unique=True) 76 | custom_column = models.IntegerField(db_column='custom') 77 | geo = models.IntegerField() 78 | geo_custom_column = models.IntegerField(db_column='geo') 79 | 80 | dict1 = DictField() 81 | dict_custom_column = DictField(db_column='dict_custom') 82 | embedded = EmbeddedModelField(CustomColumnEmbeddedModel) 83 | embedded_list = ListField(EmbeddedModelField(CustomColumnEmbeddedModel)) 84 | 85 | class Meta: 86 | unique_together = [('f2', 'custom_column'), ('f2', 'f3')] 87 | 88 | class MongoMeta: 89 | indexes = [ 90 | [('f1', -1)], 91 | {'fields': 'f2', 'sparse': True}, 92 | {'fields': [('custom_column', -1), 'f3']}, 93 | [('geo', '2d')], 94 | {'fields': [('geo_custom_column', '2d'), 'f2'], 95 | 'min': 42, 'max': 21}, 96 | {'fields': [('dict1.foo', 1)]}, 97 | {'fields': [('dict_custom_column.foo', 1)]}, 98 | {'fields': [('embedded.a', 1)]}, 99 | {'fields': [('embedded_list.a', 1)]}, 100 | ] 101 | 102 | 103 | class GridFSFieldTestModel(models.Model): 104 | gridfile = GridFSField() 105 | gridfile_nodelete = GridFSField(delete=False) 106 | gridfile_versioned = GridFSField(versioning=True) 107 | gridfile_versioned_delete = GridFSField(versioning=True, delete=True) 108 | gridstring = GridFSString() 109 | 110 | 111 | class Issue47Model(models.Model): 112 | foo = ListField(EmbeddedModelField(Post)) 113 | 114 | 115 | class CustomIDModel(models.Model): 116 | id = models.IntegerField() 117 | primary = models.IntegerField(primary_key=True) 118 | 119 | 120 | class CustomIDModel2(models.Model): 121 | id = models.IntegerField(primary_key=True, db_column='blah') 122 | 123 | 124 | class CappedCollection(models.Model): 125 | n = models.IntegerField(default=42) 126 | 127 | class MongoMeta: 128 | capped = True 129 | collection_size = 10 130 | 131 | 132 | class CappedCollection2(models.Model): 133 | 134 | class MongoMeta: 135 | capped = True 136 | collection_size = 1000 137 | collection_max = 2 138 | 139 | 140 | class CappedCollection3(models.Model): 141 | n = models.IntegerField(default=43) 142 | 143 | class MongoMeta: 144 | capped = True 145 | collection_size = 1000 146 | -------------------------------------------------------------------------------- /docs/source/topics/mapreduce.rst: -------------------------------------------------------------------------------- 1 | Map/Reduce 2 | ========== 3 | 4 | Map/Reduce, originally invented at Google, is a simple but powerful technology 5 | to efficiently process big amounts of data in parallel. 6 | 7 | For this, your processing logic must be split into two phases, the *map* and the 8 | *reduce* phase. 9 | 10 | The *map phase* takes all the input you'd like to process (in terms of MongoDB, 11 | this input are your *documents*) and emits one or more *key-value pairs* for 12 | each data record (it "maps" records to key-value pairs). 13 | 14 | The *reduce phase* "reduces" that set of key-value pairs into a single value. 15 | 16 | This document explains how to use `MongoDB's Map/Reduce functionality`_ 17 | with Django models. 18 | 19 | .. warning:: 20 | 21 | MongoDB's Map/Reduce is designed for one-time operations, i.e. it's *not* 22 | intended to be used in code that is executed on a regular basis 23 | (views, business logic, ...). 24 | 25 | .. currentmodule:: django_mongodb_engine.contrib 26 | 27 | How to Use It 28 | ------------- 29 | Map/Reduce support for Django models is provided through Django MongoDB Engine's 30 | :class:`custom Manager ` 31 | (:class:`What is a manager? `). :: 32 | 33 | from django_mongodb_engine.contrib import MongoDBManager 34 | 35 | class MapReduceableModel(models.Model): 36 | ... 37 | objects = MongoDBManager() 38 | 39 | The :class:`MongoDBManager` provides a :meth:`~MongoDBManager.map_reduce` method 40 | that has the same API as PyMongo's :meth:`~pymongo.collection.Collection.map_reduce` 41 | method (with the one exception that it adds a `drop_collection` option). :: 42 | 43 | >>> MapReduceableModel.objects.map_reduce(mapfunc, reducefunc, output_collection, ...) 44 | 45 | For very small result sets, you can also use in-memory Map/Reduce:: 46 | 47 | >>> MapReducableModel.objects.inline_map_reduce(mapfunc, reducefunc, ...) 48 | 49 | It's also possible to run Map/Reduce against a subset of documents in the database:: 50 | 51 | >>> MapReduceableModel.objects.filter(...).map_reduce(...) 52 | 53 | Both the map and the reduce function are written in Javascript. 54 | 55 | :meth:`~MongoDBManager.map_reduce` returns an iterator yielding 56 | :class:`MapReduceResult` objects. 57 | 58 | Special Reduce Function Rules 59 | ----------------------------- 60 | A sane reduce function must be both associative and commutative -- that is, 61 | in terms of MongoDB, the following conditions must hold true:: 62 | 63 | # Value order does not matter: 64 | reduce(k, [A, B]) == reduce(k, [B, A]) 65 | # Values may itself be results of other reduce operations: 66 | reduce(k, [reduce(k, ...)]) == reduce(k, ...) 67 | 68 | This is because in order to be able to process in parallel, the reduce phase 69 | is split into several sub-phases, reducing parts of the map output and eventually 70 | merging them together into one grand total. 71 | 72 | Example 73 | ------- 74 | (See also :ref:`the example in the tutorial ` and 75 | `Wikipedia `_, from which I stole the 76 | idea for the example beneath.) 77 | 78 | As an example, we'll count the number of occurrences of each word in a bunch of 79 | articles. Our models could look somewhat like this: 80 | 81 | .. literalinclude:: ../code/mapreduce/mr/models.py 82 | :start-after: models 83 | :end-before: class Author 84 | 85 | Our map function emits a ``(word, 1)`` pair for each word in an article's text 86 | (In the map function, `this` always refers to the current document). 87 | 88 | .. code-block:: js 89 | 90 | function() { 91 | this.text.split(' ').forEach( 92 | function(word) { emit(word, 1) } 93 | ) 94 | } 95 | 96 | For an input text of "Django is named after Django Reinhardt", this would emit 97 | the following key-value pairs:: 98 | 99 | Django : 1 100 | is : 1 101 | named : 1 102 | after : 1 103 | Django : 1 104 | Reinhardt : 1 105 | 106 | This pairs are now combined in such way that no key duplicates are left. :: 107 | 108 | is : [1] 109 | named : [1] 110 | after : [1] 111 | Django : [1, 1] 112 | Reinhardt : [1] 113 | 114 | To further process these pairs, we let our reduce function sum up all 115 | occurrences of each word 116 | 117 | .. code-block:: js 118 | 119 | function reduce(key, values) { 120 | return values.length; /* == sum(values) */ 121 | } 122 | 123 | so that the final result is a list of key-"sum"-pairs:: 124 | 125 | is : 1 126 | named : 1 127 | after : 1 128 | Django : 2 129 | Reinhardt : 1 130 | 131 | Show Me the Codes 132 | ----------------- 133 | Here's a full example, using the models and functions described above, on how to 134 | use Django MongoDB Engine's Map/Reduce API. 135 | 136 | .. literalinclude:: ../code/mapreduce/mr/models.py 137 | 138 | .. literalinclude:: ../code/mapreduce/mr/tests.py 139 | :end-before: __test__ 140 | 141 | .. literalinclude:: ../code/mapreduce/mr/tests.py 142 | :start-after: mr 143 | :end-before: """ 144 | 145 | 146 | .. _MongoDB's Map/Reduce functionality: http://docs.mongodb.org/manual/core/map-reduce/ 147 | -------------------------------------------------------------------------------- /docs/source/topics/lowerlevel.rst: -------------------------------------------------------------------------------- 1 | Lower-Level Operations 2 | ====================== 3 | 4 | When you hit the limit of what's possible with Django's ORM, you can always go 5 | down one abstraction layer to PyMongo_. 6 | 7 | You can use :ref:`raw queries and updates ` 8 | to update or query for model instances using raw Mongo queries, bypassing 9 | Django's model query APIs. 10 | 11 | If that isn't enough, you can skip the model layer entirely and operate on 12 | :ref:`PyMongo-level objects `. 13 | 14 | .. warning:: 15 | 16 | These APIs are available for MongoDB only, so using any of these features 17 | breaks portability to other non-relational databases (Google App Engine, 18 | Cassandra, Redis, ...). For the sake of portability you should try to avoid 19 | database-specific features whenever possible. 20 | 21 | 22 | .. _lowerlevel/raw-queries-and-updates: 23 | 24 | Raw Queries and Updates 25 | ----------------------- 26 | .. currentmodule:: django_mongodb_engine.contrib 27 | 28 | :class:`MongoDBManager` provides two methods, :meth:`~MongoDBManager.raw_query` 29 | and :meth:`~MongoDBManager.raw_update`, that let you perform raw Mongo queries. 30 | 31 | .. note:: 32 | 33 | When writing raw queries, please keep in mind that no field name substitution 34 | will be done, meaning that you'll always have to use database-level names -- 35 | e.g. `_id` instead of `id` or `foo_id` instead of `foo` for foreignkeys. 36 | 37 | Raw Queries 38 | ........... 39 | :meth:`~MongoDBManager.raw_query` takes one argument, the Mongo query to execute, 40 | and returns a standard Django queryset -- which means that it also supports 41 | indexing and further manipulation. 42 | 43 | As an example, let's do some `Geo querying`_. :: 44 | 45 | from djangotoolbox.fields import EmbeddedModelField 46 | from django_mongodb_engine.contrib import MongoDBManager 47 | 48 | class Point(models.Model): 49 | latitude = models.FloatField() 50 | longtitude = models.FloatField() 51 | 52 | class Place(models.Model): 53 | ... 54 | location = EmbeddedModelField(Point) 55 | 56 | objects = MongoDBManager() 57 | 58 | To find all places near to your current location, 42°N | π°E, 59 | you can use this raw query:: 60 | 61 | >>> here = {'latitude' : 42, 'longtitude' : 3.14} 62 | >>> Place.objects.raw_query({'location' : {'$near' : here}}) 63 | 64 | As stated above, :meth:`~MongoDBManager.raw_query` returns a standard Django 65 | queryset, for which reason you can have even more fun with raw queries: 66 | 67 | .. code-block:: pycon 68 | 69 | Limit the number of results to 10 70 | >>> Foo.objects.raw_query({'location' : ...})[:10] 71 | 72 | Keep track of most interesting places 73 | >>> Foo.objects.raw_query({'location' : ...) \ 74 | ... .update(interest=F('interest')+1) 75 | 76 | and whatnot. 77 | 78 | Raw Updates 79 | ........... 80 | :meth:`~MongoDBManager.raw_update` comes into play when Django MongoDB Engine's 81 | atomic updates through ``$set`` and ``$inc`` (using F_) 82 | are not powerful enough. 83 | 84 | The first argument is the query which describes the subset of documents the 85 | update should be executed against - as :class:`~django.db.models.Q` object or 86 | Mongo query. The second argument is the update spec. 87 | 88 | Consider this model:: 89 | 90 | from django_mongodb_engine.contrib import MongoDBManager 91 | 92 | class FancyNumbers(models.Model): 93 | foo = models.IntegerField() 94 | 95 | objects = MongoDBManager() 96 | 97 | Let's do some of those super-cool MongoDB in-place bitwise operations. :: 98 | 99 | FancyNumbers.objects.raw_update({}, {'$bit' : {'foo' : {'or' : 42}}}) 100 | 101 | That bitwise-ORs every `foo` of all documents in the database with 42. 102 | 103 | To run that update against a subset of the documents, for example against any 104 | whose `foo` is greater than π, use a non-empty filter condition:: 105 | 106 | FancyNumbers.objects.raw_update(Q(foo__gt=3.14), {'$bit' : ...}) 107 | # or 108 | FancyNumbers.objects.raw_update({'foo' : {'$gt' : 3.14}}, {'$bit' : ...}) 109 | 110 | .. _lowerlevel/pymongo: 111 | 112 | PyMongo-level 113 | ------------- 114 | :attr:`django.db.connections` is a dictionary-like object that holds all 115 | database connections -- that is, for MongoDB databases, 116 | :class:`django_mongodb_engine.base.DatabaseWrapper` instances. 117 | 118 | These instances can be used to get the PyMongo-level :class:`~pymongo.Connection`, 119 | :class:`~pymongo.database.Database` and :class:`~pymongo.collection.Collection` 120 | objects. 121 | 122 | For example, to execute a :meth:`~pymongo.collection.Collection.find_and_modify` 123 | command, you could use code similar to this:: 124 | 125 | from django.db import connections 126 | database_wrapper = connections['my_db_alias'] 127 | eggs_collection = database_wrapper.get_collection('eggs') 128 | eggs_collection.find_and_modify(...) 129 | 130 | .. _PyMongo: http://api.mongodb.org/python/current/ 131 | .. _Geo querying: http://docs.mongodb.org/manual/core/geospatial-indexes/ 132 | .. _F: https://docs.djangoproject.com/en/dev/topics/db/queries/#query-expressions 133 | -------------------------------------------------------------------------------- /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 | if errorlevel 1 exit /b 1 46 | echo. 47 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 48 | goto end 49 | ) 50 | 51 | if "%1" == "dirhtml" ( 52 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 53 | if errorlevel 1 exit /b 1 54 | echo. 55 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 56 | goto end 57 | ) 58 | 59 | if "%1" == "singlehtml" ( 60 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 61 | if errorlevel 1 exit /b 1 62 | echo. 63 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 64 | goto end 65 | ) 66 | 67 | if "%1" == "pickle" ( 68 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 69 | if errorlevel 1 exit /b 1 70 | echo. 71 | echo.Build finished; now you can process the pickle files. 72 | goto end 73 | ) 74 | 75 | if "%1" == "json" ( 76 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 77 | if errorlevel 1 exit /b 1 78 | echo. 79 | echo.Build finished; now you can process the JSON files. 80 | goto end 81 | ) 82 | 83 | if "%1" == "htmlhelp" ( 84 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 85 | if errorlevel 1 exit /b 1 86 | echo. 87 | echo.Build finished; now you can run HTML Help Workshop with the ^ 88 | .hhp project file in %BUILDDIR%/htmlhelp. 89 | goto end 90 | ) 91 | 92 | if "%1" == "qthelp" ( 93 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 94 | if errorlevel 1 exit /b 1 95 | echo. 96 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 97 | .qhcp project file in %BUILDDIR%/qthelp, like this: 98 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\DjangoMongoDBEngine.qhcp 99 | echo.To view the help file: 100 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\DjangoMongoDBEngine.ghc 101 | goto end 102 | ) 103 | 104 | if "%1" == "devhelp" ( 105 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 106 | if errorlevel 1 exit /b 1 107 | echo. 108 | echo.Build finished. 109 | goto end 110 | ) 111 | 112 | if "%1" == "epub" ( 113 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 117 | goto end 118 | ) 119 | 120 | if "%1" == "latex" ( 121 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 122 | if errorlevel 1 exit /b 1 123 | echo. 124 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 125 | goto end 126 | ) 127 | 128 | if "%1" == "text" ( 129 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 130 | if errorlevel 1 exit /b 1 131 | echo. 132 | echo.Build finished. The text files are in %BUILDDIR%/text. 133 | goto end 134 | ) 135 | 136 | if "%1" == "man" ( 137 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 138 | if errorlevel 1 exit /b 1 139 | echo. 140 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 141 | goto end 142 | ) 143 | 144 | if "%1" == "changes" ( 145 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 146 | if errorlevel 1 exit /b 1 147 | echo. 148 | echo.The overview file is in %BUILDDIR%/changes. 149 | goto end 150 | ) 151 | 152 | if "%1" == "linkcheck" ( 153 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 154 | if errorlevel 1 exit /b 1 155 | echo. 156 | echo.Link check complete; look for any errors in the above output ^ 157 | or in %BUILDDIR%/linkcheck/output.txt. 158 | goto end 159 | ) 160 | 161 | if "%1" == "doctest" ( 162 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 163 | if errorlevel 1 exit /b 1 164 | echo. 165 | echo.Testing of doctests in the sources finished, look at the ^ 166 | results in %BUILDDIR%/doctest/output.txt. 167 | goto end 168 | ) 169 | 170 | :end 171 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SOURCEDIR = source/ 6 | SPHINXOPTS = 7 | SPHINXBUILD = $(ENV) sphinx-build 8 | PAPER = 9 | BUILDDIR = build 10 | ENV = PYTHONPATH=. \ 11 | DJANGO_SETTINGS_MODULE=empty_project.settings 12 | 13 | # Internal variables. 14 | PAPEROPT_a4 = -D latex_paper_size=a4 15 | PAPEROPT_letter = -D latex_paper_size=letter 16 | ALLSPHINXOPTS = -d .doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) $(SOURCEDIR) 17 | 18 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest 19 | 20 | dev: 21 | rm -f build/html/_static/*css 22 | make html 23 | sh -c 'cd build/html/_static/&&rm mongodb.css&&ln -s ../../../source/mongodbtheme/static/mongodb.css .' 24 | 25 | help: 26 | @echo "Please use \`make ' where is one of" 27 | @echo " html to make standalone HTML files" 28 | @echo " dirhtml to make HTML files named index.html in directories" 29 | @echo " singlehtml to make a single large HTML file" 30 | @echo " pickle to make pickle files" 31 | @echo " json to make JSON files" 32 | @echo " htmlhelp to make HTML files and a HTML help project" 33 | @echo " qthelp to make HTML files and a qthelp project" 34 | @echo " devhelp to make HTML files and a Devhelp project" 35 | @echo " epub to make an epub" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " changes to make an overview of all changed/added/deprecated items" 41 | @echo " linkcheck to check all external links for integrity" 42 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 43 | 44 | clean: 45 | -rm -rf $(BUILDDIR)/* 46 | 47 | html: 48 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 49 | @echo 50 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 51 | 52 | dirhtml: 53 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 56 | 57 | singlehtml: 58 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 59 | @echo 60 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 61 | 62 | pickle: 63 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 64 | @echo 65 | @echo "Build finished; now you can process the pickle files." 66 | 67 | json: 68 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 69 | @echo 70 | @echo "Build finished; now you can process the JSON files." 71 | 72 | htmlhelp: 73 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 74 | @echo 75 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 76 | ".hhp project file in $(BUILDDIR)/htmlhelp." 77 | 78 | qthelp: 79 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 80 | @echo 81 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 82 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 83 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/DjangoMongoDBEngine.qhcp" 84 | @echo "To view the help file:" 85 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/DjangoMongoDBEngine.qhc" 86 | 87 | devhelp: 88 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 89 | @echo 90 | @echo "Build finished." 91 | @echo "To view the help file:" 92 | @echo "# mkdir -p $$HOME/.local/share/devhelp/DjangoMongoDBEngine" 93 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/DjangoMongoDBEngine" 94 | @echo "# devhelp" 95 | 96 | epub: 97 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 98 | @echo 99 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 100 | 101 | latex: 102 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 103 | @echo 104 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 105 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 106 | "(use \`make latexpdf' here to do that automatically)." 107 | 108 | latexpdf: 109 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 110 | @echo "Running LaTeX files through pdflatex..." 111 | make -C $(BUILDDIR)/latex all-pdf 112 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 113 | 114 | text: 115 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 116 | @echo 117 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 118 | 119 | man: 120 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 121 | @echo 122 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 123 | 124 | changes: 125 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 126 | @echo 127 | @echo "The overview file is in $(BUILDDIR)/changes." 128 | 129 | linkcheck: 130 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 131 | @echo 132 | @echo "Link check complete; look for any errors in the above output " \ 133 | "or in $(BUILDDIR)/linkcheck/output.txt." 134 | 135 | doctest: 136 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 137 | @echo "Testing of doctests in the sources finished, look at the " \ 138 | "results in $(BUILDDIR)/doctest/output.txt." 139 | -------------------------------------------------------------------------------- /django_mongodb_engine/storage.py: -------------------------------------------------------------------------------- 1 | import os 2 | import urlparse 3 | 4 | from django.core.exceptions import ImproperlyConfigured 5 | from django.core.files.storage import Storage 6 | from django.utils.encoding import filepath_to_uri 7 | 8 | from gridfs import GridFS, NoFile 9 | 10 | 11 | def _get_subcollections(collection): 12 | """ 13 | Returns all sub-collections of `collection`. 14 | """ 15 | # XXX: Use the MongoDB API for this once it exists. 16 | for name in collection.database.collection_names(): 17 | cleaned = name[:name.rfind('.')] 18 | if cleaned != collection.name and cleaned.startswith(collection.name): 19 | yield cleaned 20 | 21 | 22 | class GridFSStorage(Storage): 23 | """ 24 | GridFS Storage backend for Django. 25 | 26 | This backend aims to add a GridFS storage to upload files to 27 | using Django's file fields. 28 | 29 | For performance, the file hirarchy is represented as a tree of 30 | MongoDB sub-collections. 31 | 32 | (One could use a flat list, but to list a directory '/this/path/' 33 | we would have to execute a search over the whole collection and 34 | then filter the results to exclude those not starting by 35 | '/this/path' using that model.) 36 | 37 | :param location: 38 | (optional) Name of the top-level node that holds the files. This 39 | value of `location` is prepended to all file paths, so it works 40 | like the `location` setting for Django's built-in 41 | :class:`~django.core.files.storage.FileSystemStorage`. 42 | :param collection: 43 | Name of the collection the file tree shall be stored in. 44 | Defaults to 'storage'. 45 | :param database: 46 | Alias of the Django database to use. Defaults to 'default' (the 47 | default Django database). 48 | :param base_url: 49 | URL that serves the files in GridFS (for instance, through 50 | nginx-gridfs). 51 | Defaults to None (file not accessible through a URL). 52 | """ 53 | 54 | def __init__(self, location='', collection='storage', database='default', 55 | base_url=None): 56 | self.location = location.strip(os.sep) 57 | self.collection = collection 58 | self.database = database 59 | self.base_url = base_url 60 | 61 | if not self.collection: 62 | raise ImproperlyConfigured("'collection' may not be empty.") 63 | 64 | if self.base_url and not self.base_url.endswith('/'): 65 | raise ImproperlyConfigured("If set, 'base_url' must end with a " 66 | "slash.") 67 | 68 | def _open(self, path, mode='rb'): 69 | """ 70 | Returns a :class:`~gridfs.GridOut` file opened in `mode`, or 71 | raises :exc:`~gridfs.errors.NoFile` if the requested file 72 | doesn't exist and mode is not 'w'. 73 | """ 74 | gridfs, filename = self._get_gridfs(path) 75 | try: 76 | return gridfs.get_last_version(filename) 77 | except NoFile: 78 | if 'w' in mode: 79 | return gridfs.new_file(filename=filename) 80 | else: 81 | raise 82 | 83 | def _save(self, path, content): 84 | """ 85 | Saves `content` into the file at `path`. 86 | """ 87 | gridfs, filename = self._get_gridfs(path) 88 | gridfs.put(content, filename=filename) 89 | return path 90 | 91 | def delete(self, path): 92 | """ 93 | Deletes the file at `path` if it exists. 94 | """ 95 | gridfs, filename = self._get_gridfs(path) 96 | try: 97 | gridfs.delete(gridfs.get_last_version(filename=filename)._id) 98 | except NoFile: 99 | pass 100 | 101 | def exists(self, path): 102 | """ 103 | Returns `True` if the file at `path` exists in GridFS. 104 | """ 105 | gridfs, filename = self._get_gridfs(path) 106 | return gridfs.exists(filename=filename) 107 | 108 | def listdir(self, path): 109 | """ 110 | Returns a tuple (folders, lists) that are contained in the 111 | folder `path`. 112 | """ 113 | gridfs, filename = self._get_gridfs(path) 114 | assert not filename 115 | subcollections = _get_subcollections(gridfs._GridFS__collection) 116 | return set(c.split('.')[-1] for c in subcollections), gridfs.list() 117 | 118 | def size(self, path): 119 | """ 120 | Returns the size of the file at `path`. 121 | """ 122 | gridfs, filename = self._get_gridfs(path) 123 | return gridfs.get_last_version(filename=filename).length 124 | 125 | def url(self, name): 126 | if self.base_url is None: 127 | raise ValueError("This file is not accessible via a URL.") 128 | return urlparse.urljoin(self.base_url, filepath_to_uri(name)) 129 | 130 | def created_time(self, path): 131 | """ 132 | Returns the datetime the file at `path` was created. 133 | """ 134 | gridfs, filename = self._get_gridfs(path) 135 | return gridfs.get_last_version(filename=filename).upload_date 136 | 137 | def _get_gridfs(self, path): 138 | """ 139 | Returns a :class:`~gridfs.GridFS` using the sub-collection for 140 | `path`. 141 | """ 142 | path, filename = os.path.split(path) 143 | path = os.path.join(self.collection, self.location, path.strip(os.sep)) 144 | collection_name = path.replace(os.sep, '.').strip('.') 145 | 146 | if not hasattr(self, '_db'): 147 | from django.db import connections 148 | self._db = connections[self.database].database 149 | 150 | return GridFS(self._db, collection_name), filename 151 | -------------------------------------------------------------------------------- /tests/storage/tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from datetime import datetime, timedelta 3 | import os 4 | import tempfile 5 | 6 | from django.core.files.base import ContentFile, File 7 | 8 | from django_mongodb_engine.storage import GridFSStorage 9 | 10 | from .utils import TestCase 11 | 12 | 13 | class GridFSStorageTest(TestCase): 14 | storage_class = GridFSStorage 15 | temp_dir = tempfile.mktemp() 16 | 17 | def setUp(self): 18 | self.storage = self.get_storage(self.temp_dir) 19 | 20 | def tearDown(self): 21 | if hasattr(self.storage, '_db'): 22 | for collection in self.storage._db.collection_names(): 23 | if not collection.startswith('system.'): 24 | self.storage._db.drop_collection(collection) 25 | 26 | def get_storage(self, location, **kwargs): 27 | return self.storage_class(location=location, **kwargs) 28 | 29 | def test_file_access_options(self): 30 | """ 31 | Standard file access options are available, and work as 32 | expected. 33 | """ 34 | self.assertFalse(self.storage.exists('storage_test')) 35 | f = self.storage.open('storage_test', 'w') 36 | f.write('storage contents') 37 | f.close() 38 | self.assert_(self.storage.exists('storage_test')) 39 | 40 | test_file = self.storage.open('storage_test', 'r') 41 | self.assertEqual(test_file.read(), 'storage contents') 42 | 43 | self.storage.delete('storage_test') 44 | self.assertFalse(self.storage.exists('storage_test')) 45 | 46 | # def test_file_accessed_time(self): 47 | # """ 48 | # File storage returns a Datetime object for the last accessed 49 | # time of a file. 50 | # """ 51 | # self.assertFalse(self.storage.exists('test.file')) 52 | # 53 | # f = ContentFile('custom contents') 54 | # f_name = self.storage.save('test.file', f) 55 | # atime = self.storage.accessed_time(f_name) 56 | # 57 | # self.assertEqual(atime, datetime.fromtimestamp( 58 | # os.path.getatime(self.storage.path(f_name)))) 59 | # self.assertTrue(datetime.now() - self.storage.accessed_time(f_name) < 60 | # timedelta(seconds=2)) 61 | # self.storage.delete(f_name) 62 | 63 | def test_file_created_time(self): 64 | """ 65 | File storage returns a Datetime object for the creation time of 66 | a file. 67 | """ 68 | self.assertFalse(self.storage.exists('test.file')) 69 | 70 | f = ContentFile('custom contents') 71 | f_name = self.storage.save('test.file', f) 72 | ctime = self.storage.created_time(f_name) 73 | 74 | self.assertTrue(datetime.now() - self.storage.created_time(f_name) < 75 | timedelta(seconds=2)) 76 | self.storage.delete(f_name) 77 | 78 | # def test_file_modified_time(self): 79 | # """ 80 | # File storage returns a Datetime object for the last modified 81 | # time of a file. 82 | # """ 83 | # self.assertFalse(self.storage.exists('test.file')) 84 | # 85 | # f = ContentFile('custom contents') 86 | # f_name = self.storage.save('test.file', f) 87 | # mtime = self.storage.modified_time(f_name) 88 | # 89 | # self.assertTrue(datetime.now() - self.storage.modified_time(f_name) < 90 | # timedelta(seconds=2)) 91 | # 92 | # self.storage.delete(f_name) 93 | 94 | def test_file_save_without_name(self): 95 | """ 96 | File storage extracts the filename from the content object if 97 | no name is given explicitly. 98 | """ 99 | self.assertFalse(self.storage.exists('test.file')) 100 | 101 | f = ContentFile('custom contents') 102 | f.name = 'test.file' 103 | 104 | storage_f_name = self.storage.save(None, f) 105 | 106 | self.assertEqual(storage_f_name, f.name) 107 | 108 | self.storage.delete(storage_f_name) 109 | 110 | # def test_file_path(self): 111 | # """ 112 | # File storage returns the full path of a file 113 | # """ 114 | # self.assertFalse(self.storage.exists('test.file')) 115 | # 116 | # f = ContentFile('custom contents') 117 | # f_name = self.storage.save('test.file', f) 118 | # 119 | # self.assertEqual(self.storage.path(f_name), 120 | # os.path.join(self.temp_dir, f_name)) 121 | # 122 | # self.storage.delete(f_name) 123 | 124 | def test_file_url(self): 125 | """ 126 | File storage returns a url to access a given file from the Web. 127 | """ 128 | self.assertRaises(ValueError, self.storage.url, 'test.file') 129 | 130 | self.storage = self.get_storage(self.storage.location, 131 | base_url='foo/') 132 | self.assertEqual(self.storage.url('test.file'), 133 | '%s%s' % (self.storage.base_url, 'test.file')) 134 | 135 | def test_listdir(self): 136 | """ 137 | File storage returns a tuple containing directories and files. 138 | """ 139 | self.assertEqual(self.storage.listdir(''), (set(), [])) 140 | self.assertFalse(self.storage.exists('storage_test_1')) 141 | self.assertFalse(self.storage.exists('storage_test_2')) 142 | self.assertFalse(self.storage.exists('storage_dir_1')) 143 | 144 | self.storage.save('storage_test_1', ContentFile('custom content')) 145 | self.storage.save('storage_test_2', ContentFile('custom content')) 146 | storage = self.get_storage(location=os.path.join(self.temp_dir, 147 | 'storage_dir_1')) 148 | storage.save('storage_test_3', ContentFile('custom content')) 149 | 150 | dirs, files = self.storage.listdir('') 151 | self.assertEqual(set(dirs), set([u'storage_dir_1'])) 152 | self.assertEqual(set(files), 153 | set([u'storage_test_1', u'storage_test_2'])) 154 | 155 | 156 | class GridFSStorageTestWithoutLocation(GridFSStorageTest): 157 | # Now test everything without passing a location argument. 158 | temp_dir = '' 159 | -------------------------------------------------------------------------------- /docs/source/mongodbtheme/static/mongodb.css: -------------------------------------------------------------------------------- 1 | /* Fonts */ 2 | @font-face { font-family: fallback_Qlassik; src: url('Qlassik_TB.otf'); } 3 | @font-face { font-family: fallback_Qlassik; font-weight: bold; src: url('QlassikBold_TB.otf'); } 4 | 5 | 6 | body { 7 | margin: 0; 8 | color: #4c3a2c; 9 | background-color: #f6f7ee; 10 | font-family: sans-serif; 11 | } 12 | 13 | a { text-decoration: none; color: #003366; } 14 | a:hover { border-bottom: 1px dotted #003366; } 15 | a[href^=http]:after { 16 | content: url(data:image/gif;base64,R0lGODlhEAAQAIABACBRZv///yH5BAEAAAEALAAAAAAQABAAAAIgjI+py+0GIngwKjkDvkhqXmWLaDXicXbgijLYRsVyXAAAOw%3D%3D); 17 | position: relative; 18 | top: 3px; 19 | opacity: 0.5; 20 | } 21 | a[href^=http]:hover:after { opacity: 0.7; } 22 | 23 | ul > li > ul { 24 | padding-left: 1.5em; 25 | } 26 | 27 | ul.simple > li, 28 | .toctree-wrapper > ul > li { 29 | padding-bottom: 0.3em; 30 | } 31 | 32 | .clearer { clear: both; } 33 | 34 | /* 35 | div, ul { border: 1px solid green; } 36 | */ 37 | 38 | 39 | /* -------------------- Header -------------------- */ 40 | 41 | #header { 42 | background-color: #442d22; 43 | font-family: Qlassik, 'Qlassik Medium', fallback_Qlassik, sans-serif; 44 | font-size: 13pt; 45 | height: 3.2em; 46 | } 47 | #header a { 48 | opacity: 0.7; 49 | border: 0; 50 | } 51 | #header li.current > a, 52 | #header a:hover { 53 | opacity: 1; 54 | } 55 | 56 | /* Navigation level 1 */ 57 | #header > ul { 58 | margin: 0; 59 | padding: 0; 60 | position: relative; 61 | z-index: 1; 62 | top: 1.1em; 63 | } 64 | #header > ul > li { 65 | display: inline; 66 | } 67 | #header > ul > li > a { 68 | margin-left: -0.3em; 69 | padding: 1.1em 0.9em; 70 | padding-bottom: 1.2em; 71 | color: #fffdd4; 72 | } 73 | #header > ul > li:first-child a { 74 | padding-left: 1.5em; 75 | } 76 | #header > ul > li.current > a { 77 | font-weight: bold; 78 | } 79 | 80 | /* Navigation level 2 */ 81 | #header > ul > li > ul { 82 | display: none; 83 | float: left; 84 | width: 100%; 85 | margin-top: 1.2em; 86 | margin-left: -0.7em; 87 | padding-left: 0.7em; 88 | background-color: #f6f4cd; 89 | border-bottom: 3px solid #fdfcf7; 90 | list-style-type: none; 91 | } 92 | #header > ul > li > ul > li { float: left; } 93 | #header > ul > li > ul > li > a { 94 | color: #403125; 95 | padding: 0.6em; 96 | padding-bottom: 0.5em; 97 | padding-left: 1.1em; 98 | display: block; 99 | } 100 | /*#header > ul > li > ul > li + li > a { 101 | padding-left: 0.6em; 102 | }*/ 103 | 104 | /* Display sub-menus if activated */ 105 | #header > ul > li.current > ul { display: block; } 106 | 107 | /* Top right caption */ 108 | #header > a.logo { 109 | position: absolute; 110 | z-index: 1; 111 | top: 0; 112 | right: 0.2em; 113 | padding: 0.2em; 114 | font-size: 19.5pt; 115 | font-weight: bold; 116 | color: #f6f4cd; 117 | } 118 | #header > a.logo #slogan { 119 | color: #ccc; 120 | font-size: 12pt; 121 | margin-top: 1px; 122 | display: block; 123 | text-align: right; 124 | } 125 | 126 | 127 | /* ------------------- Content ------------------- */ 128 | 129 | h1, h2, h3, h4, h5, h6 { 130 | font-weight: lighter; 131 | font-family: Qlassik, 'Qlassik Medium', fallback_Qlassik, sans-serif; 132 | } 133 | h1 { font-size: 26pt; margin-bottom: 0; } 134 | h1 a.headerlink { display: none; } 135 | .body > .section > .section ~ .section { margin-top: 2em; } 136 | h2 { font-size: 20pt; margin-bottom: 0.3em; } 137 | h2 tt { font-size: 18pt; } 138 | h2 + *, h3 + * { margin-top: 4px; } 139 | h3 { margin-bottom: 0; } 140 | 141 | /* headerlink = the pilcrow (¶) showed besides headlines */ 142 | .headerlink { 143 | padding: 0 0.2em 0.2em; 144 | font-family: sans-serif; 145 | visibility: hidden; 146 | opacity: 0.2; 147 | border: 0 !important; 148 | } 149 | .headerlink:hover { opacity: 0.8; } 150 | *:hover > .headerlink { visibility: visible; } 151 | 152 | .bodywrapper { margin: 1.4em 0 0 250px; border-left: 1px solid #ccc; } 153 | 154 | /* Use some nice padding for the content */ 155 | .bodywrapper { padding: 0.2em 1.8em 2em; } 156 | 157 | /* Indent paragraphs a tiny bit */ 158 | .section > div { margin-left: 1px; } 159 | .section > p, dl { margin-left: 3px; } 160 | 161 | .section p, .section ul, .admonition, .section table, 162 | .sphinxsidebar p, .sphinxsidebar ul { 163 | font-size: 14px; 164 | line-height: 1.3; 165 | } 166 | 167 | /* No italic for internal references */ 168 | .reference.internal em { font-style: normal; } 169 | 170 | dt tt { font-size: 110%; } 171 | dt .descname { font-size: 120%; font-weight: bold; } 172 | dt em { font-size: 90%; } 173 | 174 | dl.docutils dt { font-size: 110%; font-weight: bold; margin-bottom: -0.3em; } 175 | dl.docutils dd { margin-left: 1em; margin-bottom: 1.3em; } 176 | 177 | dl.class dd p { margin-top: 0.5em; margin-bottom: 0.5em; } 178 | dl.class dd p:first { margin-top: 0; } 179 | 180 | .document li { 181 | margin-bottom: 0.2em; 182 | } 183 | 184 | .field-name { padding-right: 0.5em; } 185 | .field-body > ul { margin-top: 1.8em; margin-left: -7.5em; } 186 | .field-body strong { font-family: monospace; } 187 | 188 | .admonition { 189 | border: 1px solid #ccc; 190 | padding: 0 1em; 191 | background-color: #eff0e7; 192 | } 193 | .admonition > * { 194 | margin-left: 0.2em; 195 | } 196 | .admonition .first { 197 | margin-top: 0.6em; 198 | margin-bottom: -0.5em; 199 | font-size: 13pt; 200 | margin-left: 0; 201 | } 202 | .warning { 203 | background-color: #f4e0d8; 204 | border-color: #f5c3bc; 205 | } 206 | 207 | .sphinxsidebar { 208 | position: relative; 209 | z-index: 1; 210 | float: left; 211 | width: 250px; 212 | height: 100%; 213 | margin-top: 1.3em; 214 | } 215 | .sphinxsidebarwrapper { 216 | padding: 1em; 217 | padding-top: 0; 218 | } 219 | .sphinxsidebarwrapper > ul { 220 | list-style-type: none; 221 | padding-left: 1px; 222 | } 223 | 224 | /* ------------------- Hacks ------------------- */ 225 | 226 | /* Adjust headline position of pages without a toctree */ 227 | #page-tutorial, #page-index, #page-troubleshooting { 228 | position: relative; 229 | top: -1.1em; 230 | } 231 | #page-tutorial .sphinxsidebar, #page-troubleshooting .sphinxsidebar { 232 | margin-top: 0; 233 | } 234 | #page-index .sphinxsidebar { display: none; } 235 | #page-index .bodywrapper { margin-left: 0; } 236 | 237 | .highlight, .highlight-python { 238 | background: #f1f2e9 !important; 239 | } 240 | :not(.highlight-python) > .highlight, .highlight-python { 241 | margin-top: 2px; 242 | margin-bottom: 2px; 243 | padding: 0 0.7em; 244 | border: 1px solid #ccc; 245 | border-left: 0; 246 | border-right: 0; 247 | color: #442d22; 248 | } 249 | .highlight .go { color: #5c5c5c !important; } /* Generic.Output */ 250 | .highlight-none pre { white-space: pre-wrap; } 251 | 252 | 253 | /* ------------------- Footer ------------------- */ 254 | 255 | .footer { 256 | clear: both; 257 | font-size: 12px; 258 | margin-bottom: 5px; 259 | text-align: center; 260 | color: #616161; 261 | } 262 | -------------------------------------------------------------------------------- /django_mongodb_engine/fields.py: -------------------------------------------------------------------------------- 1 | from django.db import connections, models 2 | 3 | from gridfs import GridFS 4 | from gridfs.errors import NoFile 5 | 6 | # handle pymongo backward compatibility 7 | try: 8 | from bson.objectid import ObjectId 9 | except ImportError: 10 | from pymongo.objectid import ObjectId 11 | 12 | 13 | from django_mongodb_engine.utils import make_struct 14 | 15 | 16 | __all__ = ['GridFSField', 'GridFSString'] 17 | 18 | 19 | class GridFSField(models.Field): 20 | """ 21 | GridFS field to store large chunks of data (blobs) in GridFS. 22 | 23 | Model instances keep references (ObjectIds) to GridFS files 24 | (:class:`gridfs.GridOut`) which are fetched on first attribute 25 | access. 26 | 27 | :param delete: 28 | Whether to delete the data stored in the GridFS (as GridFS 29 | files) when model instances are deleted (default: :const:`True`). 30 | 31 | Note that this doesn't have any influence on what happens if 32 | you update the blob value by assigning a new file, in which 33 | case the old file is always deleted. 34 | """ 35 | forbids_updates = True 36 | 37 | def __init__(self, *args, **kwargs): 38 | self._versioning = kwargs.pop('versioning', False) 39 | self._autodelete = kwargs.pop('delete', not self._versioning) 40 | if self._versioning: 41 | import warnings 42 | warnings.warn("GridFSField versioning will be deprecated on " 43 | "version 0.6. If you consider this option useful " 44 | "please add a comment on issue #65 with your use " 45 | "case.", PendingDeprecationWarning) 46 | 47 | kwargs['max_length'] = 24 48 | kwargs.setdefault('default', None) 49 | kwargs.setdefault('null', True) 50 | super(GridFSField, self).__init__(*args, **kwargs) 51 | 52 | def db_type(self, connection): 53 | return 'gridfs' 54 | 55 | def contribute_to_class(self, model, name): 56 | # GridFSFields are represented as properties in the model 57 | # class. Let 'foo' be an instance of a model that has the 58 | # GridFSField 'gridf'. 'foo.gridf' then calls '_property_get' 59 | # and 'foo.gridfs = bar' calls '_property_set(bar)'. 60 | super(GridFSField, self).contribute_to_class(model, name) 61 | setattr(model, self.attname, property(self._property_get, 62 | self._property_set)) 63 | if self._autodelete: 64 | models.signals.pre_delete.connect(self._on_pre_delete, 65 | sender=model) 66 | 67 | def _property_get(self, model_instance): 68 | """ 69 | Gets the file from GridFS using the id stored in the model. 70 | """ 71 | meta = self._get_meta(model_instance) 72 | if meta.filelike is None and meta.oid is not None: 73 | gridfs = self._get_gridfs(model_instance) 74 | if self._versioning: 75 | try: 76 | meta.filelike = gridfs.get_last_version(filename=meta.oid) 77 | return meta.filelike 78 | except NoFile: 79 | pass 80 | meta.filelike = gridfs.get(meta.oid) 81 | return meta.filelike 82 | 83 | def _property_set(self, model_instance, value): 84 | """ 85 | Sets a new value. 86 | 87 | If value is an ObjectID it must be coming from Django's ORM 88 | internals being the value fetched from the database on query. 89 | In that case just update the id stored in the model instance. 90 | Otherwise it sets the value and checks whether a save is needed 91 | or not. 92 | """ 93 | meta = self._get_meta(model_instance) 94 | if isinstance(value, ObjectId) and meta.oid is None: 95 | meta.oid = value 96 | else: 97 | meta.should_save = meta.filelike != value 98 | meta.filelike = value 99 | 100 | def pre_save(self, model_instance, add): 101 | meta = self._get_meta(model_instance) 102 | if meta.should_save: 103 | gridfs = self._get_gridfs(model_instance) 104 | if not self._versioning and meta.oid is not None: 105 | # We're putting a new GridFS file, so get rid of the 106 | # old one if we weren't explicitly asked to keep it. 107 | gridfs.delete(meta.oid) 108 | meta.should_save = False 109 | if not self._versioning or meta.oid is None: 110 | meta.oid = gridfs.put(meta.filelike) 111 | else: 112 | gridfs.put(meta.filelike, filename=meta.oid) 113 | return meta.oid 114 | 115 | def _on_pre_delete(self, sender, instance, using, signal, **kwargs): 116 | """ 117 | Deletes the files associated with this isntance. 118 | 119 | If versioning is enabled all versions will be deleted. 120 | """ 121 | gridfs = self._get_gridfs(instance) 122 | meta = self._get_meta(instance) 123 | try: 124 | while self._versioning and meta.oid: 125 | last = gridfs.get_last_version(filename=meta.oid) 126 | gridfs.delete(last._id) 127 | except NoFile: 128 | pass 129 | 130 | gridfs.delete(meta.oid) 131 | 132 | def _get_meta(self, model_instance): 133 | meta_name = '_%s_meta' % self.attname 134 | meta = getattr(model_instance, meta_name, None) 135 | if meta is None: 136 | meta_cls = make_struct('filelike', 'oid', 'should_save') 137 | meta = meta_cls(None, None, None) 138 | setattr(model_instance, meta_name, meta) 139 | return meta 140 | 141 | def _get_gridfs(self, model_instance): 142 | model = model_instance.__class__ 143 | return GridFS(connections[model.objects.db].database, 144 | model._meta.db_table) 145 | 146 | 147 | class GridFSString(GridFSField): 148 | """ 149 | Similar to :class:`GridFSField`, but the data is represented as a 150 | bytestring on Python side. This implies that all data has to be 151 | copied **into memory**, so :class:`GridFSString` is for smaller 152 | chunks of data only. 153 | """ 154 | 155 | def _property_get(self, model): 156 | filelike = super(GridFSString, self)._property_get(model) 157 | if filelike is None: 158 | return '' 159 | if hasattr(filelike, 'read'): 160 | return filelike.read() 161 | return filelike 162 | 163 | 164 | try: 165 | # Used to satisfy South when introspecting models that use 166 | # GridFSField/GridFSString fields. Custom rules could be added 167 | # if needed. 168 | from south.modelsinspector import add_introspection_rules 169 | add_introspection_rules( 170 | [], ['^django_mongodb_engine\.fields\.GridFSField']) 171 | add_introspection_rules( 172 | [], ['^django_mongodb_engine\.fields\.GridFSString']) 173 | except ImportError: 174 | pass 175 | -------------------------------------------------------------------------------- /django_mongodb_engine/contrib/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from django.db import models, connections 4 | from django.db.models.query import QuerySet 5 | from django.db.models.sql.query import Query as SQLQuery 6 | 7 | 8 | ON_PYPY = hasattr(sys, 'pypy_version_info') 9 | 10 | 11 | def _compiler_for_queryset(qs, which='SQLCompiler'): 12 | connection = connections[qs.db] 13 | Compiler = connection.ops.compiler(which) 14 | return Compiler(qs.query, connection, connection.alias) 15 | 16 | 17 | class RawQuery(SQLQuery): 18 | 19 | def __init__(self, model, raw_query): 20 | super(RawQuery, self).__init__(model) 21 | self.raw_query = raw_query 22 | 23 | def clone(self, *args, **kwargs): 24 | clone = super(RawQuery, self).clone(*args, **kwargs) 25 | clone.raw_query = self.raw_query 26 | return clone 27 | 28 | 29 | class RawQueryMixin: 30 | 31 | def get_raw_query_set(self, raw_query): 32 | return QuerySet(self.model, RawQuery(self.model, raw_query), self._db) 33 | 34 | def raw_query(self, query=None): 35 | """ 36 | Does a raw MongoDB query. The optional parameter `query` is the spec 37 | passed to PyMongo's :meth:` pymongo.Collection.find`. 38 | """ 39 | return self.get_raw_query_set(query or {}) 40 | 41 | def raw_update(self, spec_or_q, update_dict, **kwargs): 42 | """ 43 | Does a raw MongoDB update. `spec_or_q` is either a MongoDB 44 | filter dict or a :class:`~django.db.models.query_utils.Q` 45 | instance that selects the records to update. `update_dict` is 46 | a MongoDB style update document containing either a new 47 | document or atomic modifiers such as ``$inc``. 48 | 49 | Keyword arguments will be passed to :meth:`pymongo.Collection.update`. 50 | """ 51 | if isinstance(spec_or_q, dict): 52 | queryset = self.get_raw_query_set(spec_or_q) 53 | else: 54 | queryset = self.filter(spec_or_q) 55 | queryset._for_write = True 56 | compiler = _compiler_for_queryset(queryset, 'SQLUpdateCompiler') 57 | compiler.execute_update(update_dict, **kwargs) 58 | 59 | raw_update.alters_data = True 60 | 61 | 62 | class MapReduceResult(object): 63 | """ 64 | Represents one item of a MapReduce result array. 65 | 66 | :param model: the model on that query the MapReduce was performed 67 | :param key: the *key* from the result item 68 | :param value: the *value* from the result item 69 | """ 70 | 71 | def __init__(self, model, key, value): 72 | self.model = model 73 | self.key = key 74 | self.value = value 75 | 76 | @classmethod 77 | def from_entity(cls, model, entity): 78 | return cls(model, entity['_id'], entity['value']) 79 | 80 | def __repr__(self): 81 | return '<%s model=%r key=%r value=%r>' % (self.__class__.__name__, 82 | self.model.__name__, 83 | self.key, self.value) 84 | 85 | 86 | class MongoDBQuerySet(QuerySet): 87 | 88 | def map_reduce(self, *args, **kwargs): 89 | """ 90 | Performs a Map/Reduce operation on all documents matching the query, 91 | yielding a :class:`MapReduceResult` object for each result entity. 92 | 93 | If the optional keyword argument `drop_collection` is ``True``, the 94 | result collection will be dropped after fetching all results. 95 | 96 | Any other arguments are passed to :meth:`Collection.map_reduce 97 | `. 98 | """ 99 | # TODO: Field name substitution (e.g. id -> _id). 100 | drop_collection = kwargs.pop('drop_collection', False) 101 | query = self._get_query() 102 | kwargs.setdefault('query', query.mongo_query) 103 | result_collection = query.collection.map_reduce(*args, **kwargs) 104 | # TODO: Get rid of this. 105 | # PyPy has no guaranteed garbage collection so we can't rely on 106 | # the 'finally' suite of a generator (_map_reduce_cpython) to 107 | # be executed in time (in fact, it isn't guaranteed to be 108 | # executed *at all*). On the other hand, we *must* drop the 109 | # collection if `drop_collection` is True so we can't use a 110 | # generator in this case. 111 | if drop_collection and ON_PYPY: 112 | return self._map_reduce_pypy_drop_collection_hack( 113 | result_collection) 114 | else: 115 | return self._map_reduce_cpython(result_collection, 116 | drop_collection) 117 | 118 | def _map_reduce_cpython(self, result_collection, drop_collection): 119 | try: 120 | for entity in result_collection.find(): 121 | yield MapReduceResult.from_entity(self.model, entity) 122 | finally: 123 | if drop_collection: 124 | result_collection.drop() 125 | 126 | def _map_reduce_pypy_drop_collection_hack(self, result_collection): 127 | try: 128 | return iter([MapReduceResult.from_entity(self.model, entity) 129 | for entity in result_collection.find()]) 130 | finally: 131 | result_collection.drop() 132 | 133 | def inline_map_reduce(self, *args, **kwargs): 134 | """ 135 | Similar to :meth:`map_reduce` but runs the Map/Reduce in memory, 136 | returning a list of :class:`MapReduceResults `. 137 | 138 | Does not take the `drop_collection` keyword argument since no result 139 | collection is involved for in-memory Map/Reduce operations. 140 | """ 141 | query = self._get_query() 142 | kwargs.setdefault('query', query.mongo_query) 143 | return [MapReduceResult.from_entity(self.model, entity) for entity in 144 | query.collection.inline_map_reduce(*args, **kwargs)] 145 | 146 | def _get_query(self): 147 | return _compiler_for_queryset(self).build_query() 148 | 149 | def distinct(self, *args, **kwargs): 150 | query = self._get_query() 151 | return query.get_cursor().distinct(*args, **kwargs) 152 | 153 | 154 | class MongoDBManager(models.Manager, RawQueryMixin): 155 | """ 156 | Lets you use Map/Reduce and raw query/update with your models:: 157 | 158 | class FooModel(models.Model): 159 | ... 160 | objects = MongoDBManager() 161 | """ 162 | 163 | def map_reduce(self, *args, **kwargs): 164 | return self.get_query_set().map_reduce(*args, **kwargs) 165 | 166 | def inline_map_reduce(self, *args, **kwargs): 167 | return self.get_query_set().inline_map_reduce(*args, **kwargs) 168 | 169 | def get_query_set(self): 170 | return MongoDBQuerySet(self.model, using=self._db) 171 | 172 | def distinct(self, *args, **kwargs): 173 | """ 174 | Runs a :meth:`~pymongo.Collection.distinct` query against the 175 | database. 176 | """ 177 | return self.get_query_set().distinct(*args, **kwargs) 178 | -------------------------------------------------------------------------------- /django_mongodb_engine/creation.py: -------------------------------------------------------------------------------- 1 | from django.db.utils import DatabaseError 2 | 3 | from pymongo import DESCENDING 4 | 5 | from djangotoolbox.db.creation import NonrelDatabaseCreation 6 | 7 | from .utils import make_index_list 8 | 9 | 10 | class DatabaseCreation(NonrelDatabaseCreation): 11 | 12 | # We'll store decimals as strings, dates and times as datetimes, 13 | # sets as lists and automatic keys as ObjectIds. 14 | data_types = dict(NonrelDatabaseCreation.data_types, **{ 15 | 'SetField': 'list', 16 | }) 17 | 18 | def db_type(self, field): 19 | """ 20 | Returns the db_type of the field for non-relation fields, and 21 | the db_type of a primary key field of a related model for 22 | ForeignKeys, OneToOneFields and ManyToManyFields. 23 | """ 24 | if field.rel is not None: 25 | field = field.rel.get_related_field() 26 | return field.db_type(connection=self.connection) 27 | 28 | def sql_indexes_for_model(self, model, termstyle): 29 | """Creates indexes for all fields in ``model``.""" 30 | meta = model._meta 31 | 32 | if not meta.managed or meta.proxy: 33 | return [] 34 | 35 | collection = self.connection.get_collection(meta.db_table) 36 | 37 | def ensure_index(*args, **kwargs): 38 | if ensure_index.first_index: 39 | print "Installing indices for %s.%s model." % \ 40 | (meta.app_label, meta.object_name) 41 | ensure_index.first_index = False 42 | return collection.ensure_index(*args, **kwargs) 43 | ensure_index.first_index = True 44 | 45 | newstyle_indexes = getattr(meta, 'indexes', None) 46 | if newstyle_indexes: 47 | self._handle_newstyle_indexes(ensure_index, meta, newstyle_indexes) 48 | else: 49 | self._handle_oldstyle_indexes(ensure_index, meta) 50 | 51 | def _handle_newstyle_indexes(self, ensure_index, meta, indexes): 52 | from djangotoolbox.fields import AbstractIterableField, \ 53 | EmbeddedModelField 54 | 55 | # Django indexes. 56 | for field in meta.local_fields: 57 | if not (field.unique or field.db_index): 58 | # field doesn't need an index. 59 | continue 60 | column = '_id' if field.primary_key else field.column 61 | ensure_index(column, unique=field.unique) 62 | 63 | # Django unique_together indexes. 64 | indexes = list(indexes) 65 | 66 | for fields in getattr(meta, 'unique_together', []): 67 | assert isinstance(fields, (list, tuple)) 68 | indexes.append({'fields': make_index_list(fields), 'unique': True}) 69 | 70 | def get_column_name(field): 71 | opts = meta 72 | parts = field.split('.') 73 | for i, part in enumerate(parts): 74 | field = opts.get_field(part) 75 | parts[i] = field.column 76 | if isinstance(field, AbstractIterableField): 77 | field = field.item_field 78 | if isinstance(field, EmbeddedModelField): 79 | opts = field.embedded_model._meta 80 | else: 81 | break 82 | return '.'.join(parts) 83 | 84 | for index in indexes: 85 | if isinstance(index, dict): 86 | kwargs = index.copy() 87 | fields = kwargs.pop('fields') 88 | else: 89 | fields, kwargs = index, {} 90 | fields = [(get_column_name(name), direction) 91 | for name, direction in make_index_list(fields)] 92 | ensure_index(fields, **kwargs) 93 | 94 | def _handle_oldstyle_indexes(self, ensure_index, meta): 95 | from warnings import warn 96 | warn("'descending_indexes', 'sparse_indexes' and 'index_together' " 97 | "are deprecated and will be ignored as of version 0.6. " 98 | "Use 'indexes' instead.", DeprecationWarning) 99 | sparse_indexes = [] 100 | descending_indexes = set(getattr(meta, 'descending_indexes', ())) 101 | 102 | # Lets normalize the sparse_index values changing [], set() to (). 103 | for idx in set(getattr(meta, 'sparse_indexes', ())): 104 | sparse_indexes.append( 105 | isinstance(idx, (tuple, set, list)) and tuple(idx) or idx) 106 | 107 | # Ordinary indexes. 108 | for field in meta.local_fields: 109 | if not (field.unique or field.db_index): 110 | # field doesn't need an index. 111 | continue 112 | column = '_id' if field.primary_key else field.column 113 | if field.name in descending_indexes: 114 | column = [(column, DESCENDING)] 115 | ensure_index(column, unique=field.unique, 116 | sparse=field.name in sparse_indexes) 117 | 118 | def create_compound_indexes(indexes, **kwargs): 119 | # indexes: (field1, field2, ...). 120 | if not indexes: 121 | return 122 | kwargs['sparse'] = tuple(indexes) in sparse_indexes 123 | indexes = [(meta.get_field(name).column, direction) for 124 | name, direction in make_index_list(indexes)] 125 | ensure_index(indexes, **kwargs) 126 | 127 | # Django unique_together indexes. 128 | for indexes in getattr(meta, 'unique_together', []): 129 | assert isinstance(indexes, (list, tuple)) 130 | create_compound_indexes(indexes, unique=True) 131 | 132 | # MongoDB compound indexes. 133 | index_together = getattr(meta, 'mongo_index_together', []) 134 | if index_together: 135 | if isinstance(index_together[0], dict): 136 | # Assume index_together = [{'fields' : [...], ...}]. 137 | for args in index_together: 138 | kwargs = args.copy() 139 | create_compound_indexes(kwargs.pop('fields'), **kwargs) 140 | else: 141 | # Assume index_together = ['foo', 'bar', ('spam', -1), etc]. 142 | create_compound_indexes(index_together) 143 | 144 | return [] 145 | 146 | def sql_create_model(self, model, *unused): 147 | """ 148 | Creates a collection that will store instances of the model. 149 | 150 | Technically we only need to precreate capped collections, but 151 | we'll create them for all models, so database introspection 152 | knows about empty "tables". 153 | """ 154 | name = model._meta.db_table 155 | if getattr(model._meta, 'capped', False): 156 | kwargs = {'capped': True} 157 | size = getattr(model._meta, 'collection_size', None) 158 | if size is not None: 159 | kwargs['size'] = size 160 | max_ = getattr(model._meta, 'collection_max', None) 161 | if max_ is not None: 162 | kwargs['max'] = max_ 163 | else: 164 | kwargs = {} 165 | 166 | collection = self.connection.get_collection(name, existing=True) 167 | if collection is not None: 168 | opts = dict(collection.options()) 169 | if opts != kwargs: 170 | raise DatabaseError("Can't change options of an existing " 171 | "collection: %s --> %s." % (opts, kwargs)) 172 | 173 | # Initialize the capped collection: 174 | self.connection.get_collection(name, **kwargs) 175 | 176 | return [], {} 177 | 178 | def set_autocommit(self): 179 | """There's no such thing in MongoDB.""" 180 | 181 | def create_test_db(self, verbosity=1, autoclobber=False): 182 | """ 183 | No need to create databases in MongoDB :) 184 | but we can make sure that if the database existed is emptied. 185 | """ 186 | test_database_name = self._get_test_db_name() 187 | 188 | self.connection.settings_dict['NAME'] = test_database_name 189 | # This is important. Here we change the settings so that all 190 | # other code thinks that the chosen database is now the test 191 | # database. This means that nothing needs to change in the test 192 | # code for working with connections, databases and collections. 193 | # It will appear the same as when working with non-test code. 194 | 195 | # Force a reconnect to ensure we're using the test database. 196 | self.connection._reconnect() 197 | 198 | # In this phase it will only drop the database if it already 199 | # existed which could potentially happen if the test database 200 | # was created but was never dropped at the end of the tests. 201 | self._drop_database(test_database_name) 202 | 203 | from django.core.management import call_command 204 | call_command('syncdb', verbosity=max(verbosity-1, 0), 205 | interactive=False, database=self.connection.alias) 206 | 207 | return test_database_name 208 | 209 | def destroy_test_db(self, old_database_name, verbosity=1): 210 | if verbosity >= 1: 211 | print "Destroying test database for alias '%s'..." % \ 212 | self.connection.alias 213 | test_database_name = self.connection.settings_dict['NAME'] 214 | self._drop_database(test_database_name) 215 | self.connection.settings_dict['NAME'] = old_database_name 216 | 217 | def _drop_database(self, database_name): 218 | for collection in self.connection.introspection.table_names(): 219 | if not collection.startswith('system.'): 220 | self.connection.database.drop_collection(collection) 221 | -------------------------------------------------------------------------------- /tests/contrib/tests.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | 3 | from functools import partial 4 | 5 | from django.db.models import Q 6 | from django.db.utils import DatabaseError 7 | 8 | from django_mongodb_engine.contrib import MapReduceResult 9 | 10 | from models import * 11 | from utils import TestCase, get_collection, skip 12 | 13 | 14 | class MapReduceTests(TestCase): 15 | 16 | def test_map_reduce(self, inline=False): 17 | mapfunc = """ 18 | function map() { 19 | for(i=0; i 13: 118 | value = value[:10] + '...' 119 | msg = "AutoField (default primary key) values must be " \ 120 | "strings representing an ObjectId on MongoDB (got " \ 121 | "%r instead)." % value 122 | if field.model._meta.db_table == 'django_site': 123 | # Also provide some useful tips for (very common) issues 124 | # with settings.SITE_ID. 125 | msg += " Please make sure your SITE_ID contains a " \ 126 | "valid ObjectId string." 127 | raise DatabaseError(msg) 128 | 129 | # PyMongo can only process datatimes? 130 | elif db_type == 'date': 131 | return datetime.datetime(value.year, value.month, value.day) 132 | elif db_type == 'time': 133 | return datetime.datetime(1, 1, 1, value.hour, value.minute, 134 | value.second, value.microsecond) 135 | 136 | return value 137 | 138 | def _value_from_db(self, value, field, field_kind, db_type): 139 | """ 140 | Deconverts keys, dates and times (also in collections). 141 | """ 142 | 143 | # It is *crucial* that this is written as a direct check -- 144 | # when value is an instance of serializer.LazyModelInstance 145 | # calling its __eq__ method does a database query. 146 | if value is None: 147 | return None 148 | 149 | # All keys have been turned into ObjectIds. 150 | if db_type == 'key': 151 | value = unicode(value) 152 | 153 | # We've converted dates and times to datetimes. 154 | elif db_type == 'date': 155 | value = datetime.date(value.year, value.month, value.day) 156 | elif db_type == 'time': 157 | value = datetime.time(value.hour, value.minute, value.second, 158 | value.microsecond) 159 | 160 | # Revert the decimal-to-string encoding. 161 | if field_kind == 'DecimalField': 162 | value = decimal.Decimal(value) 163 | 164 | return super(DatabaseOperations, self)._value_from_db( 165 | value, field, field_kind, db_type) 166 | 167 | 168 | class DatabaseClient(NonrelDatabaseClient): 169 | pass 170 | 171 | 172 | class DatabaseValidation(NonrelDatabaseValidation): 173 | pass 174 | 175 | 176 | class DatabaseIntrospection(NonrelDatabaseIntrospection): 177 | 178 | def table_names(self, cursor=None): 179 | return self.connection.database.collection_names() 180 | 181 | def sequence_list(self): 182 | # Only required for backends that use integer primary keys. 183 | pass 184 | 185 | 186 | class DatabaseWrapper(NonrelDatabaseWrapper): 187 | """ 188 | Public API: connection, database, get_collection. 189 | """ 190 | 191 | def __init__(self, *args, **kwargs): 192 | self.collection_class = kwargs.pop('collection_class', Collection) 193 | super(DatabaseWrapper, self).__init__(*args, **kwargs) 194 | 195 | self.features = DatabaseFeatures(self) 196 | self.ops = DatabaseOperations(self) 197 | self.creation = DatabaseCreation(self) 198 | self.introspection = DatabaseIntrospection(self) 199 | self.validation = DatabaseValidation(self) 200 | self.connected = False 201 | del self.connection 202 | 203 | def get_collection(self, name, **kwargs): 204 | if (kwargs.pop('existing', False) and 205 | name not in self.database.collection_names()): 206 | return None 207 | collection = self.collection_class(self.database, name, **kwargs) 208 | if settings.DEBUG: 209 | collection = CollectionDebugWrapper(collection, self.alias) 210 | return collection 211 | 212 | def __getattr__(self, attr): 213 | if attr in ['connection', 'database']: 214 | assert not self.connected 215 | self._connect() 216 | return getattr(self, attr) 217 | raise AttributeError(attr) 218 | 219 | def _connect(self): 220 | settings = copy.deepcopy(self.settings_dict) 221 | 222 | def pop(name, default=None): 223 | return settings.pop(name) or default 224 | 225 | db_name = pop('NAME') 226 | host = pop('HOST') 227 | port = pop('PORT', 27017) 228 | user = pop('USER') 229 | password = pop('PASSWORD') 230 | options = pop('OPTIONS', {}) 231 | 232 | self.operation_flags = options.pop('OPERATIONS', {}) 233 | if not any(k in ['save', 'delete', 'update'] 234 | for k in self.operation_flags): 235 | # Flags apply to all operations. 236 | flags = self.operation_flags 237 | self.operation_flags = {'save': flags, 'delete': flags, 238 | 'update': flags} 239 | 240 | # Lower-case all OPTIONS keys. 241 | for key in options.iterkeys(): 242 | options[key.lower()] = options.pop(key) 243 | 244 | read_preference = options.get('read_preference') 245 | replicaset = options.get('replicaset') 246 | 247 | if not read_preference: 248 | read_preference = options.get('slave_okay', options.get('slaveok')) 249 | if read_preference: 250 | options['read_preference'] = ReadPreference.SECONDARY 251 | warnings.warn("slave_okay has been deprecated. " 252 | "Please use read_preference instead.") 253 | 254 | conn_options = dict( 255 | host=host, 256 | port=int(port), 257 | document_class=dict, 258 | tz_aware=False 259 | ) 260 | conn_options.update(options) 261 | 262 | if replicaset: 263 | connection_class = MongoReplicaSetClient 264 | else: 265 | connection_class = MongoClient 266 | 267 | try: 268 | self.connection = connection_class(**conn_options) 269 | self.database = self.connection[db_name] 270 | except TypeError: 271 | exc_info = sys.exc_info() 272 | raise ImproperlyConfigured, exc_info[1], exc_info[2] 273 | 274 | if user and password: 275 | if not self.database.authenticate(user, password): 276 | raise ImproperlyConfigured("Invalid username or password.") 277 | 278 | self.connected = True 279 | connection_created.send(sender=self.__class__, connection=self) 280 | 281 | def _reconnect(self): 282 | if self.connected: 283 | del self.connection 284 | del self.database 285 | self.connected = False 286 | self._connect() 287 | 288 | def _commit(self): 289 | pass 290 | 291 | def _rollback(self): 292 | pass 293 | 294 | def close(self): 295 | pass 296 | --------------------------------------------------------------------------------