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:
@@ -24,7 +24,7 @@ class ReportMixin:
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
self.add_mixin(PluginMixinEnum.REPORT, True, __class__)
|
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.
|
"""Add extra context to the provided report instance.
|
||||||
|
|
||||||
By default, this method does nothing.
|
By default, this method does nothing.
|
||||||
@@ -32,11 +32,11 @@ class ReportMixin:
|
|||||||
Args:
|
Args:
|
||||||
report_instance: The report instance to add context to
|
report_instance: The report instance to add context to
|
||||||
model_instance: The model instance which initiated the report generation
|
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
|
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.
|
"""Add extra context to the provided label instance.
|
||||||
|
|
||||||
By default, this method does nothing.
|
By default, this method does nothing.
|
||||||
@@ -44,18 +44,18 @@ class ReportMixin:
|
|||||||
Args:
|
Args:
|
||||||
label_instance: The label instance to add context to
|
label_instance: The label instance to add context to
|
||||||
model_instance: The model instance which initiated the label generation
|
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
|
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.
|
"""Callback function called after a report is generated.
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
template: The ReportTemplate model
|
template: The ReportTemplate model
|
||||||
instance: The instance of the target model
|
instance: The instance of the target model
|
||||||
report: The generated report object
|
report: The generated report object
|
||||||
request: The initiating request object
|
user: The user to associate with the generated report
|
||||||
|
|
||||||
The default implementation does nothing.
|
The default implementation does nothing.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -38,41 +38,51 @@ class LabelPrintingMixin:
|
|||||||
|
|
||||||
BLOCKING_PRINT = True
|
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.
|
"""Render this label to PDF format.
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
label: The LabelTemplate object to render against
|
label: The LabelTemplate object to render against
|
||||||
instance: The model instance to render
|
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:
|
try:
|
||||||
return label.render(instance, request)
|
return label.render(instance, request=request, user=user)
|
||||||
except Exception:
|
except Exception:
|
||||||
log_error('render_to_pdf', plugin=self.slug)
|
log_error('render_to_pdf', plugin=self.slug)
|
||||||
raise ValidationError(_('Error rendering label to PDF'))
|
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.
|
"""Render this label to HTML format.
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
label: The LabelTemplate object to render against
|
label: The LabelTemplate object to render against
|
||||||
instance: The model instance to render
|
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:
|
try:
|
||||||
return label.render_as_string(instance, request)
|
return label.render_as_string(instance, request=request, user=user)
|
||||||
except Exception:
|
except Exception:
|
||||||
log_error('render_to_html', plugin=self.slug)
|
log_error('render_to_html', plugin=self.slug)
|
||||||
raise ValidationError(_('Error rendering label to HTML'))
|
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.
|
"""Render this label to PNG format.
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
label: The LabelTemplate object to render against
|
label: The LabelTemplate object to render against
|
||||||
instance: The model instance to render
|
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:
|
Keyword Arguments:
|
||||||
pdf_data: The raw PDF data of the rendered label (if already rendered)
|
pdf_data: The raw PDF data of the rendered label (if already rendered)
|
||||||
dpi: The DPI to use for the PNG rendering
|
dpi: The DPI to use for the PNG rendering
|
||||||
@@ -85,7 +95,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 = self.render_to_pdf(label, instance, request, **kwargs)
|
pdf_data = self.render_to_pdf(label, instance, request, user=user, **kwargs)
|
||||||
|
|
||||||
pdf2image_kwargs = {
|
pdf2image_kwargs = {
|
||||||
'dpi': kwargs.get('dpi', InvenTreeSetting.get_setting('LABEL_DPI', 300)),
|
'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"
|
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.
|
but this can be overridden by the particular plugin.
|
||||||
"""
|
"""
|
||||||
try:
|
# Extract user information, in decreasing order of preference
|
||||||
user = request.user
|
user = (
|
||||||
except AttributeError:
|
kwargs.pop('user', None)
|
||||||
user = None
|
or getattr(request, 'user', None)
|
||||||
|
or getattr(output, 'user', None)
|
||||||
|
)
|
||||||
|
|
||||||
# Initial state for the output print job
|
# Initial state for the output print job
|
||||||
output.progress = 0
|
output.progress = 0
|
||||||
@@ -145,11 +157,11 @@ class LabelPrintingMixin:
|
|||||||
|
|
||||||
# Generate a label output for each provided item
|
# Generate a label output for each provided item
|
||||||
for item in items:
|
for item in items:
|
||||||
context = label.get_context(item, request)
|
context = label.get_context(item, request, user=user)
|
||||||
filename = label.generate_filename(context)
|
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(
|
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 = {
|
print_args = {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ class InvenTreeLabelPlugin(LabelPrintingMixin, SettingsMixin, InvenTreePlugin):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
NAME = 'InvenTreeLabel'
|
NAME = 'InvenTreeLabel'
|
||||||
|
SLUG = 'inventreelabel'
|
||||||
TITLE = _('InvenTree PDF label printer')
|
TITLE = _('InvenTree PDF label printer')
|
||||||
DESCRIPTION = _('Provides native support for printing PDF labels')
|
DESCRIPTION = _('Provides native support for printing PDF labels')
|
||||||
VERSION = '1.1.0'
|
VERSION = '1.1.0'
|
||||||
|
|||||||
@@ -215,6 +215,7 @@ class LabelPrint(GenericAPIView):
|
|||||||
template.pk,
|
template.pk,
|
||||||
[item.pk for item in items_to_print],
|
[item.pk for item in items_to_print],
|
||||||
output.pk,
|
output.pk,
|
||||||
|
user.pk if user else None,
|
||||||
plugin.slug,
|
plugin.slug,
|
||||||
options=(plugin_serializer.data if plugin_serializer else {}),
|
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]
|
item_ids = [item.pk for item in items_to_print]
|
||||||
|
|
||||||
# Offload the task to the background worker
|
# 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()
|
output.refresh_from_db()
|
||||||
|
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ class BaseContextExtension(TypedDict):
|
|||||||
template_description: Description of the report template
|
template_description: Description of the report template
|
||||||
template_name: Name of the report template
|
template_name: Name of the report template
|
||||||
template_revision: Revision 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
|
base_url: str
|
||||||
@@ -244,37 +244,39 @@ class ReportTemplateBase(
|
|||||||
def generate_filename(self, context, **kwargs) -> str:
|
def generate_filename(self, context, **kwargs) -> str:
|
||||||
"""Generate a filename for this report."""
|
"""Generate a filename for this report."""
|
||||||
template_string = Template(self.filename_pattern)
|
template_string = Template(self.filename_pattern)
|
||||||
|
|
||||||
return template_string.render(Context(context))
|
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.
|
"""Render the report to a HTML string.
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
instance: The model instance to render against
|
instance: The model instance to render against
|
||||||
request: A HTTPRequest object (optional)
|
|
||||||
context: Django template language contexts (optional)
|
context: Django template language contexts (optional)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: HTML string
|
str: HTML string
|
||||||
"""
|
"""
|
||||||
if context is None:
|
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.
|
"""Render the template to a PDF file.
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
instance: The model instance to render against
|
instance: The model instance to render against
|
||||||
request: A HTTPRequest object (optional)
|
context: Django template language contexts (optional)
|
||||||
context: Django template langaguage contexts (optional)
|
user: The user to associate with the generated report
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bytes: PDF data
|
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)
|
pdf = HTML(string=html).write_pdf(pdf_forms=True)
|
||||||
|
|
||||||
return pdf
|
return pdf
|
||||||
@@ -323,28 +325,28 @@ class ReportTemplateBase(
|
|||||||
"""Return a filter dict which can be applied to the target model."""
|
"""Return a filter dict which can be applied to the target model."""
|
||||||
return report.validators.validate_filters(self.filters, model=self.get_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 context data (available to all templates)."""
|
||||||
return {
|
return {
|
||||||
'base_url': get_base_url(request=request),
|
'base_url': get_base_url(),
|
||||||
'date': InvenTree.helpers.current_date(),
|
'date': InvenTree.helpers.current_date(),
|
||||||
'datetime': InvenTree.helpers.current_time(),
|
'datetime': InvenTree.helpers.current_time(),
|
||||||
'template': self,
|
'template': self,
|
||||||
'template_description': self.description,
|
'template_description': self.description,
|
||||||
'template_name': self.name,
|
'template_name': self.name,
|
||||||
'template_revision': self.revision,
|
'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.
|
"""Supply context data to the generic template for rendering.
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
instance: The model instance we are printing against
|
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
|
# 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
|
# Add in an context information provided by the model instance itself
|
||||||
context = {**base_context, **instance.report_context()}
|
context = {**base_context, **instance.report_context()}
|
||||||
@@ -423,55 +425,77 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase):
|
|||||||
|
|
||||||
return report_context
|
return report_context
|
||||||
|
|
||||||
def get_context(self, instance, request=None, **kwargs):
|
def get_context(self, instance: models.Model, **kwargs):
|
||||||
"""Supply context data to the report template for rendering."""
|
"""Supply context data to the report template for rendering.
|
||||||
base_context = super().get_context(instance, request)
|
|
||||||
|
Arguments:
|
||||||
|
instance: The model instance we are printing against
|
||||||
|
"""
|
||||||
|
base_context = super().get_context(instance, **kwargs)
|
||||||
report_context: ReportContextExtension = self.get_report_context()
|
report_context: ReportContextExtension = self.get_report_context()
|
||||||
|
|
||||||
context = {**base_context, **report_context}
|
context = {**base_context, **report_context}
|
||||||
|
|
||||||
# Pass the context through to the plugin registry for any additional information
|
# 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
|
return context
|
||||||
|
|
||||||
def get_plugin_context(self, instance, request, context):
|
def get_plugin_context(self, instance: models.Model, context: dict, **kwargs):
|
||||||
"""Get the context for the plugin."""
|
"""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):
|
for plugin in registry.with_mixin(PluginMixinEnum.REPORT):
|
||||||
try:
|
try:
|
||||||
plugin.add_report_context(self, instance, request, context)
|
plugin.add_report_context(self, instance, user, context)
|
||||||
except Exception:
|
except Exception:
|
||||||
InvenTree.exceptions.log_error('add_report_context', plugin=plugin.slug)
|
InvenTree.exceptions.log_error('add_report_context', plugin=plugin.slug)
|
||||||
|
|
||||||
return context
|
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)."""
|
"""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:
|
||||||
instance.create_attachment(
|
instance.create_attachment(
|
||||||
attachment=ContentFile(report, 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=user,
|
||||||
if request and request.user.is_authenticated
|
|
||||||
else None,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def notify_plugins(self, instance, report, request):
|
def notify_plugins(self, instance, report, user):
|
||||||
"""Provide generated report to any interested plugins."""
|
"""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)
|
report_plugins = registry.with_mixin(PluginMixinEnum.REPORT)
|
||||||
|
|
||||||
for plugin in report_plugins:
|
for plugin in report_plugins:
|
||||||
try:
|
try:
|
||||||
plugin.report_callback(self, instance, report, request)
|
plugin.report_callback(self, instance, report, user)
|
||||||
except Exception:
|
except Exception:
|
||||||
InvenTree.exceptions.log_error('report_callback', plugin=plugin.slug)
|
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.
|
"""Print reports for a list of items against this template.
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
items: A list of items to print reports for (model instance)
|
items: A list of items to print reports for (model instance)
|
||||||
output: The DataOutput object to use (if provided)
|
output: The DataOutput object to use
|
||||||
request: The request object (optional)
|
user: The user to associate with the generated report
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
output: The DataOutput object representing the generated report(s)
|
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)
|
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 = []
|
outputs = []
|
||||||
|
|
||||||
debug_mode = get_global_setting('REPORT_DEBUG_MODE', False)
|
debug_mode = get_global_setting('REPORT_DEBUG_MODE', False)
|
||||||
@@ -500,9 +527,7 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase):
|
|||||||
if not output:
|
if not output:
|
||||||
output = DataOutput.objects.create(
|
output = DataOutput.objects.create(
|
||||||
total=len(items),
|
total=len(items),
|
||||||
user=request.user
|
user=user,
|
||||||
if request and request.user and request.user.is_authenticated
|
|
||||||
else None,
|
|
||||||
progress=0,
|
progress=0,
|
||||||
complete=False,
|
complete=False,
|
||||||
output_type=DataOutput.DataOutputTypes.REPORT,
|
output_type=DataOutput.DataOutputTypes.REPORT,
|
||||||
@@ -516,13 +541,13 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
if self.merge:
|
if self.merge:
|
||||||
base_context = super().base_context(request)
|
base_context = super().base_context(user=user)
|
||||||
report_context = self.get_report_context()
|
report_context = self.get_report_context()
|
||||||
item_contexts = []
|
item_contexts = []
|
||||||
for instance in items:
|
for instance in items:
|
||||||
instance_context = instance.report_context()
|
instance_context = instance.report_context()
|
||||||
instance_context = self.get_plugin_context(
|
instance_context = self.get_plugin_context(
|
||||||
instance, request, instance_context
|
instance, instance_context, user=user
|
||||||
)
|
)
|
||||||
item_contexts.append(instance_context)
|
item_contexts.append(instance_context)
|
||||||
|
|
||||||
@@ -537,9 +562,11 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
if debug_mode:
|
if debug_mode:
|
||||||
report = self.render_as_string(instance, request, contexts)
|
report = self.render_as_string(
|
||||||
|
instance, user=user, context=contexts
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
report = self.render(instance, request, contexts)
|
report = self.render(instance, user=user, context=contexts)
|
||||||
except TemplateDoesNotExist as e:
|
except TemplateDoesNotExist as e:
|
||||||
t_name = str(e) or self.template
|
t_name = str(e) or self.template
|
||||||
msg = f'Template file {t_name} does not exist'
|
msg = f'Template file {t_name} does not exist'
|
||||||
@@ -558,17 +585,15 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase):
|
|||||||
raise ValidationError(f'{msg}: {e!s}')
|
raise ValidationError(f'{msg}: {e!s}')
|
||||||
|
|
||||||
outputs.append(report)
|
outputs.append(report)
|
||||||
self.handle_attachment(
|
self.handle_attachment(instance, report, report_name, user, debug_mode)
|
||||||
instance, report, report_name, request, debug_mode
|
self.notify_plugins(instance, report, user)
|
||||||
)
|
|
||||||
self.notify_plugins(instance, report, request)
|
|
||||||
|
|
||||||
# Update the progress of the report generation
|
# Update the progress of the report generation
|
||||||
output.progress += 1
|
output.progress += 1
|
||||||
output.save()
|
output.save()
|
||||||
else:
|
else:
|
||||||
for instance in items:
|
for instance in items:
|
||||||
context = self.get_context(instance, request)
|
context = self.get_context(instance, user=user)
|
||||||
|
|
||||||
if report_name is None:
|
if report_name is None:
|
||||||
report_name = self.generate_filename(context)
|
report_name = self.generate_filename(context)
|
||||||
@@ -576,9 +601,11 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase):
|
|||||||
# Render the report output
|
# Render the report output
|
||||||
try:
|
try:
|
||||||
if debug_mode:
|
if debug_mode:
|
||||||
report = self.render_as_string(instance, request, None)
|
report = self.render_as_string(
|
||||||
|
instance, user=user, context=context
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
report = self.render(instance, request, None)
|
report = self.render(instance, user=user, context=None)
|
||||||
except TemplateDoesNotExist as e:
|
except TemplateDoesNotExist as e:
|
||||||
t_name = str(e) or self.template
|
t_name = str(e) or self.template
|
||||||
msg = f'Template file {t_name} does not exist'
|
msg = f'Template file {t_name} does not exist'
|
||||||
@@ -599,9 +626,10 @@ class ReportTemplate(TemplateUploadMixin, ReportTemplateBase):
|
|||||||
outputs.append(report)
|
outputs.append(report)
|
||||||
|
|
||||||
self.handle_attachment(
|
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
|
# Update the progress of the report generation
|
||||||
output.progress += 1
|
output.progress += 1
|
||||||
@@ -614,7 +642,6 @@ 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 if request else None,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if not report_name:
|
if not report_name:
|
||||||
@@ -704,9 +731,16 @@ class LabelTemplate(TemplateUploadMixin, ReportTemplateBase):
|
|||||||
}}
|
}}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get_context(self, instance, request=None, **kwargs):
|
def get_context(self, instance: models.Model, *args, **kwargs):
|
||||||
"""Supply context data to the label template for rendering."""
|
"""Supply context data to the label template for rendering.
|
||||||
base_context = super().get_context(instance, request, **kwargs)
|
|
||||||
|
Arguments:
|
||||||
|
instance: The model instance we are printing against
|
||||||
|
"""
|
||||||
|
user = kwargs.get('user')
|
||||||
|
|
||||||
|
base_context = super().get_context(instance, **kwargs)
|
||||||
|
|
||||||
label_context: LabelContextExtension = {
|
label_context: LabelContextExtension = {
|
||||||
'width': self.width,
|
'width': self.width,
|
||||||
'height': self.height,
|
'height': self.height,
|
||||||
@@ -724,7 +758,7 @@ class LabelTemplate(TemplateUploadMixin, ReportTemplateBase):
|
|||||||
for plugin in plugins:
|
for plugin in plugins:
|
||||||
# Let each plugin add its own context data
|
# Let each plugin add its own context data
|
||||||
try:
|
try:
|
||||||
plugin.add_label_context(self, instance, request, context)
|
plugin.add_label_context(self, instance, user, context)
|
||||||
except Exception:
|
except Exception:
|
||||||
InvenTree.exceptions.log_error('add_label_context', plugin=plugin.slug)
|
InvenTree.exceptions.log_error('add_label_context', plugin=plugin.slug)
|
||||||
|
|
||||||
@@ -734,9 +768,9 @@ class LabelTemplate(TemplateUploadMixin, ReportTemplateBase):
|
|||||||
self,
|
self,
|
||||||
items: list,
|
items: list,
|
||||||
plugin: InvenTreePlugin,
|
plugin: InvenTreePlugin,
|
||||||
output=None,
|
output: Optional[DataOutput] = None,
|
||||||
options=None,
|
options: Optional[dict] = None,
|
||||||
request=None,
|
user: Optional[AbstractUser] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> DataOutput:
|
) -> DataOutput:
|
||||||
"""Print labels for a list of items against this template.
|
"""Print labels for a list of items against this template.
|
||||||
@@ -744,9 +778,9 @@ class LabelTemplate(TemplateUploadMixin, ReportTemplateBase):
|
|||||||
Arguments:
|
Arguments:
|
||||||
items: A list of items to print labels for (model instance)
|
items: A list of items to print labels for (model instance)
|
||||||
plugin: The plugin to use for label rendering
|
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)
|
options: Additional options for the label printing plugin (optional)
|
||||||
request: The request object (optional)
|
user: The user to associate with the generated labels
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
output: The DataOutput object representing the generated label(s)
|
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}'"
|
f"Printing {len(items)} labels against template '{self.name}' using plugin '{plugin.slug}'"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
user = user or getattr(output, 'user', None)
|
||||||
|
|
||||||
if not output:
|
if not output:
|
||||||
output = DataOutput.objects.create(
|
output = DataOutput.objects.create(
|
||||||
user=request.user
|
user=user,
|
||||||
if request and request.user.is_authenticated
|
|
||||||
else None,
|
|
||||||
total=len(items),
|
total=len(items),
|
||||||
progress=0,
|
progress=0,
|
||||||
complete=False,
|
complete=False,
|
||||||
@@ -779,7 +813,9 @@ class LabelTemplate(TemplateUploadMixin, ReportTemplateBase):
|
|||||||
if hasattr(plugin, 'before_printing'):
|
if hasattr(plugin, 'before_printing'):
|
||||||
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'):
|
if hasattr(plugin, 'after_printing'):
|
||||||
plugin.after_printing()
|
plugin.after_printing()
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
"""Background tasks for the report app."""
|
"""Background tasks for the report app."""
|
||||||
|
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
from opentelemetry import trace
|
from opentelemetry import trace
|
||||||
|
|
||||||
@@ -10,13 +12,16 @@ logger = structlog.get_logger('inventree')
|
|||||||
|
|
||||||
|
|
||||||
@tracer.start_as_current_span('print_reports')
|
@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.
|
"""Print multiple reports against the provided template.
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
template_id: The ID of the ReportTemplate to use
|
template_id: The ID of the ReportTemplate to use
|
||||||
item_ids: List of item IDs to generate the report against
|
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,
|
This function is intended to be called by the background worker,
|
||||||
and will continuously update the status of the DataOutput object.
|
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')
|
log_error('report.tasks.print_reports')
|
||||||
return
|
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
|
# Fetch the items to be included in the report
|
||||||
model = template.get_model()
|
model = template.get_model()
|
||||||
items = model.objects.filter(pk__in=item_ids)
|
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
|
# Ensure they are sorted by the order of the provided item IDs
|
||||||
items = sorted(items, key=lambda item: item_ids.index(item.pk))
|
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')
|
@tracer.start_as_current_span('print_labels')
|
||||||
def 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.
|
"""Print multiple labels against the provided template.
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
template_id: The ID of the LabelTemplate to use
|
template_id: The ID of the LabelTemplate to use
|
||||||
item_ids: List of item IDs to generate the labels against
|
item_ids: List of item IDs to generate the labels against
|
||||||
output_id: The ID of the DataOutput to use (if provided)
|
output_id: The ID of the DataOutput to use
|
||||||
plugin_slug: The ID of the LabelPlugin to use (if provided)
|
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,
|
This function is intended to be called by the background worker,
|
||||||
and will continuously update the status of the DataOutput object.
|
and will continuously update the status of the DataOutput object.
|
||||||
@@ -67,6 +90,18 @@ def print_labels(
|
|||||||
log_error('report.tasks.print_labels')
|
log_error('report.tasks.print_labels')
|
||||||
return
|
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
|
# Fetch the items to be included in the report
|
||||||
model = template.get_model()
|
model = template.get_model()
|
||||||
items = model.objects.filter(pk__in=item_ids)
|
items = model.objects.filter(pk__in=item_ids)
|
||||||
@@ -83,4 +118,4 @@ def print_labels(
|
|||||||
# Extract optional arguments for label printing
|
# Extract optional arguments for label printing
|
||||||
options = kwargs.pop('options') or {}
|
options = kwargs.pop('options') or {}
|
||||||
|
|
||||||
template.print(items, plugin, output=output, options=options)
|
template.print(items, plugin, output=output, user=user, options=options)
|
||||||
|
|||||||
@@ -6,8 +6,12 @@ from io import StringIO
|
|||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.cache import cache
|
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 django.urls import reverse
|
||||||
|
|
||||||
|
from pypdf import PdfReader
|
||||||
|
|
||||||
import report.models as report_models
|
import report.models as report_models
|
||||||
from build.models import Build
|
from build.models import Build
|
||||||
from common.models import Attachment
|
from common.models import Attachment
|
||||||
@@ -299,6 +303,92 @@ class ReportTest(InvenTreeAPITestCase):
|
|||||||
self.assertIsNotNone(output.output)
|
self.assertIsNotNone(output.output)
|
||||||
self.assertTrue(output.output.name.endswith('.pdf'))
|
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):
|
class LabelTest(InvenTreeAPITestCase):
|
||||||
"""Unit tests for label templates."""
|
"""Unit tests for label templates."""
|
||||||
@@ -351,6 +441,71 @@ class LabelTest(InvenTreeAPITestCase):
|
|||||||
self.assertEqual(output.plugin, 'inventreelabel')
|
self.assertEqual(output.plugin, 'inventreelabel')
|
||||||
self.assertTrue(output.output.name.endswith('.pdf'))
|
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):
|
def test_filters(self):
|
||||||
"""Test that template filters are correctly validated."""
|
"""Test that template filters are correctly validated."""
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
|||||||
Reference in New Issue
Block a user