Monday, March 26, 2018

Why use the middleware approach for Django analytics?

Because of either proxy servers, ad blockers (like ABP, uBlock, etc.), or browser settings that stop JS scripts. Depending on the level of users and how much value they place on their privacy, they can outright BLOCK all forms of analytics. This ends up with our analytics data being unreliable or at least viewed with less confidence.

But why the middleware?

Django middleware is a good place because it's called for all request and response cycle.

The upside to this is:

  1. It's easy to customise. Django middleware is just a plain Python class with some methods. 
  2. The Django middleware is well-documented. 
  3. Allows us to setup whatever rules like exclude errors and only track HTTP 200 responses.
  4. We can setup tracking to be asynchronous. This way, our pages are not at the mercy of an external resource.
Writer's note: If your analytics needs are simple, you can install django-google-analytics and follow Usage #2 - Middleware + Celery.

There are a couple of downsides to this though:
  1. We could be impacting performance in a big way if the analytics middleware is doing too many things or worst, is misconfigured. 
  2. Not as bad as #1 but if we went down the asynchronous path, we will end up with a Celery server with some message broker like RabbitMQ on the tech stack. Another thing we'll have to manage. 
TL:DR For analytics with Django, HTML tag slows down pages and could be blocked; better to use a middleware approach.

Friday, February 23, 2018

Using Django forms to validate POST data in Rest Framework

Django Rest Framework already has a couple of ways to validate data. I believe the "preferred" way is to validate data from serializers. For example:


from rest_framework import serializers

class SomeSerializer(serializers.ModelSerializer):

    def validate(self, data):
        errors = {}
        year_built = data.get('year_built')
        year_rennovated = data.get('year_rennovated')
        
        if year_rennovated < year_built:
            errors['error'] = "You can't renovate something that doesn't exist!"
            raise serializers.ValidationError(errors)
            
        return data

But you can use Django forms, especially if you have existing ones that have the same "shape" of your post data.

Here's an example form:

from django import forms

class BuildingForm(forms.Form):

    year_built = forms.IntegerField(required=True)
    year_rennovated = forms.IntegerField(required=True)
                            
    def clean(self):
        cleaned = super(BuildingForm, self).clean()
        year_built =  cleaned.get('year_built')
        year_rennovated =  cleaned.get('year_rennovated')

        if year_rennovated < year_built:
            raise forms.ValidationError(u'Oops!')


And then in our API View:

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status

class BuildingView(APIView):
    def get(self, request):
        model_form = forms.BuildingForm(request.data)
        if model_form.is_valid():
            data = serializers.someSerializer(obj).data
            return Response(data, status=status.HTTP_200_OK)
        else:
            return Response({'errors': model_form.errors}, status=status.HTTP_400_BAD_REQUEST)

Thursday, January 11, 2018

Django lazily evals URL patterns so I thought the "include" function was broken

Little known things sometimes kick your ass and you'll need to call a friend.

Until today, I didn't realise that Django lazily evaluates it's URLs. I discovered this when the code me and my team were working on had a cascading URL include. So we had something like:

# main urls.py
urlpattern = [ 
   url(r'^path/', include('app1.urls'),
]

# 2nd url - app1's url.py
urlpattern = [
   url(r'^app1/', include('app1.subapp.urls'),
]

# 3rd url - subapp for app1 urls.py
urlpattern = [
   url(r'^subapp/$, actual_view.as_view()),
]

Problem started when I assumed that the full URL path will show in the debug page on the browser. I was looking for /path/app1/subapp/. I only saw the /path listing.

This is where I thought that Django was not registering the rest. It never dawned on me that by just doing "localhost:8000/path/" will allow me to see the rest of the URL pathing. Django just shows the same or next urls not the urls 2 levels in.

Thank you Eric (@riclags) for pointing out that fact. 

I'm a dumbass sometimes.