1

I created an app to audit operations on objects. When an object is created, updated or deleted, a new record is created in my audit table (Auditor model).

The Audit app is working now, but to use it on my models requires some work and a lot of code that I believe can be avoided with a most optimized approach.

Which django resources or approach can I use to integrate the Audit app on my models, without write so much code? I need a simplified way to archieve this integration of the Auditor app with all my models and other projects.

I'll give an example how I using the Auditor model with a model (Car) that I want to audit.

Thank you.

Car model

class Car(models.Model):
    name = models.CharField(max_length=50)
    brand = models.CharField(max_length=50)
    color = models.CharField(max_length=50)
    is_available = models.BooleanField(default=True)

Auditor model

class Auditor(models.Model):
    field = models.CharField(max_length=255, blank=True)
    action = models.CharField(max_length=6)
    old_value = models.TextField(blank=True)
    new_value = models.TextField(blank=True)
    stamp = models.DateTimeField(auto_now_add=True)
    user = models.ForeignKey(settings.AUTH_USER_MODEL)
    content_type = models.ForeignKey(ContentType, blank=True)
    object_id = models.PositiveIntegerField(blank=True)
    content_object = GenericForeignKey('content_type', 'object_id')
    deleted_object = models.CharField(max_length=255, blank=True)

Car view

from audittest.apps.auditor.models import Auditor
from django.contrib.contenttypes.models import ContentType

#Function for audit creation. I know that the view is not the right place to put this function, but I put this here for test.
    def create_audit(obj, request, action, obj_id=False):
        if action == 'CREATE':
            audit = Auditor(content_type = ContentType.objects.get_for_model(obj), object_id = obj.id, user = request.user, action = action)
        elif action == 'DELETE':
            audit = Auditor(content_type = ContentType.objects.get_for_model(obj), object_id = obj_id, user = request.user, action = action, deleted_object = obj)
        audit.save()

def new(request, template_name='cars/form.html'):
    form = CarForm(request.POST or None)
    if form.is_valid():
        obj = form.save()
        create_audit(obj, request, 'CREATE')
        return redirect('car:admin')
    return render(request, template_name, {'form':form, 'title':u'Novo Car'})


def edit(request, pk, template_name='cars/form.html'):
    car = get_object_or_404(Car, pk=pk)
    form = CarForm(request.POST or None, instance=car, request=request)
    if form.is_valid():
        form.save()
        return redirect('car:admin')
    return render(request, template_name, {'form':form,'title':u'Editar Car'})


def delete(request, pk, template_name='cars/confirm_delete.html'):
    car = get_object_or_404(Car, pk=pk)
    obj_id = car.id
    if request.method=='POST':
        car.delete()
        create_audit(car, request, 'DELETE', obj_id=obj_id)
        messages.success(request, u'Car excluído com sucesso.')
        return redirect('car:admin')
    return render(request, template_name, {'object':car})

Car form

class CarForm(ModelForm):

    def __init__(self, *args, **kwargs):
        self.request = kwargs.pop('request', None)
        super(CarForm, self).__init__(*args, **kwargs)  

    def clean(self):
        cleaned_data = super(CarForm, self).clean()

        # Audit updated fields
        if self.instance.pk is not None:
            fields = []
            for field in self.instance._meta.get_all_field_names():
                if field != 'id' and getattr(self.instance, field) != cleaned_data[field]:
                    #fields.append((field, getattr(self.instance, field), cleaned_data[field]))
                    audit = Auditor(content_type = ContentType.objects.get_for_model(self.instance), object_id = self.instance.pk, user = self.request.user, action = 'UPDATE', field = self.instance._meta.get_field(field).verbose_name, old_value = getattr(self.instance, field), new_value = cleaned_data[field])
                    audit.save()

        return cleaned_data
bleroy
  • 311
  • 1
  • 2
  • 8

1 Answers1

2

Use class based views in your applications so you can use the full advantages of the cool Mixins, I usually create a Mixin that can be added to any CreateView or UpdateView.

class withAudit(object):

    """
    A mixin that will create an audit record wither the action is 
    Create or Update
    """
    def get_success_url(self):
        """
        This will be called when the form is valid and saved. 
        """

        # create the record
        audit = Auditor(content_type= ContentType.objects.get_for_model(self.model))
        audit.object_id = self.object.pk
        audit.user = request.user

        # You will need a way to capture this action. 
        # there are many ways to do it. 
        audit.action = "Create"
        audit.save()

        return super(withAudit, self).get_success_url()

In your views you have to use it this way

class CarCreate(withAudit, CreateView):
    model = Car

For update

class CarUpdate(withAudit, UpdateView):
    model = Car

You can do the following to any UpdateView or CreateView in your application. However, For Deleting the object, I think you will need another mixin which will capture the data before performing the action. You need to see the class based views docs in order to customise these as you want.

The same idea can be done with decorators if you really want keep using method based views.


If you have a big application with high traffic, this process should be done in the background where you define a stack or queue an you keep passing these information to it, which will provide a better performance indeed. Some big applications using another database for logs and audit.

Othman
  • 2,942
  • 3
  • 22
  • 31
  • Thanks @Othman. In Mixin, how can I determine if the object is new or not, so I can set the appropriate action? I'm a really beginner with mixins. – bleroy May 04 '15 at 13:57
  • @bleroy check this http://stackoverflow.com/questions/18806703/within-a-template-distinguish-between-create-and-update-form – Othman May 04 '15 at 15:23
  • @bleroy checking the `form.instance` should be done BEFORE the method `get_success_url ` gets called by the view. read the class based views docs in order to understand which method should be used. – Othman May 04 '15 at 15:24
  • Thanks @Othman. Now I can determine the action using form_valid method and pass to get_success_url. But I'm having trouble getting the original object data to compare with self.object in get_success_url method to determine which fields were updated. How can I get the initial data from update form in the mixin? – bleroy May 05 '15 at 09:25
  • @bleroy you can in the same `Mixin` have multiple methods such as `form_valid ` you can do a `deepcopy` in it and store it in the old object as variable in the class, and use it in the future in `get_success_url`. – Othman May 05 '15 at 15:06