Smart serializzation with django (aka making django working with extjs)

Coding

It’s been a while that i didn’t write anything. Well i’ve been too busy with work&studies. But today i’ve developed a nice and painless way to resolve relationships between models in django such that i can serialize the relationship using one field only getting the most significant one. This approach is very useful in ajax/javascript (extjs actually scenarios).
Let’s assume that we have an application in django and i want it to send data to a extjs client application (yep extjs based javascript is a program by itself IMHO). The problem in this scenario is that we have to emit django models as json representations such that extjs can read them. I’ve been googling for a while and i found that the used approach is to get the dictionary-like representation of the instance and send it. But this approach has many drawbacks: you end up having a lot of field_id fields which are exactly the relations and a lot of fields starting with _ which are private python memebers.

My solution was to add a .flat() method such that when called it serializes as python dict the instance, expanding relationships using the most significant fields. Who is the one which sets that significant fields ? It’s you, straight in the model definition. In this way the configuration is distributed within models and you obtain exactly what you wanna have for that relationships. You can also get more than one field for each relation and just join them using a separator. But let’s look at the code:

class CommonObj(object):
    def __repr__(self):
        return str(self.__dict__)

    def __str__(self):
        return self.__repr__()

    def flat(self, mapping={}, func=lambda x:x, separator=' '):
        '''
            Emit a dictionary representing the class. It's able to expand the relationship between classes
            using the special __config__ class you can define in your models. When representation will occour
            it will expand the relations and will return a single field value which is the result of the join
            using the fields_separator of the main_fields list defined in __config__ for each related class.
            If the __config__ is missed it will try to use the name of the relation as the value for the emitted
            dictionary.

            mapping defines how to map the results. it's a dictionary composed by {existent_key:rep_key ...} which
            changes all the existent methods of the object (the existent_key) with the rep_key
        '''

        def get_wanted_rep(key):
            if key in mapping:
                return mapping[key]
            return key

        res = dict()
        iter = dict(self.__dict__)
        for k,v in iter.iteritems():
            if k[0] == '_':
                continue
            elif k[-3:] == '_id' and k != 'id':
                ist = getattr(self, k[:-3])
                if ist:
                    try:
                        attrs = ist.__class__.__config__.main_fields
                    except AttributeError:
                        attrs = [k[:-3]]

                    fields = list()
                    for attr in attrs:
                        fields.append(func(unicode(getattr(ist, attr))))

                    try:
                        sep = ist.__class__.__config__.fields_separator
                        if sep:
                            separator = sep
                    except AttributeError:
                        pass

                    res[get_wanted_rep(k[:-3])] = separator.join(fields)
            else:
                try:
                    res[get_wanted_rep(k)] = func(v)
                except AttributeError:
                    #needed to print numbers
                    res[get_wanted_rep(k)] = v
        return res

class Persona(CommonObj, models.Model):
    nome = models.CharField(max_length=30)
    cognome = models.CharField(max_length=30)
    secondo_cognome = models.CharField(max_length=30, blank=True, null=True)
    indirizzo = models.CharField(max_length=100, blank=True, null=True)
    citta = models.CharField(max_length=15, blank=True, null=True)
    provincia = models.CharField(max_length=30, blank=True, null=True)
    cap = models.IntegerField(blank=True, null=True)
    telefono = models.CharField(max_length=15, blank=True, null=True)
    cellulare = models.CharField(max_length=10, blank=True, null=True)
    email = models.EmailField(blank=True, null=True)
    cliente = models.BooleanField()
    problemi_udito = models.BooleanField()
    porta_apparecchio = models.BooleanField()
    venuto = models.BooleanField()
    spontaneo = models.BooleanField()
    perdita_media = models.CharField(max_length=5, blank=True, null=True, choices=perdita_media_choices)
    tipo_apparecchio = models.CharField(max_length=30, blank=True, null=True)
    lato_apparecchio = models.IntegerField(blank=True, null=True, choices=lato_choices)
    pila = models.IntegerField(blank=True, null=True, choices=pila_choices)
    note = models.TextField(blank=True, null=True)
    preventivo = models.TextField(blank=True, null=True)

We have defined a model which inherits from the CommonObj class which adds the .flat() method. It’s a smart and configurable serializer that expands relations trying to guess the value (it gets the value which has the same name of the relation) it’s able to perform operations thanks to the lambda function parameter to the returned values. Here is an example of this powerful serializer.

goshawk@earth:~/Projects/cacerp/cacerp$ python manage.py shell
Python 2.6.6 (r266:84292, Sep 15 2010, 16:22:56)
Type "copyright", "credits" or "license" for more information.

IPython 0.10 -- An enhanced Interactive Python.
?         -> Introduction and overview of IPython's features.
%quickref -> Quick reference.
help      -> Python's own help system.
object?   -> Details about 'object'. ?object also works, ?? prints more.

In [1]: from  callcenter.models import Persona

In [2]: p = Persona.objects.get(pk=1)

In [4]: p.flat()
Out[4]:
{'cap': 20151L,
 'cellulare': u'',
 'citta': u'MILANO',
 'cliente': False,
 'cognome': u'XXXXX',
 'email': u'',
 'id': 1L,
 'indirizzo': u'VIA MARIO BORSA 24',
 'lato_apparecchio': None,
 'nome': u'XXXXXXX',
 'note': u'AS ',
 'perdita_media': None,
 'pila': None,
 'porta_apparecchio': False,
 'preventivo': u'',
 'problemi_udito': False,
 'provincia': u'MI',
 'secondo_cognome': u'',
 'spontaneo': False,
 'telefono': u'XXXXXXX',
 'tipo_apparecchio': u'',
 'venuto': False}

It’s possible do define a mapping between the model names and the wanted result. Let’s assume we want CAP instead of cap as result and we want city instead of citta. It’s very easy to accomplish it:

In [9]: p.flat(mapping={'cap':'CAP', 'citta':'city'})
Out[9]:
{'CAP': 20151L,
 'cellulare': u'',
 'city': u'MILANO',
 'cliente': False,
 'cognome': u'XXXXX',
 'email': u'',
 'id': 1L,
 'indirizzo': u'VIA MARIO BORSA 24',
 'lato_apparecchio': None,
 'nome': u'XXXXXX',
 'note': u'AS ',
 'perdita_media': None,
 'pila': None,
 'porta_apparecchio': False,
 'preventivo': u'',
 'problemi_udito': False,
 'provincia': u'MI',
 'secondo_cognome': u'',
 'spontaneo': False,
 'telefono': u'XXXXXX',
 'tipo_apparecchio': u'',
 'venuto': False}

As you can see it’s changed as our defined mapping. Another powerful feature is to do an action on the values thanks to the func function parameter. Here is what we can do:

In [30]: p.flat(func=lambda x: x.capitalize())
Out[30]:
{'cap': 20151L,
 'cellulare': u'',
 'citta': u'Milano',
 'cliente': False,
 'cognome': u'Xxxxxx',
 'email': u'',
 'id': 1L,
 'indirizzo': u'Via mario borsa 24',
 'lato_apparecchio': None,
 'nome': u'Xxxxx',
 'note': u'As ',
 'perdita_media': None,
 'pila': None,
 'porta_apparecchio': False,
 'preventivo': u'',
 'problemi_udito': False,
 'provincia': u'Mi',
 'secondo_cognome': u'',
 'spontaneo': False,
 'telefono': u'xxxxxxxx',
 'tipo_apparecchio': u'',
 'venuto': False}

As you can verify, the values have been capitalized (first character with maiusc on and with maiusc off all the others).
Hope you like it and helps!

Previous
How to do nothing when lid is closed on Ubuntu
Next
How to install psycopg2 under virtualenv

Leave a Reply

%d bloggers like this: