Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

post_delete signal called for both parent and child models, causing orders to be incorrect #320

Open
kylepollina opened this issue Feb 27, 2024 · 0 comments

Comments

@kylepollina
Copy link

kylepollina commented Feb 27, 2024

Issue

If I have a parent and child model defined, ordered with respect to some other model:

class Foobar(models.Model):
    name = models.CharField(max_length=100)

class Parent(OrderedModel):
    name = models.CharField(max_length=100)
    foobar = models.ForeignKey(Foobar, on_delete=models.CASCADE)
    
    order_with_respect_to = 'foobar'
    
class Child(Parent):
    age = models.IntegerField()

When calling delete() on one of the child models, the related parent model is also deleted, since Django stores a pointer from the child model to the parent model in the database. This causes the post_delete signal to be run for both the child model and the parent model. Each time the post_delete signal is called, the orders are shifted to accommodate.

import pytest
@pytest.mark.django_db
def test_parent_child_order():
    foobar = Foobar.objects.create(name="foobar")
    child1 = Child.objects.create(name="child1", foobar=foobar, age=1)
    child2 = Child.objects.create(name="child2", foobar=foobar, age=2)
    child3 = Child.objects.create(name="child3", foobar=foobar, age=3)
    child4 = Child.objects.create(name="child4", foobar=foobar, age=4)
    
    # This is the order of the children at the start
    assert child1.order == 0
    assert child2.order == 1
    assert child3.order == 2
    assert child4.order == 3
    
    # Delete the first child
    # This causes the parent to be deleted as well
    child1.delete()
    
    # Refresh the db
    child2.refresh_from_db()
    child3.refresh_from_db()
    child4.refresh_from_db()

    # The order of the children should be updated
    # The expected order
    assert child2.order == 0
    assert child3.order == 1
    assert child4.order == 2

This test produces this outcome:

>       assert child3.order == 1
E       assert 0 == 1
E        +  where 0 = <Child: Child object (3)>.order
 AssertionError

You would expect child3.order to be 1, but since the post_delete is called twice, it gets shifted twice. In reality, child2.order is 0, child3.order is 0, and child4.order is 1

Expected result

I would expect that the parent models do not have the post_delete signal called on them. As that is unexpected behavior to have the orders shifted twice.

Workaround

I found that I could disconnect the signal from the parent model, in my apps.py file:

from django.apps import AppConfig

from django.db.models.signals import post_delete


class MyAppConfig(AppConfig):
    ...
    def ready(self):
        from myapp.models import Parent
        res = post_delete.disconnect(sender=Parent, dispatch_uid=Parent.__name__)
        if res == False:
            raise AssertionError("Failed to disconnect post_delete signal from Parent model. Make sure that `ordered_model` is loaded before `myapp` in the settings.py file.")

This only works if my app is set after the ordered_model app in the INSTALLED_APPS list in settings.py

INSTALLED_APPS = [
    ...,
    "ordered_model",
    "myapp",
    ...
]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant