I recently began trying generating PDF reports with Django and Weasyprint, but I cannot make it work somehow.
I already have file upload, file download (even PDFs, but already generated ones that were uploaded), CSV generation and download, XLSX generation and download in my application, but I'm not able to return PDFs generated with WeasyPrint so far.
I want my users to be able to create reports, and they'll be saved for further new download.
So I created this basic model :
from django.db import models
class HelpdeskReport(models.Model):
class Meta:
app_label = "helpdesk"
db_table = "helpdesk_report"
verbose_name = "Helpdesk Report"
verbose_name_plural = "Helpdesk Reports"
default_permissions = ["view", "add", "delete"]
permissions = [
("download_helpdeskreport", "Can download Heldpesk Report"),
]
def _report_path(instance, filename) -> str:
return f"helpdesk/reports/{instance.id}"
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name = models.CharField(max_length=100, unique=True)
file = models.FileField(upload_to=_report_path)
Since I use service classes, I also did :
import uuid
from io import BytesIO
from django.core.files import File
from django.template.loader import render_to_string
from weasyprint import HTML
class HeldpeskReportService:
def create_report(self) -> HelpdeskReport:
report_name = f"helpdesk_report_{uuid.uuid4().hex}.pdf"
html_source = render_to_string(template_name="reports/global_report.html")
html_report = HTML(string=html_source)
pdf_report = html_report.write_pdf()
# print("check : ", pdf_report[:5]) # It gives proper content beginning (PDF header)
pdf_bytes = BytesIO(pdf_report)
pdf_file = File(file=pdf_file, name=report_name)
report = HelpdeskReport(name=report_name, file=pdf_file)
report.full_clean()
report.save()
return report
Note that for testing purpose, the template is very (too?) basic :
<html>
<head>
<meta http-equiv="Content-Type" content="text/html" charset="utf-8">
</head>
<body>
<article>
<h2>test report</h2>
</article>
</body>
</html>
Then the view to call this is as follow (I removed auth classes and so on on purpose to simplify code here, as well as intermediary layer that normally instantiate service classes) :
from django.http import HttpResponse
from rest_framework.decorators import api_view
from rest_framework.request import Request
@api_view(http_method_names=["POST"])
def create_report(request: Request) -> HttpResponse:
"""
Create a HelpdeskReport.
"""
report = HelpdeskReportService().create_report()
response = HttpResponse(content=report.file)
response["Content-Type"] = "application/pdf"
response["Content-Disposition"] = f"attachment; filename={report.name}"
return response
It does download the file, but once I try to OPEN it in Google Chrome, it does the following, with no other kind of error whatsoever (even in my backend django container) :
Speaking of backend django container, my development backend container Dockerfile contains this :
FROM python:3.12.7
...
RUN apt-get install -y weasyprint
...
RUN pip install -r requirements/development.txt
Note that in my development.txt
I also have weasyprint==63.0
.
I have no error saying that I have a missing weasyprint dependency.
If some information is missing, tell me and I'll add it.
If someone has already had the problem, I would be grateful to have some help.
Thanks in advance.
EDIT :
I tried to use qpdf
as @AKX suggested, here is the result :
qpdf helpdesk_report_414b3290fe6b4aae83293694cfa64c25.pdf -
WARNING: helpdesk_report_414b3290fe6b4aae83293694cfa64c25.pdf: file is damaged
WARNING: helpdesk_report_414b3290fe6b4aae83293694cfa64c25.pdf (offset 2612): xref not found
WARNING: helpdesk_report_414b3290fe6b4aae83293694cfa64c25.pdf: Attempting to reconstruct cross-reference table
WARNING: helpdesk_report_414b3290fe6b4aae83293694cfa64c25.pdf (object 6 0, offset 196): expected endstream
WARNING: helpdesk_report_414b3290fe6b4aae83293694cfa64c25.pdf (object 6 0, offset 66): attempting to recover stream length
WARNING: helpdesk_report_414b3290fe6b4aae83293694cfa64c25.pdf (object 6 0, offset 66): recovered stream length: 224
WARNING: helpdesk_report_414b3290fe6b4aae83293694cfa64c25.pdf (object 10 0, offset 1560): expected endstream
WARNING: helpdesk_report_414b3290fe6b4aae83293694cfa64c25.pdf (object 10 0, offset 373): attempting to recover stream length
WARNING: helpdesk_report_414b3290fe6b4aae83293694cfa64c25.pdf (object 10 0, offset 373): recovered stream length: 2194
WARNING: helpdesk_report_414b3290fe6b4aae83293694cfa64c25.pdf (object 11 0, offset 2894): expected endstream
WARNING: helpdesk_report_414b3290fe6b4aae83293694cfa64c25.pdf (object 11 0, offset 2636): attempting to recover stream length
WARNING: helpdesk_report_414b3290fe6b4aae83293694cfa64c25.pdf (object 11 0, offset 2636): recovered stream length: 455
WARNING: helpdesk_report_414b3290fe6b4aae83293694cfa64c25.pdf (object 16 0, offset 3889): expected endstream
WARNING: helpdesk_report_414b3290fe6b4aae83293694cfa64c25.pdf (object 16 0, offset 3187): attempting to recover stream length
WARNING: helpdesk_report_414b3290fe6b4aae83293694cfa64c25.pdf (object 16 0, offset 3187): recovered stream length: 1248
WARNING: helpdesk_report_414b3290fe6b4aae83293694cfa64c25.pdf (object 17 0, offset 4628): expected endstream
WARNING: helpdesk_report_414b3290fe6b4aae83293694cfa64c25.pdf (object 17 0, offset 4567): attempting to recover stream length
WARNING: helpdesk_report_414b3290fe6b4aae83293694cfa64c25.pdf (object 17 0, offset 4567): recovered stream length: 96
WARNING: helpdesk_report_414b3290fe6b4aae83293694cfa64c25.pdf (xref stream: object 17 0, offset 4628): expected endstream
WARNING: helpdesk_report_414b3290fe6b4aae83293694cfa64c25.pdf (xref stream: object 17 0, offset 4567): attempting to recover stream length
WARNING: helpdesk_report_414b3290fe6b4aae83293694cfa64c25.pdf (xref stream: object 17 0, offset 4567): recovered stream length: 96
WARNING: helpdesk_report_414b3290fe6b4aae83293694cfa64c25.pdf (offset 4567): error decoding stream data for object 17 0: stream inflate: inflate: data: incorrect header check
qpdf: helpdesk_report_414b3290fe6b4aae83293694cfa64c25.pdf: error decoding candidate xref stream while recovering damaged file
From the beginning I made the assomptions that :
So I took the problem the other way and went to my frontend (a bit late to be honest) and I realized I forgot to put the responseType
as blob
... :facepalm:
BUT, after figuring out this, another problem was still there. To create my report (and return it alongside the response), I used an AXIOS POST request, which doesn't seem to work with { responseType: 'blob' }
. And I didn't know about it.
So I have 2 options :
If anyone knows why I'm not able to make it work with a POST request, I'd be glad to learn.
Anyway, the result is this :
def create_report(self) -> HelpdeskReport:
report_name = f"helpdesk_report_{uuid.uuid4()}.pdf"
html_source = render_to_string(template_name="reports/global_report.html")
pdf_report = HTML(string=html_source).write_pdf()
pdf_bytes = BytesIO(pdf_report)
pdf_file = File(file=pdf_bytes, name=report_name)
report = HelpdeskReport(name=report_name, file=pdf_file)
report.full_clean()
report.save()
return report
@api_view(http_method_names=["GET"])
@authentication_classes([...])
@permission_classes([...])
def create_report(request: Request) -> HttpResponse:
# Simplified version here :
report = HelpdeskReportService().create_report()
response = HttpResponse(content=report.file)
response["Content-Type"] = "application/pdf"
response["Content-Disposition"] = f"attachment; filename={report.name}"
return response
Frontend is Vue.js v2 :
methods: {
async createReport () {
this.loadingCreation = true
const response = await HelpdeskReportingService.createReport()
FileService.downloadFile(response)
this.loadingCreation = false
},
}
where createReport
is :
static createReport = async () => {
try {
const response = await http.get('helpdesk/reports/create', { responseType: 'blob' })
return response
} catch (error) {
handleAxiosError(error)
}
}
AKX wrote
...you can use createObjectURL and create an ` element and simulate a click on it for the same effect.
and it's exactly what I do in my FileService :
static downloadFile (httpResponse) {
const filename = this.getFilename(httpResponse)
const file = httpResponse.data
const blob = new Blob([file], { type: file.type })
const link = document.createElement('a')
link.href = window.URL.createObjectURL(blob)
link.download = filename
link.click()
URL.revokeObjectURL(link.href)
}
Hope it might help some people.