Skip to content
Swizec Teller - a geek with a hatswizec.com

Splitting and merging django models with perfect transparency

This is a howto that might come in handy to some people, but mostly I just want to document how I poked around some very angry django dragons and created something marvelous. There are also people on twitter who were wondering what the fuck I was doing.

So let's start by describing the problem. We have a base user model named pUser (yes stupid naming convention) that is tied to a cookie, which holds an id. These users are then tied to a number of different API accounts. In my case it is Delicious, Twitter and Facebook. The user_id is also used to tie a bunch of meta data in different other models to them.

The problem is that we do not want to trouble users with a special login for our service. But they are using different computers and browsers, so the same physical user can have multiple user id's.

However through their Delicious et al. credentials we can tie them back together into a single entity. But we do not want to trouble the rest of the code with this detail, it should just work seamlessly because otherwise we'd be forced to introduce checking for this stuff at about 50 different places in the project.

My approach to solving this goes as follows; at the end will be the three tests that indicate that the solution is valid. A hardcore test through the actual UI also confirmed that everything works.

Funky geek stuff follows, you have been warned

First we introduce a model that connects different user id's to the main user (i.e. the first id said user was given)

class UserNormalisation(models.Model):
main_id = models.IntegerField()
sub_id = models.IntegerField()
class Meta:
unique_together = ("main_id", "sub_id")

Then we give our Delicious model a ModelManager that will perform duplicity checking and tie different users together as needed.

class DeliciousManager(models.Manager):
def create(self, **kwargs):
try:
old = Delicious.objects.get(username=kwargs['username'])
new = super(DeliciousManager, self).create(**kwargs)
try:
UserNormalisation(main_id = old.user.id,
sub_id = new.user.id).save()
except IntegrityError:
pass
new.delete()
return old
except Delicious.DoesNotExist:
return super(DeliciousManager, self).create(**kwargs)
class Delicious(models.Model):
user = models.ForeignKey( pUser )
username = models.CharField( max_length=255 )
password = models.CharField( max_length=255 )
isScrobbled = models.BooleanField( default=False )
objects = DeliciousManager()

Basically when the _create_function is called it checks whether a Delicious model with the same username already exists and if it does, then a row is added to the UserNormalisation table to tie the two user id's together.

And here's the real magic, the changes we did to the pUser model.

class pUserManager(models.Manager):
def get(self, **kwargs):
user = super(pUserManager, self).get(**kwargs)
try:
id = UserNormalisation.objects.get(sub_id=user.id).main_id
user = super(pUserManager, self).get(id=id)
except UserNormalisation.DoesNotExist:
pass
return user
class pUser(models.Model):
username = models.CharField( max_length=50 )
password = models.CharField( max_length=255 )
creation = models.DateTimeField( auto_now=True )
objects = pUserManager()
def __init__(self, *args, **kwargs):
super(pUser, self).__init__(*args, **kwargs)
try:
id = UserNormalisation.objects.get(sub_id=self.id).main_id
self.id = id
except UserNormalisation.DoesNotExist:
pass

The pUserManager should have a few more functions that do essentially the same thing for other operations (filter comes to mind). Essentially whenever a pUser is fetched from the db the manager will return the real user as per the UserNormalisation model.

Another trick that makes this work seamlessly even when used as a connecting model (primary key for instance) in a different table is that __init__ function. What I've discovered is that there it's enough to just change the user's id in place and everything will work.

Here are the tests that confirm all of this funky stuff actually performs as expected

def test_normalisation(self):
user = pUser(username="test", password="test")
user.save()
user2 = pUser(username="test2", password="test")
user2.save()
norm = UserNormalisation(main_id=user.id, sub_id=user2.id)
norm.save()
fixture = pUser.objects.get(id=user2.id)
self.assertEquals( fixture.id, user.id )
def test_normalisation2(self):
user = pUser()
user.save()
user2 = pUser()
user2.save()
user.delicious_set.create(username="test", password="test")
fixture = user2.delicious_set.create(username="test", password="test")
self.assertEquals( fixture.user.id, user.id )
self.assertEquals( UserNormalisation.objects.get(sub_id=user2.id).main_id, user.id )
self.assertEquals( fixture.user, user )
def test_normalisation3(self):
user = pUser()
user.save()
user2 = pUser()
user2.save()
user.delicious_set.create(username="test", password="test")
fixture = user2.delicious_set.create(username="test", password="test")
norm = UserNormalisation.objects.all()
Concepts.relate(user=user2, concept1="tag1", concept2="tag2")
relation = ConceptRelation.objects.filter(user=user2, concept1="tag1")[0]
self.assertEquals( relation.user.id, user.id )
self.assertEquals( relation.user, user )

Take special note to the latter two examples. In test_normalisation2 you can see that when a delicious_set is created for user2, the two users become the same thing because both we're using the same delicious username both times. Something similar happens in test_normalisation3, but there we see that creating a ConceptRelation for user2 actually creates it for the first user because they both behave as if they were the original user.

Did you enjoy this article?

Published on February 18th, 2010 in django, food for thought, intrigues, Programming, python, Uncategorized

Learned something new?
Want to become a high value JavaScript expert?

Here's how it works 👇

Leave your email and I'll send you an Interactive Modern JavaScript Cheatsheet 📖right away. After that you'll get thoughtfully written emails every week about React, JavaScript, and your career. Lessons learned over my 20 years in the industry working with companies ranging from tiny startups to Fortune5 behemoths.

Start with an interactive cheatsheet 📖

Then get thoughtful letters 💌 on mindsets, tactics, and technical skills for your career.

"Man, love your simple writing! Yours is the only email I open from marketers and only blog that I give a fuck to read & scroll till the end. And wow always take away lessons with me. Inspiring! And very relatable. 👌"

~ Ashish Kumar

Join over 10,000 engineers just like you already improving their JS careers with my letters, workshops, courses, and talks. ✌️

Have a burning question that you think I can answer? I don't have all of the answers, but I have some! Hit me up on twitter or book a 30min ama for in-depth help.

Ready to Stop copy pasting D3 examples and create data visualizations of your own?  Learn how to build scalable dataviz components your whole team can understand with React for Data Visualization

Curious about Serverless and the modern backend? Check out Serverless Handbook, modern backend for the frontend engineer.

Ready to learn how it all fits together and build a modern webapp from scratch? Learn how to launch a webapp and make your first 💰 on the side with ServerlessReact.Dev

Want to brush up on your modern JavaScript syntax? Check out my interactive cheatsheet: es6cheatsheet.com

By the way, just in case no one has told you it yet today: I love and appreciate you for who you are ❤️