0

I'm trying to hold a kind of table of contents structure in my database. Simplified example:

models.py

class Section (models.Model):
    title = models.CharField(max_length=80)
    order = models.IntegerField()

class SectionClickable(Section):
    link = models.CharField(max_length=80)

class SectionHeading(Section):
    background_color = models.CharField(max_length=6)

views.py

sections = Section.objects.filter(title="Hello!")
for section in sections:
        if(section.sectionheading):
            logger.debug("It's a heading")

I need to do some processing operations if it's a SectionHeading instance, but (as in the Django manual), accessing section.sectionheading will throw a DoesNotExist error if the object is not of type SectionHeading.

I've been looking into alternatives to this kind of problem, and I'm skimming over Generic Foreign Keys in the contenttypes package. However, this seems like it would cause even more headaches at the Django Admin side of things. Could anyone advise on a better solution than the one above?

Edit: I avoided abstract inheritence because of the order field. I would have to join the two QuerySets together and sort them by order

bcoughlan
  • 25,987
  • 18
  • 90
  • 141

4 Answers4

2

well you could check the type:

if isinstance(section, SectionHeading)

but duck typing is generally preferred

edit:

actually, that probably won't work. the object will be a Section. but you can look for the attribute:

if hasattr(section, 'sectionheading')

or

try:
    do_something_with(section.sectionheading)
except AttributeError:
    pass  # i guess it wasn't one of those
second
  • 28,029
  • 7
  • 75
  • 76
1

The solution I came up using involved an extra field pointing to the (rather useful) ContentType class:

class Section(models.Model):
    name = models.CharField(max_length=50)
    content_type = models.ForeignKey(ContentType,editable=False,null=True)

    def __unicode__(self):
        try:
            return self.as_leaf_class().__unicode__()
        except:
            return self.name

    def save(self, *args, **kwargs):
        if(not self.content_type):
            self.content_type = ContentType.objects.get_for_model(self.__class__)
        super(Section, self).save(*args, **kwargs)

    def as_leaf_class(self):
        content_type = self.content_type
        model = content_type.model_class()
        if(model == Section):
            return self
        return model.objects.get(id=self.id)

If you're going through "base" object, I think this solution is pretty nice and comfortable to work with.

mccc
  • 2,354
  • 1
  • 20
  • 22
0

I've been using something similar to what second suggests in his edit:

class SomeBaseModel(models.Model):
    reverse_name_cache = models.CharField(_('relation cache'), max_length=10, 
                                          null=True, editable=False)

    def get_reverse_instance(self):
        try:
            return getattr(self, self.reverse_name_cache)
        except AttributeError:
            for name in ['sectionclickable', 'sectionheading']:
                try:
                    i = getattr(self, name)
                    self.reverse_name_cache = name
                    return i
                except ObjectDoesNotExist:
                    pass

Now, this isn't exactly pretty, but it returns the subclass instance from a central place so I don't need to wrap other statements with try. Perhaps the hardcoding of subclass reverse manager names could be avoided but this approach was enough for my needs.

Community
  • 1
  • 1
Davor Lucic
  • 28,970
  • 8
  • 66
  • 76
0

OP here.

While second's answer is correct for the question, I wanted to add that I believe multi-table inheritence is an inefficient approach for this scenario. Accessing the attribute of the sub-class model would cause a query to occur - thus requiring a query for every row returned. Ouch. As far as I can tell, select_related doesn't work for multi-table inheritence yet.

I also ruled out ContentTypes because it wouldn't do it elegantly enough and seemed to require a lot of queries also.

I settled on using an abstract class:

class Section (models.Model):
    title = models.CharField(max_length=80)
    order = models.IntegerField()

    class Meta:
        abstract=True
        ordering=['order']

Queried both tables:

section_clickables = SectionClickable.objects.filter(video=video)
section_headings= SectionHeading.objects.filter(video=video)

and joined the two querysets together

#Join querysets http://stackoverflow.com/questions/431628/how-to-combine-2-or-more-querysets-in-a-django-view
s = sorted(chain(section_headings, section_clickables), key=attrgetter('order'))

Lastly I made a template tag to check the instance:

from my.models import SectionHeading, SectionClickable

@register.filter()
def is_instance(obj, c):
    try:
        return isinstance(obj, eval(c))
    except:
        raise ObjectDoesNotExist('Class supplied to is_instance could not be found. Import it in the template tag file.')

so that in my template (HamlPy) I could do this:

- if s|is_instance:"SectionClickable"
    %span {{s.title}}
- if s|is_instance:"SectionHeading"
    %span{'style':'color: #{{s.color}};'}
      {{s.title}}

The result is that I only used two queries, one to get the SectionClickable objects and one for the SectionHeading objects

bcoughlan
  • 25,987
  • 18
  • 90
  • 141