Returning PDF through Django

September 30, 2011

I wanted to allow PDFs to be downloaded from my Django application. I wanted to log who did the download, and when it happened, and I also wanted to make sure that I stamped the PDF file with the username downloading, and then encrypted the file so it couldn't be changed.

Django View

Here's the view code. This returns a 404 if the key is invalid, or if it has expired, and then uses a helper class (PartPdf) to return the PDF file directly from the function. We also set a Content-Disposition header so that we control the filename that the PDF is saved as.

def download_pdf(request, pPdfKey):
    """
    Download a pdf
    """
    try:
        lKey = PdfDownloadKey.objects.filter(key=pPdfKey).select_related()[0]
    except IndexError:
        raise Http404

    if lKey.expires < datetime.now():
        return render_auth(request, 'pdf/expired.html')

    lResponse = HttpResponse(PartPdf(lKey.file.filename, pPdfKey, request), mimetype='application/pdf')
    lResponse['Content-Disposition'] = 'inline; filename="' + lKey.file.filename + '"'
    return lResponse

PartPdf Object

This object provides the logging functionality for the PDF, and also stamps it with the username then encrypts it to prevent changes. This class inherits directly from file and is responsible for reading the file from disk and returning it.

class PartPdf(file):
    def __init__(self, pFilename, pPdfKey, pRequest, *args, **kwargs):
            self.filename = pFilename
            self.filepath = "%s/%s" % (settings.PDF_FILE_PATH, self.filename)
            self.request = pRequest
            self.key = pPdfKey

            lStampedFilepath = self.stamp_pdf()
            super(PartPdf, self).__init__(lStampedFilepath, *args, **kwargs)

        def stamp_pdf(self):
            """
            Stamp pdf with current username
            """
            # extract metadata
            lUsername = self.request.user.username
            lMetaDataFilename = "/tmp/%s_%s.metadata" % (self.filename, lUsername)
            lCommand = "pdftk %s dump_data output %s" % (self.filepath, lMetaDataFilename)  
            os.system(lCommand)

            lMetaDataFile = open(lMetaDataFilename, "a")
            lMetaDataFile.write("InfoKey: user\n")
            lMetaDataFile.write("InfoValue: %s\n" % lUsername)
            lMetaDataFile.close()

            lOutputFilename = "/tmp/%s_%s" % (self.filename, lUsername)
            lCommand = "pdftk %s update_info %s output %s owner_pw PASSWORD allow printing" % 
                                                                    (self.filepath, lMetaDataFilename, lOutputFilename)  
            os.system(lCommand)

        def close(self):
            """
            File has completed downloading so log it
            """
            super(PartPdf, self).close()
            lCounter = PdfDownloadLog()
            lCounter.user = self.request.user
            lCounter.ip = self.request.META['REMOTE_ADDR']
            lCounter.user_agent = self.request.META['HTTP_USER_AGENT']
            lDownloadKey = PdfDownloadKey.objects.filter(key=self.key)[0]
            if lDownloadKey.downloads_available > 0:
                lDownloadKey.downloads_available -= 1
                lDownloadKey.save()
            lCounter.key = lDownloadKey
            lCounter.save()

            lStampedFilepath = "/tmp/%s_%s" % (self.filename, self.request.user.username)
            os.remove(lStampedFilepath)
            os.remove("%s.metadata" % lStampedFilepath)

Testing

This project has 100% coverage, and I wanted to ensure I kept that. Here's the test code that makes sure the file can be downloaded and is logged correctly in the database.

lDownloadLogCountBefore = PdfDownloadLog.objects.all().count()
response = c.get('/pdf/FRED/', {}, REMOTE_ADDR='127.1.2.3',
                                       HTTP_USER_AGENT='Useragent')
self.assertEquals(response.status_code, 200)
lContent = response.content
self.assertIsNotNone(lContent)
response.close()
lDownloadLogCountAfter = PdfDownloadLog.objects.all().count()
self.assertEquals(lDownloadLogCountBefore + 1, lDownloadLogCountAfter)