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 Send the label to the printer
kwargs: kwargs:
pdf_file: The PDF file object of the rendered label (WeasyTemplateResponse object)
pdf_data: Raw PDF data of the rendered label pdf_data: Raw PDF data of the rendered label
filename: The filename of this PDF label filename: The filename of this PDF label
label_instance: The instance of the label model which triggered the print_label() method 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 label: The LabelTemplate object to render
item: The item to render the label with item: The item to render the label with
""" """
return ( return self.render_to_pdf(label, item, **kwargs)
self.render_to_pdf(label, item, **kwargs)
.get_document() # type: ignore
.write_pdf()
)
def render_to_html(self, label: LabelTemplate, item: models.Model, **kwargs) -> str: 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. """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') pdf_data = kwargs.get('pdf_data')
if not pdf_data: if not pdf_data:
pdf_data = ( pdf_data = self.render_to_pdf(label, instance, request, **kwargs)
self.render_to_pdf(label, instance, request, **kwargs)
.get_document()
.write_pdf()
)
pdf2image_kwargs = { pdf2image_kwargs = {
'dpi': kwargs.get('dpi', InvenTreeSetting.get_setting('LABEL_DPI', 300)), 'dpi': kwargs.get('dpi', InvenTreeSetting.get_setting('LABEL_DPI', 300)),
@ -152,14 +148,12 @@ class LabelPrintingMixin:
for item in items: for item in items:
context = label.get_context(item, request) context = label.get_context(item, request)
filename = label.generate_filename(context) filename = label.generate_filename(context)
pdf_file = self.render_to_pdf(label, item, request, **kwargs) pdf_data = self.render_to_pdf(label, item, request, **kwargs)
pdf_data = pdf_file.get_document().write_pdf()
png_file = self.render_to_png( png_file = self.render_to_png(
label, item, request, pdf_data=pdf_data, **kwargs label, item, request, pdf_data=pdf_data, **kwargs
) )
print_args = { print_args = {
'pdf_file': pdf_file,
'pdf_data': pdf_data, 'pdf_data': pdf_data,
'png_file': png_file, 'png_file': png_file,
'filename': filename, 'filename': filename,
@ -179,9 +173,6 @@ class LabelPrintingMixin:
else: else:
# Offload the print task to the background worker # 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 # Exclude the 'context' object - cannot be pickled
print_args.pop('context', None) print_args.pop('context', None)
@ -216,7 +207,6 @@ class LabelPrintingMixin:
"""Print a single label (blocking). """Print a single label (blocking).
kwargs: kwargs:
pdf_file: The PDF file object of the rendered label (WeasyTemplateResponse object)
pdf_data: Raw PDF data of the rendered label pdf_data: Raw PDF data of the rendered label
filename: The filename of this PDF label filename: The filename of this PDF label
label_instance: The instance of the label model which triggered the print_label() method 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).""" """Default label printing plugin (supports PDF generation)."""
import io
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from pypdf import PdfWriter
from InvenTree.helpers import str2bool from InvenTree.helpers import str2bool
from plugin import InvenTreePlugin from plugin import InvenTreePlugin
from plugin.mixins import LabelPrintingMixin, SettingsMixin from plugin.mixins import LabelPrintingMixin, SettingsMixin
@ -50,7 +54,7 @@ class InvenTreeLabelPlugin(LabelPrintingMixin, SettingsMixin, InvenTreePlugin):
output = self.render_to_html(label, instance, None, **kwargs) output = self.render_to_html(label, instance, None, **kwargs)
else: else:
# Output is already provided # Output is already provided
output = kwargs['pdf_file'] output = kwargs.get('pdf_data')
self.outputs.append(output) self.outputs.append(output)
@ -65,15 +69,17 @@ class InvenTreeLabelPlugin(LabelPrintingMixin, SettingsMixin, InvenTreePlugin):
filename = 'labels.html' filename = 'labels.html'
else: else:
# Stitch together the PDF outputs # Stitch together the PDF outputs
pages = [] pdf_writer = PdfWriter()
for output in self.outputs: for output in self.outputs:
doc = output.get_document() output_file = io.BytesIO(output)
pdf_writer.append(output_file)
for page in doc.pages: pdf_file = io.BytesIO()
pages.append(page) 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') filename = kwargs.get('filename', 'labels.pdf')
return ContentFile(data, name=filename) return ContentFile(data, name=filename)

View File

@ -1,25 +1,24 @@
"""Report template model definitions.""" """Report template model definitions."""
import io
import os import os
import sys import sys
from django.conf import settings 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.exceptions import ValidationError
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.core.files.storage import default_storage from django.core.files.storage import default_storage
from django.core.validators import FileExtensionValidator, MinValueValidator from django.core.validators import FileExtensionValidator, MinValueValidator
from django.db import models from django.db import models
from django.http import HttpRequest
from django.template import Context, Template from django.template import Context, Template
from django.template.exceptions import TemplateDoesNotExist from django.template.exceptions import TemplateDoesNotExist
from django.template.loader import render_to_string 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.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
import structlog import structlog
from pypdf import PdfWriter
import InvenTree.exceptions import InvenTree.exceptions
import InvenTree.helpers import InvenTree.helpers
@ -33,10 +32,10 @@ from plugin import InvenTreePlugin
from plugin.registry import registry from plugin.registry import registry
try: try:
from django_weasyprint import WeasyTemplateResponseMixin from weasyprint import HTML
except OSError as err: # pragma: no cover except OSError as err: # pragma: no cover
print(f'OSError: {err}') 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.') print('You may require some further system packages to be installed.')
sys.exit(1) sys.exit(1)
@ -44,32 +43,6 @@ except OSError as err: # pragma: no cover
logger = structlog.getLogger('inventree') 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): def rename_template(instance, filename):
"""Function to rename a report template once uploaded. """Function to rename a report template once uploaded.
@ -200,33 +173,34 @@ class ReportTemplateBase(MetadataMixin, InvenTree.models.InvenTreeModel):
return template_string.render(Context(context)) 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. """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) context = self.get_context(instance, request, **kwargs)
return render_to_string(self.template_name, context, request) 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. """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 return 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)
filename_pattern = models.CharField( filename_pattern = models.CharField(
default='output.pdf', default='output.pdf',
@ -407,10 +381,6 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase):
# Start with a default report name # Start with a default report name
report_name = None report_name = None
if request is None:
# Generate a dummy request object
request = dummy_print_request()
report_plugins = registry.with_mixin('report') report_plugins = registry.with_mixin('report')
# If a ReportOutput object is not provided, create a new one # If a ReportOutput object is not provided, create a new one
@ -419,7 +389,7 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase):
template=self, template=self,
items=len(items), items=len(items),
user=request.user user=request.user
if request.user and request.user.is_authenticated if request and request.user and request.user.is_authenticated
else None, else None,
progress=0, progress=0,
complete=False, complete=False,
@ -451,12 +421,11 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase):
# Attach the generated report to the model instance (if required) # Attach the generated report to the model instance (if required)
if self.attach_to_model and not debug_mode: if self.attach_to_model and not debug_mode:
data = report.get_document().write_pdf()
instance.create_attachment( instance.create_attachment(
attachment=ContentFile(data, report_name), attachment=ContentFile(report, report_name),
comment=_(f'Report generated from template {self.name}'), comment=_(f'Report generated from template {self.name}'),
upload_user=request.user upload_user=request.user
if request.user.is_authenticated if request and request.user.is_authenticated
else None, else None,
) )
@ -481,7 +450,7 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase):
raise ValidationError({ raise ValidationError({
'error': _('Error generating report'), 'error': _('Error generating report'),
'detail': str(exc), 'detail': str(exc),
'path': request.path, 'path': request.path if request else None,
}) })
if not report_name.endswith('.pdf'): if not report_name.endswith('.pdf'):
@ -492,18 +461,23 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase):
data = '\n'.join(outputs) data = '\n'.join(outputs)
report_name = report_name.replace('.pdf', '.html') report_name = report_name.replace('.pdf', '.html')
else: else:
pages = [] # Merge the outputs back together into a single PDF file
pdf_writer = PdfWriter()
try: try:
for report in outputs: for report in outputs:
doc = report.get_document() # Construct file object with raw PDF data
for page in doc.pages: report_file = io.BytesIO(report)
pages.append(page) pdf_writer.append(report_file)
data = outputs[0].get_document().copy(pages).write_pdf() # Generate raw output
except TemplateDoesNotExist as exc: pdf_file = io.BytesIO()
t_name = str(exc) or self.template pdf_writer.write(pdf_file)
raise ValidationError(f'Template file {t_name} does not exist') data = pdf_file.getvalue()
pdf_file.close()
except Exception:
InvenTree.exceptions.log_error('report.print')
data = None
# Save the generated report to the database # Save the generated report to the database
output.complete = True output.complete = True
@ -622,7 +596,9 @@ class LabelTemplate(TemplateUploadMixin, ReportTemplateBase):
template=self, template=self,
items=len(items), items=len(items),
plugin=plugin.slug, plugin=plugin.slug,
user=request.user if request else None, user=request.user
if request and request.user.is_authenticated
else None,
progress=0, progress=0,
complete=False, complete=False,
) )
@ -630,11 +606,6 @@ class LabelTemplate(TemplateUploadMixin, ReportTemplateBase):
if options is None: if options is None:
options = {} 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: try:
if hasattr(plugin, 'before_printing'): if hasattr(plugin, 'before_printing'):
plugin.before_printing() plugin.before_printing()

View File

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

View File

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