22
Jun
2008

Filed under: django

3 comments

Immutable Django model fields

I wanted to make a field on a Django model read-only after it was assigned an initial value. Maybe I was having a slow day, or my Google-fu was especially lacking, but I couldn't find a documented way to do this. There's the editable Field option, but I wanted to ensure I couldn't mistakenly overwrite the initial value anywhere — not just in the admin interface or form processing. It turned out to be pretty easy.

The solution was to override the Model's __setattr__ method and intercept writes for the Field in question. Here's an example Model:

def random_hash(): 
  digest = hashlib.sha512()
  digest.update("%s%s%s" % (settings.SECRET_KEY, time.time(), random.random()))
  l = long(digest.hexdigest(), 16)
  return struct.pack('d', l).encode('base64').replace('\n', '').strip('=')

class Order(models.Model):
  confirmation_id = models.CharField(max_length=128, default=random_hash)

  def __setattr__(self, name, value):
    if name == 'confirmation_id':
        if getattr(self, 'confirmation_id', None):
            return
    super(Order, self).__setattr__(name, value)

  def __unicode__(self):
    return u"Order %s, confirmation_id: %s" % (self.id, self.confirmation_id)

Not elegant, but it works:

>>> o = Order()
>>> o.save()
>>> o
<Order: Order 4, confirmation_id: vzb0jXVz6l8>
>>> o.confirmation_id = 'something else'
>>> o
<Order: Order 4, confirmation_id: vzb0jXVz6l8>
>>>

A more general solution would be a Model subclass with a list of the immutable fields. You can't put that in the model's Meta class (see this ticket), so here's what I use:

class ImmutableModel(models.Model):
    immutable_fields = []
    mutable_fields = []
    class Meta:
        abstract = True

    def __setattr__(self, name, value):
        if name == 'immutable_fields' or (not self.immutable_fields or name in self.immutable_fields) or (self.mutable_fields and name not in self.mutable_fields):
            try:
                current_value = getattr(self, name, None)
            except:
                current_value = None
            if current_value is not None and current_value is not '':
                return
        super(ImmutableModel, self).__setattr__(name, value)

Make your model a subclass of that and set the immutable_fields attribute to a list of fields that should be immutable once set. Conversely, you can define the fields that should be mutable; all others will be immutable.

Good enough to protect you from yourself unless you're really trying.

Comments (3)

Ryan — 7 February 2009 11:50
note that in the ImmutableModel __setattr__ method, the getattr if statement should be:

if getattr(self, name, None) is not None:

because some returned values could evaluate to a boolean false (i.e. fields with a value of 0), and thus bypass the immutability
john — 7 February 2009 12:20
Oh, good catch. I've actually been using a newer version of this class without that error, and with the ability to define mutable fields. I'll update the post.

Thanks Ryan.
James Pearce — 4 August 2009 16:31
I think is *is* quite elegant. Far more than the pre_save route I was just about to head off down! :-)

Comments have been turned off for this article, but you can always contact us about it.