Django 5 + ReportLab: Generate Dynamic PDFs from your Database

If you caught our previous tutorial, you saw how the ReportLab library works inside a Django 5 environment. Now, let’s up the ante. In this guide, we will build a complete Django 5 application that pulls real data from a database and compiles it dynamically into clean, downloadable PDF files.

Prerequisites: A Unix-like system (Linux, macOS, or WSL) and Python 3 installed.

Let’s open the terminal and execute following command to create a new folder:

mkdir myapp

Go inside this newly created folder:

cd myapp

Then create virtual environment by executing following command:

python3 -m venv .env

Activate virtual environment:

source .env/bin/activate

(Once the virtual environment is activated, your terminal defaults to the local sandbox, meaning you can simply use python and pip instead of python3 or pip3.)

Now, install Django 5 along with ReportLab library by executing following command:

pip install django==5 reportlab

Then create the project:

django-admin startproject project .

Next, create an app named posts:

python manage.py startapp posts

In order to define routes for our posts app, create urls.py inside posts folder by executing:

touch posts/urls.py

Then, open posts/urls.py and put following code inside it:

from django.urls import path
from . import views

app_name = "posts"
urlpatterns = [
    path('', views.posts, name='posts'),
    path('pdf/<int:id>', views.pdf, name='pdf'),
    path('report', views.report, name='report'),
]

Now, let’s propagate our app’s routes by amending project/urls.py as follows:

from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path("posts/", include("posts.urls")),
    path("admin/", admin.site.urls),
]

In order to define our app’s models, let’s change posts/models.py as follows:

from django.db import models

class Post(models.Model):
    title = models.CharField(max_length=200)
    body = models.TextField(blank=False, default=None)
    def __str__(self):
        return self.title

Now, add post model to admin panel by amending posts/admin.py:

from django.contrib import admin
from .models import Post

admin.site.register(Post)

Next, amend project/settings.py as follows, in order to add posts app into project:

INSTALLED_APPS = [
    'posts.apps.PostsConfig',
    # ... default django apps
]

Now, let’s have our views’ logic by amending posts/views.py as follows:

from django.shortcuts import render
from .models import Post
from django.http import FileResponse
from . import utils

def posts(request):
	posts = Post.objects.all()
	return render(request, 'posts/index.html', context={'posts':posts})

def pdf(request,id):
	buffer = utils.generate_pdf(id)
	return FileResponse(buffer, as_attachment=True, filename="post.pdf")

def report(request):
	buffer = utils.generate_report()
	return FileResponse(buffer, as_attachment=True, filename="report.pdf")

Create another file named utils.py inside posts folder, by entering following command:

touch posts/utils.py

We’ve created the utils.py file just to keep our views.py clean. Now, insert PDF generating logic inside utils.py file:

from .models import Post
import io
from django.shortcuts import get_object_or_404
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import letter
from reportlab.lib.enums import TA_RIGHT
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.platypus import Paragraph, Table
from reportlab.lib import colors

def generate_pdf(id):
p = get_object_or_404(Post, pk=id)

buffer = io.BytesIO()
c = canvas.Canvas(buffer, pagesize=letter)

h = 800
h = h-50
c.drawString(50, h, p.title)
h = h-20
c.drawString(50, h, p.body)

c.showPage()
c.save()
buffer.seek(0)
return buffer

def generate_report():
posts = Post.objects.all()

buffer = io.BytesIO()
c = canvas.Canvas(buffer, pagesize=letter)

heading3 = getSampleStyleSheet()["h3"]
data_style = ParagraphStyle(name="Data", fontSize=10)
amount_style = ParagraphStyle(name="amount", fontSize=10, alignment=TA_RIGHT)

h = 800
count = len(posts)
h = h-50

data = [[Paragraph("id", heading3), Paragraph("title", heading3), Paragraph("body", heading3)]]
data.extend([[Paragraph(str(p.id), amount_style), Paragraph(str(p.title), data_style), Paragraph(str(p.body), data_style)] for p in posts])
table = Table(data, colWidths=[40, 100, 100], style=[('LINEABOVE',(0,1),(3,1),1,colors.black)])

th = count*20
h = h - th

table.wrapOn(c, 50, h)
table.drawOn(c, 50, h)

c.showPage()
c.save()
buffer.seek(0)
return buffer

Developer Note: The hardcoded coordinate spaces (h = h - 50) used above expect short string lengths. For production environments with dynamic text lengths, consider using to ReportLab’s Flowable layout managers (SimpleDocTemplate).

Execute following command to create migration:

python manage.py makemigrations

Run the migrations:

python manage.py migrate

Now, let’s move on to client-side. So, create template folder posts/templates/posts by executing following command:

mkdir -p posts/templates/posts

Create base.html and index.html inside the above folder. You may execute following command to create these files in one go:

touch posts/templates/posts/{base,index}.html

Put following code inside posts/templates/posts/base.html:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="description" content="">
    <title>{% block title %}My App{% endblock %}</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
</head>
  <body>

    <div class="container-fluid">
      <div class="row">

        <main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">

          {% block content %}
          {% endblock %}

        </main>
      </div>
    </div>
  
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4" crossorigin="anonymous"></script>
  </body>
</html>

Then, put following code inside posts/templates/posts/index.html:

{% extends "posts/base.html" %}
{% block content %}

<div class="">
  <table class="table table-striped table-sm">
    <thead>
      <tr>
        <th>#</th>
        <th>Title</th>
        <th>Body</th>
        <th>Action</th>
      </tr>
    </thead>
    <tbody>
      {% for post in posts %}
      <tr>
        <td>{{ post.id }}</td>
        <td>{{ post.title }}</td>
        <td>{{ post.body }}</td>
        <td>
          <div class="row row-cols-sm-auto">
            <a href="{% url 'posts:pdf' post.id %}" class="btn btn-outline-secondary btn-sm">Generate PDF</a>

          </div>
        </td>
      </tr>
      {% endfor %}
    </tbody>
  </table>
</div>
<div>
  <a href="{% url 'posts:report' %}" class="btn btn-primary btn-sm">Generate Report of all Posts</a>
</div>

{% endblock %}

Now, let’s create superuser by entering following command:

python manage.py createsuperuser

Follow the prompts to configure credentials, then boot the local engine:

python manage.py runserver

Access admin panel by visiting:

localhost:8000/admin

Then use admin panel to add some posts.

Next, go to localhost:8000/posts to see the posts get listed. You can generate PDF of each post as well as All Posts Report.

Leave a Comment