django-polymorphic icon indicating copy to clipboard operation
django-polymorphic copied to clipboard

Model object accessible via old class after class changed raises TypeError

Open amoskopp opened this issue 8 years ago • 3 comments

This Django application demonstrates a bug in django-polymorphic.

The application djangoPolymorphicTestcase has three model classes:

  • Base, a polymorphic.models.PolymorphicModel
  • VariantA, which inherits from Base
  • VariantB, which inherits from Base

The test creates a VariantA object with primary key of DUPLICATE. It then creates a VariantB object with a primary key of DUPLICATE. As a result we do not have two objects, but a single VariantB object. Accessing it via VariantA.objects() is possible, but throws TypeError.

Test Result

# 2017-04-25T15:17+02:00 $home/src/djangoPolymorphicTestcase ~? master(9daca92) = github.com/master
; PYTHONPATH=/usr/lib/python3/dist-packages/ python3 ./manage.py test
Creating test database for alias 'default'...
FE
======================================================================
ERROR: test_duplicate_demonstration (djangoPolymorphicTestcase.tests.tests.BaseTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/nils/src/djangoPolymorphicTestcase/djangoPolymorphicTestcase/tests/tests.py", line 19, in test_duplicate_demonstration
    bug_here = VariantA.objects.first()
  File "/usr/lib/python3/dist-packages/django/db/models/manager.py", line 85, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "/usr/lib/python3/dist-packages/django/db/models/query.py", line 556, in first
    objects = list((self if self.ordered else self.order_by('pk'))[:1])
  File "/usr/lib/python3/dist-packages/django/db/models/query.py", line 256, in __iter__
    self._fetch_all()
  File "/usr/lib/python3/dist-packages/django/db/models/query.py", line 1087, in _fetch_all
    self._result_cache = list(self.iterator())
  File "/usr/lib/python3/dist-packages/polymorphic/query.py", line 445, in iterator
    real_results = self._get_real_instances(base_result_objects)
  File "/usr/lib/python3/dist-packages/polymorphic/query.py", line 328, in _get_real_instances
    real_concrete_class = base_object.get_real_instance_class()
  File "/usr/lib/python3/dist-packages/polymorphic/models.py", line 117, in get_real_instance_class
    and not issubclass(model, self.__class__._meta.proxy_for_model):
TypeError: issubclass() arg 2 must be a class or tuple of classes

======================================================================
FAIL: test_duplicate_count (djangoPolymorphicTestcase.tests.tests.BaseTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/nils/src/djangoPolymorphicTestcase/djangoPolymorphicTestcase/tests/tests.py", line 15, in test_duplicate_count
    self.assertEqual(VariantA.objects.count(), 0)  # Doesn't trigger bug.
AssertionError: 1 != 0

----------------------------------------------------------------------
Ran 2 tests in 0.012s

FAILED (failures=1, errors=1)
Destroying test database for alias 'default'...
# exited 1

amoskopp avatar Apr 25 '17 13:04 amoskopp

It seems to me that the behaviour of upcasting results with a polymorphic_ctype_id that does not match the expected self_model_class_id was intentionally introduced in commit 58c4f6f69760a1cb1b12d4c9ec77bf0f832c1653. I have no idea why it was added, though.

amoskopp avatar Apr 25 '17 15:04 amoskopp

Also getting the same issue, but happened when running a migration against production...yikes!

Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "/app/.heroku/python/lib/python2.7/site-packages/polymorphic/query.py", line 458, in __repr__
    return super(PolymorphicQuerySet, self).__repr__(*args, **kwargs)
  File "/app/.heroku/python/lib/python2.7/site-packages/django/db/models/query.py", line 116, in __repr__
    data = list(self[:REPR_OUTPUT_SIZE + 1])
  File "/app/.heroku/python/lib/python2.7/site-packages/django/db/models/query.py", line 141, in __iter__
    self._fetch_all()
  File "/app/.heroku/python/lib/python2.7/site-packages/django/db/models/query.py", line 966, in _fetch_all
    self._result_cache = list(self.iterator())
  File "/app/.heroku/python/lib/python2.7/site-packages/polymorphic/query.py", line 445, in iterator
    real_results = self._get_real_instances(base_result_objects)
  File "/app/.heroku/python/lib/python2.7/site-packages/polymorphic/query.py", line 328, in _get_real_instances
    real_concrete_class = base_object.get_real_instance_class()
  File "/app/.heroku/python/lib/python2.7/site-packages/polymorphic/models.py", line 98, in get_real_instance_class
    and not issubclass(model, self.__class__._meta.proxy_for_model):
TypeError: issubclass() arg 2 must be a class or tuple of classes

This happened even after updating the polymorphic_ctype as @nmoskopp referenced.

new_ct = ContentType.objects.get_for_model(MyModel)
MyModel.objects.filter(polymorphic_ctype__isnull=True).update(polymorphic_ctype=new_ct)

mosspaper avatar May 18 '17 21:05 mosspaper

I've faced with the same exception when I was trying to open admin page for sub-model. After about 1 hour of mindgames I figured out what is the root cause.

My migration was applied incorrectly: parent and sub-entities had same value for the "polymorphic_ctype" and, as result, type of all the sub-entities was recognized as type of parent. That happened because of I copy-pasted migration from here.

The solution is to change forwards_func source code to handle existing parent-child inheritance relationships as follows:

from django.db import migrations, models
from django.db.models import Q

def forwards_func(apps, schema_editor):
    models = apps.get_app_config('projectid').get_models()
    content_type = apps.get_model('contenttypes', 'ContentType')
    for model in models:
        new_ct = content_type.objects.get_for_model(model)
        objects = model.objects.filter(
            Q(polymorphic_ctype__isnull=True)
            | ~Q(polymorphic_ctype=new_ct))

        objects.update(polymorphic_ctype=new_ct)

class Migration(migrations.Migration):
    .... and etc.

My env:

Django==2.0.4
django-polymorphic==2.0.3
django-rest-polymorphic==0.1.7

vsfedorenko avatar Sep 20 '18 00:09 vsfedorenko