Polymorphism in Django models - django

I'm developing django application, and I have such a model structure
class Animal(models.Model):
aul = models.ForeignKey(Aul)
age = models.IntegerField()
def __unicode__(self):
return u'Animal'
class Sheep(Animal):
wool = models.IntegerField()
def __unicode__(self):
return u'Sheep'
And I pass animal_set to template and output every object like this {{ animal }}. It outputs Animal, but I created objects of sheep type and want to use __unicode__ method of sheep not of animal.
Do polymorphism work in Django models? I have found several answers, but there are snippets of code to write inside models, but I'm interested in native solutions.

At the time of writing, Django latest version was 1.2
But it needs some additional elements to work.
You need to assign a custom models.Manager object for each animal model which will call its own custom QuerySet object.
Basically, instead of returning Animal instances (this is what you get), SubclassingQuerySet calls as_leaf_class() method to check if item's model is Animal or not - if it is, then just return it, otherwise perform search in its model context. Thats it.
#models.py
from django.db import models
from django.contrib.contenttypes.models import ContentType
from django.db.models.query import QuerySet
class SubclassingQuerySet(QuerySet):
def __getitem__(self, k):
result = super(SubclassingQuerySet, self).__getitem__(k)
if isinstance(result, models.Model):
return result.as_leaf_class()
return result
def __iter__(self):
for item in super(SubclassingQuerySet, self).__iter__():
yield item.as_leaf_class()
class AnimalManager(models.Manager):
def get_query_set(self): # Use get_queryset for Django >= 1.6
return SubclassingQuerySet(self.model)
class Animal(models.Model):
name = models.CharField(max_length=100)
content_type = models.ForeignKey(ContentType, editable=False, null=True)
objects = AnimalManager()
def __unicode__(self):
return "Animal: %s" % (self.name)
def save(self, *args, **kwargs):
if not self.content_type:
self.content_type = ContentType.objects.get_for_model(self.__class__)
super(Animal, self).save(*args, **kwargs)
def as_leaf_class(self):
content_type = self.content_type
model = content_type.model_class()
if model == Animal:
return self
return model.objects.get(id=self.id)
class Sheep(Animal):
wool = models.IntegerField()
objects = AnimalManager()
def __unicode__(self):
return 'Sheep: %s' % (self.name)
Testing:
>>> from animals.models import *
>>> Animal.objects.all()
[<Sheep: Sheep: White sheep>, <Animal: Animal: Dog>]
>>> s, d = Animal.objects.all()
>>> str(s)
'Sheep: White sheep'
>>> str(d)
'Animal: Dog'
>>>

You might be successful by accessing {{ animal.sheep }} - the model inheritance is not what you would think, there is a heavy metaclass machinery under the cover that "converts" such inheritance into an implicit OneToOneField relationship.

There's a very simple django app called django-polymorphic-models that helps you with that. It will provide you with a downcast() method on the model itself that will return your "child" object, as well as a special queryset class to deal with these problems!
It can also be very useful to know that using select_related() on the base model's queryset will also get the child objects, that are referenced through a OneToOneField, which can be a nice performance boost sometimes!

I would recommend using Django proxy models, e.g. if you have the base model Animal which is subclassed by Sheep and Horse you would use:
class Animal(models.Model):
pass
class Horse(Animal):
class Meta(Animal.Meta):
proxy = True
class Sheep(Animal):
class Meta(Animal.Meta):
proxy = True
This is not what Proxy models are intended for but I wouldn't recommend using Django polymorphism unless you need the benefits of storing model specific data in separate tables. If you have a hundred horse specific attributes that all have default values stored in the database, and then only have 2 horse objects, but have a million sheep, you have a million rows, each with a hundred horse specific values you don't care about, but again this is only really relevant if you don't have enough disk space, which is unlikely. When polymorphism works well it's fine, but when it doesn't it's a pain.

You should check this answer: https://stackoverflow.com/a/929982/684253
The solution it proposes is similar to using django-polymorphic-models, that was already mentioned by #lazerscience. But I'd say django-model-utils is a little bit better documented than django-polymorphic, and the library is easier to use. Check the readme under "Inheritance Manager": https://github.com/carljm/django-model-utils/#readme

Related

Filter by custom QuerySet of a related model in Django

Let's say I have two models: Book and Author
class Author(models.Model):
name = models.CharField()
country = models.CharField()
approved = models.BooleanField()
class Book(models.Model):
title = models.CharField()
approved = models.BooleanField()
author = models.ForeignKey(Author)
Each of the two models has an approved attribute, which shows or hides the object from the website. If the Book is not approved, it is hidden. If the Author is not approved, all his books are hidden.
In order to define these criteria in a DRY manner, making a custom QuerySet looks like a perfect solution:
class AuthorQuerySet(models.query.QuerySet):
def for_site():
return self.filter(approved=True)
class BookQuerySet(models.query.QuerySet):
def for_site():
reuturn self.filter(approved=True).filter(author__approved=True)
After hooking up these QuerysSets to the corresponding models, they can be queried like this: Book.objects.for_site(), without the need to hardcode all the filtering every time.
Nevertheless, this solution is still not perfect. Later I can decide to add another filter to authors:
class AuthorQuerySet(models.query.QuerySet):
def for_site():
return self.filter(approved=True).exclude(country='Problematic Country')
but this new filter will only work in Author.objects.for_site(), but not in Book.objects.for_site(), since there it is hardcoded.
So my questions is: is it possible to apply a custom queryset of a related model when filtering on a different model, so that it looks similar to this:
class BookQuerySet(models.query.QuerySet):
def for_site():
reuturn self.filter(approved=True).filter(author__for_site=True)
where for_site is a custom QuerySet of the Author model.
I think, I've come up with a solution based on Q objects, which are described in the official documentation. This is definitely not the most elegant solution one can invent, but it works. See the code below.
from django.db import models
from django.db.models import Q
######## Custom querysets
class QuerySetRelated(models.query.QuerySet):
"""Queryset that can be applied in filters on related models"""
#classmethod
def _qq(cls, q, related_name):
"""Returns a Q object or a QuerySet filtered with the Q object, prepending fields with the related_name if specified"""
if not related_name:
# Returning Q object without changes
return q
# Recursively updating keywords in this and nested Q objects
for i_child in range(len(q.children)):
child = q.children[i_child]
if isinstance(child, Q):
q.children[i_child] = cls._qq(child, related_name)
else:
q.children[i_child] = ('__'.join([related_name, child[0]]), child[1])
return q
class AuthorQuerySet(QuerySetRelated):
#classmethod
def for_site_q(cls, q_prefix=None):
q = Q(approved=True)
q = q & ~Q(country='Problematic Country')
return cls._qq(q, q_prefix)
def for_site(self):
return self.filter(self.for_site_q())
class BookQuerySet(QuerySetRelated):
#classmethod
def for_site_q(cls, q_prefix=None):
q = Q(approved=True) & AuthorQuerySet.for_site_q('author')
return cls._qq(q, q_prefix)
def for_site(self):
return self.filter(self.for_site_q())
######## Models
class Author(models.Model):
name = models.CharField(max_length=255)
country = models.CharField(max_length=255)
approved = models.BooleanField()
objects = AuthorQuerySet.as_manager()
class Book(models.Model):
title = models.CharField(max_length=255)
approved = models.BooleanField()
author = models.ForeignKey(Author)
objects = BookQuerySet.as_manager()
This way, whenever the AuthorQuerySet.for_site_q() method is changed, it will be automatically reflected in the BookQuerySet.for_site() method.
Here the custom QuerySet classes perform selection at the class level by combining different Q objects, instead of using filter() or exclude() methods at the object level. Having a Q object allows 3 different ways of using it:
put it inside a filter() call, to filter a queryset in place;
combine it with other Q objects using & (AND) or | (OR) operators;
dynamically change names of keywords used in the Q objects by accessing its children attribute, which is defined in the superclass django.utils.tree.Node
The _qq() method defined in every custom QuerySet class takes care of prepending the specified related_name to all filter keys.
If we have a q = Q(approved=True) object, then we can have the following outputs:
self._qq(q) – is equivalent to self.filter(approved=True);
self._qq(q, 'author') – is equivalent to self.filter(author__approved=True)
This solution still has serious drawbacks:
one has to import and call custom QuerySet class of the related model explicitly;
for each filter method one has to define two methods filter_q (class method) and filter (instance method);
UPDATE: The drawback 2. can be partially reduced by creating filter methods dynamically:
# in class QuerySetRelated
#classmethod
def add_filters(cls, names):
for name in names:
method_q = getattr(cls, '{0:s}_q'.format(name))
def function(self, *args, **kwargs):
return self.filter(method_q(*args, **kwargs))
setattr(cls, name, function)
AuthorQuerySet.add_filters(['for_site'])
BookQuerySet.add_filters(['for_site'])
Therefore, if someone comes up with a more elegant solution, please suggest it. It would be very appreciated.

How to access custom QuerySet methods from the Manager of a ForeignKey

I'm using Django Managers to make a higher API to interact with my database and keeping my code cleaner and more readable. But in case I have a Foreignkey relationship, I can't use the manager of the ForeignKey model. The queries are more complex as below, but I just simplified the example so, It can be easier to read and get the idea of the question:
models.py:
class Community(models.Model):
objects = CommunityManager()
...
class Inscription(models.Model):
objects = InscriptionManager()
...
community = models.ForeignKey("Community", related_name="inscriptions")
created_at = models.DateTimeField()
managers.py:
from datetime import date
from django.db import models
class InscriptionQuerySet(models.query.QuerySet):
def by_day(self, day=date.today()):
return self.filter(created_at__day=day)
... # more queries
class InscriptionManager(models.Manager):
def get_query_set(self):
return InscriptionQuerySet(self.model, using=self._db)
def today(self):
return self.get_query_set().by_day()
... # more queries
class CommunityQuerySet(models.query.QuerySet):
def by_type(self, type):
return self.filter(type=type)
... # more queries
class CommunityManager(models.Manager):
def get_query_set(self):
return OrganistaionQuerySet(self.model, using=self._db)
def by_type(self, type):
return self.get_query_set().by_type(type)
... # more queries
Usage:
Inscription.objects.by_day() # return correctly all the inscriptions made today
Community.objects.by_type('type1') # return correctly all Communities that match
Problem: but here lies the problem
community_b = Community.objects.get(id=12)
community_b.inscriptions.by_day()
>>> AttributeError: 'ForeignRelatedObjectsDescriptor' object has no attribute 'by_day'
How can I fixe this. How to customize the manager to take in consideration the models relation.
I don't see the need of both Manager and QuerySet in your approach. You could just do away with a QuerySet. This is how I generally implement custom managers, and I tried accessing custom manager method on reverse relationship in one of my projects, and it works fine:
class InscriptionQuerySet(models.QuerySet):
def by_day(self, day=date.today()):
return self.filter(created_at__day=day)
def today(self):
return self.by_day()
class CommunityQuerySet(models.QuerySet):
def by_type(self, type):
return self.filter(type=type)
And then in your models, change your objects like this:
class Community(models.Model):
objects = CommunityQuerySet.as_manager()
...
class Inscription(models.Model):
objects = InscriptionQuerySet.as_manager()
I think you would be able to access custom queryset method with this setup.

Django abstract models with M2M fields

Let's suppose I have the following:
class Base(Model):
m2m_1 = ManyToManyField("SomeModel1")
m2m_2 = ManyToManyField("SomeModel2")
class Meta:
abstract = True
class A(Base):
def __init__(self):
super(A, self).__init__()
pass
class B(Base):
def __init__(self):
super(B, self).__init__()
pass
However, I cannot do that because it requires related name for M2M field. However, that does not help as the model is abstract and django tries to create the same related name for both A and B models.
Any ideas how to specify related names for each model separately or even do not use them at all?
The answer is right in the docs for abstract classes (under section entitled "Be careful with related_name"):
m2m = models.ManyToManyField(OtherModel, related_name="%(app_label)s_%(class)s_related")

Geo Django Subclassing Queryset

I'm using GeoDjango to search for a bunch of locations of different types. For example, both House and Appartment models are subclasses of Location.
Using the Subclassing Queryset below, I'm able to do something like Location.objects.all() and have it return to me [<House: myhouse>, <House: yourhouse>, <Appartment: myappartment>], which is my desire.
However, I also want to determine the distance to each location. Normally, without the Subclassing Queryset, the code from Exhibit 2 returns for me the distances from the given point to each location.... [ (<Location: Location object>, Distance(m=866.092847284))]
However, if I try to find the distances using the Subclassing Querysets, I get an error such as:
AttributeError: 'House' object has no attribute 'distance'
Do you know how I can preserve the ability return a queryset of subclassed objects yet have the distance property available on the subclass objects? Any advice is much appreciated.
Exhibit 1:
class SubclassingQuerySet(models.query.GeoQuerySet):
def __getitem__(self, k):
result = super(SubclassingQuerySet, self).__getitem__(k)
if isinstance(result, models.Model) :
return result.as_leaf_class()
else :
return result
def __iter__(self):
for item in super(SubclassingQuerySet, self).__iter__():
yield item.as_leaf_class()
class LocationManager(models.GeoManager):
def get_query_set(self):
return SubclassingQuerySet(self.model)
class Location(models.Model):
content_type = models.ForeignKey(ContentType,editable=False,null=True)
objects = LocationManager()
class House(Location):
address = models.CharField(max_length=255, blank=True, null=True)
objects = LocationManager()
class Appartment(Location):
address = models.CharField(max_length=255, blank=True, null=True)
unit = models.CharField(max_length=255, blank=True, null=True)
objects = LocationManager()
Exhibit 2:
from django.contrib.gis.measure import D
from django.contrib.gis.geos import fromstr
ref_pnt = fromstr('POINT(-87.627778 41.881944)')
location_objs = Location.objects.filter(
point__distance_lte=(ref_pnt, D(m=1000)
)).distance(ref_pnt).order_by('distance')
[ (l, l.distance) for l in location_objs.distance(ref_pnt) ] # <--- errors out here
I'm busy trying to solve this one to. How about this:
class QuerySetManager(models.GeoManager):
'''
Generates a new QuerySet method and extends the original query object manager in the Model
'''
def get_query_set(self):
return super(QuerySetManager, self).get_query_set()
And the rest can follow from this DjangoSnippet.
You have to reassign the manager in all subclasses.
From Django documentation:
Managers defined on non-abstract base classes are not inherited by child classes. If you want to reuse a manager from a non-abstract base, redeclare it explicitly on the child class. These sorts of managers are likely to be fairly specific to the class they are defined on, so inheriting them can often lead to unexpected results (particularly as far as the default manager goes). Therefore, they aren't passed onto child classes.
https://docs.djangoproject.com/en/dev/topics/db/managers/#custom-managers-and-model-inheritance

Django Managers

I have the following models code :
from django.db import models
from categories.models import Category
class MusicManager(models.Manager):
def get_query_set(self):
return super(MusicManager, self).get_query_set().filter(category='Music')
def count_music(self):
return self.all().count()
class SportManager(models.Manager):
def get_query_set(self):
return super(MusicManager, self).get_query_set().filter(category='Sport')
class Event(models.Model):
title = models.CharField(max_length=120)
category = models.ForeignKey(Category)
objects = models.Manager()
music = MusicManager()
sport = SportManager()
Now by registering MusicManager() and SportManager() I am able to call Event.music.all() and Event.sport.all() queries. But how can I create Event.music.count() ? Should I call self.all() in count_music() function of MusicManager to query only on elements with 'Music' category or do I still need to filter through them in search for category first ?
You can think of a manager as a 'starting point' for a query - you can continue to chain filters just as if you'd started out with the default manager.
For example, Event.objects.filter(category='Music').filter(title='Beatles Concert') is functionally equivalent to Event.music.filter(title='Beatles Concert')
So, as Daniel says, you don't really need to do anything special, just choose one of your custom managers instead of objects and go from there.
You don't need to do anything (and your count_music method is unnecessary). The count() method will use the existing query as defined by get_query_set.