Невозможно открыть PDF-файл при создании с помощью Django и WeasyPrint
Недавно я начал пробовать генерировать PDF-отчеты с помощью Django и Weasyprint, но никак не могу заставить их работать.
В моем приложении уже есть загрузка файлов, скачивание файлов (даже PDF, но уже сгенерированных, которые были загружены), генерация и скачивание CSV, генерация и скачивание XLSX, но я пока не могу вернуть PDF, сгенерированные с помощью WeasyPrint.
Я хочу, чтобы мои пользователи могли создавать отчеты, и они сохранялись для последующей новой загрузки.
Итак, я создал эту базовую модель :
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)
Поскольку я использую сервисные классы, я также сделал :
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
Обратите внимание, что для целей тестирования шаблон очень (слишком?) простой :
<html>
<head>
<meta http-equiv="Content-Type" content="text/html" charset="utf-8">
</head>
<body>
<article>
<h2>test report</h2>
</article>
</body>
</html>
Тогда представление для вызова этого выглядит следующим образом (я специально удалил классы auth и т.д., чтобы упростить код, а также промежуточный слой, который обычно инстанцирует классы сервисов) :
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
Он загружает файл, но когда я пытаюсь открыть его в Google Chrome, он делает следующее, без каких-либо других ошибок (даже в моем внутреннем контейнере django) :
Говоря о бэкенд-контейнере django, мой Dockerfile для бэкенд-контейнера разработки содержит следующее :
FROM python:3.12.7
...
RUN apt-get install -y weasyprint
...
RUN pip install -r requirements/development.txt
Обратите внимание, что в моем development.txt
у меня также есть weasyprint==63.0
.
У меня нет ошибки, говорящей о том, что у меня отсутствует зависимость weasyprint.
Если какой-то информации не хватает, скажите мне, и я добавлю ее.
Если кто-то уже сталкивался с этой проблемой, я буду благодарен за помощь.
Заранее спасибо.
report.file
will be a FieldFile
[Django- doc] (not to be confused with a FileField
model field [Django-doc]), so if you use this as content
, it will call str(..)
on it, and thus write the name of the filepath.
Вы можете открыть файл и таким образом выгрузить его содержимое в ответ:
@api_view(http_method_names=['POST'])
def create_report(request: Request) -> HttpResponse:
report = HelpdeskReportService().create_report()
with report.file.open(mode='rb') as fh:
response = HttpResponse(content=fh.read())
response['Content-Type'] = 'application/pdf'
response['Content-Disposition'] = f"attachment; filename={report.name}"
return response
Но это не хорошая идея: Django не умеет обслуживать файлы. Обычно для этого используется веб-сервер типа nginx, который имеет развитые механизмы потоковой передачи и кэширования.
Вы можете позволить веб-серверу, например nginx, обслуживать файл, возвращая ответ с заголовком X-Accel-Redirect
:
@api_view(http_method_names=['POST'])
def create_report(request: Request) -> HttpResponse:
report = HelpdeskReportService().create_report()
response = HttpResponse()
response['Content-Type'] = 'application/pdf'
response['X-Accel-Redirect'] = f'/protected/{report.file.path}'
response['Content-Disposition'] = f"attachment; filename={report.name}"
return response
и затем позвольте Nginx самому обработать файл с помощью:
location /protected/ {
internal;
alias /path/to/directory/where/you/store/pdf/files/;
}
С самого начала я утверждал, что :
- Проблема была в сгенерированном PDF-файле .
- После того, как я исправил это (или выяснил, что это не так), я подумал, что это мой способ возврата (что было странно, так как я уже делал что-то подобное в другом месте моего кода).
Так что я решил проблему с другой стороны и пошел на фронтенд (немного поздновато, если честно) и понял, что забыл поставить responseType
как blob
... :facepalm:
Но после того, как я разобрался с этим, осталась еще одна проблема. Чтобы создать отчет (и вернуть его вместе с ответом), я использовал запрос AXIOS POST, который, похоже, не работает с { responseType: 'blob' }
. И я не знал об этом.
Итак, у меня есть 2 варианта:
- Сделайте GET-запрос, чтобы создать и загрузить мой PDF-отчет в одном запросе.
- Сделайте POST-запрос, чтобы создать его и вернуть 'id' отчета, а затем GET-запрос, чтобы загрузить его.
Если кто-нибудь знает, почему я не могу заставить его работать с POST-запросом, я был бы рад узнать.
В любом случае, результат таков:
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
Фронтенд - Vue.js v2 :
methods: {
async createReport () {
this.loadingCreation = true
const response = await HelpdeskReportingService.createReport()
FileService.downloadFile(response)
this.loadingCreation = false
},
}
где createRepor
это :
static createReport = async () => {
try {
const response = await http.get('helpdesk/reports/create', { responseType: 'blob' })
return response
} catch (error) {
handleAxiosError(error)
}
}
AKX написал
... Вы можете использовать createObjectURL и создать элемент ` и имитировать щелчок на нем для того же эффекта.
и это именно то, что я делаю в своем 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)
}
Надеюсь, это поможет некоторым людям.