views:

642

answers:

2

Given:

from django.db import models

class Food(models.Model):
     """Food, by name."""
     name = models.CharField(max_length=25)

class Cat(models.Model):
     """A cat eats one type of food"""
     food = models.ForeignKey(Food)

class Cow(models.Model):
     """A cow eats one type of food"""
     food = models.ForeignKey(Food)

class Human(models.Model):
     """A human may eat lots of types of food"""
     food = models.ManyToManyField(Food)

How can one, given only the class Food, get a set of all classes that it has "reverse relationships" to. I.e. given the class Food, how can one get the classes Cat, Cow and Human.

I would think it's possible because Food has the three "reverse relations": Food.cat_set, Food.cow_set, and Food.human_set.

Help's appreciated & thank you!

+6  A: 

Some digging in the source code revealed:

django/db/models/options.py:

def get_all_related_objects(self, local_only=False):

def get_all_related_many_to_many_objects(self, local_only=False)

And, using these functions on the models from above, you hypothetically get:

>>> Food._meta.get_all_related_objects()
[<RelatedObject: app_label:cow related to food>,
    <RelatedObject: app_label:cat related to food>,]

>>> Food._meta.get_all_related_many_to_many_objects()
[<RelatedObject: app_label:human related to food>,]

# and, per django/db/models/related.py
# you can retrieve the model with
>>> Food._meta.get_all_related_objects()[0].model
<class 'app_label.models.Cow'>

Note: I hear Model._meta is 'unstable', and perhaps ought not to be relied upon in the post Django-1.0 world.

Thanks for reading. :)

Brian M. Hunt
+6  A: 

Either

A) Use multiple table inheritance and create a "Eater" base class, that Cat, Cow and Human inherit from.

B) Use a Generic Relation, where Food could be linked to any other Model.

Those are well-documented and officially supported features, you'd better stick to them to keep your own code clean, avoid workarounds and be sure it'll be still supported in the future.

-- EDIT ( A.k.a. "how to be a reputation whore" )

So, here is a recipe for that particular case.

Let's assume you absolutely want separate models for Cat, Cow and Human. In a real-world application, you want to ask to yourself why a "category" field wouldn't do the job.

It's easier to get to the "real" class through generic relations, so here is the implementation for B. We can't have that 'food' field in Person, Cat or Cow, or we'll run into the same problems. So we'll create an intermediary "FoodConsumer" model. We'll have to write additional validation tests if we don't want more than one food for an instance.

from django.db import models
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes import generic

class Food(models.Model):
     """Food, by name."""
     name = models.CharField(max_length=25)

# ConsumedFood has a foreign key to Food, and a "eaten_by" generic relation
class ConsumedFood(models.Model):
    food = models.ForeignKey(Food, related_name="eaters")
    content_type = models.ForeignKey(ContentType, null=True)
    object_id = models.PositiveIntegerField(null=True)
    eaten_by = generic.GenericForeignKey('content_type', 'object_id')

class Person(models.Model):
    first_name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)
    birth_date = models.DateField()
    address = models.CharField(max_length=100)
    city = models.CharField(max_length=50)
    foods = generic.GenericRelation(ConsumedFood)

class Cat(models.Model):
    name = models.CharField(max_length=50)
    foods = generic.GenericRelation(ConsumedFood)    

class Cow(models.Model):
    farmer = models.ForeignKey(Person)
    foods = generic.GenericRelation(ConsumedFood)

Now, to demonstrate it let's just write this working doctest:

"""
>>> from models import *

Create some food records

>>> weed = Food(name="weed")
>>> weed.save()

>>> burger = Food(name="burger")
>>> burger.save()

>>> pet_food = Food(name="Pet food")
>>> pet_food.save()

John the farmer likes burgers

>>> john = Person(first_name="John", last_name="Farmer", birth_date="1960-10-12")
>>> john.save()
>>> john.foods.create(food=burger)
<ConsumedFood: ConsumedFood object>

Wilma the cow eats weed

>>> wilma = Cow(farmer=john)
>>> wilma.save()
>>> wilma.foods.create(food=weed)
<ConsumedFood: ConsumedFood object>

Felix the cat likes pet food

>>> felix = Cat(name="felix")
>>> felix.save()
>>> pet_food.eaters.create(eaten_by=felix)
<ConsumedFood: ConsumedFood object>

What food john likes again ?
>>> john.foods.all()[0].food.name
u'burger'

Who's getting pet food ?
>>> living_thing = pet_food.eaters.all()[0].eaten_by
>>> isinstance(living_thing,Cow)
False
>>> isinstance(living_thing,Cat)
True

John's farm is in fire ! He looses his cow.
>>> wilma.delete()

John is a lot poorer right now
>>> john.foods.clear()
>>> john.foods.create(food=pet_food)
<ConsumedFood: ConsumedFood object>

Who's eating pet food now ?
>>> for consumed_food in pet_food.eaters.all():
...    consumed_food.eaten_by
<Cat: Cat object>
<Person: Person object>

Get the second pet food eater
>>> living_thing = pet_food.eaters.all()[1].eaten_by

Try to find if it's a person and reveal his name
>>> if isinstance(living_thing,Person): living_thing.first_name
u'John'

"""
vincent
Hi Vincent, thanks for the response. Examples of how exactly to use MTI or GR's to answer the question would be much appreciated :).
Brian M. Hunt
Brilliant, btw. :) Thanks!
Brian M. Hunt