django-model-utils
django-model-utils copied to clipboard
InheritanceManager doesn't work with Proxy Model
I created an app to demonstrate this bug here https://github.com/chhantyal/mptt-test
Please see the models here https://github.com/chhantyal/mptt-test/blob/master/mptt_test/app/models.py
When I use select_subclasses(), it doesn't give subclass object but superclass object (in this case ProxyPage object itself).
In [1]: from mptt_test.app.models import Blog, PageProxy
In [2]: Blog.objects.create(title='Title', quote='blog')
Out[2]: <Blog: Blog object>
In [3]: PageProxy.objects.all().select_subclasses()
Out[3]: [<PageProxy: PageProxy object>]
Thanks for the report! This does look like a bug. I'm not sure if or when I'll have time to look into it, but I'd be happy to review a pull request.
At least at a glance, the problem appears to stem from model._meta.get_all_related_objects(), which for a proxy model (Pageproxy) yields []; it is instead bound to the concrete parent (Page) as [<OneToOneRel: ...>]
Further investigation, across both Django 1.7 and 1.8 (because both finally raise exceptions for select_related), given the following:
class InheritanceProxyConcrete(models.Model):
pass
class InheritanceProxy(InheritanceProxyConcrete):
class Meta:
proxy = True
class InheritanceProxyChild(InheritanceProxy):
pass
the following is given when trying to use a normal select_related (ie: before hitting our InheritanceManager magic):
>>> InheritanceProxyConcrete.objects.select_related('anything')
FieldError: Invalid field name(s) given in select_related: 'anything'. Choices are: inheritanceproxychild
>>> InheritanceProxy.objects.select_related('anything')
FieldError: Invalid field name(s) given in select_related: 'anything'. Choices are: (none)
which sort of implies to me that we couldn't support it currently anyway, even if we could resolve the correct model by doing something like:
if model._meta.proxy is True:
relations = model._meta.proxy_for_model._meta.get_all_related_objects()
else:
relations = model._meta.get_all_related_objects()
If either of you can replicate my findings, I'll open a ticket upstream and see what happens.
Hi @kezabelle - I haven't explored it myself, but based on what you're seeing it certainly does look like this is an upstream bug. If select_related doesn't work on proxy models, then select_subclasses isn't going to work either. So the correct fix seems to be to make select_related work first.
I've opened a ticket upstream reflecting my findings. If it comes back as my mistake, I can re-evaluate the problem in this ticket, possibly with a better understanding of what I got wrong ;)
Hi @kezabelle !
The corresponding ticket has been fixed/closed.
@chhantyal you can try to upgrade to Django 1.10 and see if you still encounter this issue.
Hi. I have the same issue.
Django==2.0.3 django-model-utils==3.0.0
class Offer(models.Model):
objects = InheritanceQuerySet.as_manager()
class Meta:
verbose_name = _('offer')
verbose_name_plural = _('offers')
class Order(Offer):
class Meta:
managed = False
proxy = True
verbose_name = _('order')
verbose_name_plural = _('orders')
result = Offer.objects.select_subclasses()
All instances in result are instances of Offer.
Any ideas please?
Same issue with:
- Django 2.2
- django-model-utils 3.1.2
Hi,
This hit me as well.
However, looking a bit more in detail into it, I feel like model-utils does the right thing / the best it can.
Proxy models in django only work on the Python level - there is no information stored about the proxy in the database (the table is shared with the base model). So when you query objects from a certain model (class), you get instances of that class. This is by design.
In that situation, model-utils has no way to know which model is "the right one" - because both are, there's no difference on the db level! So it uses the non-proxy one in select_subclasses.
See also https://docs.djangoproject.com/en/3.1/topics/db/models/#querysets-still-return-the-model-that-was-requested.
(note: this is only my understanding of it, maybe I'm wrong)
EDIT:
To complete my point above, I would like to emphasize that proxy models in django are not subclasses in the MTI sense - they are other "views" of the same data. Accordingly, if you have a proxy model B to some model A, you can create instances of A or B and retrieve any of them later as A or B instance transparently.
In django, which model you want to retrieve instances of is explicit: from the model you take the manager of, from the definition of relation fields, from the arguments of select_related, etc.
In model-utils however the whole point of select_subclasses is to "guess" the return types, by scanning through possible subclasses in the db. So proxy models cannot be retrieved as they do not correspond to tables in the db on their own (only present as a row in django_content_type).
One case where model-utils could do better (or forbid this case altogether) is when one explicitly passes a proxy model as argument in select_subclasses:
class C(models.Model):
objects = InheritanceManager()
class D(C):
pass
class E(D):
class Meta:
proxy = True
>>> D().save() # or E().save(), doesn't make a difference
>>> C.objects.select_subclasses()
<InheritanceQuerySet [<D: D object (1)>]>
>>> C.objects.select_subclasses(D)
<InheritanceQuerySet [<D: D object (1)>]>
>>> D.objects.select_subclasses()
<InheritanceQuerySet [<D: D object (1)>]>
>>> E.objects.select_subclasses(E)
<InheritanceQuerySet [<E: E object (1)>]>
>>> C.objects.select_subclasses(E)
<InheritanceQuerySet [<D: D object (1)>]>
>>> D.objects.select_subclasses(E)
Traceback (most recent call last):
File "<console>", line 1, in <module>
File "/home/matpi/.local/share/virtualenvs/tmp_django_mti/lib/python3.8/site-packages/model_utils/managers.py", line 219, in select_subclasses
return self.get_queryset().select_subclasses(*subclasses)
File "/home/matpi/.local/share/virtualenvs/tmp_django_mti/lib/python3.8/site-packages/model_utils/managers.py", line 70, in select_subclasses
raise ValueError(
ValueError: '' is not in the discovered subclasses, tried:
The first four calls to select_subclasses make sense. One could however argue that the last two should both succeed and return E instances.