1

I am using Django:

>>> django.VERSION
(1, 11, 15, u'final', 0)

MPTT:

 django-mptt     0.9.1

In Models:

from mptt.models import MPTTModel, TreeForeignKey
class Location(MPTTModel):
    id              = models.AutoField(primary_key=True)
    name            = models.CharField(max_length=75,null=False)
    parent          = TreeForeignKey('self', on_delete=models.PROTECT, null=True, blank=True)

I can view and add properly as per django-mptt documentation. But I am not able to delete a child node. It messes up all the tree structure. Here is how delete is used in views:

Location.objects.get(id=loc_id).delete()

where loc_id is the id of the node i want to delete. I am not sure how to use Delete properly or maybe there is a bug in mptt. I looked up for any example on its official doc. It says nothing more than the following on this topic:

class mptt.models.MPTTModel(*args, **kwargs)

MPTTModel.delete(*args, **kwargs)
Calling delete on a node will delete it as well as its full subtree, as opposed to reattaching all the subnodes to its parent node. There are no argument specific to a MPTT model, all the arguments will be passed directly to the django’s Model.delete.

delete will not return anything.

Thanks

Areebah
  • 43
  • 8
  • may be the problem is in `on_delete=models.PROTECT,`? may be that should be changed to `CASCADE`? – Chiefir Sep 25 '18 at 12:03

2 Answers2

1

Once I had this problem with protected children deleting (django-mptt==0.9.1). After some searching I found out the next issue: problem occurs inside delete() method of the MPTTodel. First it tries to update the tree structure, then delete the object. So if we have protected relation, our tree could be messed up.

tree_width = (self._mpttfield('right') -                                
              self._mpttfield('left') + 1)                              
target_right = self._mpttfield('right')                                 
tree_id = self._mpttfield('tree_id')                                    
self._tree_manager._close_gap(tree_width, target_right, tree_id)        
parent = cached_field_value(self, self._mptt_meta.parent_attr)          
if parent:                                                              
    right_shift = -self.get_descendant_count() - 2                      
    self._tree_manager._post_insert_update_cached_parent_right(parent, right_shift)
                                                                        
return super(MPTTModel, self).delete(*args, **kwargs)                   

I've solved this issue using transactions. Something like:

from django.db import transaction

def delete(self, *args, **kwargs):
    with transaction.atomic():
        super().delete(*args, **kwargs)

test in shell (django 3.1.7 and django-mptt 0.12.0)

from mptt.models import MPTTModel, TreeForeignKey
from django.db import models, transaction

class Test(MPTTModel):
    name = models.TextField()
    parent = TreeForeignKey(
        'self', 
        on_delete=models.PROTECT, 
        null=True,
        blank=True,
        related_name='children'
    )
from testing.models import Test

a = Test.objects.create(name='a')
b = Test.objects.create(name='b', parent=a)
c = Test.objects.create(name='c', parent=b)

print(a.get_descendants())
print(c.get_ancestors())

b.delete()

#  refresh from db
a = Test.objects.get(pk=a.pk)
c = Test.objects.get(pk=c.pk)

print(a.get_descendants())
print(c.get_ancestors())
  1. Without transaction:
>>> print(a.get_descendants())
<TreeQuerySet [<Test: Test object (20)>, <Test: Test object (21)>]>
>>> print(c.get_ancestors())
<TreeQuerySet [<Test: Test object (19)>, <Test: Test object (20)>]>
>>> 
>>> b.delete()
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "/home/vladimir/Desktop/test/.venv/lib/python3.8/site-packages/mptt/models.py", line 1138, in delete
    return super().delete(*args, **kwargs)
  File "/home/vladimir/Desktop/test/.venv/lib/python3.8/site-packages/django/db/models/base.py", line 946, in delete
    collector.collect([self], keep_parents=keep_parents)
  File "/home/vladimir/Desktop/test/.venv/lib/python3.8/site-packages/django/db/models/deletion.py", line 302, in collect
    raise ProtectedError(
django.db.models.deletion.ProtectedError: ("Cannot delete some instances of model 'Test' because they are referenced through protected foreign keys: 'Test.parent'.", {<Test: Test object (21)>})
>>> 
>>> #  refresh from db
>>> a = Test.objects.get(pk=a.pk)
>>> c = Test.objects.get(pk=c.pk)
>>> 
>>> print(a.get_descendants())
<TreeQuerySet []>
>>> print(c.get_ancestors())
<TreeQuerySet [<Test: Test object (20)>]>
  1. Within transaction
>>> print(a.get_descendants())
<TreeQuerySet [<Test: Test object (23)>, <Test: Test object (24)>]>
>>> print(c.get_ancestors())
<TreeQuerySet [<Test: Test object (22)>, <Test: Test object (23)>]>
>>> 
>>> b.delete()
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "/home/vladimir/Desktop/test/someproject/testing/models.py", line 16, in delete
    super().delete(*args, **kwargs)
  File "/home/vladimir/Desktop/test/.venv/lib/python3.8/site-packages/mptt/models.py", line 1138, in delete
    return super().delete(*args, **kwargs)
  File "/home/vladimir/Desktop/test/.venv/lib/python3.8/site-packages/django/db/models/base.py", line 946, in delete
    collector.collect([self], keep_parents=keep_parents)
  File "/home/vladimir/Desktop/test/.venv/lib/python3.8/site-packages/django/db/models/deletion.py", line 302, in collect
    raise ProtectedError(
django.db.models.deletion.ProtectedError: ("Cannot delete some instances of model 'Test' because they are referenced through protected foreign keys: 'Test.parent'.", {<Test: Test object (24)>})
>>> 
>>> #  refresh from db
>>> a = Test.objects.get(pk=a.pk)
>>> c = Test.objects.get(pk=c.pk)
>>> 
>>> print(a.get_descendants())
<TreeQuerySet [<Test: Test object (23)>, <Test: Test object (24)>]>
>>> print(c.get_ancestors())
<TreeQuerySet [<Test: Test object (22)>, <Test: Test object (23)>]>
Vladimir
  • 21
  • 2
0

Ok so turns out that for the time being just to get the sane table structure i can use

Location.objects.rebuild()

This rebuilds the whole table structure i.e. its lft, rght columns. So my messed up tree is fine again. I use this line after deleting a node.

This might not be the perfect solution.

I am still looking for a proper delete code because rebuilding tree for a large data set can be time consuming.

Hope somebody can still help in this regard.

Areebah
  • 43
  • 8
  • this is so strange, cuz I am currently working on the project with MPTT and simple deletion of parents removes also their children. – Chiefir Sep 26 '18 at 09:52
  • and btw, what is `.rebuild()`? – Chiefir Sep 26 '18 at 09:53
  • Reference rebuild(): https://django-mptt.github.io/django-mptt/mptt.models.html#mptt.models.TreeManager.rebuild It does delete correctly but tree structure messes up in display with code here in templates section: https://django-mptt.readthedocs.io/en/latest/tutorial.html#template – Areebah Sep 26 '18 at 10:05