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.