2
0
mirror of https://github.com/inventree/InvenTree.git synced 2026-05-17 23:08:28 +00:00

[bug] Fix for report generation context (#11941)

* Provide explicit user information to report context

* Pass user information through to offloaded task

* Pass user information back to the plugin

* Fix user passing

* Add more complex unit test for report printing

- Test debug ON / OFF
- Test merge ON / OFF
- Check generated data output

* Unit testing for label generation

* Further refactoring

- Remove "request" from printing context entirely

* Simplify argument drilling

* Robustify unit tests
This commit is contained in:
Oliver
2026-05-14 11:25:20 +10:00
committed by GitHub
parent 3d2577b411
commit 6e2a867c92
7 changed files with 339 additions and 93 deletions
@@ -24,7 +24,7 @@ class ReportMixin:
super().__init__()
self.add_mixin(PluginMixinEnum.REPORT, True, __class__)
def add_report_context(self, report_instance, model_instance, request, context):
def add_report_context(self, report_instance, model_instance, user, context):
"""Add extra context to the provided report instance.
By default, this method does nothing.
@@ -32,11 +32,11 @@ class ReportMixin:
Args:
report_instance: The report instance to add context to
model_instance: The model instance which initiated the report generation
request: The request object which initiated the report generation
user: The user to associate with the generated report
context: The context dictionary to add to
"""
def add_label_context(self, label_instance, model_instance, request, context):
def add_label_context(self, label_instance, model_instance, user, context):
"""Add extra context to the provided label instance.
By default, this method does nothing.
@@ -44,18 +44,18 @@ class ReportMixin:
Args:
label_instance: The label instance to add context to
model_instance: The model instance which initiated the label generation
request: The request object which initiated the label generation
user: The user to associate with the generated label
context: The context dictionary to add to
"""
def report_callback(self, template, instance, report, request, **kwargs):
def report_callback(self, template, instance, report, user, **kwargs):
"""Callback function called after a report is generated.
Arguments:
template: The ReportTemplate model
instance: The instance of the target model
report: The generated report object
request: The initiating request object
user: The user to associate with the generated report
The default implementation does nothing.
"""
@@ -38,41 +38,51 @@ class LabelPrintingMixin:
BLOCKING_PRINT = True
def render_to_pdf(self, label: LabelTemplate, instance, request, **kwargs):
def render_to_pdf(
self, label: LabelTemplate, instance, request, user=None, **kwargs
):
"""Render this label to PDF format.
Arguments:
label: The LabelTemplate object to render against
instance: The model instance to render
request: The HTTP request object which triggered this print job
request: The HTTP request object which triggered this print job (optional, may be None)
user: The user who triggered this print job (optional, may be None)
"""
try:
return label.render(instance, request)
return label.render(instance, request=request, user=user)
except Exception:
log_error('render_to_pdf', plugin=self.slug)
raise ValidationError(_('Error rendering label to PDF'))
def render_to_html(self, label: LabelTemplate, instance, request, **kwargs):
def render_to_html(
self, label: LabelTemplate, instance, request, user=None, **kwargs
):
"""Render this label to HTML format.
Arguments:
label: The LabelTemplate object to render against
instance: The model instance to render
request: The HTTP request object which triggered this print job
request: The HTTP request object which triggered this print job (optional, may be None)
user: The user who triggered this print job (optional, may be None)
"""
try:
return label.render_as_string(instance, request)
return label.render_as_string(instance, request=request, user=user)
except Exception:
log_error('render_to_html', plugin=self.slug)
raise ValidationError(_('Error rendering label to HTML'))
def render_to_png(self, label: LabelTemplate, instance, request=None, **kwargs):
def render_to_png(
self, label: LabelTemplate, instance, request=None, user=None, **kwargs
):
"""Render this label to PNG format.
Arguments:
label: The LabelTemplate object to render against
instance: The model instance to render
request: The HTTP request object which triggered this print job
request: The HTTP request object which triggered this print job (optional, may be None)
user: The user who triggered this print job (optional, may be None)
Keyword Arguments:
pdf_data: The raw PDF data of the rendered label (if already rendered)
dpi: The DPI to use for the PNG rendering
@@ -85,7 +95,7 @@ class LabelPrintingMixin:
pdf_data = kwargs.get('pdf_data')
if not pdf_data:
pdf_data = self.render_to_pdf(label, instance, request, **kwargs)
pdf_data = self.render_to_pdf(label, instance, request, user=user, **kwargs)
pdf2image_kwargs = {
'dpi': kwargs.get('dpi', InvenTreeSetting.get_setting('LABEL_DPI', 300)),
@@ -128,10 +138,12 @@ class LabelPrintingMixin:
The default implementation simply calls print_label() for each label, producing multiple single label output "jobs"
but this can be overridden by the particular plugin.
"""
try:
user = request.user
except AttributeError:
user = None
# Extract user information, in decreasing order of preference
user = (
kwargs.pop('user', None)
or getattr(request, 'user', None)
or getattr(output, 'user', None)
)
# Initial state for the output print job
output.progress = 0
@@ -145,11 +157,11 @@ class LabelPrintingMixin:
# Generate a label output for each provided item
for item in items:
context = label.get_context(item, request)
context = label.get_context(item, request, user=user)
filename = label.generate_filename(context)
pdf_data = self.render_to_pdf(label, item, request, **kwargs)
pdf_data = self.render_to_pdf(label, item, request, user=user, **kwargs)
png_file = self.render_to_png(
label, item, request, pdf_data=pdf_data, **kwargs
label, item, request, pdf_data=pdf_data, user=user, **kwargs
)
print_args = {
@@ -20,6 +20,7 @@ class InvenTreeLabelPlugin(LabelPrintingMixin, SettingsMixin, InvenTreePlugin):
"""
NAME = 'InvenTreeLabel'
SLUG = 'inventreelabel'
TITLE = _('InvenTree PDF label printer')
DESCRIPTION = _('Provides native support for printing PDF labels')
VERSION = '1.1.0'
+8 -1
View File
@@ -215,6 +215,7 @@ class LabelPrint(GenericAPIView):
template.pk,
[item.pk for item in items_to_print],
output.pk,
user.pk if user else None,
plugin.slug,
options=(plugin_serializer.data if plugin_serializer else {}),
)
@@ -297,7 +298,13 @@ class ReportPrint(GenericAPIView):
item_ids = [item.pk for item in items_to_print]
# Offload the task to the background worker
offload_task(report.tasks.print_reports, template.pk, item_ids, output.pk)
offload_task(
report.tasks.print_reports,
template.pk,
item_ids,
output.pk,
user.pk if user else None,
)
output.refresh_from_db()
+99 -63
View File
@@ -148,7 +148,7 @@ class BaseContextExtension(TypedDict):
template_description: Description of the report template
template_name: Name of the report template
template_revision: Revision of the report template
user: User who made the request to render the template
user: User who is creating the report (if available)
"""
base_url: str
@@ -244,37 +244,39 @@ class ReportTemplateBase(
def generate_filename(self, context, **kwargs) -> str:
"""Generate a filename for this report."""
template_string = Template(self.filename_pattern)
return template_string.render(Context(context))
def render_as_string(self, instance, request=None, context=None, **kwargs) -> str:
def render_as_string(
self, instance: models.Model, context: Optional[dict] = None, **kwargs
) -> str:
"""Render the report to a HTML string.
Arguments:
instance: The model instance to render against
request: A HTTPRequest object (optional)
context: Django template language contexts (optional)
Returns:
str: HTML string
"""
if context is None:
context = self.get_context(instance, request, **kwargs)
context = self.get_context(instance, **kwargs)
return render_to_string(self.template_name, context, request)
return render_to_string(self.template_name, context)
def render(self, instance, request=None, context=None, **kwargs) -> bytes:
def render(
self, instance: models.Model, context: Optional[dict] = None, **kwargs
) -> bytes:
"""Render the template to a PDF file.
Arguments:
instance: The model instance to render against
request: A HTTPRequest object (optional)
context: Django template langaguage contexts (optional)
context: Django template language contexts (optional)
user: The user to associate with the generated report
Returns:
bytes: PDF data
"""
html = self.render_as_string(instance, request, context, **kwargs)
html = self.render_as_string(instance, context=context, **kwargs)
pdf = HTML(string=html).write_pdf(pdf_forms=True)
return pdf
@@ -323,28 +325,28 @@ class ReportTemplateBase(
"""Return a filter dict which can be applied to the target model."""
return report.validators.validate_filters(self.filters, model=self.get_model())
def base_context(self, request=None) -> BaseContextExtension:
def base_context(self, **kwargs) -> BaseContextExtension:
"""Return base context data (available to all templates)."""
return {
'base_url': get_base_url(request=request),
'base_url': get_base_url(),
'date': InvenTree.helpers.current_date(),
'datetime': InvenTree.helpers.current_time(),
'template': self,
'template_description': self.description,
'template_name': self.name,
'template_revision': self.revision,
'user': request.user if request else None,
'user': kwargs.get('user'),
}
def get_context(self, instance, request=None, **kwargs):
def get_context(self, instance: models.Model, **kwargs):
"""Supply context data to the generic template for rendering.
Arguments:
instance: The model instance we are printing against
request: The request object (optional)
user: The user to associate with the generated report
"""
# Provide base context information to all templates
base_context = self.base_context(request=request)
base_context = self.base_context(**kwargs)
# Add in an context information provided by the model instance itself
context = {**base_context, **instance.report_context()}
@@ -423,55 +425,77 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase):
return report_context
def get_context(self, instance, request=None, **kwargs):
"""Supply context data to the report template for rendering."""
base_context = super().get_context(instance, request)
def get_context(self, instance: models.Model, **kwargs):
"""Supply context data to the report template for rendering.
Arguments:
instance: The model instance we are printing against
"""
base_context = super().get_context(instance, **kwargs)
report_context: ReportContextExtension = self.get_report_context()
context = {**base_context, **report_context}
# Pass the context through to the plugin registry for any additional information
context = self.get_plugin_context(instance, request, context)
context = self.get_plugin_context(instance, context, **kwargs)
return context
def get_plugin_context(self, instance, request, context):
"""Get the context for the plugin."""
def get_plugin_context(self, instance: models.Model, context: dict, **kwargs):
"""Get the context for the plugin.
Arguments:
instance: The model instance we are printing against
context: The context dictionary to add to
user: The user to associate with the generated report
"""
user = kwargs.get('user')
for plugin in registry.with_mixin(PluginMixinEnum.REPORT):
try:
plugin.add_report_context(self, instance, request, context)
plugin.add_report_context(self, instance, user, context)
except Exception:
InvenTree.exceptions.log_error('add_report_context', plugin=plugin.slug)
return context
def handle_attachment(self, instance, report, report_name, request, debug_mode):
def handle_attachment(self, instance, report, report_name, user, debug_mode):
"""Attach the generated report to the model instance (if required)."""
if self.attach_to_model and not debug_mode:
instance.create_attachment(
attachment=ContentFile(report, report_name),
comment=_(f'Report generated from template {self.name}'),
upload_user=request.user
if request and request.user.is_authenticated
else None,
upload_user=user,
)
def notify_plugins(self, instance, report, request):
"""Provide generated report to any interested plugins."""
def notify_plugins(self, instance, report, user):
"""Provide generated report to any interested plugins.
Arguments:
instance: The model instance we are printing against
report: The generated report object
user: The user to associate with the generated report
"""
report_plugins = registry.with_mixin(PluginMixinEnum.REPORT)
for plugin in report_plugins:
try:
plugin.report_callback(self, instance, report, request)
plugin.report_callback(self, instance, report, user)
except Exception:
InvenTree.exceptions.log_error('report_callback', plugin=plugin.slug)
def print(self, items: list, request=None, output=None, **kwargs) -> DataOutput:
def print(
self,
items: list,
output: Optional[DataOutput] = None,
user: Optional[AbstractUser] = None,
**kwargs,
) -> DataOutput:
"""Print reports for a list of items against this template.
Arguments:
items: A list of items to print reports for (model instance)
output: The DataOutput object to use (if provided)
request: The request object (optional)
output: The DataOutput object to use
user: The user to associate with the generated report
Returns:
output: The DataOutput object representing the generated report(s)
@@ -489,6 +513,9 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase):
"""
logger.info("Printing %s reports against template '%s'", len(items), self.name)
# Extract user information from the provided context
user = user or getattr(output, 'user', None)
outputs = []
debug_mode = get_global_setting('REPORT_DEBUG_MODE', False)
@@ -500,9 +527,7 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase):
if not output:
output = DataOutput.objects.create(
total=len(items),
user=request.user
if request and request.user and request.user.is_authenticated
else None,
user=user,
progress=0,
complete=False,
output_type=DataOutput.DataOutputTypes.REPORT,
@@ -516,13 +541,13 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase):
try:
if self.merge:
base_context = super().base_context(request)
base_context = super().base_context(user=user)
report_context = self.get_report_context()
item_contexts = []
for instance in items:
instance_context = instance.report_context()
instance_context = self.get_plugin_context(
instance, request, instance_context
instance, instance_context, user=user
)
item_contexts.append(instance_context)
@@ -537,9 +562,11 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase):
try:
if debug_mode:
report = self.render_as_string(instance, request, contexts)
report = self.render_as_string(
instance, user=user, context=contexts
)
else:
report = self.render(instance, request, contexts)
report = self.render(instance, user=user, context=contexts)
except TemplateDoesNotExist as e:
t_name = str(e) or self.template
msg = f'Template file {t_name} does not exist'
@@ -558,17 +585,15 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase):
raise ValidationError(f'{msg}: {e!s}')
outputs.append(report)
self.handle_attachment(
instance, report, report_name, request, debug_mode
)
self.notify_plugins(instance, report, request)
self.handle_attachment(instance, report, report_name, user, debug_mode)
self.notify_plugins(instance, report, user)
# Update the progress of the report generation
output.progress += 1
output.save()
else:
for instance in items:
context = self.get_context(instance, request)
context = self.get_context(instance, user=user)
if report_name is None:
report_name = self.generate_filename(context)
@@ -576,9 +601,11 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase):
# Render the report output
try:
if debug_mode:
report = self.render_as_string(instance, request, None)
report = self.render_as_string(
instance, user=user, context=context
)
else:
report = self.render(instance, request, None)
report = self.render(instance, user=user, context=None)
except TemplateDoesNotExist as e:
t_name = str(e) or self.template
msg = f'Template file {t_name} does not exist'
@@ -599,9 +626,10 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase):
outputs.append(report)
self.handle_attachment(
instance, report, report_name, request, debug_mode
instance, report, report_name, user, debug_mode
)
self.notify_plugins(instance, report, request)
self.notify_plugins(instance, report, user)
# Update the progress of the report generation
output.progress += 1
@@ -614,7 +642,6 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase):
raise ValidationError({
'error': _('Error generating report'),
'detail': str(exc),
'path': request.path if request else None,
})
if not report_name:
@@ -704,9 +731,16 @@ class LabelTemplate(TemplateUploadMixin, ReportTemplateBase):
}}
"""
def get_context(self, instance, request=None, **kwargs):
"""Supply context data to the label template for rendering."""
base_context = super().get_context(instance, request, **kwargs)
def get_context(self, instance: models.Model, *args, **kwargs):
"""Supply context data to the label template for rendering.
Arguments:
instance: The model instance we are printing against
"""
user = kwargs.get('user')
base_context = super().get_context(instance, **kwargs)
label_context: LabelContextExtension = {
'width': self.width,
'height': self.height,
@@ -724,7 +758,7 @@ class LabelTemplate(TemplateUploadMixin, ReportTemplateBase):
for plugin in plugins:
# Let each plugin add its own context data
try:
plugin.add_label_context(self, instance, request, context)
plugin.add_label_context(self, instance, user, context)
except Exception:
InvenTree.exceptions.log_error('add_label_context', plugin=plugin.slug)
@@ -734,9 +768,9 @@ class LabelTemplate(TemplateUploadMixin, ReportTemplateBase):
self,
items: list,
plugin: InvenTreePlugin,
output=None,
options=None,
request=None,
output: Optional[DataOutput] = None,
options: Optional[dict] = None,
user: Optional[AbstractUser] = None,
**kwargs,
) -> DataOutput:
"""Print labels for a list of items against this template.
@@ -744,9 +778,9 @@ class LabelTemplate(TemplateUploadMixin, ReportTemplateBase):
Arguments:
items: A list of items to print labels for (model instance)
plugin: The plugin to use for label rendering
output: The DataOutput object to use (if provided)
output: The DataOutput object to use
options: Additional options for the label printing plugin (optional)
request: The request object (optional)
user: The user to associate with the generated labels
Returns:
output: The DataOutput object representing the generated label(s)
@@ -758,11 +792,11 @@ class LabelTemplate(TemplateUploadMixin, ReportTemplateBase):
f"Printing {len(items)} labels against template '{self.name}' using plugin '{plugin.slug}'"
)
user = user or getattr(output, 'user', None)
if not output:
output = DataOutput.objects.create(
user=request.user
if request and request.user.is_authenticated
else None,
user=user,
total=len(items),
progress=0,
complete=False,
@@ -779,7 +813,9 @@ class LabelTemplate(TemplateUploadMixin, ReportTemplateBase):
if hasattr(plugin, 'before_printing'):
plugin.before_printing()
plugin.print_labels(self, output, items, request, printing_options=options)
plugin.print_labels(
self, output, items, None, user=user, printing_options=options
)
if hasattr(plugin, 'after_printing'):
plugin.after_printing()
+42 -7
View File
@@ -1,5 +1,7 @@
"""Background tasks for the report app."""
from django.contrib.auth import get_user_model
import structlog
from opentelemetry import trace
@@ -10,13 +12,16 @@ logger = structlog.get_logger('inventree')
@tracer.start_as_current_span('print_reports')
def print_reports(template_id: int, item_ids: list[int], output_id: int, **kwargs):
def print_reports(
template_id: int, item_ids: list[int], output_id: int, user_id: int, **kwargs
):
"""Print multiple reports against the provided template.
Arguments:
template_id: The ID of the ReportTemplate to use
item_ids: List of item IDs to generate the report against
output_id: The ID of the DataOutput to use (if provided)
output_id: The ID of the DataOutput to use
user_id: The ID of the user to associate with the generated report
This function is intended to be called by the background worker,
and will continuously update the status of the DataOutput object.
@@ -31,6 +36,18 @@ def print_reports(template_id: int, item_ids: list[int], output_id: int, **kwarg
log_error('report.tasks.print_reports')
return
# Fetch user information
user = None
if user_id:
try:
user = get_user_model().objects.get(pk=user_id)
except Exception:
log_error('report.tasks.print_reports', user_id=user_id)
if not user:
user = getattr(output, 'user', None)
# Fetch the items to be included in the report
model = template.get_model()
items = model.objects.filter(pk__in=item_ids)
@@ -38,20 +55,26 @@ def print_reports(template_id: int, item_ids: list[int], output_id: int, **kwarg
# Ensure they are sorted by the order of the provided item IDs
items = sorted(items, key=lambda item: item_ids.index(item.pk))
template.print(items, output=output)
template.print(items, output=output, user=user)
@tracer.start_as_current_span('print_labels')
def print_labels(
template_id: int, item_ids: list[int], output_id: int, plugin_slug: str, **kwargs
template_id: int,
item_ids: list[int],
output_id: int,
user_id: int,
plugin_slug: str,
**kwargs,
):
"""Print multiple labels against the provided template.
Arguments:
template_id: The ID of the LabelTemplate to use
item_ids: List of item IDs to generate the labels against
output_id: The ID of the DataOutput to use (if provided)
plugin_slug: The ID of the LabelPlugin to use (if provided)
output_id: The ID of the DataOutput to use
user_id: The ID of the user to associate with the generated labels
plugin_slug: The ID of the LabelPlugin to use
This function is intended to be called by the background worker,
and will continuously update the status of the DataOutput object.
@@ -67,6 +90,18 @@ def print_labels(
log_error('report.tasks.print_labels')
return
# Fetch user information
user = None
if user_id:
try:
user = get_user_model().objects.get(pk=user_id)
except Exception:
log_error('report.tasks.print_labels', user_id=user_id)
if not user:
user = getattr(output, 'user', None)
# Fetch the items to be included in the report
model = template.get_model()
items = model.objects.filter(pk__in=item_ids)
@@ -83,4 +118,4 @@ def print_labels(
# Extract optional arguments for label printing
options = kwargs.pop('options') or {}
template.print(items, plugin, output=output, options=options)
template.print(items, plugin, output=output, user=user, options=options)
+155
View File
@@ -6,8 +6,12 @@ from io import StringIO
from django.apps import apps
from django.conf import settings
from django.core.cache import cache
from django.core.files.base import ContentFile
from django.core.files.storage import default_storage
from django.urls import reverse
from pypdf import PdfReader
import report.models as report_models
from build.models import Build
from common.models import Attachment
@@ -299,6 +303,92 @@ class ReportTest(InvenTreeAPITestCase):
self.assertIsNotNone(output.output)
self.assertTrue(output.output.name.endswith('.pdf'))
def test_print_custom_template(self):
"""Create a new template, print it, and check the output."""
template_string = """
Hello {{ user.username }}
Your user ID is {{ user.pk }}.
Template name: {{ template.name }}
{% if merge %}
REPORT OUTPUT: MERGE = ENABLED
{% for instance in instances %}
Part Name: {{ instance.part.name }}
Stock ID: {{ instance.stock_item.pk }}
{% endfor %}
{% else %}
REPORT OUTPUT: MERGE = DISABLED
Part Name: {{ part.name }}
Stock ID: {{ stock_item.pk }}
{% endif %}
"""
template_file = ContentFile(
template_string.encode('utf-8'), name='TestPrintTemplate.html'
)
# Create a new report template with the above string as the template
template = ReportTemplate.objects.create(
name='Test report template',
model_type='stockitem',
template=template_file,
filename_pattern='unit_test_report.pdf',
)
item = StockItem.objects.first()
test_strings = [
f'Hello {self.user.username}',
f'Your user ID is {self.user.pk}.',
f'Template name: {template.name}',
f'Part Name: {item.part.name}',
f'Stock ID: {item.pk}',
]
url = reverse('api-report-print')
post_data = {'template': template.pk, 'items': [item.pk]}
# Test with "debug" both enabled and disabled
for debug in [True, False]:
set_global_setting('REPORT_DEBUG_MODE', debug)
# Test with "merge" both enabled and disabled
for merge in [True, False]:
template.merge = merge
template.save()
# Generate report via the API
data = self.post(url, data=post_data).data
self.assertEqual(data['user'], self.user.pk)
self.assertIsNotNone(data['output'])
self.assertTrue(data['output'].endswith('.html' if debug else '.pdf'))
self.assertIn('unit_test_report', data['output'])
if debug:
# Read raw HTML file
output = default_storage.open(
data['output'].replace('/media/', '', 1)
)
file_content = str(output.read(), 'utf-8')
else:
# Convert from PDF bytes to string for testing purposes
output_path = os.path.join(
settings.MEDIA_ROOT, data['output'].replace('/media/', '', 1)
)
reader = PdfReader(output_path)
file_content = ''.join(page.extract_text() for page in reader.pages)
# Replace any newline characters for testing purposes
file_content = file_content.replace('\n', ' ')
for ts in test_strings:
self.assertIn(ts, file_content)
self.assertIn(
f'REPORT OUTPUT: MERGE = {"ENABLED" if merge else "DISABLED"}',
file_content,
)
class LabelTest(InvenTreeAPITestCase):
"""Unit tests for label templates."""
@@ -351,6 +441,71 @@ class LabelTest(InvenTreeAPITestCase):
self.assertEqual(output.plugin, 'inventreelabel')
self.assertTrue(output.output.name.endswith('.pdf'))
def test_print_custom_template(self):
"""Test printing against a custom template file."""
template_string = """
Hello {{ user.username }} - your user ID is {{ user.pk }}.
Template name: {{ template.name }}
Barcode: {{ qr_data }}
Location ID: {{ location.pk }}
Base URL: {{ base_url }}
"""
template_file = ContentFile(
template_string.encode('utf-8'), name='TestLabelTemplate.html'
)
# Create a new label template with the above string as the template
template = LabelTemplate.objects.create(
name='Test label template',
model_type='stocklocation',
template=template_file,
filename_pattern='unit_test_label.pdf',
)
location = StockItem.objects.exclude(location=None).first().location
url = reverse('api-label-print')
post_data = {'template': template.pk, 'items': [location.pk]}
plugin = registry.get_plugin('inventreelabel')
test_strings = [
f'Hello {self.user.username} - your user ID is {self.user.pk}.',
f'Template name: {template.name}',
f'Location ID: {location.pk}',
f'INV-SL{location.pk}',
]
# Test with "debug" both enabled and disabled
for debug in [True, False]:
plugin.set_setting('DEBUG', debug)
# Generate label via the API
data = self.post(url, data=post_data).data
self.assertEqual(data['user'], self.user.pk)
self.assertTrue(data['output'].endswith('.html' if debug else '.pdf'))
# Read the file contents back out, and validate
if debug:
# Read raw HTML file
output = default_storage.open(data['output'].replace('/media/', '', 1))
file_content = str(output.read(), 'utf-8')
else:
# Convert from PDF bytes to string for testing purposes
output_path = os.path.join(
settings.MEDIA_ROOT, data['output'].replace('/media/', '', 1)
)
reader = PdfReader(output_path)
file_content = ''.join(page.extract_text() for page in reader.pages)
# Replace any newline for testing purposes
file_content = file_content.replace('\n', ' ')
for ts in test_strings:
self.assertIn(ts, file_content)
def test_filters(self):
"""Test that template filters are correctly validated."""
from django.core.exceptions import ValidationError