diumenge, 2 d’octubre del 2011

django: moving business rules from views and forms to model

Since django 1.2 it is able to write validation code on model. When we work with modelforms, instance.full_clean() is called on form validation. But this code is not execute on save() method:

Note that full_clean() will not be called automatically when you call your model’s save() method, nor as a result of ModelForm validation. 

For this reason, I alltimes call clean() model method before save model. I invoke this call trhough pre_save signal.

For more business rules issolation, I write all them in a separate python module: rules.py.

This is models.py ---------------------------------------------------------------

from django.db import models

class Issue(models.Model):
    ....

    def clean(self):
        rules.Issue_clean(self)


from issues import rules
rules.connect()

This is rules.py ---------------------------------------------------------------

from issues.models import Issue


def connect():
    from django.db.models.signals import post_save, pre_save, pre_delete
    #issues
    pre_delete.connect(Issue_pre_delete, sender= Incidencia)
    pre_save.connect(Issue_pre_save, sender = Incidencia )
    post_save.connect(Issue_post_save, sender = Incidencia )


def Incidencia_clean( instance ):
    import datetime as dt

  errors = {}
    #dia i hora sempre informats    
    if not instance.dia_incidencia:
        errors.setdefault('dia_incidencia',[]).append(u'Falten Dades: Cal dia i franja')
       
    #dia i hora sempre informats    
    if not  instance.franja_incidencia:
        errors.setdefault('franja_incidencia',[]).append(u'Falten Dades: Cal dia i franja')
    #Només es poden posar incidències més ennlà de 7 dies
    if instance.dia_incidencia < ( dt.date.today() + dt.timedelta( days = -7) ):
        errors.setdefault('dia_incidencia 1',[]).append(u'''No es poden posar o modificar incidències amb més d' una setmana)''')
    #No incidències al futur.
    if instance.getDate() > datetime.now():
        errors.setdefault('dia_incidencia 2',[]).append(u'''Encara no pots posar incidències en aquesta classe. Encara no s'ha realitzat.''')
     
    #No incidencies alumne que és baixa:
    if instance.alumne.data_baixa is not None and instance.alumne.data_baixa < instance.dia_incidencia:
        errors.setdefault('dia_incidencia 3',[]).append(u'''L'alumne estava de baixa en aquesta data.''')
    if len( errors ) > 0:
        raise ValidationError(errors)


def Issue_pre_save(sender, instance, **kwargs):
    instance.clean()




In order to show errors on form, you should include this on form template:


{% if form.non_field_errors %}
      {% for error in form.non_field_errors %}
        {{error}}

      {% endfor %}
{% endif %}  

The reason is that model validation erros ara binded to non_field_errors error dictionary entry.

When you save or delete a model out of a form:


    try:
        issue.delete()
    except ValidationError, e:
        import itertools
        errors = list( itertools.chain( *e.message_dict.values() ) )


To add errors to a form dictionary on no modelforms:


        try:
            #provoco els errors per mostrar-los igualment al formulari.
            issue.clean()
        except ValidationError, e:
            form._errors = {}
            for _, v in e.message_dict.items():
                form._errors.setdefault(NON_FIELD_ERRORS, []).extend(  v  )


You can see samples on ghap project source code.

Here some samples: