Sitemap
Press enter or click to view image in full size
Photo by Mark Tegethoff on Unsplash

Django proxy models: one table, two behaviors

--

As an ORM, Django models lie at the heart of the entire framework’s design philosophy: everything else that Django does revolves around them. The model fields themselves also provide a very robust and strong framework for defining those models in a reliable way. In today’s article, I’ll be covering one very powerful but often overlooked feature of Django models: proxy models.

Before I can introduce what these are, let’s start with an example of a Django application where we are tracking customer orders. We start implementing this app with a straightforward model which stores one record per customer order, including all the basics including the order ID, name, customer FK, invoiced amount, and the current status of the order. The status field will be the main focus of this article.

from django.db import models

class Customer(models.Model):
name = models.CharField(max_length=255)
email = models.EmailField(unique=True)

def __str__(self):
return self.name


class Order(models.Model):
STATUS_CHOICES = [
('PENDING', 'Pending'),
('SHIPPED', 'Shipped'),
('COMPLETED', 'Completed'),
]
name = models.CharField(max_length=255)
description = models.TextField(blank=True, null=True)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='PENDING')
customer = models.ForeignKey(Customer, on_delete=models.CASCADE, related_name='orders')
created_at = models.DateTimeField(auto_now_add=True)

As we can see, every customer order has its own status. The Django project we are building around these models may want to dictate a different set of behaviors for these records dependent on which state they are in. For example, we may want to apply a different permission scheme to completed orders than we do for pending orders. Perhaps our finance team wants to make sure that once an Order record becomes completed, it becomes finalized and cannot be edited again, even by our admins.

While we can very easily address this conundrum on the CBV side by creating a simple Mixin class which checks the status of the current Order being edited, this approach wouldn’t apply to the Django admin site or any relevant DRF APIs.

Our ideal solution would be a unified approach that applies to all the items above. This is where proxy models come in.

What is a Proxy model?

A proxy model allows us to create a second, separate model which operates over the same database table as another model. In practice, this works the same way as a sub-class of a Model class, with the key difference that the database table and records are the same.

Let’s create a new proxy model for our Order model above and call it CompletedOrder, this is what it looks like:

class ActiveOrder(Order):
objects = ActiveOrderManager()

class Meta:
proxy = True
verbose_name = 'Active Order'
verbose_name_plural = 'Active Orders'


class CompletedOrder(Order):
objects = CompletedOrderManager()

class Meta:
proxy = True
verbose_name = 'Completed Order'
verbose_name_plural = 'Completed Orders'

def save(self, *args, **kwargs):
self.status = 'COMPLETED'
super().save(*args, **kwargs)

Remember to make and run migrations after creating the proxy models! Even though these models don’t have their own database tables, Django still tracks them via migrations.

Our proxy model CompletedOrder is nearly identical to its parent model, Order but with one key difference: the default manager. In this proxy model, we’ve replaced the default manager with a custom one which filters for orders whose status is Completed, and updated the save() method to automatically set the appropriate status.

As noted earlier, any new records we insert into the CompletedOrder model will be accessible by the Order model. Here is what it looks like in practice:

>>> from apps.orders.models import Order, CompletedOrder, Customer
>>> alice = Customer.objects.create(name='Alice', email='alice@address.com')
<Customer: Alice>
>>> CompletedOrder.objects.create(name='Apples and Bananas', description='I like to eat apples and bananas', customer=alice)
<CompletedOrder: Apples and Bananas>
>>> Order.objects.last()
<Order: Apples and Bananas>
>>> Order.objects.last().status
'COMPLETED'

Going the other way, not every Order we create will be visible by the CompletedOrder model, since the default manager filters out records without the Completed status:

>>> Order.objects.create(name='2 Calling Birds', description='No trees please', customer=alice, status='PENDING')
<Order: 2 Calling Birds>
>>> Order.objects.all()
<QuerySet [<Order: Apples and Bananas>, <Order: 2 Calling Birds>]>
>>> CompletedOrder.objects.all()
<QuerySet [<CompletedOrder: Apples and Bananas>]>

Updating the new record’s status now makes it visible to our proxy model:

>>> Order.objects.filter(pk=2).update(status='COMPLETED')
1
>>> CompletedOrder.objects.all()
<QuerySet [<CompletedOrder: Apples and Bananas>, <CompletedOrder: 2 Calling Birds>]>

Next, let’s apply these models to the other layers of our application.

Controlling Django admin access using Proxy Models

Let’s start with our Django admin users. We want to restrict all admin users from editing the Completed records, but the remaining records are fine to update. The way we will accomplish this is by creating admin classes for the proxy models only, and omit the base model:

from django.contrib import admin
from apps.orders.models import ActiveOrder, CompletedOrder

@admin.register(ActiveOrder)
class ActiveOrderAdmin(admin.ModelAdmin):
search_fields = ['name', 'description']
list_filter = ['status']
list_display = ('name', 'description', 'status', 'customer')

@admin.register(CompletedOrder)
class CompletedOrderAdmin(admin.ModelAdmin):
search_fields = ['name', 'description']
list_display = ('name', 'description', 'customer')

def has_add_permission(self, request):
return False

def has_delete_permission(self, request, obj=None):
return False

def has_change_permission(self, request, obj=None):
return False

This also carries the added benefit that we can different columns, filters, and default ordering to the user for each of the different proxy models.

Our admin site now shows two distinct entries for our “orders” app, with the “completed orders” being completely read-only.

Clicking through each of the two model admins above, only the relevant records are shown for each of the proxies. The benefit of this approach is that permissions for ActiveOrders and CompletedOrders can be granularly granted or denied to individual users or groups independent of each other.

Proxy models and CBVs

Using proxy models within CBVs is not all that different from regular models. To illustrate this, we can create “My completed orders” view which shows a customer all their orders that are in the COMPLETED state:

from django.views.generic import ListView
from apps.orders.models import CompletedOrder, Customer

class MyCompletedOrdersView(ListView):
model = CompletedOrder

def get_queryset(self):
customer = Customer.objects.get(email=self.request.user.email)
return CompletedOrder.objects.filter(customer=customer)

Note that the model used here is CompletedOrder instead of Order. The remainder of the CBV is all standard. Using this pattern, we could also create a “My active orders” view in the same manner.

Next, let’s take this to the next level and explore another benefit of this approach.

Inheritance using Proxy models

With proxy models, we can leverage inheritance for our model methods to avoid having to write a long series of if blocks and cleanly separate the logic for each order status. One such example is writing a process_order method to go in our Order model and subclasses. Here is what this could look like:

class Order(models.Model):
...
def process_order(self):
total = self.calculate_total()
if total < 0.0:
raise ValueError("Total cannot be negative")
self.notify_customer()

class PendingOrder(Order):
...
def process_order(self):
super().process_order()
self.generate_packing_slip()
self.notify_shippers()

class ShippedOrder(Order):
...
def process_order(self):
super().process_order()
self.status = 'COMPLETED'
self.save()

class CompletedOrder(Order):
...
def process_order(self):
super().process_order()
self.award_loyalty_points()

In practice, this means we can abstractly call the process_order method on any subclass of the Order model, and the appropriate logic would run. We’ve also avoided duplicating boilerplate code by promoting it to the same method in the parent class.

Proxy models are Django’s way of providing multiple representations of the same database table. Even though all the records share the same fields, the proxy models provide a way to cleanly separate business logic per type without fragmenting the database tables. This gives us multiple benefits, including:

  • Different permission class
  • Different default ordering
  • Different default manager/queryset
  • Polymorphism
  • Different Admin classes

While most of these benefits can be achieved without proxy models, doing so provides us a much cleaner architecture for our Django project.

Recommended Reading

--

--