2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-05-14 02:53:08 +00:00

[refactor] Remove django-weasyprint entirely (#9316)

* Remove django-weasyprint entirely

* Handle null request

* Bug fix
This commit is contained in:
Oliver 2025-03-17 07:51:29 +11:00 committed by GitHub
parent 3afafe594b
commit a453c9b286
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 64 additions and 104 deletions

View File

@ -148,7 +148,6 @@ class MyLabelPrinter(LabelPrintingMixin, InvenTreePlugin):
Send the label to the printer
kwargs:
pdf_file: The PDF file object of the rendered label (WeasyTemplateResponse object)
pdf_data: Raw PDF data of the rendered label
filename: The filename of this PDF label
label_instance: The instance of the label model which triggered the print_label() method

View File

@ -141,11 +141,7 @@ class LabelPrinterBaseDriver(BaseDriver):
label: The LabelTemplate object to render
item: The item to render the label with
"""
return (
self.render_to_pdf(label, item, **kwargs)
.get_document() # type: ignore
.write_pdf()
)
return self.render_to_pdf(label, item, **kwargs)
def render_to_html(self, label: LabelTemplate, item: models.Model, **kwargs) -> str:
"""Helper method to render a label to HTML format for a specific item.

View File

@ -86,11 +86,7 @@ class LabelPrintingMixin:
pdf_data = kwargs.get('pdf_data')
if not pdf_data:
pdf_data = (
self.render_to_pdf(label, instance, request, **kwargs)
.get_document()
.write_pdf()
)
pdf_data = self.render_to_pdf(label, instance, request, **kwargs)
pdf2image_kwargs = {
'dpi': kwargs.get('dpi', InvenTreeSetting.get_setting('LABEL_DPI', 300)),
@ -152,14 +148,12 @@ class LabelPrintingMixin:
for item in items:
context = label.get_context(item, request)
filename = label.generate_filename(context)
pdf_file = self.render_to_pdf(label, item, request, **kwargs)
pdf_data = pdf_file.get_document().write_pdf()
pdf_data = self.render_to_pdf(label, item, request, **kwargs)
png_file = self.render_to_png(
label, item, request, pdf_data=pdf_data, **kwargs
)
print_args = {
'pdf_file': pdf_file,
'pdf_data': pdf_data,
'png_file': png_file,
'filename': filename,
@ -179,9 +173,6 @@ class LabelPrintingMixin:
else:
# Offload the print task to the background worker
# Exclude the 'pdf_file' object - cannot be pickled
print_args.pop('pdf_file', None)
# Exclude the 'context' object - cannot be pickled
print_args.pop('context', None)
@ -216,7 +207,6 @@ class LabelPrintingMixin:
"""Print a single label (blocking).
kwargs:
pdf_file: The PDF file object of the rendered label (WeasyTemplateResponse object)
pdf_data: Raw PDF data of the rendered label
filename: The filename of this PDF label
label_instance: The instance of the label model which triggered the print_label() method

View File

@ -1,8 +1,12 @@
"""Default label printing plugin (supports PDF generation)."""
import io
from django.core.files.base import ContentFile
from django.utils.translation import gettext_lazy as _
from pypdf import PdfWriter
from InvenTree.helpers import str2bool
from plugin import InvenTreePlugin
from plugin.mixins import LabelPrintingMixin, SettingsMixin
@ -50,7 +54,7 @@ class InvenTreeLabelPlugin(LabelPrintingMixin, SettingsMixin, InvenTreePlugin):
output = self.render_to_html(label, instance, None, **kwargs)
else:
# Output is already provided
output = kwargs['pdf_file']
output = kwargs.get('pdf_data')
self.outputs.append(output)
@ -65,15 +69,17 @@ class InvenTreeLabelPlugin(LabelPrintingMixin, SettingsMixin, InvenTreePlugin):
filename = 'labels.html'
else:
# Stitch together the PDF outputs
pages = []
pdf_writer = PdfWriter()
for output in self.outputs:
doc = output.get_document()
output_file = io.BytesIO(output)
pdf_writer.append(output_file)
for page in doc.pages:
pages.append(page)
pdf_file = io.BytesIO()
pdf_writer.write(pdf_file)
data = pdf_file.getvalue()
pdf_file.close()
data = self.outputs[0].get_document().copy(pages).write_pdf()
filename = kwargs.get('filename', 'labels.pdf')
return ContentFile(data, name=filename)

View File

@ -1,25 +1,24 @@
"""Report template model definitions."""
import io
import os
import sys
from django.conf import settings
from django.contrib.auth.models import AnonymousUser, User
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.core.files.base import ContentFile
from django.core.files.storage import default_storage
from django.core.validators import FileExtensionValidator, MinValueValidator
from django.db import models
from django.http import HttpRequest
from django.template import Context, Template
from django.template.exceptions import TemplateDoesNotExist
from django.template.loader import render_to_string
from django.test.client import RequestFactory
from django.test.utils import override_settings
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
import structlog
from pypdf import PdfWriter
import InvenTree.exceptions
import InvenTree.helpers
@ -33,10 +32,10 @@ from plugin import InvenTreePlugin
from plugin.registry import registry
try:
from django_weasyprint import WeasyTemplateResponseMixin
from weasyprint import HTML
except OSError as err: # pragma: no cover
print(f'OSError: {err}')
print("Unable to import 'django_weasyprint' module.")
print("Unable to import 'weasyprint' module.")
print('You may require some further system packages to be installed.')
sys.exit(1)
@ -44,32 +43,6 @@ except OSError as err: # pragma: no cover
logger = structlog.getLogger('inventree')
MOCK_PRINT_HOST = 'localhost'
@override_settings(SITE_URL=MOCK_PRINT_HOST)
def dummy_print_request() -> HttpRequest:
"""Generate a dummy HTTP request object.
- This is required for internal print calls, as WeasyPrint *requires* a request object.
- Additionally, we have to mock the HOST header, as WeasyPrint requires a valid HOST URL.
"""
factory = RequestFactory()
request = factory.get('/', headers={'host': MOCK_PRINT_HOST})
request.user = AnonymousUser()
return request
class WeasyprintReport(WeasyTemplateResponseMixin):
"""Class for rendering a HTML template to a PDF."""
def __init__(self, request, template, **kwargs):
"""Initialize the report mixin with some standard attributes."""
self.request = request
self.template_name = template
self.pdf_filename = kwargs.get('filename', 'output.pdf')
def rename_template(instance, filename):
"""Function to rename a report template once uploaded.
@ -200,33 +173,34 @@ class ReportTemplateBase(MetadataMixin, InvenTree.models.InvenTreeModel):
return template_string.render(Context(context))
def render_as_string(self, instance, request=None, **kwargs):
def render_as_string(self, instance, request=None, **kwargs) -> str:
"""Render the report to a HTML string.
Useful for debug mode (viewing generated code)
Arguments:
instance: The model instance to render against
request: A HTTPRequest object (optional)
Returns:
str: HTML string
"""
context = self.get_context(instance, request, **kwargs)
return render_to_string(self.template_name, context, request)
def render(self, instance, request=None, **kwargs):
def render(self, instance, request=None, **kwargs) -> bytes:
"""Render the template to a PDF file.
Uses django-weasyprint plugin to render HTML template against Weasyprint
Arguments:
instance: The model instance to render against
request: A HTTPRequest object (optional)
Returns:
bytes: PDF data
"""
context = self.get_context(instance, request)
html = self.render_as_string(instance, request, **kwargs)
pdf = HTML(string=html).write_pdf()
# Render HTML template to PDF
wp = WeasyprintReport(
request,
self.template_name,
base_url=get_base_url(request=request),
presentational_hints=True,
filename=self.generate_filename(context),
**kwargs,
)
return wp.render_to_response(context, **kwargs)
return pdf
filename_pattern = models.CharField(
default='output.pdf',
@ -407,10 +381,6 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase):
# Start with a default report name
report_name = None
if request is None:
# Generate a dummy request object
request = dummy_print_request()
report_plugins = registry.with_mixin('report')
# If a ReportOutput object is not provided, create a new one
@ -419,7 +389,7 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase):
template=self,
items=len(items),
user=request.user
if request.user and request.user.is_authenticated
if request and request.user and request.user.is_authenticated
else None,
progress=0,
complete=False,
@ -451,12 +421,11 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase):
# Attach the generated report to the model instance (if required)
if self.attach_to_model and not debug_mode:
data = report.get_document().write_pdf()
instance.create_attachment(
attachment=ContentFile(data, report_name),
attachment=ContentFile(report, report_name),
comment=_(f'Report generated from template {self.name}'),
upload_user=request.user
if request.user.is_authenticated
if request and request.user.is_authenticated
else None,
)
@ -481,7 +450,7 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase):
raise ValidationError({
'error': _('Error generating report'),
'detail': str(exc),
'path': request.path,
'path': request.path if request else None,
})
if not report_name.endswith('.pdf'):
@ -492,18 +461,23 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase):
data = '\n'.join(outputs)
report_name = report_name.replace('.pdf', '.html')
else:
pages = []
# Merge the outputs back together into a single PDF file
pdf_writer = PdfWriter()
try:
for report in outputs:
doc = report.get_document()
for page in doc.pages:
pages.append(page)
# Construct file object with raw PDF data
report_file = io.BytesIO(report)
pdf_writer.append(report_file)
data = outputs[0].get_document().copy(pages).write_pdf()
except TemplateDoesNotExist as exc:
t_name = str(exc) or self.template
raise ValidationError(f'Template file {t_name} does not exist')
# Generate raw output
pdf_file = io.BytesIO()
pdf_writer.write(pdf_file)
data = pdf_file.getvalue()
pdf_file.close()
except Exception:
InvenTree.exceptions.log_error('report.print')
data = None
# Save the generated report to the database
output.complete = True
@ -622,7 +596,9 @@ class LabelTemplate(TemplateUploadMixin, ReportTemplateBase):
template=self,
items=len(items),
plugin=plugin.slug,
user=request.user if request else None,
user=request.user
if request and request.user.is_authenticated
else None,
progress=0,
complete=False,
)
@ -630,11 +606,6 @@ class LabelTemplate(TemplateUploadMixin, ReportTemplateBase):
if options is None:
options = {}
if request is None:
# If the request object is not provided, we need to create a dummy one
# Otherwise, WeasyPrint throws an error
request = dummy_print_request()
try:
if hasattr(plugin, 'before_printing'):
plugin.before_printing()

View File

@ -26,7 +26,6 @@ django-structlog # Structured logging
django-stdimage # Advanced ImageField management
django-taggit # Tagging support
django-otp==1.3.0 # Two-factor authentication (legacy to ensure migrations) https://github.com/inventree/InvenTree/pull/6293
django-weasyprint # django weasyprint integration
djangorestframework<3.15 # DRF framework # FIXED 2024-06-26 see https://github.com/inventree/InvenTree/pull/7521
djangorestframework-simplejwt[crypto] # JWT authentication
django-xforwardedfor-middleware # IP forwarding metadata
@ -40,6 +39,7 @@ pillow # Image manipulation
pint # Unit conversion
pip-licenses # License information for installed packages
ppf.datamatrix # Data Matrix barcode generator
pypdf # PDF manipulation tools
python-barcode[images] # Barcode generator
python-dotenv # Environment variable management
pyyaml>=6.0.1 # YAML parsing

View File

@ -411,7 +411,6 @@ django==4.2.20 \
# django-stdimage
# django-structlog
# django-taggit
# django-weasyprint
# django-xforwardedfor-middleware
# djangorestframework
# djangorestframework-simplejwt
@ -517,10 +516,6 @@ django-taggit==6.1.0 \
--hash=sha256:ab776264bbc76cb3d7e49e1bf9054962457831bd21c3a42db9138b41956e4cf0 \
--hash=sha256:c4d1199e6df34125dd36db5eb0efe545b254dec3980ce5dd80e6bab3e78757c3
# via -r src/backend/requirements.in
django-weasyprint==2.3.1 \
--hash=sha256:09cc1c40c92db34bed80154be7c959fea03d6001dc46fd599f3fd464d6a6dc72 \
--hash=sha256:cd35b8bd24b28128a17a2416d0e6f3e64cb727f25c53467150b4be16ccd01c19
# via -r src/backend/requirements.in
django-xforwardedfor-middleware==2.0 \
--hash=sha256:16fd1cb27f33a5541b6f3e0b43afb1b7334a76f27a1255b69e14ec5c440f0b24
# via -r src/backend/requirements.in
@ -1213,6 +1208,10 @@ pyjwt[crypto]==2.10.1 \
# via
# django-allauth
# djangorestframework-simplejwt
pypdf==5.4.0 \
--hash=sha256:9af476a9dc30fcb137659b0dec747ea94aa954933c52cf02ee33e39a16fe9175 \
--hash=sha256:db994ab47cadc81057ea1591b90e5b543e2b7ef2d0e31ef41a9bfe763c119dab
# via -r src/backend/requirements.in
pyphen==0.17.2 \
--hash=sha256:3a07fb017cb2341e1d9ff31b8634efb1ae4dc4b130468c7c39dd3d32e7c3affd \
--hash=sha256:f60647a9c9b30ec6c59910097af82bc5dd2d36576b918e44148d8b07ef3b4aa3
@ -1626,6 +1625,7 @@ typing-extensions==4.12.2 \
# opentelemetry-sdk
# pint
# py-moneyed
# pypdf
# referencing
# structlog
tzdata==2025.1 \
@ -1652,9 +1652,7 @@ wcwidth==0.2.13 \
weasyprint==64.1 \
--hash=sha256:28b02f2c6409bafce1b1220d9d76a7345875bd3bd08c4f6dfbf510bb92a94757 \
--hash=sha256:f7c88ea8ce0ce0c527cbb9c802689e035fae50016d7efc5dfdaba4b75abf68f4
# via
# -r src/backend/requirements.in
# django-weasyprint
# via -r src/backend/requirements.in
webencodings==0.5.1 \
--hash=sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 \
--hash=sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923