Guarding Your Data: How to Prevent Invalid State Changes with Django Proxy Models

Maintaining Data Integrity in Your Django Applications

Vlad Ogir
Django Unleashed
Published in
6 min readMar 7, 2024

--

Generated by Microsoft Copilot

When working with any code base, understanding what data is kept in tables can become challenging as a project grows.

Why is the notification flag column is false whilst the notification send column is true?

Whilst there are ways of addressing this issue from the database perspective, I would like to show you how we can keep a tighter grip over state changes with the help of the state pattern and proxy models.

Looking at the transition only, in this article, I demonstrate a simple way you can implement this pattern. (Most of the time, I find that a simple version is more than sufficient for most Django-based projects.)

What is a state pattern?

The state pattern is a behavioral software design pattern that allows an object to alter its behavior when its internal state changes. (Wikipedia)

In other words, methods (for example save()) should work differently depending on your state. If a model is locked, you shouldn’t have the ability to edit it. With this in mind, proxy models can be a nice way of handling states because they have access to the parent’s methods (which they can then override).

class MyModel(models.Model):
def my_method(self):
# do magic
...

class ProxyModel(MyModel):
def my_method(self):
# dont do magic
...

However, as a project grows, the state pattern can become a burden if you have to keep overriding every single method. This is why a hybrid approach might be more appropriate; one where you can override the key aspects only or take the parts of the state pattern that are of benefit.

The issue with state transitions

The trouble with state transitions is that if we are to use Django’s save() method then we can set the state to be whatever we want. (This defeats the object!) Why have states when they can be bypassed? So, the question is — How can we set up guardrails to enforce state transitions?

In the image below, we have 3 different states. The middle state, Submitted for review, is the gatekeeper between the first and the last state. (You can't move from thePost Created state straight to thePublished state.)

Example transition

How can we set up guardrails to enforce state transitions?

Given the issue is with the save() (outlined above) we must look at how the state can be enforced before the data is saved.

So, let's imagine that we have a model that is currently in the POST_CREATED state. The only state that it can transition to is SUBMITTED_FOR_REVIEW.

However, you have a new engineer in the team, who is not aware of the transitions you have, and they have created a function that updates the status from POST_CREATED to PUBLISHED.

This is identified and fixed, but you now need to find a way to prevent this from happening again in future.

Let’s begin!

First thing first, let's write a test. The goal is to prevent save() from working. I aim to raise an assertion during the save if there is an issue. This is what I will be using to test my work going forward.

def test_invalid_transition(self):
post: Post = PostFactory.create()
post.status = STATUS_READY_FOR_REVIEW

with self.assertRaises(ValueError):
post.save()

Since we have a list of transitions that should work, let's define it in the class. (This clear definition can act as a guide to other engineers and will help us to enforce our guardrails.)

class Post(models.Model):
TRANSITIONS = {
POST_CREATED: [SUBMITTED_FOR_REVIEW],
SUBMITTED_FOR_REVIEW: [PUBLISHED],
}

status = models.CharField(max_length=30, default=POST_CREATED)

Next, we need to detect if the status field is being modified. However, we have a problem. Django has no out-of-the-box tracking to detect which fields have changed. (So, unless you are stating that you want to update the status field, when using the save()method, Django will have no idea what will change.)

Below is an example of what the save() method looks like.

def save(
self,
force_insert=False,
force_update=False,
using=None,
update_fields=None
):

We can solve this problem by updating the variable that represents the column. But, I don’t think that this is a very good solution because someone can still update the _status directly. (See this below.)

class Post(models.Model):
_status = models.CharField(max_length=30, default=POST_CREATED)

@property
def status(self):
return self._status

@status.setter
def status(self, status):
has_changed = self._status != status

if has_changed and status in self.TRANSITIONS.get(self._status):
self._status = status

So, what can we do instead? We can use proxy models!

Using proxy models to set up guardrails

Each proxy represents a specific state. (In short, the proxy is the state.) So, we can detect if the proxy’s expected status does not match the status on the model, and this indicates whether or not the status has changed. At the same time, each proxy can define its transitions. (This way transitions are right at the source.)

Below is an example of how we can implement this solution.

Step One

Define the transitions that each proxy can transition to, as shown in the code below:

class DraftPost(Post):
ALLOWED_TRANSITIONS = [SUBMITTED_FOR_REVIEW]

class Meta:
proxy = True

Step Two

Next, we need to derive the proxy’s representation of the status. For this purpose, I extended the mapper class with the resolve_proxy_status method. This method does a reverse mapper look-up in the mapper class. Refer to my previous article to learn more about the proxy mapper.

Below is an example of the property that will detect if the status changed.


class Post(models.Model):
...

@property
def __status_changed(self) -> bool:
current_status = Mapper.resolve_proxy_status(self.__class__)
return current_status != self.status

Step Three

Validate the status before saving. If the status is invalid, we need to raise an exception. I used Django’s full_clean() for validation, this way validation is kept in the model. (This function also validates the whole object for anything missing, a bonus!)

Below is an example of validation in the model class.

class Post(models.Model):
...

def clean(self):
if self.__status_changed and self.status not in self.ALLOWED_TRANSITIONS:
raise ValueError(f"Invalid status: {self.status}. Valid statuses are: {[values for values in self.ALLOWED_TRANSITIONS]}")

return super().clean()

def save(self, *args, **kwargs):
self.full_clean()

super().save(*args, **kwargs)

Step Four

The last thing to do is to resolve the proxy post-save.

Below is an example of post-save proxy resolving.

def save(self, *args, **kwargs):
self.full_clean()

super().save(*args, **kwargs)

if self.__status_changed:
self._resolve_proxy_model()

Alternative to using full_clean

In some cases full_clean() might not be appropriate. As an alternative Python’s __setattr__ might be a great solution. __setattr__ can validate the value of an attribute before setting it.

Here is an example of how to implement it (we are checking if the attribute requested is the status and then perform the same logic as before):

def __setattr__(self, __name: str, __value: Any) -> None:
if __name == "status" and self.__status_changed(__value) and __value not in self.ALLOWED_TRANSITIONS:
raise ValueError(f"Invalid status: {__value}. Valid statuses are: {[values for values in self.ALLOWED_TRANSITIONS]}")

super().__setattr__(__name, __value)

We would still need to have a resolver when save happens to update the underlining proxy in case the status changes.

def save(self, *args, **kwargs):
super().save(*args, **kwargs)

if self.__status_changed:
self._resolve_proxy_model()

Conclusion

Proxy models have many uses and protecting transition integrity is just one of them. Transitions don’t have to be hard and once the code is implemented this pattern can be reused again and again, bringing lots of value as your project scales. Best of all, it can act as documentation for engineers who are new to the code base.

p.s. source code for example proxy model implementation.

I’d love to hear your thoughts! Please share your questions and insights in the comments below or contact me directly.

Want more articles like this? Subscribe to my email list for exclusive content and updates on my latest work.

--

--

Vlad Ogir
Django Unleashed

Staff software engineer with passion for software delivery, architecture and design.