14

I have defined some models which look like this:

class ABClass(models.Model):
   #common attributes
   class Meta:
     abstract = True

class ConcreteClass1(ABClass):
   #implementation specific attributes

class ConcreteClass2(ABClass):
   #implementation specific attributes

class ModifiedConcreteClass1(ConcreteClass1):
   #implementation specific attributes

I have a view which takes a 'type' parameter which specifies the type of class to display. If 'type=None' I want to display all descendents of ABClass (in this case, ConcreteClass{1,2}, ModifiedConcreteClass1). I have poked around the documentation and cannot seem to find a way of doing this which does not require list of ConcreteClasses be maintained. ABClass.__subclasses__() seemed promising but it returns an empty list when I call it.

At present the best strategy I have is to create a CLASS_LIST variable in models.py which is populated with all descendents of ABClass at runtime using inspect methods. Is there a way to do this using the django api?

Thanks

pisswillis
  • 1,569
  • 2
  • 14
  • 19
  • Why don't you use concrete base class instead? With django-polymodels or django-polymorphic it is really easy to achieve what you want. – utapyngo May 09 '14 at 02:44

5 Answers5

9

A list of ConcreteClasses is already maintained if you have 'django.contrib.contenttypes' in INSTALLED_APPS.

Here is how I do it:

class GetSubclassesMixin(object):
    @classmethod
    def get_subclasses(cls):
        content_types = ContentType.objects.filter(app_label=cls._meta.app_label)
        models = [ct.model_class() for ct in content_types]
        return [model for model in models
                if (model is not None and
                    issubclass(model, cls) and
                    model is not cls)]

class ABClass(GetSubclassesMixin, models.Model):
    pass

When you need a list of subclasses, just call ABClass.get_subclasses().

I am using this with concrete base classes, but I don't see a reason for this not to work with abstract ones.

This approach works even if your concrete subclasses are in different Django apps. Note however that they should have the same app_label as the base class so the code in get_subclasses() can find them.

utapyngo
  • 6,946
  • 3
  • 44
  • 65
8

I haven't look at what Django does when you set abstract True in the backend, but I played with this and I found that this works. Please note, it only works in Python 2.6

from abc import ABCMeta

class ABClass():
    __metaclass__ = ABCMeta

class ConcreteClass1(ABClass):
     pass

class ConcreteClass2(ABClass):
     pass

print ABClass.__subclasses__()

Results in

[<class '__main__.ConcreteClass1'>, <class '__main__.ConcreteClass2'>]

Without using the ABCMeta and __metaclass__, you will receive an empty list.

You can read a good description of what's going on here. Only problem with this, and I'm not sure if it will affect you is I can't quite figure out why when I create an instance of ABClass, it can't find the subclasses. Perhaps by playing around a bit and reading the doc more it might get you somewhere.

Let me know how this works for you, as I am genuinely curious on the true answer.

Bartek
  • 15,269
  • 2
  • 58
  • 65
5

Take a look at this similar question:

How do I access the child classes of an object in django without knowing the name of the child class?

My solution to that was essentially this:

def get_children(self):
    rel_objs = self._meta.get_all_related_objects()
    return [getattr(self, x.get_accessor_name()) for x in rel_objs if x.model != type(self)]

Though there are a number of other good solutions there as well. It seems likely that you actually want the children of the base class, and not just the child classes. If not, the answers there may serve as a motivator for your solution.

Community
  • 1
  • 1
Paul McMillan
  • 19,693
  • 9
  • 57
  • 71
  • Isnt this a horribly inefficient approach? I suppose there are going to be n + 1 queries, where n is number of subclasses. Correct me If I am wrong! – iankit Jul 27 '13 at 12:27
  • Yep, it's pretty inefficient. For most cases like this, we know N to be small enough that we can ignore it. – Paul McMillan Jul 03 '14 at 19:40
2

I was able to use the __subclass__() method but not on an abtract=True class. Removing the abstract=True will add another table to your database (and more overhead, I would guess) but but then you can get all its subclasses as you described.

thornomad
  • 6,707
  • 10
  • 53
  • 78
  • 2
    A nice work around, but I definitely would like to keep the abstract=True to avoid another table being created. – pisswillis Nov 13 '09 at 15:53
1

I realize that this is kind of an ugly solution, but I've done this before:

class ABClass(models.Model):
   #common attributes
   is_ABClass = True
   class Meta:
     abstract = True

class ConcreteClass1(ABClass):
   #implementation specific attributes

class ConcreteClass2(ABClass):
   #implementation specific attributes

class ModifiedConcreteClass1(ConcreteClass1):
   #implementation specific attributes

def get_ABClasses():
   this = modules[__name__]
   return [getattr(this, attr) for attr in dir(this)
           if hasattr(getattr(this, attr), 'is_ABClass') and attr != 'ABClass']
Jeff
  • 3,252
  • 3
  • 23
  • 13