Skip to content

Commit

Permalink
Merge pull request #11 from conversocial/adds_cascade_triggers
Browse files Browse the repository at this point in the history
fix: noticket - allows_pre_delete_signals_on_reverse_delete_rule_cascade
  • Loading branch information
jamiewinspear authored Apr 4, 2018
2 parents 722802f + 56e5fee commit 4d9cf05
Show file tree
Hide file tree
Showing 4 changed files with 222 additions and 12 deletions.
2 changes: 1 addition & 1 deletion mongoengine/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
__all__ = (document.__all__ + fields.__all__ + connection.__all__ +
queryset.__all__ + signals.__all__)

VERSION = (0, 6, 24)
VERSION = (0, 6, 25)


def get_version():
Expand Down
15 changes: 13 additions & 2 deletions mongoengine/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from mongoengine import signals
from base import (DocumentMetaclass, TopLevelDocumentMetaclass, BaseDocument,
BaseDict, BaseList)
from queryset import OperationError
from queryset import OperationError, QuerySet
from connection import get_db, DEFAULT_CONNECTION_NAME

__all__ = ['Document', 'EmbeddedDocument', 'DynamicDocument',
Expand Down Expand Up @@ -286,14 +286,25 @@ def update(self, **kwargs):
# Need to add shard key to query, or you get an error
return self.__class__.objects(**self._object_key).update_one(**kwargs)


@property
def _qs(self):
"""
Returns the queryset to use for updating / reloading / deletions
"""
if not hasattr(self, '__objects'):
self.__objects = QuerySet(self, self._get_collection())
return self.__objects

def delete(self, w=1):
"""Delete the :class:`~mongoengine.Document` from the database. This
will only take effect if the document has been previously saved.
"""
signals.pre_delete.send(self.__class__, document=self)

try:
self.__class__.objects(**self._object_key).delete(w=w)
self._qs.filter(**self._object_key).delete(
w=w, _from_doc_delete=True)
except pymongo.errors.OperationFailure, err:
message = u'Could not delete document (%s)' % err.message
raise OperationError(message)
Expand Down
29 changes: 20 additions & 9 deletions mongoengine/queryset.py
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ def clone(self):

for prop in copy_props:
val = getattr(self, prop)
setattr(c, prop, copy.deepcopy(val))
setattr(c, prop, copy.copy(val))

return c

Expand Down Expand Up @@ -1216,32 +1216,43 @@ def read_preference(self, read_preference):
self._read_preference = read_preference
return self

def delete(self, w=1):
def delete(self, w=1, _from_doc_delete=False):
"""Delete the documents matched by the query.
"""
doc = self._document
queryset = self.clone()

# This is taken from actual MongoEngine, url
# https://github.com/MongoEngine/mongoengine/pull/105
has_delete_signal = (
signals.pre_delete.has_receivers_for(self._document) or
signals.post_delete.has_receivers_for(self._document))

if has_delete_signal and not _from_doc_delete:
for d in queryset:
d.delete()
return

# Check for DENY rules before actually deleting/nullifying any other
# references
for rule_entry in doc._meta['delete_rules']:
for rule_entry in queryset._document._meta['delete_rules']:
document_cls, field_name = rule_entry
rule = doc._meta['delete_rules'][rule_entry]
rule = queryset._document._meta['delete_rules'][rule_entry]
if rule == DENY \
and document_cls.objects(**{field_name + '__in': self}).count() > 0: # noqa
msg = \
u'Could not delete document (at least %s.%s refers to it)' \
% (document_cls.__name__, field_name)
raise OperationError(msg)

for rule_entry in doc._meta['delete_rules']:
for rule_entry in queryset._document._meta['delete_rules']:
document_cls, field_name = rule_entry
rule = doc._meta['delete_rules'][rule_entry]
rule = queryset._document._meta['delete_rules'][rule_entry]
if rule == CASCADE:
document_cls.objects(**{field_name + '__in': self}).delete(w=w)
elif rule == NULLIFY:
document_cls.objects(**{field_name + '__in': self}).update(
w=w,
**{'unset__%s' % field_name: 1})
w=w,
**{'unset__%s' % field_name: 1})

self._collection.remove(self._query, w=w)

Expand Down
188 changes: 188 additions & 0 deletions tests/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from mongoengine.base import NotRegistered, InvalidDocumentError
from mongoengine.queryset import InvalidQueryError
from mongoengine.connection import get_db, register_db
from mongoengine import signals


class DocumentTest(unittest.TestCase):
Expand Down Expand Up @@ -2023,6 +2024,193 @@ class BlogPost(Document):
author.delete()
self.assertEqual(len(BlogPost.objects), 0)

def test_shallow_cascade_triggers_pre_delete_signal(self):
class Editor(self.Person):
review_queue = IntField(default=0)

class BlogPost(Document):
content = StringField()
author = ReferenceField(self.Person, reverse_delete_rule=CASCADE)
editor = ReferenceField(Editor)

@classmethod
def pre_delete(cls, sender, document, **kwargs):
document.editor.update(dec__review_queue=1)

signals.pre_delete.connect(BlogPost.pre_delete, sender=BlogPost)

self.Person.drop_collection()
BlogPost.drop_collection()
Editor.drop_collection()

author = self.Person(name='Will S.')
author.save()
editor = Editor(name='Max P.', review_queue=1)
editor.save()
BlogPost(content='wrote some books', author=author,
editor=editor).save()

author.delete()
self.assertEqual(BlogPost.objects.all().count(), 0)
editor = Editor.objects(name='Max P.').get()
self.assertEqual(editor.review_queue, 0)

def test_shallow_cascade_triggers_post_delete_signal(self):

class Editor(self.Person):
review_queue = IntField(default=0)

class BlogPost(Document):
content = StringField()
author = ReferenceField(self.Person, reverse_delete_rule=CASCADE)
editor = ReferenceField(Editor)

@classmethod
def post_delete(cls, sender, document, **kwargs):
document.editor.update(dec__review_queue=1)

signals.post_delete.connect(BlogPost.post_delete, sender=BlogPost)

self.Person.drop_collection()
BlogPost.drop_collection()
Editor.drop_collection()

author = self.Person(name='Will S.')
author.save()
editor = Editor(name='Max P.', review_queue=1)
editor.save()
BlogPost(content='wrote some books', author=author,
editor=editor).save()

author.delete()
self.assertEqual(BlogPost.objects.all().count(), 0)
editor = Editor.objects(name='Max P.').get()
self.assertEqual(editor.review_queue, 0)

def test_mid_cascade_signal_triggered_on_deep_delete(self):
class Editor(self.Person):
review_queue = IntField(default=0)

class BlogPost(Document):
content = StringField()
author = ReferenceField(self.Person, reverse_delete_rule=CASCADE)
editor = ReferenceField(Editor)

@classmethod
def post_delete(cls, sender, document, **kwargs):
# decrement the docs-to-review count
document.editor.update(dec__review_queue=1)

class Comment(Document):
blogpost = ReferenceField(BlogPost, reverse_delete_rule=CASCADE)

signals.post_delete.connect(BlogPost.post_delete, sender=BlogPost)

self.Person.drop_collection()
BlogPost.drop_collection()
Editor.drop_collection()

author = self.Person(name='Will S.')
author.save()
editor = Editor(name='Max P.', review_queue=1)
editor.save()
BlogPost(content='wrote some books', author=author,
editor=editor).save()

author.delete()
self.assertEqual(BlogPost.objects.all().count(), 0)
editor = Editor.objects(name='Max P.').get()
self.assertEqual(editor.review_queue, 0)

def test_delete_hooks_trigger_through_deep_cascades(self):

class Editor(self.Person):
comments_to_review = IntField(default=0)
review_queue = IntField(default=0)

class BlogPost(Document):
content = StringField()
author = ReferenceField(self.Person, reverse_delete_rule=CASCADE)
editor = ReferenceField(Editor)

@classmethod
def pre_delete(cls, sender, document, **kwargs):
document.editor.update(dec__review_queue=1)

class Comment(Document):
blogpost = ReferenceField(BlogPost, reverse_delete_rule=CASCADE)

@classmethod
def pre_delete(cls, sender, document, **kwargs):
document.blogpost.editor.update(dec__comments_to_review=1)

signals.pre_delete.connect(BlogPost.pre_delete, sender=BlogPost)
signals.pre_delete.connect(Comment.pre_delete, sender=Comment)

self.Person.drop_collection()
BlogPost.drop_collection()
Editor.drop_collection()

author = self.Person(name='Will S.')
author.save()
editor = Editor(name='Max P.', review_queue=1, comments_to_review=1)
editor.save()
blogpost = BlogPost(content='wrote some books', author=author,
editor=editor)
blogpost.save()
Comment(blogpost=blogpost).save()

author.delete()

self.assertEqual(BlogPost.objects.all().count(), 0)
self.assertEqual(Comment.objects.all().count(), 0)
editor = Editor.objects(name='Max P.').get()
self.assertEqual(editor.review_queue, 0)
self.assertEqual(editor.comments_to_review, 0)

def test_leaf_delete_hooks_trigger_with_no_parent_trigger(self):

class Editor(self.Person):
comments_to_review = IntField(default=0)

class BlogPost(Document):
content = StringField()
author = ReferenceField(self.Person, reverse_delete_rule=CASCADE)
editor = ReferenceField(Editor)

class Comment(Document):
blogpost = ReferenceField(BlogPost, reverse_delete_rule=CASCADE)

@classmethod
def pre_delete(cls, sender, document, **kwargs):
# decrement the comments-to-review count
document.blogpost.editor.update(dec__comments_to_review=1)

signals.pre_delete.connect(Comment.pre_delete, sender=Comment)

self.Person.drop_collection()
BlogPost.drop_collection()
Editor.drop_collection()

author = self.Person(name='Will S.')
author.save()
editor = Editor(name='Max P.', comments_to_review=1)
editor.save()
blogpost = BlogPost(content='wrote some books', author=author,
editor=editor)
blogpost.save()

Comment(blogpost=blogpost).save()

# delete the author, the post is also deleted due to the CASCADE rule
author.delete()

self.assertEqual(BlogPost.objects.all().count(), 0)
self.assertEqual(Comment.objects.all().count(), 0)
# the pre-delete signal should have decremented the editor's queue
editor = Editor.objects(name='Max P.').get()
self.assertEqual(editor.comments_to_review, 0)

def test_invalid_reverse_delete_rules_raise_errors(self):

def throw_invalid_document_error():
Expand Down

0 comments on commit 4d9cf05

Please sign in to comment.