F() objects and Model Managers in Django

So, I discovered something new. Even though class methods are accessible in the instance of the class, there are a few things that are different in Django. Turns out model managers (the things that let you do Foo.objects.whatever()) are not accessible directly via the model instances.

What does that mean? It means you can't do self.objects.whatever() in the code. This was further confirmed by a note here.

Managers are accessible only via model classes, rather than from model instances, to enforce a separation between “table-level” operations and “record-level” operations.

Now, that makes sense. But how are you supposed to trigger queries like

Class.objects.all().exclude(pk=pk).update(active=not(F('active')))

The update method is an important method when you want to affect change on all the objects in the class except the one you're calling it from. An example where you would need to use it, would be having only one Object in the system that has a certain attribute. Let me be more specific

class Account(models.Model):  
    username = models.EmailField()
    password = models.CharField(max_length=20)
    active = models.BooleanField(default=False)

Say if my logic dictated that at a given time only one object of type Account can be active (meaning active=True). Which means that everytime you save this object, your other objects should toggle their actives to False. You could obviously use things like django-exclusivebooleanfield but that only solves the problem for BooleanFields. So we need to find a viable solution for all cases/field types.

I came up with 4 solutions. Some are hacky and others are not so hacky.

Solution #1

Use self.__class__

If you do a dir(self) you'll see the __class__ attribute on objects inherting from model.Model. This is a direct reference to the Class of which the object is an instance of.

However as we know, things with double underscore are generally considered "magic" objects and shouldn't be used unless they are properly documented. Same applies to single underscore attributes since they are for internal use.

So, just to summarize, our call becomes.

self.__class__.objects.all().exclude(pk=pk).update(active=not(F('active')))  

Solution #2

Use type()

This made me laugh out loud because of how much of a hack this was. But since Python is the way it is, it's fun to see this work.

We all know when you do type(object) you get the class of the object. So Int() and Str() and in our example it would be class Account(). Brilliant!

So our call becomes

self.type(self).objects.all().exclude(pk=pk).update(active=not(F('active')))  

Hah, let's move on.

Solution #3

Use Django Save Signals

Django has this amazing signal system that allows us to hook into services provided by Django. Think something akin to webhooks (well not exactly, but similar purpose). So, you have something called the Django post_save() signal that you can use to call a method on the Class.

I've used post_save() signals in creating things like transaction ledgers. I am not talking about Django DB transactions. No I am talking about a client side utility. Say you make a payment or a transaction in an accounting app. You want that posted on a ledger to keep your book. That's one of the places where you can use post_save() so that whenever a type of transaction occurs you can record it on a different model.

You can read more about signals here

Solution #4

Use Django Model _Meta()

Now this, is interesting. It's not ideal, the ideal is #3. But it's not non-standard practice any more. Before we even go into what, we have to answer why are we allowed to do that.

When we talk about standards we mean something that doesn't break in subsequent releases without a prior warning. Following standards is a good idea because that builds trust. Trust within your code. You know that if a certain thing is done the way it was documented then it won't just break arbitrarily. We will be warned and then it will be deprecated in the subsequent releases.

The thing is _meta() is an internal class native to Django's model classes. Notice the underscore? So by principle it's not a good idea to use it. But the exception rises when it is properly documented.

So, now we have access to the model attribute through _meta. That way our query becomes

self._meta.model.objects.all().exclude(pk=pk).update(active=not(F('active')))  

And so that's what I used. I didn't want to setup the signals so I just used _meta.