Opened 19 months ago

Last modified 18 months ago

#27460 assigned New feature

Allow declaring a GenericRelation from an abstract model to another abstract model

Reported by: Wolfgang Grim Owned by: Alex Hill
Component: Database layer (models, ORM) Version: 1.10
Severity: Normal Keywords: GenericRelation, Abstract, ContentFramework
Cc: Alex Hill Triage Stage: Accepted
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

Creating the following models does work in Django 1.9, however in Django 1.10 the following error is raised as soon as the class that inherits is created (in the example as soon as class DetailTest is created):

<function make_generic_foreign_order_accessors at 0x7f5444873488>: (models.E022) <function make_generic_foreign_order_accessors at 0x7f5444873488> contains a lazy reference to testac.abstractmaster, but app 'testac' doesn't provide model 'abstractmaster'.
testac.DetailTest.relatedMasterModel: (fields.E307) The field testac.DetailTest.relatedMasterModel was declared with a lazy reference to 'testac.abstractmaster', but app 'testac' doesn't provide model 'abstractmaster'.

models.py

from __future__ import unicode_literals

from django.db import models

from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
from django.contrib.contenttypes.models import ContentType


class AbstractMaster (models.Model):
    class Meta: abstract=True
    
    detailModel_type = models.ForeignKey(ContentType, blank=True, null=True)
    detailModel_id = models.PositiveIntegerField(blank=True, null=True)
    detail_Model = GenericForeignKey('detailModel_type', 'detailModel_id')
    
class AbstractDetail (models.Model):
    class Meta: abstract=True
    
    masterModel_type = models.ForeignKey(ContentType, blank=True, null=True)
    masterModel_id = models.PositiveIntegerField(blank=True, null=True)
    master_Model = GenericForeignKey('masterModel_type', 'masterModel_id')
    
    relatedMasterModel = GenericRelation(AbstractMaster, object_id_field='detailModel_id', content_type_field='detailModel_id', related_query_name='%(app_label)s_%(class)s_detail_model')
    
class MasterTest (AbstractMaster):
    pass

class DetailTest (AbstractDetail):
    pass

Change History (10)

comment:1 Changed 19 months ago by Tim Graham

Cc: Alex Hill added
Component: UncategorizedCore (Other)

Alex, was this an oversight in 2ff7ef15b0a1d41e3f121e96cb72a383863046c0?

comment:2 Changed 19 months ago by Alex Hill

I'm pretty sure this is just a more explicit failure of something that didn't work before. You can create the models as described, but see it blow up in 1.9 in the sample below when you actually try to use it.

I might be thinking about this wrong, but what would a GenericRelation to an abstract model mean? Which model would queries on the field yield instances of?

Wolfgang - can you provide an example of how you were successfully using this in 1.9?

>>> from ag.models import MasterTest, DetailTest
>>> mt = MasterTest.objects.create()
>>> dt = DetailTest.objects.create()
>>> mt.detail_Model = dt
>>> mt.save()
>>> dt.master_Model = mt
>>> dt.save()
>>> 
>>> dt.relatedMasterModel.all()
Traceback (most recent call last):
  File "/home/alexh/djexperiments/env/lib/python3.5/site-packages/django/core/management/commands/shell.py", line 69, in handle
    self.run_shell(shell=options['interface'])
  File "/home/alexh/djexperiments/env/lib/python3.5/site-packages/django/core/management/commands/shell.py", line 61, in run_shell
    raise ImportError
ImportError

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "/home/alexh/djexperiments/env/lib/python3.5/site-packages/django/db/models/fields/related_descriptors.py", line 468, in __get__
    return self.related_manager_cls(instance)
  File "/home/alexh/djexperiments/env/lib/python3.5/site-packages/django/utils/functional.py", line 33, in __get__
    res = instance.__dict__[self.name] = self.func(instance)
  File "/home/alexh/djexperiments/env/lib/python3.5/site-packages/django/contrib/contenttypes/fields.py", line 448, in related_manager_cls
    self.rel.model._default_manager.__class__,
AttributeError: type object 'AbstractMaster' has no attribute '_default_manager'
>>> 

comment:3 Changed 19 months ago by Wolfgang Grim

Hmm I also don't get it to run now - But I can explain what I want to achieve:

models.py

from __future__ import unicode_literals

from django.db import models

from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
from django.contrib.contenttypes.models import ContentType


class AbstractMaster (models.Model):
    class Meta: abstract=True
    
    detailModel_type = models.ForeignKey(ContentType, blank=True, null=True)
    detailModel_id = models.PositiveIntegerField(blank=True, null=True)
    detail_Model = GenericForeignKey('detailModel_type', 'detailModel_id')
    
class AbstractDetail (models.Model):
    class Meta: abstract=True
    
    name = models.CharField (max_length=255, null=True)
    masterModel_type = models.ForeignKey(ContentType, blank=True, null=True)
    masterModel_id = models.PositiveIntegerField(blank=True, null=True)
    master_Model = GenericForeignKey('masterModel_type', 'masterModel_id')
    
    relatedMasterModel = GenericRelation(AbstractMaster, object_id_field='detailModel_id', content_type_field='detailModel_id', related_query_name='%(app_label)s_%(class)s_detail_model')
    
class MasterTest (AbstractMaster):
    pass

class DetailTest (AbstractDetail):
    pass

python manage.py shell

from testac.models import MasterTest, DetailTest
mt = MasterTest.objects.create()
dt = DetailTest.objects.create(name="Test")
mt.detail_Model = dt
mt.save()
dt.master_Model = mt
dt.save()

mt.detail_Model.name
# Out[8]: 'Test' (So this is working)

MasterTest.objects.filter(detail_Model__name="Test")
# Throws: FieldError: Field 'detail_Model' does not generate an automatic reverse relation and therefore cannot be used for reverse querying. If it is a GenericForeignKey, consider adding a GenericRelation.

comment:4 Changed 19 months ago by Tim Graham

If the use case is invalid, perhaps the error message about " app 'testac' doesn't provide model 'abstractmaster'" could be improved with an explanation about abstract models?

comment:5 Changed 19 months ago by Alex Hill

Hi Tim,

The message when you try to declare a normal relation to an abstract model is this:

ag.DetailTest.something: (fields.E300) Field defines a relation with model 'AbstractSomething', which is either not installed, or is abstract.

I'll look at how we can catch the GenericRelation case in the same branch, because that's much nicer.

Wolfgang: the trend in Django is to alert the user to problems earlier rather than later, and I'm quite sure that's what's happening here. There hasn't actually been any change in functionality since 1.9.

When you do MasterTest.objects.filter(detail_Model__name="Test"), Django doesn't know which table to look for a matching "name" field in, because the relation is generic and that information is stored in the database. It would technically be possible to implement this, and return a heterogeneous collection of objects that inherit from the abstract model, but the code would be messy, it would be completely new ground for the Django ORM, and for such a corner case I just don't think it would be accepted.

There are third-party libraries that provide more polymorphism in Django, and I'd recommend looking into those to see if they meet your needs. Here are a couple:

https://django-polymorphic.readthedocs.io/en/stable/
https://django-model-utils.readthedocs.io/en/latest/managers.html#inheritancemanager

comment:6 Changed 19 months ago by Alex Hill

Owner: changed from nobody to Alex Hill
Status: newassigned

Tim: should I accept this and change its description to indicate that the error message needs improvement, or close it and create a new ticket?

comment:7 Changed 19 months ago by Wolfgang Grim

Hello! At least the following should work right?

from __future__ import unicode_literals

from django.db import models

from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
from django.contrib.contenttypes.models import ContentType
    
class AbstractDetail (models.Model):
    class Meta: abstract=True
    
    relatedTrackModel = GenericRelation("TrackDetail", object_id_field='detailModel_id', content_type_field='detailModel_id', related_query_name='%(app_label)s_%(class)s_detail_model')
  
class TrackDetail (models.Model):
    detailModel_type = models.ForeignKey(ContentType, blank=True, null=True)
    detailModel_id = models.PositiveIntegerField(blank=True, null=True)
    detail_Model = GenericForeignKey('detailModel_type', 'detailModel_id')
    
    added = models.DateTimeField(null=True)
  

class DetailTest (AbstractDetail):
    name = models.CharField(max_length=200, null=True)
    pass

shell:

from testac.models import DetailTest, TrackDetail
from django.utils import timezone


dt = DetailTest.objects.create(name="name1")
track = TrackDetail.objects.create()
track.detail_Model=dt 
track.added = timezone.now()

x = TrackDetail.objects.filter(testac_detailtest_detail_model__name="name")
FieldError: Cannot resolve keyword 'testac_detailtest_detail_model' into field. Choices are: %(app_label)s_%(class)s_detail_model, added, detailModel_id, detailModel_type, detailModel_type_id, detail_Model, id

comment:8 Changed 19 months ago by Alex Hill

That could work, I think.

Does it work in 1.9, or is this a new feature?

Last edited 19 months ago by Alex Hill (previous) (diff)

comment:9 Changed 19 months ago by Wolfgang Grim

It doesn't work neither in 1.9 nor in 1.10 I receive

FieldError: Cannot resolve keyword 'testac_detailtest_detail_model' into field. Choices are: %(app_label)s_%(class)s_detail_model, added, detailModel_id, detailModel_type, detailModel_type_id, detail_Model, id

using dt.relatedTrackModel() I get an empty Queryset

comment:10 Changed 18 months ago by Tim Graham

Component: Core (Other)Database layer (models, ORM)
Summary: GenericRelation doesn't work from AbstractClass to AbstractClass in Django 1.10(.3)Allow declaring a GenericRelation from an abstract model to another abstract model
Triage Stage: UnreviewedAccepted
Type: BugNew feature

Tentatively accepting and retitling based on a skim of the issue.

Note: See TracTickets for help on using tickets.
Back to Top