jazzband / django-polymorphic

Improved Django model inheritance with automatic downcasting
https://django-polymorphic.readthedocs.io
Other
1.66k stars 282 forks source link

Deletion doesn't function with self referential foreign key #540

Open will-ockmore opened 1 year ago

will-ockmore commented 1 year ago

Django's .delete() functionality doesn't work when a polymorphic model subclass is referenced by an instance of the base class using a self referential foreign key. See below for model definitions and code to reproduce issue.

# myapp/models.py

from django.db import models
from polymorphic.models import PolymorphicModel

class A(PolymorphicModel):
    self_referential = models.ForeignKey("self", null=True, blank=True, on_delete=models.CASCADE)

class B(A):
    name = models.CharField(max_length=256)

### script.py

from myapp.models import A, B

b = B.objects.create(self_referential=None, name="b")
a = A.objects.create(self_referential=b)

print(hasattr(a, "a_ptr"))
print(hasattr(b, "a_ptr"))

A.objects.all().delete()

Running the above results in the following output:

False
True
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
File /script.py:29
     26 print(hasattr(a, "a_ptr"))
     27 print(hasattr(b, "a_ptr"))
---> 29 A.objects.all().delete()
     31 # %%

File /.venv/lib/python3.10/site-packages/django/db/models/query.py:745, in QuerySet.delete(self)
    742 del_query.query.clear_ordering(force_empty=True)
    744 collector = Collector(using=del_query.db)
--> 745 collector.collect(del_query)
    746 deleted, _rows_count = collector.delete()
    748 # Clear the result cache, in case this QuerySet gets reused.

File /.venv/lib/python3.10/site-packages/django/db/models/deletion.py:256, in Collector.collect(self, objs, source, nullable, collect_related, source_attr, reverse_dependency, keep_parents, fail_on_restricted)
    254     for ptr in concrete_model._meta.parents.values():
    255         if ptr:
--> 256             parent_objs = [getattr(obj, ptr.name) for obj in new_objs]
    257             self.collect(parent_objs, source=model,
    258                          source_attr=ptr.remote_field.related_name,
    259                          collect_related=False,
    260                          reverse_dependency=True,
    261                          fail_on_restricted=False)
    262 if not collect_related:

File /.venv/lib/python3.10/site-packages/django/db/models/deletion.py:256, in <listcomp>(.0)
    254     for ptr in concrete_model._meta.parents.values():
    255         if ptr:
--> 256             parent_objs = [getattr(obj, ptr.name) for obj in new_objs]
    257             self.collect(parent_objs, source=model,
    258                          source_attr=ptr.remote_field.related_name,
    259                          collect_related=False,
    260                          reverse_dependency=True,
    261                          fail_on_restricted=False)
    262 if not collect_related:

AttributeError: 'A' object has no attribute 'a_ptr'

Package versions

❯ poetry export -f requirements.txt | rg '^django==|polymorphic'
django-polymorphic==3.1.0 ; python_version >= "3.10" and python_version < "3.11" \
django==3.2.18 ; python_version >= "3.10" and python_version < "3.11" \