From 7530f03bf60d7f7f2cf2eb66b0e275d01dfc623b Mon Sep 17 00:00:00 2001 From: Matthew Ellison Date: Fri, 24 Apr 2015 13:36:41 -0400 Subject: [PATCH 1/2] Removed Django Support from MongoEngine Django support has now been split out of MongoEngine and will be revisted as a new but separate module. Closes #958 --- .travis.yml | 21 +- docs/changelog.rst | 1 + docs/django.rst | 173 +-------- mongoengine/__init__.py | 1 - mongoengine/django/__init__.py | 0 mongoengine/django/auth.py | 412 ---------------------- mongoengine/django/mongo_auth/__init__.py | 0 mongoengine/django/mongo_auth/models.py | 119 ------- mongoengine/django/sessions.py | 124 ------- mongoengine/django/shortcuts.py | 47 --- mongoengine/django/storage.py | 112 ------ mongoengine/django/tests.py | 31 -- mongoengine/django/utils.py | 6 - setup.py | 4 +- tests/test_django.py | 330 ----------------- tests/test_jinja.py | 47 --- 16 files changed, 10 insertions(+), 1418 deletions(-) delete mode 100644 mongoengine/django/__init__.py delete mode 100644 mongoengine/django/auth.py delete mode 100644 mongoengine/django/mongo_auth/__init__.py delete mode 100644 mongoengine/django/mongo_auth/models.py delete mode 100644 mongoengine/django/sessions.py delete mode 100644 mongoengine/django/shortcuts.py delete mode 100644 mongoengine/django/storage.py delete mode 100644 mongoengine/django/tests.py delete mode 100644 mongoengine/django/utils.py delete mode 100644 tests/test_django.py delete mode 100644 tests/test_jinja.py diff --git a/.travis.yml b/.travis.yml index 38b72cc9..37ec8cf7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,24 +8,18 @@ python: - pypy - pypy3 env: -- PYMONGO=2.7.2 DJANGO=dev -- PYMONGO=2.7.2 DJANGO=1.7.1 -- PYMONGO=2.7.2 DJANGO=1.6.8 -- PYMONGO=2.7.2 DJANGO=1.5.11 -- PYMONGO=2.8 DJANGO=dev -- PYMONGO=2.8 DJANGO=1.7.1 -- PYMONGO=2.8 DJANGO=1.6.8 -- PYMONGO=2.8 DJANGO=1.5.11 +- PYMONGO=2.7.2 +- PYMONGO=2.8 matrix: exclude: - python: '2.6' - env: PYMONGO=2.7.2 DJANGO=dev + env: PYMONGO=2.7.2 - python: '2.6' - env: PYMONGO=2.8 DJANGO=dev + env: PYMONGO=2.8 - python: '2.6' - env: PYMONGO=2.7.2 DJANGO=1.7.1 + env: PYMONGO=2.7.2 - python: '2.6' - env: PYMONGO=2.8 DJANGO=1.7.1 + env: PYMONGO=2.8 allow_failures: - python: pypy3 fast_finish: true @@ -43,9 +37,6 @@ install: true; fi - if [[ $PYMONGO != 'dev' ]]; then travis_retry pip install pymongo==$PYMONGO; true; fi -- if [[ $DJANGO == 'dev' ]]; then travis_retry pip install git+https://github.com/django/django.git; - fi -- if [[ $DJANGO != 'dev' ]]; then travis_retry pip install Django==$DJANGO; fi - travis_retry pip install https://pypi.python.org/packages/source/p/python-dateutil/python-dateutil-2.1.tar.gz#md5=1534bb15cf311f07afaa3aacba1c028b - travis_retry pip install coveralls - travis_retry python setup.py install diff --git a/docs/changelog.rst b/docs/changelog.rst index 3dbcc2f8..5617a5b1 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -11,6 +11,7 @@ Changes in 0.9.X - DEV - Use sets for populating dbrefs to dereference - Fixed unpickled documents replacing the global field's list. #888 - Fixed storage of microseconds in ComplexDateTimeField and unused separator option. #910 +- Django support was removed and will be available as a separate extension. #958 Changes in 0.9.0 ================ diff --git a/docs/django.rst b/docs/django.rst index 5d2423f2..4b9a04bd 100644 --- a/docs/django.rst +++ b/docs/django.rst @@ -2,176 +2,5 @@ Django Support ============== -.. note:: Updated to support Django 1.5 - -Connecting -========== -In your **settings.py** file, ignore the standard database settings (unless you -also plan to use the ORM in your project), and instead call -:func:`~mongoengine.connect` somewhere in the settings module. - -.. note:: - If you are not using another Database backend you may need to add a dummy - database backend to ``settings.py`` eg:: - - DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.dummy' - } - } - -Authentication -============== -MongoEngine includes a Django authentication backend, which uses MongoDB. The -:class:`~mongoengine.django.auth.User` model is a MongoEngine -:class:`~mongoengine.Document`, but implements most of the methods and -attributes that the standard Django :class:`User` model does - so the two are -moderately compatible. Using this backend will allow you to store users in -MongoDB but still use many of the Django authentication infrastructure (such as -the :func:`login_required` decorator and the :func:`authenticate` function). To -enable the MongoEngine auth backend, add the following to your **settings.py** -file:: - - AUTHENTICATION_BACKENDS = ( - 'mongoengine.django.auth.MongoEngineBackend', - ) - -The :mod:`~mongoengine.django.auth` module also contains a -:func:`~mongoengine.django.auth.get_user` helper function, that takes a user's -:attr:`id` and returns a :class:`~mongoengine.django.auth.User` object. - -.. versionadded:: 0.1.3 - -Custom User model -================= -Django 1.5 introduced `Custom user Models -`_ -which can be used as an alternative to the MongoEngine authentication backend. - -The main advantage of this option is that other components relying on -:mod:`django.contrib.auth` and supporting the new swappable user model are more -likely to work. For example, you can use the ``createsuperuser`` management -command as usual. - -To enable the custom User model in Django, add ``'mongoengine.django.mongo_auth'`` -in your ``INSTALLED_APPS`` and set ``'mongo_auth.MongoUser'`` as the custom user -user model to use. In your **settings.py** file you will have:: - - INSTALLED_APPS = ( - ... - 'django.contrib.auth', - 'mongoengine.django.mongo_auth', - ... - ) - - AUTH_USER_MODEL = 'mongo_auth.MongoUser' - -An additional ``MONGOENGINE_USER_DOCUMENT`` setting enables you to replace the -:class:`~mongoengine.django.auth.User` class with another class of your choice:: - - MONGOENGINE_USER_DOCUMENT = 'mongoengine.django.auth.User' - -The custom :class:`User` must be a :class:`~mongoengine.Document` class, but -otherwise has the same requirements as a standard custom user model, -as specified in the `Django Documentation -`_. -In particular, the custom class must define :attr:`USERNAME_FIELD` and -:attr:`REQUIRED_FIELDS` attributes. - -Sessions -======== -Django allows the use of different backend stores for its sessions. MongoEngine -provides a MongoDB-based session backend for Django, which allows you to use -sessions in your Django application with just MongoDB. To enable the MongoEngine -session backend, ensure that your settings module has -``'django.contrib.sessions.middleware.SessionMiddleware'`` in the -``MIDDLEWARE_CLASSES`` field and ``'django.contrib.sessions'`` in your -``INSTALLED_APPS``. From there, all you need to do is add the following line -into your settings module:: - - SESSION_ENGINE = 'mongoengine.django.sessions' - SESSION_SERIALIZER = 'mongoengine.django.sessions.BSONSerializer' - -Django provides session cookie, which expires after ```SESSION_COOKIE_AGE``` seconds, but doesn't delete cookie at sessions backend, so ``'mongoengine.django.sessions'`` supports `mongodb TTL -`_. - -.. note:: ``SESSION_SERIALIZER`` is only necessary in Django 1.6 as the default - serializer is based around JSON and doesn't know how to convert - ``bson.objectid.ObjectId`` instances to strings. - -.. versionadded:: 0.2.1 - -Storage -======= -With MongoEngine's support for GridFS via the :class:`~mongoengine.fields.FileField`, -it is useful to have a Django file storage backend that wraps this. The new -storage module is called :class:`~mongoengine.django.storage.GridFSStorage`. -Using it is very similar to using the default FileSystemStorage.:: - - from mongoengine.django.storage import GridFSStorage - fs = GridFSStorage() - - filename = fs.save('hello.txt', 'Hello, World!') - -All of the `Django Storage API methods -`_ have been -implemented except :func:`path`. If the filename provided already exists, an -underscore and a number (before # the file extension, if one exists) will be -appended to the filename until the generated filename doesn't exist. The -:func:`save` method will return the new filename.:: - - >>> fs.exists('hello.txt') - True - >>> fs.open('hello.txt').read() - 'Hello, World!' - >>> fs.size('hello.txt') - 13 - >>> fs.url('hello.txt') - 'http://your_media_url/hello.txt' - >>> fs.open('hello.txt').name - 'hello.txt' - >>> fs.listdir() - ([], [u'hello.txt']) - -All files will be saved and retrieved in GridFS via the :class:`FileDocument` -document, allowing easy access to the files without the GridFSStorage -backend.:: - - >>> from mongoengine.django.storage import FileDocument - >>> FileDocument.objects() - [] - -.. versionadded:: 0.4 - -Shortcuts -========= -Inspired by the `Django shortcut get_object_or_404 -`_, -the :func:`~mongoengine.django.shortcuts.get_document_or_404` method returns -a document or raises an Http404 exception if the document does not exist:: - - from mongoengine.django.shortcuts import get_document_or_404 - - admin_user = get_document_or_404(User, username='root') - -The first argument may be a Document or QuerySet object. All other passed arguments -and keyword arguments are used in the query:: - - foo_email = get_document_or_404(User.objects.only('email'), username='foo', is_active=True).email - -.. note:: Like with :func:`get`, a MultipleObjectsReturned will be raised if more than one - object is found. - - -Also inspired by the `Django shortcut get_list_or_404 -`_, -the :func:`~mongoengine.django.shortcuts.get_list_or_404` method returns a list of -documents or raises an Http404 exception if the list is empty:: - - from mongoengine.django.shortcuts import get_list_or_404 - - active_users = get_list_or_404(User, is_active=True) - -The first argument may be a Document or QuerySet object. All other passed -arguments and keyword arguments are used to filter the query. +.. note:: Django support has been split from MongoEngine and will be available on Github as a separate module. `See Here `_. diff --git a/mongoengine/__init__.py b/mongoengine/__init__.py index b215181a..f1b2b30d 100644 --- a/mongoengine/__init__.py +++ b/mongoengine/__init__.py @@ -10,7 +10,6 @@ import signals from signals import * from errors import * import errors -import django __all__ = (list(document.__all__) + fields.__all__ + connection.__all__ + list(queryset.__all__) + signals.__all__ + list(errors.__all__)) diff --git a/mongoengine/django/__init__.py b/mongoengine/django/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/mongoengine/django/auth.py b/mongoengine/django/auth.py deleted file mode 100644 index 0a309c4c..00000000 --- a/mongoengine/django/auth.py +++ /dev/null @@ -1,412 +0,0 @@ -from mongoengine import * - -from django.utils.encoding import smart_str -from django.contrib.auth.models import _user_has_perm, _user_get_all_permissions, _user_has_module_perms -from django.db import models -from django.contrib.contenttypes.models import ContentTypeManager -from django.contrib import auth -from django.contrib.auth.models import AnonymousUser -from django.utils.translation import ugettext_lazy as _ - -from .utils import datetime_now - -REDIRECT_FIELD_NAME = 'next' - -try: - from django.contrib.auth.hashers import check_password, make_password -except ImportError: - """Handle older versions of Django""" - from django.utils.hashcompat import md5_constructor, sha_constructor - - def get_hexdigest(algorithm, salt, raw_password): - raw_password, salt = smart_str(raw_password), smart_str(salt) - if algorithm == 'md5': - return md5_constructor(salt + raw_password).hexdigest() - elif algorithm == 'sha1': - return sha_constructor(salt + raw_password).hexdigest() - raise ValueError('Got unknown password algorithm type in password') - - def check_password(raw_password, password): - algo, salt, hash = password.split('$') - return hash == get_hexdigest(algo, salt, raw_password) - - def make_password(raw_password): - from random import random - algo = 'sha1' - salt = get_hexdigest(algo, str(random()), str(random()))[:5] - hash = get_hexdigest(algo, salt, raw_password) - return '%s$%s$%s' % (algo, salt, hash) - - -class ContentType(Document): - name = StringField(max_length=100) - app_label = StringField(max_length=100) - model = StringField(max_length=100, verbose_name=_('python model class name'), - unique_with='app_label') - objects = ContentTypeManager() - - class Meta: - verbose_name = _('content type') - verbose_name_plural = _('content types') - # db_table = 'django_content_type' - # ordering = ('name',) - # unique_together = (('app_label', 'model'),) - - def __unicode__(self): - return self.name - - def model_class(self): - "Returns the Python model class for this type of content." - from django.db import models - return models.get_model(self.app_label, self.model) - - def get_object_for_this_type(self, **kwargs): - """ - Returns an object of this type for the keyword arguments given. - Basically, this is a proxy around this object_type's get_object() model - method. The ObjectNotExist exception, if thrown, will not be caught, - so code that calls this method should catch it. - """ - return self.model_class()._default_manager.using(self._state.db).get(**kwargs) - - def natural_key(self): - return (self.app_label, self.model) - - -class SiteProfileNotAvailable(Exception): - pass - - -class PermissionManager(models.Manager): - def get_by_natural_key(self, codename, app_label, model): - return self.get( - codename=codename, - content_type=ContentType.objects.get_by_natural_key(app_label, model) - ) - - -class Permission(Document): - """The permissions system provides a way to assign permissions to specific - users and groups of users. - - The permission system is used by the Django admin site, but may also be - useful in your own code. The Django admin site uses permissions as follows: - - - The "add" permission limits the user's ability to view the "add" - form and add an object. - - The "change" permission limits a user's ability to view the change - list, view the "change" form and change an object. - - The "delete" permission limits the ability to delete an object. - - Permissions are set globally per type of object, not per specific object - instance. It is possible to say "Mary may change news stories," but it's - not currently possible to say "Mary may change news stories, but only the - ones she created herself" or "Mary may only change news stories that have - a certain status or publication date." - - Three basic permissions -- add, change and delete -- are automatically - created for each Django model. - """ - name = StringField(max_length=50, verbose_name=_('username')) - content_type = ReferenceField(ContentType) - codename = StringField(max_length=100, verbose_name=_('codename')) - # FIXME: don't access field of the other class - # unique_with=['content_type__app_label', 'content_type__model']) - - objects = PermissionManager() - - class Meta: - verbose_name = _('permission') - verbose_name_plural = _('permissions') - # unique_together = (('content_type', 'codename'),) - # ordering = ('content_type__app_label', 'content_type__model', 'codename') - - def __unicode__(self): - return u"%s | %s | %s" % ( - unicode(self.content_type.app_label), - unicode(self.content_type), - unicode(self.name)) - - def natural_key(self): - return (self.codename,) + self.content_type.natural_key() - natural_key.dependencies = ['contenttypes.contenttype'] - - -class Group(Document): - """Groups are a generic way of categorizing users to apply permissions, - or some other label, to those users. A user can belong to any number of - groups. - - A user in a group automatically has all the permissions granted to that - group. For example, if the group Site editors has the permission - can_edit_home_page, any user in that group will have that permission. - - Beyond permissions, groups are a convenient way to categorize users to - apply some label, or extended functionality, to them. For example, you - could create a group 'Special users', and you could write code that would - do special things to those users -- such as giving them access to a - members-only portion of your site, or sending them members-only - e-mail messages. - """ - name = StringField(max_length=80, unique=True, verbose_name=_('name')) - permissions = ListField(ReferenceField(Permission, verbose_name=_('permissions'), required=False)) - - class Meta: - verbose_name = _('group') - verbose_name_plural = _('groups') - - def __unicode__(self): - return self.name - - -class UserManager(models.Manager): - def create_user(self, username, email, password=None): - """ - Creates and saves a User with the given username, e-mail and password. - """ - now = datetime_now() - - # Normalize the address by lowercasing the domain part of the email - # address. - try: - email_name, domain_part = email.strip().split('@', 1) - except ValueError: - pass - else: - email = '@'.join([email_name, domain_part.lower()]) - - user = self.model(username=username, email=email, is_staff=False, - is_active=True, is_superuser=False, last_login=now, - date_joined=now) - - user.set_password(password) - user.save(using=self._db) - return user - - def create_superuser(self, username, email, password): - u = self.create_user(username, email, password) - u.is_staff = True - u.is_active = True - u.is_superuser = True - u.save(using=self._db) - return u - - def make_random_password(self, length=10, allowed_chars='abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789'): - "Generates a random password with the given length and given allowed_chars" - # Note that default value of allowed_chars does not have "I" or letters - # that look like it -- just to avoid confusion. - from random import choice - return ''.join([choice(allowed_chars) for i in range(length)]) - - -class User(Document): - """A User document that aims to mirror most of the API specified by Django - at http://docs.djangoproject.com/en/dev/topics/auth/#users - """ - username = StringField(max_length=30, required=True, - verbose_name=_('username'), - help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters")) - - first_name = StringField(max_length=30, - verbose_name=_('first name')) - - last_name = StringField(max_length=30, - verbose_name=_('last name')) - email = EmailField(verbose_name=_('e-mail address')) - password = StringField(max_length=128, - verbose_name=_('password'), - help_text=_("Use '[algo]$[iterations]$[salt]$[hexdigest]' or use the change password form.")) - is_staff = BooleanField(default=False, - verbose_name=_('staff status'), - help_text=_("Designates whether the user can log into this admin site.")) - is_active = BooleanField(default=True, - verbose_name=_('active'), - help_text=_("Designates whether this user should be treated as active. Unselect this instead of deleting accounts.")) - is_superuser = BooleanField(default=False, - verbose_name=_('superuser status'), - help_text=_("Designates that this user has all permissions without explicitly assigning them.")) - last_login = DateTimeField(default=datetime_now, - verbose_name=_('last login')) - date_joined = DateTimeField(default=datetime_now, - verbose_name=_('date joined')) - - user_permissions = ListField(ReferenceField(Permission), verbose_name=_('user permissions'), - help_text=_('Permissions for the user.')) - - USERNAME_FIELD = 'username' - REQUIRED_FIELDS = ['email'] - - meta = { - 'allow_inheritance': True, - 'indexes': [ - {'fields': ['username'], 'unique': True, 'sparse': True} - ] - } - - def __unicode__(self): - return self.username - - def get_full_name(self): - """Returns the users first and last names, separated by a space. - """ - full_name = u'%s %s' % (self.first_name or '', self.last_name or '') - return full_name.strip() - - def is_anonymous(self): - return False - - def is_authenticated(self): - return True - - def set_password(self, raw_password): - """Sets the user's password - always use this rather than directly - assigning to :attr:`~mongoengine.django.auth.User.password` as the - password is hashed before storage. - """ - self.password = make_password(raw_password) - self.save() - return self - - def check_password(self, raw_password): - """Checks the user's password against a provided password - always use - this rather than directly comparing to - :attr:`~mongoengine.django.auth.User.password` as the password is - hashed before storage. - """ - return check_password(raw_password, self.password) - - @classmethod - def create_user(cls, username, password, email=None): - """Create (and save) a new user with the given username, password and - email address. - """ - now = datetime_now() - - # Normalize the address by lowercasing the domain part of the email - # address. - if email is not None: - try: - email_name, domain_part = email.strip().split('@', 1) - except ValueError: - pass - else: - email = '@'.join([email_name, domain_part.lower()]) - - user = cls(username=username, email=email, date_joined=now) - user.set_password(password) - user.save() - return user - - def get_group_permissions(self, obj=None): - """ - Returns a list of permission strings that this user has through his/her - groups. This method queries all available auth backends. If an object - is passed in, only permissions matching this object are returned. - """ - permissions = set() - for backend in auth.get_backends(): - if hasattr(backend, "get_group_permissions"): - permissions.update(backend.get_group_permissions(self, obj)) - return permissions - - def get_all_permissions(self, obj=None): - return _user_get_all_permissions(self, obj) - - def has_perm(self, perm, obj=None): - """ - Returns True if the user has the specified permission. This method - queries all available auth backends, but returns immediately if any - backend returns True. Thus, a user who has permission from a single - auth backend is assumed to have permission in general. If an object is - provided, permissions for this specific object are checked. - """ - - # Active superusers have all permissions. - if self.is_active and self.is_superuser: - return True - - # Otherwise we need to check the backends. - return _user_has_perm(self, perm, obj) - - def has_module_perms(self, app_label): - """ - Returns True if the user has any permissions in the given app label. - Uses pretty much the same logic as has_perm, above. - """ - # Active superusers have all permissions. - if self.is_active and self.is_superuser: - return True - - return _user_has_module_perms(self, app_label) - - def email_user(self, subject, message, from_email=None): - "Sends an e-mail to this User." - from django.core.mail import send_mail - send_mail(subject, message, from_email, [self.email]) - - def get_profile(self): - """ - Returns site-specific profile for this user. Raises - SiteProfileNotAvailable if this site does not allow profiles. - """ - if not hasattr(self, '_profile_cache'): - from django.conf import settings - if not getattr(settings, 'AUTH_PROFILE_MODULE', False): - raise SiteProfileNotAvailable('You need to set AUTH_PROFILE_MO' - 'DULE in your project settings') - try: - app_label, model_name = settings.AUTH_PROFILE_MODULE.split('.') - except ValueError: - raise SiteProfileNotAvailable('app_label and model_name should' - ' be separated by a dot in the AUTH_PROFILE_MODULE set' - 'ting') - - try: - model = models.get_model(app_label, model_name) - if model is None: - raise SiteProfileNotAvailable('Unable to load the profile ' - 'model, check AUTH_PROFILE_MODULE in your project sett' - 'ings') - self._profile_cache = model._default_manager.using(self._state.db).get(user__id__exact=self.id) - self._profile_cache.user = self - except (ImportError, ImproperlyConfigured): - raise SiteProfileNotAvailable - return self._profile_cache - - -class MongoEngineBackend(object): - """Authenticate using MongoEngine and mongoengine.django.auth.User. - """ - - supports_object_permissions = False - supports_anonymous_user = False - supports_inactive_user = False - _user_doc = False - - def authenticate(self, username=None, password=None): - user = self.user_document.objects(username=username).first() - if user: - if password and user.check_password(password): - backend = auth.get_backends()[0] - user.backend = "%s.%s" % (backend.__module__, backend.__class__.__name__) - return user - return None - - def get_user(self, user_id): - return self.user_document.objects.with_id(user_id) - - @property - def user_document(self): - if self._user_doc is False: - from .mongo_auth.models import get_user_document - self._user_doc = get_user_document() - return self._user_doc - -def get_user(userid): - """Returns a User object from an id (User.id). Django's equivalent takes - request, but taking an id instead leaves it up to the developer to store - the id in any way they want (session, signed cookie, etc.) - """ - if not userid: - return AnonymousUser() - return MongoEngineBackend().get_user(userid) or AnonymousUser() diff --git a/mongoengine/django/mongo_auth/__init__.py b/mongoengine/django/mongo_auth/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/mongoengine/django/mongo_auth/models.py b/mongoengine/django/mongo_auth/models.py deleted file mode 100644 index ad4ceff8..00000000 --- a/mongoengine/django/mongo_auth/models.py +++ /dev/null @@ -1,119 +0,0 @@ -from django.conf import settings -from django.contrib.auth.hashers import make_password -from django.contrib.auth.models import UserManager -from django.core.exceptions import ImproperlyConfigured -from django.db import models -try: - from django.utils.module_loading import import_module -except ImportError: - """Handle older versions of Django""" - from django.utils.importlib import import_module -from django.utils.translation import ugettext_lazy as _ - - -__all__ = ( - 'get_user_document', -) - - -MONGOENGINE_USER_DOCUMENT = getattr( - settings, 'MONGOENGINE_USER_DOCUMENT', 'mongoengine.django.auth.User') - - -def get_user_document(): - """Get the user document class used for authentication. - - This is the class defined in settings.MONGOENGINE_USER_DOCUMENT, which - defaults to `mongoengine.django.auth.User`. - - """ - - name = MONGOENGINE_USER_DOCUMENT - dot = name.rindex('.') - module = import_module(name[:dot]) - return getattr(module, name[dot + 1:]) - - -class MongoUserManager(UserManager): - """A User manager wich allows the use of MongoEngine documents in Django. - - To use the manager, you must tell django.contrib.auth to use MongoUser as - the user model. In you settings.py, you need: - - INSTALLED_APPS = ( - ... - 'django.contrib.auth', - 'mongoengine.django.mongo_auth', - ... - ) - AUTH_USER_MODEL = 'mongo_auth.MongoUser' - - Django will use the model object to access the custom Manager, which will - replace the original queryset with MongoEngine querysets. - - By default, mongoengine.django.auth.User will be used to store users. You - can specify another document class in MONGOENGINE_USER_DOCUMENT in your - settings.py. - - The User Document class has the same requirements as a standard custom user - model: https://docs.djangoproject.com/en/dev/topics/auth/customizing/ - - In particular, the User Document class must define USERNAME_FIELD and - REQUIRED_FIELDS. - - `AUTH_USER_MODEL` has been added in Django 1.5. - - """ - - def contribute_to_class(self, model, name): - super(MongoUserManager, self).contribute_to_class(model, name) - self.dj_model = self.model - self.model = get_user_document() - - self.dj_model.USERNAME_FIELD = self.model.USERNAME_FIELD - username = models.CharField(_('username'), max_length=30, unique=True) - username.contribute_to_class(self.dj_model, self.dj_model.USERNAME_FIELD) - - self.dj_model.REQUIRED_FIELDS = self.model.REQUIRED_FIELDS - for name in self.dj_model.REQUIRED_FIELDS: - field = models.CharField(_(name), max_length=30) - field.contribute_to_class(self.dj_model, name) - - - def get(self, *args, **kwargs): - try: - return self.get_query_set().get(*args, **kwargs) - except self.model.DoesNotExist: - # ModelBackend expects this exception - raise self.dj_model.DoesNotExist - - @property - def db(self): - raise NotImplementedError - - def get_empty_query_set(self): - return self.model.objects.none() - - def get_query_set(self): - return self.model.objects - - -class MongoUser(models.Model): - """"Dummy user model for Django. - - MongoUser is used to replace Django's UserManager with MongoUserManager. - The actual user document class is mongoengine.django.auth.User or any - other document class specified in MONGOENGINE_USER_DOCUMENT. - - To get the user document class, use `get_user_document()`. - - """ - - objects = MongoUserManager() - - class Meta: - app_label = 'mongo_auth' - - def set_password(self, password): - """Doesn't do anything, but works around the issue with Django 1.6.""" - make_password(password) diff --git a/mongoengine/django/sessions.py b/mongoengine/django/sessions.py deleted file mode 100644 index f260951b..00000000 --- a/mongoengine/django/sessions.py +++ /dev/null @@ -1,124 +0,0 @@ -from bson import json_util -from django.conf import settings -from django.contrib.sessions.backends.base import SessionBase, CreateError -from django.core.exceptions import SuspiciousOperation -try: - from django.utils.encoding import force_unicode -except ImportError: - from django.utils.encoding import force_text as force_unicode - -from mongoengine.document import Document -from mongoengine import fields -from mongoengine.queryset import OperationError -from mongoengine.connection import DEFAULT_CONNECTION_NAME - -from .utils import datetime_now - - -MONGOENGINE_SESSION_DB_ALIAS = getattr( - settings, 'MONGOENGINE_SESSION_DB_ALIAS', - DEFAULT_CONNECTION_NAME) - -# a setting for the name of the collection used to store sessions -MONGOENGINE_SESSION_COLLECTION = getattr( - settings, 'MONGOENGINE_SESSION_COLLECTION', - 'django_session') - -# a setting for whether session data is stored encoded or not -MONGOENGINE_SESSION_DATA_ENCODE = getattr( - settings, 'MONGOENGINE_SESSION_DATA_ENCODE', - True) - - -class MongoSession(Document): - session_key = fields.StringField(primary_key=True, max_length=40) - session_data = fields.StringField() if MONGOENGINE_SESSION_DATA_ENCODE \ - else fields.DictField() - expire_date = fields.DateTimeField() - - meta = { - 'collection': MONGOENGINE_SESSION_COLLECTION, - 'db_alias': MONGOENGINE_SESSION_DB_ALIAS, - 'allow_inheritance': False, - 'indexes': [ - { - 'fields': ['expire_date'], - 'expireAfterSeconds': 0 - } - ] - } - - def get_decoded(self): - return SessionStore().decode(self.session_data) - - -class SessionStore(SessionBase): - """A MongoEngine-based session store for Django. - """ - - def _get_session(self, *args, **kwargs): - sess = super(SessionStore, self)._get_session(*args, **kwargs) - if sess.get('_auth_user_id', None): - sess['_auth_user_id'] = str(sess.get('_auth_user_id')) - return sess - - def load(self): - try: - s = MongoSession.objects(session_key=self.session_key, - expire_date__gt=datetime_now)[0] - if MONGOENGINE_SESSION_DATA_ENCODE: - return self.decode(force_unicode(s.session_data)) - else: - return s.session_data - except (IndexError, SuspiciousOperation): - self.create() - return {} - - def exists(self, session_key): - return bool(MongoSession.objects(session_key=session_key).first()) - - def create(self): - while True: - self._session_key = self._get_new_session_key() - try: - self.save(must_create=True) - except CreateError: - continue - self.modified = True - self._session_cache = {} - return - - def save(self, must_create=False): - if self.session_key is None: - self._session_key = self._get_new_session_key() - s = MongoSession(session_key=self.session_key) - if MONGOENGINE_SESSION_DATA_ENCODE: - s.session_data = self.encode(self._get_session(no_load=must_create)) - else: - s.session_data = self._get_session(no_load=must_create) - s.expire_date = self.get_expiry_date() - try: - s.save(force_insert=must_create) - except OperationError: - if must_create: - raise CreateError - raise - - def delete(self, session_key=None): - if session_key is None: - if self.session_key is None: - return - session_key = self.session_key - MongoSession.objects(session_key=session_key).delete() - - -class BSONSerializer(object): - """ - Serializer that can handle BSON types (eg ObjectId). - """ - def dumps(self, obj): - return json_util.dumps(obj, separators=(',', ':')).encode('ascii') - - def loads(self, data): - return json_util.loads(data.decode('ascii')) - diff --git a/mongoengine/django/shortcuts.py b/mongoengine/django/shortcuts.py deleted file mode 100644 index 9cc8370b..00000000 --- a/mongoengine/django/shortcuts.py +++ /dev/null @@ -1,47 +0,0 @@ -from mongoengine.queryset import QuerySet -from mongoengine.base import BaseDocument -from mongoengine.errors import ValidationError - -def _get_queryset(cls): - """Inspired by django.shortcuts.*""" - if isinstance(cls, QuerySet): - return cls - else: - return cls.objects - -def get_document_or_404(cls, *args, **kwargs): - """ - Uses get() to return an document, or raises a Http404 exception if the document - does not exist. - - cls may be a Document or QuerySet object. All other passed - arguments and keyword arguments are used in the get() query. - - Note: Like with get(), an MultipleObjectsReturned will be raised if more than one - object is found. - - Inspired by django.shortcuts.* - """ - queryset = _get_queryset(cls) - try: - return queryset.get(*args, **kwargs) - except (queryset._document.DoesNotExist, ValidationError): - from django.http import Http404 - raise Http404('No %s matches the given query.' % queryset._document._class_name) - -def get_list_or_404(cls, *args, **kwargs): - """ - Uses filter() to return a list of documents, or raise a Http404 exception if - the list is empty. - - cls may be a Document or QuerySet object. All other passed - arguments and keyword arguments are used in the filter() query. - - Inspired by django.shortcuts.* - """ - queryset = _get_queryset(cls) - obj_list = list(queryset.filter(*args, **kwargs)) - if not obj_list: - from django.http import Http404 - raise Http404('No %s matches the given query.' % queryset._document._class_name) - return obj_list diff --git a/mongoengine/django/storage.py b/mongoengine/django/storage.py deleted file mode 100644 index 9df6f9e8..00000000 --- a/mongoengine/django/storage.py +++ /dev/null @@ -1,112 +0,0 @@ -import os -import itertools -import urlparse - -from mongoengine import * -from django.conf import settings -from django.core.files.storage import Storage -from django.core.exceptions import ImproperlyConfigured - - -class FileDocument(Document): - """A document used to store a single file in GridFS. - """ - file = FileField() - - -class GridFSStorage(Storage): - """A custom storage backend to store files in GridFS - """ - - def __init__(self, base_url=None): - - if base_url is None: - base_url = settings.MEDIA_URL - self.base_url = base_url - self.document = FileDocument - self.field = 'file' - - def delete(self, name): - """Deletes the specified file from the storage system. - """ - if self.exists(name): - doc = self.document.objects.first() - field = getattr(doc, self.field) - self._get_doc_with_name(name).delete() # Delete the FileField - field.delete() # Delete the FileDocument - - def exists(self, name): - """Returns True if a file referened by the given name already exists in the - storage system, or False if the name is available for a new file. - """ - doc = self._get_doc_with_name(name) - if doc: - field = getattr(doc, self.field) - return bool(field.name) - else: - return False - - def listdir(self, path=None): - """Lists the contents of the specified path, returning a 2-tuple of lists; - the first item being directories, the second item being files. - """ - def name(doc): - return getattr(doc, self.field).name - docs = self.document.objects - return [], [name(d) for d in docs if name(d)] - - def size(self, name): - """Returns the total size, in bytes, of the file specified by name. - """ - doc = self._get_doc_with_name(name) - if doc: - return getattr(doc, self.field).length - else: - raise ValueError("No such file or directory: '%s'" % name) - - def url(self, name): - """Returns an absolute URL where the file's contents can be accessed - directly by a web browser. - """ - if self.base_url is None: - raise ValueError("This file is not accessible via a URL.") - return urlparse.urljoin(self.base_url, name).replace('\\', '/') - - def _get_doc_with_name(self, name): - """Find the documents in the store with the given name - """ - docs = self.document.objects - doc = [d for d in docs if hasattr(getattr(d, self.field), 'name') and getattr(d, self.field).name == name] - if doc: - return doc[0] - else: - return None - - def _open(self, name, mode='rb'): - doc = self._get_doc_with_name(name) - if doc: - return getattr(doc, self.field) - else: - raise ValueError("No file found with the name '%s'." % name) - - def get_available_name(self, name): - """Returns a filename that's free on the target storage system, and - available for new content to be written to. - """ - file_root, file_ext = os.path.splitext(name) - # If the filename already exists, add an underscore and a number (before - # the file extension, if one exists) to the filename until the generated - # filename doesn't exist. - count = itertools.count(1) - while self.exists(name): - # file_ext includes the dot. - name = os.path.join("%s_%s%s" % (file_root, count.next(), file_ext)) - - return name - - def _save(self, name, content): - doc = self.document() - getattr(doc, self.field).put(content, filename=name) - doc.save() - - return name diff --git a/mongoengine/django/tests.py b/mongoengine/django/tests.py deleted file mode 100644 index b130acc8..00000000 --- a/mongoengine/django/tests.py +++ /dev/null @@ -1,31 +0,0 @@ -#coding: utf-8 - -from unittest import TestCase - -from mongoengine import connect -from mongoengine.connection import get_db - - -class MongoTestCase(TestCase): - """ - TestCase class that clear the collection between the tests - """ - - @property - def db_name(self): - from django.conf import settings - return 'test_%s' % getattr(settings, 'MONGO_DATABASE_NAME', 'dummy') - - def __init__(self, methodName='runtest'): - connect(self.db_name) - self.db = get_db() - super(MongoTestCase, self).__init__(methodName) - - def dropCollections(self): - for collection in self.db.collection_names(): - if collection.startswith('system.'): - continue - self.db.drop_collection(collection) - - def tearDown(self): - self.dropCollections() diff --git a/mongoengine/django/utils.py b/mongoengine/django/utils.py deleted file mode 100644 index d3ef8a4b..00000000 --- a/mongoengine/django/utils.py +++ /dev/null @@ -1,6 +0,0 @@ -try: - # django >= 1.4 - from django.utils.timezone import now as datetime_now -except ImportError: - from datetime import datetime - datetime_now = datetime.now diff --git a/setup.py b/setup.py index 88519788..3ae5aeac 100644 --- a/setup.py +++ b/setup.py @@ -53,12 +53,12 @@ CLASSIFIERS = [ extra_opts = {"packages": find_packages(exclude=["tests", "tests.*"])} if sys.version_info[0] == 3: extra_opts['use_2to3'] = True - extra_opts['tests_require'] = ['nose', 'coverage', 'blinker', 'jinja2==2.6', 'Pillow>=2.0.0', 'django>=1.5.1'] + extra_opts['tests_require'] = ['nose', 'coverage', 'blinker', 'Pillow>=2.0.0'] if "test" in sys.argv or "nosetests" in sys.argv: extra_opts['packages'] = find_packages() extra_opts['package_data'] = {"tests": ["fields/mongoengine.png", "fields/mongodb_leaf.png"]} else: - extra_opts['tests_require'] = ['nose', 'coverage', 'blinker', 'django>=1.4.2', 'Pillow>=2.0.0', 'jinja2>=2.6', 'python-dateutil'] + extra_opts['tests_require'] = ['nose', 'coverage', 'blinker', 'Pillow>=2.0.0', 'python-dateutil'] if sys.version_info[0] == 2 and sys.version_info[1] == 6: extra_opts['tests_require'].append('unittest2') diff --git a/tests/test_django.py b/tests/test_django.py deleted file mode 100644 index cb7efe6f..00000000 --- a/tests/test_django.py +++ /dev/null @@ -1,330 +0,0 @@ -import sys -sys.path[0:0] = [""] -import unittest -from nose.plugins.skip import SkipTest - -from mongoengine import * -from mongoengine.django.shortcuts import get_document_or_404 - -import django -from django.http import Http404 -from django.template import Context, Template -from django.conf import settings -from django.core.paginator import Paginator - -settings.configure( - USE_TZ=True, - INSTALLED_APPS=('django.contrib.auth', 'mongoengine.django.mongo_auth'), - AUTH_USER_MODEL=('mongo_auth.MongoUser'), - AUTHENTICATION_BACKENDS = ('mongoengine.django.auth.MongoEngineBackend',) -) - -try: - # For Django >= 1.7 - if hasattr(django, 'setup'): - django.setup() -except RuntimeError: - pass - -try: - from django.contrib.auth import authenticate, get_user_model - from mongoengine.django.auth import User - from mongoengine.django.mongo_auth.models import ( - MongoUser, - MongoUserManager, - get_user_document, - ) - DJ15 = True -except Exception: - DJ15 = False -from mongoengine.django.sessions import SessionStore, MongoSession -from mongoengine.django.tests import MongoTestCase -from datetime import tzinfo, timedelta -ZERO = timedelta(0) - - -class FixedOffset(tzinfo): - """Fixed offset in minutes east from UTC.""" - - def __init__(self, offset, name): - self.__offset = timedelta(minutes=offset) - self.__name = name - - def utcoffset(self, dt): - return self.__offset - - def tzname(self, dt): - return self.__name - - def dst(self, dt): - return ZERO - - -def activate_timezone(tz): - """Activate Django timezone support if it is available. - """ - try: - from django.utils import timezone - timezone.deactivate() - timezone.activate(tz) - except ImportError: - pass - - -class QuerySetTest(unittest.TestCase): - - def setUp(self): - connect(db='mongoenginetest') - - class Person(Document): - name = StringField() - age = IntField() - self.Person = Person - - def test_order_by_in_django_template(self): - """Ensure that QuerySets are properly ordered in Django template. - """ - self.Person.drop_collection() - - self.Person(name="A", age=20).save() - self.Person(name="D", age=10).save() - self.Person(name="B", age=40).save() - self.Person(name="C", age=30).save() - - t = Template("{% for o in ol %}{{ o.name }}-{{ o.age }}:{% endfor %}") - - d = {"ol": self.Person.objects.order_by('-name')} - self.assertEqual(t.render(Context(d)), u'D-10:C-30:B-40:A-20:') - d = {"ol": self.Person.objects.order_by('+name')} - self.assertEqual(t.render(Context(d)), u'A-20:B-40:C-30:D-10:') - d = {"ol": self.Person.objects.order_by('-age')} - self.assertEqual(t.render(Context(d)), u'B-40:C-30:A-20:D-10:') - d = {"ol": self.Person.objects.order_by('+age')} - self.assertEqual(t.render(Context(d)), u'D-10:A-20:C-30:B-40:') - - self.Person.drop_collection() - - def test_q_object_filter_in_template(self): - - self.Person.drop_collection() - - self.Person(name="A", age=20).save() - self.Person(name="D", age=10).save() - self.Person(name="B", age=40).save() - self.Person(name="C", age=30).save() - - t = Template("{% for o in ol %}{{ o.name }}-{{ o.age }}:{% endfor %}") - - d = {"ol": self.Person.objects.filter(Q(age=10) | Q(name="C"))} - self.assertEqual(t.render(Context(d)), 'D-10:C-30:') - - # Check double rendering doesn't throw an error - self.assertEqual(t.render(Context(d)), 'D-10:C-30:') - - def test_get_document_or_404(self): - p = self.Person(name="G404") - p.save() - - self.assertRaises(Http404, get_document_or_404, self.Person, pk='1234') - self.assertEqual(p, get_document_or_404(self.Person, pk=p.pk)) - - def test_pagination(self): - """Ensure that Pagination works as expected - """ - class Page(Document): - name = StringField() - - Page.drop_collection() - - for i in xrange(1, 11): - Page(name=str(i)).save() - - paginator = Paginator(Page.objects.all(), 2) - - t = Template("{% for i in page.object_list %}{{ i.name }}:{% endfor %}") - for p in paginator.page_range: - d = {"page": paginator.page(p)} - end = p * 2 - start = end - 1 - self.assertEqual(t.render(Context(d)), u'%d:%d:' % (start, end)) - - def test_nested_queryset_template_iterator(self): - # Try iterating the same queryset twice, nested, in a Django template. - names = ['A', 'B', 'C', 'D'] - - class CustomUser(Document): - name = StringField() - - def __unicode__(self): - return self.name - - CustomUser.drop_collection() - - for name in names: - CustomUser(name=name).save() - - users = CustomUser.objects.all().order_by('name') - template = Template("{% for user in users %}{{ user.name }}{% ifequal forloop.counter 2 %} {% for inner_user in users %}{{ inner_user.name }}{% endfor %} {% endifequal %}{% endfor %}") - rendered = template.render(Context({'users': users})) - self.assertEqual(rendered, 'AB ABCD CD') - - def test_filter(self): - """Ensure that a queryset and filters work as expected - """ - - class LimitCountQuerySet(QuerySet): - def count(self, with_limit_and_skip=True): - return super(LimitCountQuerySet, self).count(with_limit_and_skip) - - class Note(Document): - meta = dict(queryset_class=LimitCountQuerySet) - name = StringField() - - Note.drop_collection() - - for i in xrange(1, 101): - Note(name="Note: %s" % i).save() - - # Check the count - self.assertEqual(Note.objects.count(), 100) - - # Get the first 10 and confirm - notes = Note.objects[:10] - self.assertEqual(notes.count(), 10) - - # Test djangos template filters - # self.assertEqual(length(notes), 10) - t = Template("{{ notes.count }}") - c = Context({"notes": notes}) - self.assertEqual(t.render(c), "10") - - # Test with skip - notes = Note.objects.skip(90) - self.assertEqual(notes.count(), 10) - - # Test djangos template filters - self.assertEqual(notes.count(), 10) - t = Template("{{ notes.count }}") - c = Context({"notes": notes}) - self.assertEqual(t.render(c), "10") - - # Test with limit - notes = Note.objects.skip(90) - self.assertEqual(notes.count(), 10) - - # Test djangos template filters - self.assertEqual(notes.count(), 10) - t = Template("{{ notes.count }}") - c = Context({"notes": notes}) - self.assertEqual(t.render(c), "10") - - # Test with skip and limit - notes = Note.objects.skip(10).limit(10) - - # Test djangos template filters - self.assertEqual(notes.count(), 10) - t = Template("{{ notes.count }}") - c = Context({"notes": notes}) - self.assertEqual(t.render(c), "10") - - -class _BaseMongoDBSessionTest(unittest.TestCase): - backend = SessionStore - - def setUp(self): - connect(db='mongoenginetest') - MongoSession.drop_collection() - super(_BaseMongoDBSessionTest, self).setUp() - - def assertIn(self, first, second, msg=None): - self.assertTrue(first in second, msg) - - def assertNotIn(self, first, second, msg=None): - self.assertFalse(first in second, msg) - - def test_first_save(self): - session = SessionStore() - session['test'] = True - session.save() - self.assertTrue('test' in session) - - def test_session_expiration_tz(self): - activate_timezone(FixedOffset(60, 'UTC+1')) - # create and save new session - session = SessionStore() - session.set_expiry(600) # expire in 600 seconds - session['test_expire'] = True - session.save() - # reload session with key - key = session.session_key - session = SessionStore(key) - self.assertTrue('test_expire' in session, 'Session has expired before it is expected') - - -try: - # SessionTestsMixin isn't available for import on django > 1.8a1 - from django.contrib.sessions.tests import SessionTestsMixin - - class _MongoDBSessionTest(SessionTestsMixin): - pass - - class MongoDBSessionTest(_BaseMongoDBSessionTest): - pass - -except ImportError: - class MongoDBSessionTest(_BaseMongoDBSessionTest): - pass - - -class MongoAuthTest(unittest.TestCase): - user_data = { - 'username': 'user', - 'email': 'user@example.com', - 'password': 'test', - } - - def setUp(self): - if not DJ15: - raise SkipTest('mongo_auth requires Django 1.5') - connect(db='mongoenginetest') - User.drop_collection() - super(MongoAuthTest, self).setUp() - - def test_get_user_model(self): - self.assertEqual(get_user_model(), MongoUser) - - def test_get_user_document(self): - self.assertEqual(get_user_document(), User) - - def test_user_manager(self): - manager = get_user_model()._default_manager - self.assertTrue(isinstance(manager, MongoUserManager)) - - def test_user_manager_exception(self): - manager = get_user_model()._default_manager - self.assertRaises(MongoUser.DoesNotExist, manager.get, - username='not found') - - def test_create_user(self): - manager = get_user_model()._default_manager - user = manager.create_user(**self.user_data) - self.assertTrue(isinstance(user, User)) - db_user = User.objects.get(username='user') - self.assertEqual(user.id, db_user.id) - - def test_authenticate(self): - get_user_model()._default_manager.create_user(**self.user_data) - user = authenticate(username='user', password='fail') - self.assertEqual(None, user) - user = authenticate(username='user', password='test') - db_user = User.objects.get(username='user') - self.assertEqual(user.id, db_user.id) - - -class MongoTestCaseTest(MongoTestCase): - def test_mongo_test_case(self): - self.db.dummy_collection.insert({'collection': 'will be dropped'}) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_jinja.py b/tests/test_jinja.py deleted file mode 100644 index 0449f868..00000000 --- a/tests/test_jinja.py +++ /dev/null @@ -1,47 +0,0 @@ -import sys -sys.path[0:0] = [""] - -import unittest - -from mongoengine import * - -import jinja2 - - -class TemplateFilterTest(unittest.TestCase): - - def setUp(self): - connect(db='mongoenginetest') - - def test_jinja2(self): - env = jinja2.Environment() - - class TestData(Document): - title = StringField() - description = StringField() - - TestData.drop_collection() - - examples = [('A', '1'), - ('B', '2'), - ('C', '3')] - - for title, description in examples: - TestData(title=title, description=description).save() - - tmpl = """ -{%- for record in content -%} - {%- if loop.first -%}{ {%- endif -%} - "{{ record.title }}": "{{ record.description }}" - {%- if loop.last -%} }{%- else -%},{% endif -%} -{%- endfor -%} -""" - ctx = {'content': TestData.objects} - template = env.from_string(tmpl) - rendered = template.render(**ctx) - - self.assertEqual('{"A": "1","B": "2","C": "3"}', rendered) - - -if __name__ == '__main__': - unittest.main() From 1a0cad7f5f0c661f4fdf2ae7cd21ab756c2cf428 Mon Sep 17 00:00:00 2001 From: Matthew Ellison Date: Tue, 28 Apr 2015 10:02:39 -0400 Subject: [PATCH 2/2] Updated Django Support Documentation Added "Call to Arms" for new Django Extension. --- docs/django.rst | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/django.rst b/docs/django.rst index 4b9a04bd..6982fe64 100644 --- a/docs/django.rst +++ b/docs/django.rst @@ -2,5 +2,18 @@ Django Support ============== -.. note:: Django support has been split from MongoEngine and will be available on Github as a separate module. `See Here `_. +.. note:: Django support has been split from the main MongoEngine + repository. The *legacy* Django extension may be found bundled with the + 0.9 release of MongoEngine. + + +Help Wanted! +------------ + +The MongoEngine team is looking for help contributing and maintaining a new +Django extension for MongoEngine! If you have Django experience and would like +to help contribute to the project, please get in touch on the +`mailing list `_ or by +simpily contributing on +`GitHub `_.