Skip to main content

How to use Django's Proxy Models

Modeling custom behavior without touching the database

Django’s proxy models are one of those features that I remember reading about and thinking, “oh, cool… I guess” and then moving along. From time to time, they do come in very handy, but there’s not that much written about how to best make use of them

What are they?

A proxy model is just another class that provides a different interface for the same underlying database model.
That’s it. Really.
A proxy model is a subclass of a database-table defining model. Typically creating a subclass of a model results in a new database table with a reference back to the original model’s table - multi-table inheritance.
A proxy model doesn’t get its own database table. Instead it operates on the original table.
class MyModel(models.Model):
    name = models.CharField(max_length=100)

    def __str__(self):
        return self.name

class UpperModel(MyModel):
    class Meta:
        proxy = True

    def __str__(self):
        return self.name.upper()
In the contrived example above we’ve created a second model class, a proxy class. If you were to compare the field values for the instance at primary key 12 for either an instance of MyModel or UpperModel they would be exactly the same. The only difference is that the UpperModel instance would print as the uppercase name of the model.
It’s a contrived example, so let’s examine the real usefulness.

When to use them?

There are certainly legacy or brownfield use cases, when you need to fit your models around an existing database, but they’re useful in new projects too.
Let’s say you’ve got a content type that has some different sub-types which all differ in some minor way. There’s the main content type, which we’ll call a “story”. Some stories have slightly different content needs - one may require an image file that is displayed and another a geographic reference. With these exceptions the content types all look roughly similar and behave in the same way.
You could create distinct models for each of these with a distinct database table for each, but do we really need a separate table for each of these? This is unnecessary and it’ll make aggregating these stories significantly more challenging if we want to use the ORM.
An alternative is to create a field - a column, if you like - for tracking the type of story and then creating proxy models for each story type based on this value.

Working with them in practice

In our example we want to be able to provide simple editing interfaces to all of these models to the content editors. On the front end, there’s going to be a single list view of all stories, and then list views for the different types, as well as detailed views for individual stories.
Aggregating heterogeneous types isn’t straight forward using Django’s ORM. We could make use of the ContentType model and filter on objects that were in one of our several story model types, or we could join together several querysets as Python lists. We could also perform a UNION query in a Raw queryset. That’s pretty appealing, but the one thing I don’t like about adding too many raw queries into Django apps is the fragility.
So what we want to do is use proxy models to create unique editing experiences with one underlying database model.
We’ll start with the base model.
Here’s a simplified and contrived example Story model.
STORY_TYPES = (
    ('f', 'Feature'),
    ('i', 'Infographic'),
    ('g', 'Gallery'),
)

class Story(models.Model):
    type = models.CharField(max_length=1, choices=STORY_TYPES)
    title = models.CharField(max_length=100)
    body = models.TextField(blank=True, null=True)
    infographic = models.ImageField(blank=True, null=True)
    link = models.URLField(blank=True, null=True)
    gallery = models.ForeignKey(Gallery, blank=True, null=True)
The field of note here is type. This is going to be used to toggle the story type.
Our proxy models will look like this:
class FeatureStory(Story):
    objects = FeatureManager()
    class Meta:
        proxy = True

class InfographicStory(Story):
    objects = InfographicManager()
    class Meta:
        proxy = True

class GalleryStory(Story):
    objects = GalleryManager()
    class Meta:
        proxy = True
The proxy = True statement in the class Meta section indicates that these classes are proxy models. It’s the manager class that we’re using here to differentiate the classes.
Each manager class simply returns a queryset that filters for the appropriate typevalue.
class FeatureManager(models.Manager):
    def get_queryset(self):
        return super(FeatureManager, self).get_queryset().filter(
            type='f')

class InfographicManager(models.Manager):
    def get_queryset(self):
        return super(InfographicManager, self).get_queryset().filter(
            type='i')

class GalleryManager(models.Manager):
    def get_queryset(self):
        return super(GalleryManager, self).get_queryset().filter(
            type='g')
Well what good is this, you say?
For one, it provides a nice interface for content type specific views. If you have an infographic view, for example, the view can fetch the specific infographic from Infographic.objects.get(pk=view_pk).
And moreover, now we can create distinct admin interfaces.

In the admin

The benefit here is that we can create different admin interfaces for different objects that happen to be stored in the same database table. Perhaps a blog post has an author and an author photo while a news item should only have a title, summary, and outbound link.
Since we have proxy models for these, we can create different admin interfaces (remember that Django picks up on registered models).

Managers and a better interface

In this second example, we have media assets that need to be used in a gallery. There are two useful models: an image and a video.
class MediaAsset(models.Admin):
    type = models.CharField(max_length=5, default='image')
    caption = models.TextField()
    video_url = models.URLField(blank=True, null=True)
    image = models.ImageField(blank=True, null=True)

class Image(MediaAsset):
    objects = ImageManager()
    class Meta:
        proxy = True

class Video(MediaAsset):
    objects = VideoManager()
    class Meta:
        proxy = True
Again with the managers. Here’s what these managers look like though:
class ImageManager(models.Manager):
    def get_queryset(self):
        return super(ImageManager, self).get_queryset().filter(
            type='image')

class VideoManager(models.Manager):
    def get_queryset(self):
        return super(VideoManager, self).get_queryset().filter(
            type='video')

    def create(self, **kwargs):
        kwargs.update({'type': 'video'})
        return super(VideoManager, self).create(**kwargs)
You see what we did here is extend the create method available for the Video class and not for the Image model. What we want is to be able to create an instance of one of our classes without having to specify the type value. Since the default value for the base model is image, we don’t need to specify a create method for Imageinstances - they’re the default.
The base model’s default type is image, so if we do this…
image = Image.objects.create(image=some_image_file, caption="")
…we get back an image, or rather, a MediaAsset with its type attribute set to image. For a video, we need to ensure that the create method updates the keyword argument to the base manager, and then this is valid:
video = Video.objects.create(video=some_embed_url, caption="")
Now, why bother with this? It’s not necessary, but what it allows us to do is provide an interface that differs in class only. If you’re using class based views, all you need to do is specify the class name in the view and everything else flows from that.
Now we have a gallery view that displays all of the media assets for a given gallery, and for any asset CRUD needs we can provide class specific views without checking for the type value in the code and filtering on that in our views.
In the case of the gallery view, we don’t want logic checks in the template for the type of asset used for generating the thumbnails, so we’ll add this method to the underlying MediaAsset class to provide a consistent interface.
class MediaAsset(models.Admin):
    type = models.CharField(max_length=5, default='image')
    caption = models.TextField()
    video_url = models.URLField(blank=True, null=True)
    image = models.ImageField(blank=True, null=True)

    def thumbnail(self):
        if self.type == 'video':
            return some_video_thumbnail(self.video_url)
        return some_image_thumbanil(self.image)
Seeing the specific class name is also helpful for understanding exactly what you’re working with.
Having unique manager classes allows you to treat the proxy models as first class models in the rest of your code. You can use the managers to ensure the standard model interface is applied.

To use and not to use

There’s nothing complicated about proxy models, there’s just a little bit of thought required in regards to how they can solve your problems.
The use case for proxy models, I’ve found, is the exception rather than the rule. The key here is that our models, our end models that is, are all pretty closely related. The content attributes don’t differ all that much. If your models differ greatly and you’ve no need for simple aggregation, then skip the proxy models.

Popular posts from this blog

How to read or extract text data from passport using python utility.

Hi ,  Lets get start with some utility which can be really helpful in extracting the text data from passport documents which can be images, pdf.  So instead of jumping to code directly lets understand the MRZ, & how it works basically. MRZ Parser :                 A machine-readable passport (MRP) is a machine-readable travel document (MRTD) with the data on the identity page encoded in optical character recognition format Most travel passports worldwide are MRPs.  It can have 2 lines or 3 lines of machine-readable data. This method allows to process MRZ written in accordance with ICAO Document 9303 (endorsed by the International Organization for Standardization and the International Electrotechnical Commission as ISO/IEC 7501-1)). Some applications will need to be able to scan such data of someway, so one of the easiest methods is to recognize it from an image file. I 'll show you how to retrieve the MRZ infor...

How to generate class diagrams pictures in a Django/Open-edX project from console

A class diagram in the Unified Modeling Language ( UML ) is a type of static structure diagram that describes the structure of a system by showing the system’s classes, their attributes, operations (or methods), and the relationships among objects. https://github.com/django-extensions/django-extensions Step 1:   Install django extensions Command:  pip install django-extensions Step 2:  Add to installed apps INSTALLED_APPS = ( ... 'django_extensions' , ... ) Step 3:  Install diagrams generators You have to choose between two diagram generators: Graphviz or Dotplus before using the command or you will get: python manage.py graph_models -a -o myapp_models.png Note:  I prefer to use   pydotplus   as it easier to install than Graphviz and its dependencies so we use   pip install pydotplus . Command:  pip install pydotplus Step 4:  Generate diagrams Now we have everything installed...

How to Remove course from Open-edX

Go to vagrant  => 1. In the edx-platform directory:  - cd /edx/app/edxapp/edx-platform 2. Run the following Django management command:   - sudo -u www-data /edx/bin/python.edxapp /edx/bin/manage.edxapp lms dump_course_ids --settings aws    - sudo -u www-data /edx/bin/python.edxapp /edx/bin/manage.edxapp lms dump_course_ids --settings=devstack 3. Find the course ID which you'd like to delete in the resulting list of course IDs. 4. Copy the course ID into the following command and run it:  - sudo -u www-data /edx/bin/python.edxapp /edx/bin/manage.edxapp cms delete_course <COURSE_ID> --settings aws  -   sudo -u www-data /edx/bin/python.edxapp /edx/bin/manage.edxapp cms delete_course <COURSE_ID> --settings=devstack  - You'll be asked to verify the deletion . To verify the deletion, run the command from step 2 above and ensure that the course ID is not in the list. Help reference : ...