django-contentrelations 1.1 documentation

Linking Resources to other models

Overview

Having a unified interface for different models is wonderful, but how do we make these relationships of Resources?

Borrowing some chops from Charles Leifer’s django-genericm2m, Django Supply Closet provides helpful utilities.

  1. All querying and connecting logic is in a single attribute that acts on both model instances and the model class
  2. Any model to be used as the intermediary “through” model and a default model is already available
  3. Uses an optimized lookup for GenericForeignKeys
  4. Provides a InlineModelAdmin class for use in the Django admin
  5. You can dynamically set up the relationship in your settings.py

Adding to a model

Before you start creating relationships, you’ll need to add a RelatedObjectsDescriptor to any model you plan on relating to other models.

Here’s a quick example:

from django.db import models

from contentrelations.related import RelatedObjectsDescriptor


class Food(models.Model):
    name = models.CharField(max_length=255)

    related = RelatedObjectsDescriptor()

    def __unicode__(self):
        return self.name


class Beverage(models.Model):
    name = models.CharField(max_length=255)

    related = RelatedObjectsDescriptor()

    def __unicode__(self):
        return self.name

If you’d like to add relationships to a model that you don’t control (for example the User model from django.contrib.auth), you can use the SETUP_RESOURCES setting:

SUPPLYCLOSET_SETTINGS = {
    'SETUP_RESOURCES': ['auth.User.related']
}

What is the RelatedResource class?

The “related” attribute from the previous examples is the way the generic many-to-many is exposed for each model. Behind-the-scenes it is using RelatedResource.

There’s not really too much that should be weird about this model. It contains two GenericForeignKeys, one to represent the “from” object, the source of the connection, and another to represent to “to” object (what “from” is being connected with).

Creating relationships

A custom model manager is exposed on each model via the RelatedObjectsDescriptor. The API for creating and querying relationships is exposed via this descriptor.

Here is a sample interactive terminal session:

>>> # create a handful of objects to use in our demo
>>> pizza = Food.objects.create(name='pizza')
>>> cereal = Food.objects.create(name='cereal')
>>> beer = Beverage.objects.create(name='beer')
>>> soda = Beverage.objects.create(name='soda')
>>> milk = Beverage.objects.create(name='milk')
>>> healthy_eater = User.objects.create_user('healthy_eater', 'healthy@health.com', 'secret')
>>> chocula = User.objects.create_user('chocula', 'chocula@postcereal.com', 'garlic')

Now that we have some Food, Beverage and User objects, create some connections between them:

>>> rel_obj = pizza.related.connect(beer, relation_type='goes well with')
>>> type(rel_obj) # what did we just create?
<class 'contentrelations.related.RelatedResource'>

The object that represents the connection is an instance of whatever is passed to the RelatedObjectDescriptor when it is added to a model. The default is RelatedResource. Here are the interesting properties of the new related object:

>>> rel_obj.source
<Food: pizza>
>>> rel_obj.object
<Beverage: beer>
>>> rel_obj.relation_type
'goes well with'

Querying relationships

These relationships can be queried:

>>> pizza.related.all() # find all objects that pizza has been related to
[<RelatedResource: pizza related to beer (goes well with)>]

Retrieving the objects instead of the RelatedResource objects

When the relationship is defined with a GenericForeignKey, as is the case here, the :py:class`RelatedObjectsDescriptor` (here defined as related) will return a special Django QuerySet class that provides an optimized lookup of any GenericForeignKey-ed objects:

>>> type(pizza.related.all())
<class 'contentrelations.generic.GFKOptimizedQuerySet'>
>>> pizza.related.all().generic_objects() # traverse the GFK relationships
[<Beverage: beer>]

If the object on the back-side of the relationship also has a RelatedObjectsDescriptor with the same intermediary model, reverse lookups are possible:

>>> beer.related.related_to() # query the back-side of the relationship
[<RelatedResource: pizza related to beer (goes well with)>]

Create some more connections - any combination of models can be used. Below I’m connectiong a Food (cereal) to both Beverage objects (milk) and User objects (Chocula):

>>> cereal.related.connect(milk) # connecting to a beverage
<RelatedResource: cereal related to milk>
>>> cereal.related.connect(chocula) # connecting to a user
<RelatedResource: cereal related to chocula>

>>> cereal.related.all() # show what cereal is related to
[<RelatedResource: cereal related to chocula>,
 <RelatedResource: cereal related to milk>]

>>> chocula.related.all() # relationships are ONE WAY
[]
>>> chocula.related.related_to() # querying the backside shows what has been connected to chocula
[<RelatedResource: cereal related to chocula ("")>]

Querying all relations to a Model

Also worth noting is that the RelatedObjectsDescriptor works on both the instance-level (pizza) and the class-level (Food), so if we wanted to see all objects related to foods:

>>> Food.related.all() # anything that has been related to a food
[<RelatedResource: cereal related to chocula>,
 <RelatedResource: cereal related to milk>,
 <RelatedResource: pizza related to beer (goes well with)>]

Using a custom “through” model

It’s possible to use a custom “through” model in place of the default RelatedResource. If you know you’re only going to be using a couple models, this can be a handy way to save queries. Here’s another silly example where we have a RelatedBeverage model that our Food model will use:

class RelatedBeverage(models.Model):
    food = models.ForeignKey('Food')
    beverage = models.ForeignKey('Beverage')

    class Meta:
        ordering = ('-id',)

class Food(models.Model):
    # ... same as above except for this new attribute:
    related_beverages = RelatedObjectsDescriptor(RelatedBeverage, 'food', 'beverage')

The “related_beverages” attribute is an instance of RelatedObjectsDescriptor, but it is instantiated with a couple of arguments:

  • RelatedBeverage: the model to be used to hold the “connections”
  • food: the field name on the above model which maps to the “from” object
  • beverage: the field name which maps to the “to” object

Continuing the shell session from above with the same models, foods can be connected to beverages using the new “related_beverages” attribute:

>>> pizza.related_beverages.connect(soda)
<RelatedBeverage: RelatedBeverage object>

Querying provides the same interface, but since the “to” object is a direct ForeignKey to Beverage, a normal Django QuerySet is used:

>>> pizza.related_beverages.all()
[<RelatedBeverage: RelatedBeverage object>]
>>> type(pizza.related_beverages.all())
<class 'django.db.models.query.QuerySet'>

A TypeError will be raised if you try to connect an invalid object, such as a Person to the “related_beverages”:

>>> pizza.related_beverages.connect(mario)
*** TypeError: Unable to query ...

And lastly, just like before, its possible to query on the class to get all the RelatedBeverage objects for our foods:

>>> Food.related_beverages.all()
[<RelatedBeverage: RelatedBeverage object>]

Adding to the admin

Add RelatedInline to your inlines:

from contentrelations.admin import RelatedInline

class SimpleAdmin(admin.ModelAdmin):
    list_display = ('name', )
    search_fields = ('name',)
    inlines = [RelatedInline]

If you changed the name from the default related, you need to give the inline a bit of help so it can find the name of the related field.

from contentrelations.admin import RelatedInline

    class AlternateInline(RelatedInline):
        rel_name = 'resources'

    class AnotherAdmin(admin.ModelAdmin):
        list_display = ('name', )
        search_fields = ('name',)
        inlines = [AlternateInline]

To change the name of the inline fieldset:

from contentrelations.admin import RelatedInline

    class AlternateInline(RelatedInline):
        verbose_name_plural = "Resource Carousel"

    class AnotherAdmin(admin.ModelAdmin):
        list_display = ('name', )
        search_fields = ('name',)
        inlines = [AlternateInline]

To exclude either the relation_type or order field you have to include the excluded fields in the parent class:

from contentrelations.admin import RelatedInline

    class AlternateInline(RelatedInline):
        exclude = ('source_type', 'source_id', 'relation_type')

    class AnotherAdmin(admin.ModelAdmin):
        list_display = ('name', )
        search_fields = ('name',)
        inlines = [AlternateInline]