Rendering PDF from django templates

using drf and wkhtmltopdf

Again, more of a note to self than a real tutorial. I'll try not to make it obtuse.

Your pod/container must have a compatible wkhtmltopdf installed (yes, I'm weaned off of docker. It's podman all the way baby!). This is very important and something that has tripped me up more than once.

Other than that just follow the installation and setup tutorials of django, django rest framework and django-dbtemplates to get the settings and urls in order.

Key points

  • Templates can be files on disk or stored in database
  • If the template renders on the server, it will render as PDF
  • PDF renders extremely true to the template

The files

requirements.txt

django
djangorestframework
pdfkit
# django-dbtemplates
# pip package lags behind
git+https://github.com/jazzband/django-dbtemplates.git#egg=django-dbtemplates

serializers.py


from dbtemplates.models import Template
from rest_framework import serializers

class PDFCreatorSerializer(serializers.Serializer):                                                                                                                 
    id = serializers.IntegerField(read_only=True)                                                                                          
    template = serializers.CharField(max_length=256)                                                                                                 
    template_data = serializers.JSONField()                                                                                                          
    filestore_data = serializers.JSONField() 

class FileStoreResponseSerializer:  # Topic for another blog post
    ...

class TemplateSerializer(serializers.ModelSerializer):
    class Meta:
        model = Template
        fields: str = "__all__"

views.py

from io import BytesIO
import uuid
import json
from typing import Tuple
from django.conf import settings
from django.http import HttpRequest
from dbtemplate.models import Template
from rest_framework import viewsets, permissions
from rest_framework.response import Response
from . import serializers

class RenderPDFTemplate(viewsets.ViewSet):
     serializer_class = serializers.PDFCreatorSerializer
    def create(self, request: HttpRequest) - > Response:
        pdf, filename = self.create_pdf_from_template(
            request.POST.get("template_data", ""),
            request.POST.get("template", "")
       )
       .
       .
       . do file store stuff..
       return Response(...)

    def create_pdf_from_template(self, data: str, template_name: str) -> Tuple[BytesIO, str]:
        data = json.loads(data)
        data.update({"base": settings.BASE_DIR.parent.parent})
        template = get_template(template_name=template_name)
        outfile = uuid.uuid1().hex + ".pdf"
        html = template.render(data)
        options = {
            "page-size": "A4",
            "margin-top": "0.55in",
            "margin-right": "0.25in",
            "margin-bottom": "0.25in",
            "margin-left": "0.25in",
            "encoding": "UTF-8",
            "quiet": "",
            "enable-forms": None,
           "enable-local-file-access": "",
        }
        pdf = pdfkit.from_string(html, False, options)
        return (pdf, outfile)

class BaseModelViewSetWithPermissions(viewsets.ModelViewSet):
    """CRUD"""
    permission_classes = (permissions.IsAuthenticated | permissions.IsAdminUser,)                                                                                                                   

class TemplateViewSet(BaseModelViewSetWithPermissions):
    queryset = Template.objects.all()
    serializer_class = serializers.TemplateSerializer

To test it, create a client

import requests
import json

URL = "https://yourdjangoserver/renderpdf/"
TOKEN = "The DjangoRestFrameworkToken you created in admin"
HEADERS = {"Authorization": f"{TOKEN}"}

DATA = {
    "template_name": "yourtemplate.html" , 
    "template_data": json.dumps({"somehing": "else", "more": "of the same", "date": "2022-02-02"}, default=str),
} 
res = requests.post(URL, headers=HEADERS, data=DATA, verify=False)

Simple as that!