Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Django model class decorator

Tags:

python

django

I have a need to track changes on Django model instances. I'm aware of solutions like django-reversion but they are overkill for my cause.

I had the idea to create a parameterized class decorator to fit this purpose. The arguments are the field names and a callback function. Here is the code I have at this time:

def audit_fields(fields, callback_fx):
    def __init__(self, *args, **kwargs):
        self.__old_init(*args, **kwargs)
        self.__old_state = self.__get_state_helper()

    def save(self, *args, **kwargs):
        new_state = self.__get_state_helper()

        for k,v in new_state.items():
            if (self.__old_state[k] != v):
                callback_fx(self, k, self.__old_state[k], v)

        val = self.__old_save(*args, **kwargs)
        self.__old_state = self.__get_state_helper()
        return val

    def __get_state_helper(self):
        # make a list of field/values.
        state_dict = dict()
        for k,v in [(field.name, field.value_to_string(self)) for field in self._meta.fields if field.name in fields]:
            state_dict[k] = v
        return state_dict

    def fx(clazz):
        # Stash originals
        clazz.__old_init = clazz.__init__
        clazz.__old_save = clazz.save

        # Override (and add helper)
        clazz.__init__ = __init__
        clazz.__get_state_helper = __get_state_helper
        clazz.save = save
        return clazz

    return fx

And use it as follows (only relevant part):

@audit_fields(["status"], fx)
class Order(models.Model):
    BASKET = "BASKET"
    OPEN = "OPEN"
    PAID = "PAID"
    SHIPPED = "SHIPPED"
    CANCELED = "CANCELED"
    ORDER_STATES = ( (BASKET, 'BASKET'),
                 (OPEN, 'OPEN'),
                 (PAID, 'PAID'),
                 (SHIPPED, 'SHIPPED'),
                 (CANCELED, 'CANCELED') )
    status = models.CharField(max_length=16, choices=ORDER_STATES, default=BASKET)

And test on the Django shell with:

from store.models import Order
o=Order()
o.status=Order.OPEN
o.save()

The error I receive then is:

TypeError: int() argument must be a string or a number, not 'Order'

The full stacktrace is here: https://gist.github.com/4020212

Thanks in advance and let me know if you would need more info!

EDIT: Question answered by randomhuman, code edited and usable as shown!

like image 923
Dominiek Avatar asked Jan 19 '26 20:01

Dominiek


1 Answers

You do not need to explicitly pass a reference to self on this line:

val = self.__old_save(self, *args, **kwargs)

It is a method being called on an object reference. Passing it explicitly in this way is causing it to be seen as one of the other parameters of the save method, one which is expected to be a string or a number.

like image 182
randomhuman Avatar answered Jan 22 '26 10:01

randomhuman