Fix Django Rest Framework Tracking (drf-tracking) errors with Django model FileField
I was working recently on logging Django Rest Framework (DRF) requests and responses and also creating a download endpoint so I can protect files from being accessed by other users with the ability log requests to see who's downloading what.
So I came across this awesome DRF plugin called drf-tracking.
It so easy to use that you just need to add rest_framework_tracking.mixins.LoggingMixin to your existing DRF views like the code below:
from rest_framework import generics
from rest_framework.response import Response
from rest_framework_tracking.mixins import LoggingMixin
class LoggingView(LoggingMixin, generics.GenericAPIView):
def get(self, request):
return Response('with logging')
And you should see the logs in your django admin page.
Upon testing, I noticed that I get an exception when I'm sending a POST request with a file and also have the decoding error on my download api.
So I have to override the mixin methods.
from rest_framework import viewsets, renderers
from rest_framework.response import Response
from rest_framework.decorators import detail_route
from django.http import FileResponse, HttpResponse
from rest_framework_tracking.mixins import LoggingMixin
from .models import File
class BinaryFileRenderer(renderers.BaseRenderer):
media_type = 'application/octet-stream'
format = None
charset = None
render_style = 'binary'
def render(self, data, media_type=None, renderer_context=None):
return data
class FileViewSet(LoggingMixin, viewsets.ModelViewSet):
queryset = File.objects.all()
# Override finalize_response to check if response is FileResponse then I will return the rendered_content response without
# processing it to avoid decode exceptions
def finalize_response(self, request, response, *args, **kwargs):
if response.__class__.__name__ == 'FileResponse':
mixinResponse = super(FileViewSet, self).finalize_response(request, response, *args, **kwargs)
if hasattr(response, 'rendered_content'):
rendered_content = response.rendered_content
else:
rendered_content = response.getvalue()
return HttpResponse(rendered_content)
return super(FileViewSet, self).finalize_response(request, response, *args, **kwargs)
# Override _clean_data to decode data with ignore instead of replace to ignore
# errors in decode so when the logger inserts the data to db, it will not hit
# any decoding/encoding issues
def _clean_data(self, data):
if isinstance(data, bytes):
data = data.decode(errors='ignore')
return super(FileViewSet, self)._clean_data(data)
@detail_route(methods=['get'], renderer_classes=(BinaryFileRenderer,))
def download(self, request, pk=None):
queryset = File.objects.get(id=pk)
documentFile = queryset.document
file_handle = documentFile.open()
# send file
response = FileResponse(file_handle)
return response