diff --git a/.travis.yml b/.travis.yml index abc9157..94efbf6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,11 +6,17 @@ python: - "3.5" - "pypy" env: - - DJANGO_VERSION=1.8.18 - - DJANGO_VERSION=1.11.10 + - DJANGO="Django>=1.11,<2.0" + - DJANGO="Django>=2.0,<2.2" +matrix: + exclude: + - python: "2.7" + env: DJANGO="Django>=2.0,<2.2" + - python: "pypy" + env: DJANGO="Django>=2.0,<2.2" # command to install dependencies install: - pip install -r requirements.txt - - pip install -q Django==$DJANGO_VERSION + - pip install -q $DJANGO # command to run tests script: python setup.py -q nosetests diff --git a/README.rst b/README.rst index 549d1cd..63507a0 100644 --- a/README.rst +++ b/README.rst @@ -8,10 +8,17 @@ Django-MongoEngine THIS IS UNSTABLE PROJECT, IF YOU WANT TO USE IT - FIX WHAT YOU NEED -Right now we're targeting to get things working on Django 1.9 +Right now we're targeting to get things working on Django 1.11; +2.0 support added, but not tested in production. -Working / Django 1.9 --------------------- +WARNING: +-------- +Maybe there is better option for mongo support, take a look at https://nesdis.github.io/djongo/; +It's python3 only and i have not tried it yet, but looks promising. + + +Working / Django 1.11 +--------------------- * [ok] sessions * [ok] models/fields, fields needs testing diff --git a/django_mongoengine/document.py b/django_mongoengine/document.py index 4af4752..57ce77a 100644 --- a/django_mongoengine/document.py +++ b/django_mongoengine/document.py @@ -38,7 +38,8 @@ class DjangoFlavor(object): _get_pk_val = Model.__dict__["_get_pk_val"] def __init__(self, *args, **kwargs): - self._state = ModelState(self._meta.get("db_alias", me.DEFAULT_CONNECTION_NAME)) + self._state = ModelState() + self._state.db = self._meta.get("db_alias", me.DEFAULT_CONNECTION_NAME) super(DjangoFlavor, self).__init__(*args, **kwargs) def _get_unique_checks(self, exclude=None): @@ -52,14 +53,17 @@ class Document(django_meta(mtc.TopLevelDocumentMetaclass, DjangoFlavor, me.Document)): swap_base = True + class DynamicDocument(django_meta(mtc.TopLevelDocumentMetaclass, DjangoFlavor, me.DynamicDocument)): swap_base = True + class EmbeddedDocument(django_meta(mtc.DocumentMetaclass, DjangoFlavor, me.EmbeddedDocument)): swap_base = True + class DynamicEmbeddedDocument(django_meta(mtc.DocumentMetaclass, - DjangoFlavor, me.DynamicEmbeddedDocument)): + DjangoFlavor, me.DynamicEmbeddedDocument)): swap_base = True diff --git a/django_mongoengine/fields/__init__.py b/django_mongoengine/fields/__init__.py index a392df9..ae7ac2f 100644 --- a/django_mongoengine/fields/__init__.py +++ b/django_mongoengine/fields/__init__.py @@ -34,6 +34,9 @@ def patch_mongoengine_field(field_name): for k in ["__eq__", "__lt__", "__hash__", "attname"]: if k not in field.__dict__: setattr(field, k, djangoflavor.DjangoField.__dict__[k]) + # set auto_created False for check in django db model when delete + if field_name == "ObjectIdField": + setattr(field, "auto_created", False) for f in ["StringField", "ObjectIdField"]: diff --git a/django_mongoengine/fields/djangoflavor.py b/django_mongoengine/fields/djangoflavor.py index 6906c17..014bee0 100644 --- a/django_mongoengine/fields/djangoflavor.py +++ b/django_mongoengine/fields/djangoflavor.py @@ -28,6 +28,8 @@ def __init__(self, *args, **kwargs): if "required" in kwargs: raise ImproperlyConfigured("`required` option is not supported. Use Django-style `blank` instead.") kwargs["required"] = not kwargs["blank"] + if hasattr(self, "auto_created"): + kwargs.pop("auto_created") self.verbose_name = kwargs.pop("verbose_name", None) super(DjangoField, self).__init__(*args, **kwargs) self.remote_field = None diff --git a/django_mongoengine/mongo_admin/options.py b/django_mongoengine/mongo_admin/options.py index 0655e31..d857481 100644 --- a/django_mongoengine/mongo_admin/options.py +++ b/django_mongoengine/mongo_admin/options.py @@ -3,7 +3,7 @@ from django import forms from django.forms.formsets import all_valid -from django.core.urlresolvers import reverse +from django.urls import reverse from django.contrib.admin.exceptions import DisallowedModelAdminToField from django.contrib.admin import widgets, helpers from django.contrib.admin.utils import ( @@ -54,7 +54,7 @@ def get_content_type_for_model(obj): get_content_type_for_model=get_content_type_for_model, ) -class BaseDocumentAdmin(djmod.BaseModelAdmin): +class BaseDocumentAdmin(djmod.ModelAdmin): """Functionality common to both ModelAdmin and InlineAdmin.""" form = DocumentForm @@ -62,7 +62,6 @@ def formfield_for_dbfield(self, db_field, **kwargs): """ Hook for specifying the form Field instance for a given database Field instance. - If kwargs are given, they're passed to the form Field's constructor. """ request = kwargs.pop("request", None) @@ -162,10 +161,11 @@ def __init__(self, model, admin_site): self.model = model self.opts = model._meta self.admin_site = admin_site - super(DocumentAdmin, self).__init__() + super(DocumentAdmin, self).__init__(model, admin_site) self.log = not settings.DATABASES.get('default', {}).get( 'ENGINE', 'django.db.backends.dummy' ).endswith('dummy') + self.change_list_template = 'admin/change_document_list.html' # XXX: add inline init somewhere def _get_inline_instances(self): @@ -231,11 +231,17 @@ def get_changelist_formset(self, request, **kwargs): fields=self.list_editable, **defaults ) + def get_changelist(self, request, **kwargs): + """ + Returns the ChangeList class for use on the changelist page. + """ + from django_mongoengine.mongo_admin.views import DocumentChangeList + return DocumentChangeList + def log_addition(self, request, object, message): """ Log that an object has been successfully added. - The default implementation creates an admin LogEntry object. """ if not self.log: @@ -245,7 +251,6 @@ def log_addition(self, request, object, message): def log_change(self, request, object, message): """ Log that an object has been successfully changed. - The default implementation creates an admin LogEntry object. """ if not self.log: @@ -256,13 +261,16 @@ def log_deletion(self, request, object, object_repr): """ Log that an object will be deleted. Note that this method is called before the deletion. - The default implementation creates an admin LogEntry object. """ if not self.log: return super(DocumentAdmin, self).log_deletion(request, object, object_repr) + @property + def media(self): + return djmod.ModelAdmin.media.fget(self) + @csrf_protect_m def changeform_view(self, request, object_id=None, form_url='', extra_context=None): @@ -471,41 +479,10 @@ def history_view(self, request, object_id, extra_context=None): "admin/object_history.html" ], context) - def get_search_results(self, request, queryset, search_term): - """ - Returns a tuple containing a queryset to implement the search, - and a boolean indicating if the results may contain duplicates. - """ - # Apply keyword searches. - def construct_search(field_name): - if field_name.startswith('^'): - return "%s__istartswith" % field_name[1:] - elif field_name.startswith('='): - return "%s__iexact" % field_name[1:] - elif field_name.startswith('@'): - return "%s__search" % field_name[1:] - else: - return "%s__icontains" % field_name - - use_distinct = False - search_fields = self.get_search_fields(request) - if search_fields and search_term: - orm_lookups = [construct_search(str(search_field)) - for search_field in search_fields] - for bit in search_term.split(): - or_queries = [Q(**{orm_lookup: bit}) - for orm_lookup in orm_lookups] - queryset = queryset.filter(reduce(operator.or_, or_queries)) - - return queryset, use_distinct - - media = djmod.ModelAdmin.media - class InlineDocumentAdmin(BaseDocumentAdmin): """ Options for inline editing of ``model`` instances. - Provide ``name`` to specify the attribute name of the ``ForeignKey`` from ``model`` to its parent. This is required if ``model`` has more than one ``ForeignKey`` to its parent. diff --git a/django_mongoengine/mongo_admin/views.py b/django_mongoengine/mongo_admin/views.py index 9dc0014..73594df 100644 --- a/django_mongoengine/mongo_admin/views.py +++ b/django_mongoengine/mongo_admin/views.py @@ -9,27 +9,26 @@ from django.utils.encoding import smart_str from mongoengine import Q +from functools import reduce class DocumentChangeList(ChangeList): - def __init__(self, request, model, list_display, list_display_links, - list_filter, date_hierarchy, search_fields, list_select_related, - list_per_page, list_max_show_all, list_editable, model_admin): - try: - super(DocumentChangeList, self).__init__( - request, model, list_display, list_display_links, list_filter, - date_hierarchy, search_fields, list_select_related, - list_per_page, list_max_show_all, list_editable, model_admin) - except TypeError: - self.list_max_show_all = list_max_show_all - # The init for django <= 1.3 takes one parameter less - super(DocumentChangeList, self).__init__( - request, model, list_display, list_display_links, list_filter, - date_hierarchy, search_fields, list_select_related, - list_per_page, list_editable, model_admin) + def __init__(self, *args, **kwargs): + super(DocumentChangeList, self).__init__(*args, **kwargs) self.pk_attname = self.lookup_opts.pk_name def get_results(self, request): + # query_set has been changed to queryset + try: + self.query_set + except: + self.query_set = self.queryset + # root_query_set has been changed to root_queryset + try: + self.root_query_set + except: + self.root_query_set = self.root_queryset + paginator = self.model_admin.get_paginator(request, self.query_set, self.list_per_page) # Get the number of objects, with admin filters applied. @@ -84,7 +83,7 @@ def get_ordering(self, request=None, queryset=None): ordering field. """ if queryset is None: - # with Django < 1.4 get_ordering works without fixes for mongoengine + # with Django < 1.4 get_ordering works without fixes for mongoengine return super(DocumentChangeList, self).get_ordering() params = self.params @@ -106,9 +105,12 @@ def get_ordering(self, request=None, queryset=None): continue # Invalid ordering specified, skip it. # Add the given query's ordering fields, if any. - sign = lambda t: t[1] > 0 and '+' or '-' - qs_ordering = [sign(t) + t[0] for t in queryset._ordering] - ordering.extend(qs_ordering) + try: + sign = lambda t: t[1] > 0 and '+' or '-' + qs_ordering = [sign(t) + t[0] for t in queryset._ordering] + ordering.extend(qs_ordering) + except: + pass # Ensure that the primary key is systematically present in the list of # ordering fields so we can guarantee a deterministic order across all @@ -152,7 +154,12 @@ def _lookup_param_1_3(self): ) return lookup_params - def get_query_set(self, request=None): + def get_queryset(self, request=None): + # root_query_set has been changed to root_queryset + try: + self.root_query_set + except: + self.root_query_set = self.root_queryset # First, we collect all the declared list filters. qs = self.root_query_set.clone() @@ -174,7 +181,7 @@ def get_query_set(self, request=None): # string (i.e. those that haven't already been processed by the # filters). qs = qs.filter(**remaining_lookup_params) - # TODO: This should probably be mongoengine exceptions + # TODO: This should probably be mongoengine exceptions except (SuspiciousOperation, ImproperlyConfigured): # Allow certain types of errors to be re-raised as-is so that the # caller can treat them in a special way. @@ -184,7 +191,7 @@ def get_query_set(self, request=None): # have any other way of validating lookup parameters. They might be # invalid if the keyword arguments are incorrect, or if the values # are not in the correct type, so we might get FieldError, - # ValueError, ValidationError, or ?. + # ValueError, ValidationError, or ?. raise IncorrectLookupParameters(e) # Set ordering. diff --git a/django_mongoengine/mongo_auth/models.py b/django_mongoengine/mongo_auth/models.py index fee8a04..be4cfd2 100644 --- a/django_mongoengine/mongo_auth/models.py +++ b/django_mongoengine/mongo_auth/models.py @@ -15,8 +15,24 @@ from django_mongoengine import document from django_mongoengine import fields +from django_mongoengine.queryset import QuerySetManager from .managers import MongoUserManager + +def ct_init(self, *args, **kwargs): + super(QuerySetManager, self).__init__(*args, **kwargs) + self._cache = {} + + +ContentTypeManager = type( + "ContentTypeManager", + (QuerySetManager,), + dict( + ContentTypeManager.__dict__, + __init__=ct_init, + ), +) + try: from django.contrib.auth.hashers import check_password, make_password except ImportError: @@ -88,7 +104,7 @@ class SiteProfileNotAvailable(Exception): pass -class PermissionManager(models.Manager): +class PermissionManager(QuerySetManager): def get_by_natural_key(self, codename, app_label, model): return self.get( codename=codename, @@ -134,9 +150,10 @@ class Meta: def __unicode__(self): return u"%s | %s | %s" % ( - unicode(self.content_type.app_label), - unicode(self.content_type), - unicode(self.name)) + self.content_type.app_label, + self.content_type, + self.name, + ) def natural_key(self): return (self.codename,) + self.content_type.natural_key() diff --git a/django_mongoengine/views/edit.py b/django_mongoengine/views/edit.py index 5b90868..1391b3e 100644 --- a/django_mongoengine/views/edit.py +++ b/django_mongoengine/views/edit.py @@ -20,6 +20,7 @@ # django 1.10 FormMixin = djmod.FormMixin + class WrapDocumentForm(WrapDocument, FormMixin): pass @@ -41,6 +42,7 @@ def get_success_url(self): " a get_absolute_url method on the Model.") return url + @copy_class(djmod.CreateView) class CreateView(six.with_metaclass( WrapDocumentForm, diff --git a/docs/examples.rst b/docs/examples.rst index 95bee4a..798d2f3 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -4,16 +4,25 @@ Examples Embedded Fields =============== - **models.py**:: - class ContactInfo(fields.EmbeddedDocument): + from django.db import models + from django_mongoengine import Document, EmbeddedDocument, fields + + class Product(Document): + name = fields.StringField() + retail_price = fields.DecimalField( + max_digits=15, decimal_places=2, default=0) + inventory_price = fields.DecimalField( + max_digits=15, decimal_places=2, default=0) + + class ContactInfo(EmbeddedDocument): web = fields.URLField(help_text=_("""List of languages for your application (the first one will be the default language)""")) email = fields.EmailField(verbose_name=_('e-mail address')) phone = fields.StringField(verbose_name=_('phone number')) class Application(Document): - name = fields.StringField(max_length=255, required=True) + name = fields.StringField(max_length=255, blank=False) contact = fields.EmbeddedDocumentField(ContactInfo) LOCALES = (('es', 'Spanish'), ('en', 'English'), ('de', 'German'), ('fr', 'French'), ('it', 'Italian'), ('ru', 'Russian')) locales = fields.ListField(fields.StringField(choices=LOCALES), help_text=_("""List of languages for your application (the first one will be the default language)""")) diff --git a/example/tumblelog/tumblelog/models.py b/example/tumblelog/tumblelog/models.py index 2143114..cefb356 100644 --- a/example/tumblelog/tumblelog/models.py +++ b/example/tumblelog/tumblelog/models.py @@ -1,4 +1,7 @@ -from django.core.urlresolvers import reverse +try: + from django.urls import reverse +except ImportError: + from django.core.urlresolvers import reverse from django_mongoengine import Document, EmbeddedDocument from django_mongoengine import fields @@ -14,6 +17,7 @@ class Comment(EmbeddedDocument): email = fields.EmailField(verbose_name="Email", blank=True) body = fields.StringField(verbose_name="Comment") + class Post(Document): created_at = fields.DateTimeField( default=datetime.datetime.now, editable=False, diff --git a/requirements.txt b/requirements.txt index 196caac..4133abd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -django>=1.7 -mongoengine>=0.8.3 +django>=1.11 +mongoengine>=0.14 diff --git a/setup.py b/setup.py index 33fe304..370c91f 100644 --- a/setup.py +++ b/setup.py @@ -12,10 +12,11 @@ """ from setuptools import setup, find_packages -import sys, os +import sys +import os -__version__ = '0.2.1' +__version__ = '0.3' __description__ = 'Django support for MongoDB via MongoEngine', __license__ = 'BSD' __author__ = 'Ross Lawley', diff --git a/tests/views/edit.py b/tests/views/edit.py index 99d5ae2..8e4aa9a 100644 --- a/tests/views/edit.py +++ b/tests/views/edit.py @@ -4,7 +4,7 @@ import unittest from django.core.exceptions import ImproperlyConfigured -from django.core.urlresolvers import reverse +from django.urls import reverse from django.views.generic.edit import FormMixin from django_mongoengine import forms @@ -43,7 +43,7 @@ def test_create(self): 'name': 'Randall Munroe', 'slug': 'randall-munroe'}) self.assertEqual(res.status_code, 302) - self.assertRedirects(res, 'http://testserver/list/authors/') + self.assertRedirects(res, '/list/authors/') self.assertQuerysetEqual(Author.objects.all(), ['']) @@ -63,7 +63,7 @@ def test_create_with_object_url(self): 'name': 'Rene Magritte'}) self.assertEqual(res.status_code, 302) artist = Artist.objects.get(name='Rene Magritte') - self.assertRedirects(res, 'http://testserver/detail/artist/%s/' % + self.assertRedirects(res, '/detail/artist/%s/' % artist.pk) self.assertQuerysetEqual(Artist.objects.all(), ['']) @@ -74,7 +74,7 @@ def test_create_with_redirect(self): 'name': 'Randall Munroe', 'slug': 'randall-munroe'}) self.assertEqual(res.status_code, 302) - self.assertRedirects(res, 'http://testserver/edit/authors/create/') + self.assertRedirects(res, '/edit/authors/create/') self.assertQuerysetEqual(Author.objects.all(), ['']) @@ -87,7 +87,7 @@ def test_create_with_interpolated_redirect(self): ['']) self.assertEqual(res.status_code, 302) pk = Author.objects.all()[0].pk - self.assertRedirects(res, 'http://testserver/edit/author/%s/update/' % + self.assertRedirects(res, '/edit/author/%s/update/' % pk) def test_create_with_special_properties(self): @@ -112,10 +112,10 @@ def test_create_with_special_properties(self): def test_create_without_redirect(self): try: - self.client.post('/edit/authors/create/naive/', - {'id': 1, - 'name': 'Randall Munroe', - 'slug': 'randall-munroe'}) + self.client.post('/edit/authors/create/naive/', { + 'id': 1, + 'name': 'Randall Munroe', + 'slug': 'randall-munroe'}) self.fail( 'Should raise exception -- No redirect URL provided, and no get_absolute_url provided') except ImproperlyConfigured: @@ -144,7 +144,7 @@ def test_update_post(self): 'name': 'Randall Munroe (xkcd)', 'slug': 'randall-munroe'}) self.assertEqual(res.status_code, 302) - self.assertRedirects(res, 'http://testserver/list/authors/') + self.assertRedirects(res, '/list/authors/') self.assertQuerysetEqual(Author.objects.all(), ['']) @@ -165,7 +165,7 @@ def test_update_put(self): # the form will be invalid and redisplayed with errors (status code 200). # See also #12635 self.assertEqual(res.status_code, 302) - self.assertRedirects(res, 'http://testserver/list/authors/') + self.assertRedirects(res, '/list/authors/') self.assertQuerysetEqual(Author.objects.all(), ['']) @@ -189,7 +189,7 @@ def test_update_with_object_url(self): {'id': '1', 'name': 'Rene Magritte'}) self.assertEqual(res.status_code, 302) - self.assertRedirects(res, 'http://testserver/detail/artist/%s/' % a.pk) + self.assertRedirects(res, '/detail/artist/%s/' % a.pk) self.assertQuerysetEqual(Artist.objects.all(), ['']) @@ -202,7 +202,7 @@ def test_update_with_redirect(self): 'name': 'Randall Munroe (author of xkcd)', 'slug': 'randall-munroe'}) self.assertEqual(res.status_code, 302) - self.assertRedirects(res, 'http://testserver/edit/authors/create/') + self.assertRedirects(res, '/edit/authors/create/') self.assertQuerysetEqual(Author.objects.all(), ['']) @@ -219,7 +219,7 @@ def test_update_with_interpolated_redirect(self): ['']) self.assertEqual(res.status_code, 302) pk = Author.objects.all()[0].pk - self.assertRedirects(res, 'http://testserver/edit/author/%s/update/' % + self.assertRedirects(res, '/edit/author/%s/update/' % pk) def test_update_with_special_properties(self): @@ -239,23 +239,19 @@ def test_update_with_special_properties(self): 'name': 'Randall Munroe (author of xkcd)', 'slug': 'randall-munroe'}) self.assertEqual(res.status_code, 302) - self.assertRedirects(res, 'http://testserver/detail/author/%s/' % a.pk) + self.assertRedirects(res, '/detail/author/%s/' % a.pk) self.assertQuerysetEqual(Author.objects.all(), ['']) def test_update_without_redirect(self): - try: + with self.assertRaises(ImproperlyConfigured): a = Author.objects.create(id='1', name='Randall Munroe', slug='randall-munroe', ) - self.client.post('/edit/author/%s/update/naive/' % a.pk, - {'id': '1', - 'name': 'Randall Munroe (author of xkcd)', - 'slug': 'randall-munroe'}) - self.fail( - 'Should raise exception -- No redirect URL provided, and no get_absolute_url provided') - except ImproperlyConfigured: - pass + self.client.post('/edit/author/%s/update/naive/' % a.pk, { + 'id': '1', + 'name': 'Randall Munroe (author of xkcd)', + 'slug': 'randall-munroe'}) def test_update_get_object(self): a = Author.objects.create(pk='1', @@ -274,7 +270,7 @@ def test_update_get_object(self): 'name': 'Randall Munroe (xkcd)', 'slug': 'randall-munroe'}) self.assertEqual(res.status_code, 302) - self.assertRedirects(res, 'http://testserver/list/authors/') + self.assertRedirects(res, '/list/authors/') self.assertQuerysetEqual(Author.objects.all(), ['']) @@ -297,7 +293,7 @@ def test_delete_by_post(self): # Deletion with POST res = self.client.post('/edit/author/%s/delete/' % a.pk) self.assertEqual(res.status_code, 302) - self.assertRedirects(res, 'http://testserver/list/authors/') + self.assertRedirects(res, '/list/authors/') self.assertQuerysetEqual(Author.objects.all(), []) def test_delete_by_delete(self): @@ -307,7 +303,7 @@ def test_delete_by_delete(self): 'slug': 'randall-munroe'}) res = self.client.delete('/edit/author/%s/delete/' % a.pk) self.assertEqual(res.status_code, 302) - self.assertRedirects(res, 'http://testserver/list/authors/') + self.assertRedirects(res, '/list/authors/') self.assertQuerysetEqual(Author.objects.all(), []) def test_delete_with_redirect(self): @@ -316,7 +312,7 @@ def test_delete_with_redirect(self): 'slug': 'randall-munroe'}) res = self.client.post('/edit/author/%s/delete/redirect/' % a.pk) self.assertEqual(res.status_code, 302) - self.assertRedirects(res, 'http://testserver/edit/authors/create/') + self.assertRedirects(res, '/edit/authors/create/') self.assertQuerysetEqual(Author.objects.all(), []) def test_delete_with_special_properties(self): @@ -332,7 +328,7 @@ def test_delete_with_special_properties(self): res = self.client.post('/edit/author/%s/delete/special/' % a.pk) self.assertEqual(res.status_code, 302) - self.assertRedirects(res, 'http://testserver/list/authors/') + self.assertRedirects(res, '/list/authors/') self.assertQuerysetEqual(Author.objects.all(), []) def test_delete_without_redirect(self): diff --git a/tests/views/views.py b/tests/views/views.py index fc2cd1d..9dc9f52 100644 --- a/tests/views/views.py +++ b/tests/views/views.py @@ -4,7 +4,7 @@ from django.contrib.auth.decorators import login_required from django.core.paginator import Paginator -from django.core.urlresolvers import reverse +from django.urls import reverse from django.utils.decorators import method_decorator from django.views.generic import TemplateView