Add raise_error template tag (#12940)

This commit is contained in:
Petr Ledvina
2026-09-28 07:04:49 +10:00
committed by GitHub
parent 5d0c4d9646
commit 967f7a55d1
5 changed files with 183 additions and 1 deletions
+20
View File
@@ -14,6 +14,26 @@ Some common functions are provided for use in custom report and label templates.
!!! tip "Use the Source, Luke"
To see the full range of available helper functions, refer to the source file [report.py]({{ sourcefile("src/backend/InvenTree/report/templatetags/report.py") }}) where these functions are defined!
## Raising Template Errors
Use `raise_error` to stop rendering a report or label template with an actionable error message. The message is required and can be a quoted string or a template variable.
::: report.templatetags.report.raise_error
options:
show_docstring_description: false
show_source: False
```html
{% raw %}
{% load report %}
{% if not stock_item.serial %}
{% raise_error "Serial number is required for this label" %}
{% endif %}
{% endraw %}
```
The tag raises a validation error. For labels, how this error affects printing depends on the printing plugin. See [label validation](./labels.md#validating-label-data) for sheet and batch behavior.
## Assigning Variables
When making use of helper functions within a template, it can be useful to store the result of the function to a variable, rather than immediately rendering the output.
+20
View File
@@ -120,6 +120,26 @@ OUT OF STOCK
{% endraw %}
```
### Validating Label Data
Use the [raise_error helper](./helpers.md#raising-template-errors) to reject data which cannot produce a valid label:
```html
{% raw %}
{% load report %}
{% if not stock_item.serial %}
{% raise_error "Serial number is required for this label" %}
{% endif %}
{% endraw %}
```
When evaluated, the tag raises a validation error and stops rendering the current label. With the InvenTree PDF label printer, this stops the print job and retains the supplied message in the failed print result. Correct the data and submit a new print job to print again.
The InvenTree Label Sheet Printer handles this like other rendering errors: it logs the error, inserts an error cell, and continues generating the sheet. The tag does not abort the entire sheet job.
!!! warning "Batch printing"
Validation happens when each template is evaluated. Earlier labels may already have printed or been queued by the printing plugin. This does not validate the entire batch before printing or undo earlier printing. Custom plugins must propagate template validation errors for this behavior to apply.
### Label Filters
Each label template provides a set of programmable filters which can be used to determine the relevance of that particular label. It may be the case that a particular label template is only applicable if certain conditions are met.
@@ -49,6 +49,19 @@ register = template.Library()
logger = logging.getLogger('inventree')
@register.simple_tag()
def raise_error(message: str):
"""Stop rendering a report or label template with an error message.
Arguments:
message: The message to include in the validation error.
Raises:
ValidationError: Whenever this tag is evaluated.
"""
raise ValidationError(message)
def get_locale(locale: Optional[str] = None) -> Locale:
"""Resolve and return a babel Locale.
+30 -1
View File
@@ -12,7 +12,7 @@ from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.template import Context, Template
from django.test import TestCase, override_settings
from django.test import SimpleTestCase, TestCase, override_settings
from django.utils import timezone
from django.utils.safestring import SafeString
@@ -28,6 +28,35 @@ from report.templatetags import barcode as barcode_tags
from report.templatetags import report as report_tags
class RaiseErrorTagTest(SimpleTestCase):
"""Test deliberate validation failures in templates."""
def test_raise_error(self):
"""Literal and variable messages are preserved by the template tag."""
message = 'Serial number is required for this label'
for argument in [f'"{message}"', 'error_message']:
with self.subTest(argument=argument):
template = Template(
'{% load report %}{% raise_error ' + argument + ' %}'
)
with self.assertRaises(ValidationError) as raised:
template.render(Context({'error_message': message}))
self.assertEqual(raised.exception.messages, [message])
def test_conditional_failure(self):
"""A tag inside a false condition does not stop rendering."""
template = Template(
'{% load report %}'
'{% if not stock_item.serial %}{% raise_error "Serial required" %}{% endif %}'
'{{ stock_item.serial }}'
)
self.assertEqual(
template.render(Context({'stock_item': {'serial': '123'}})), '123'
)
class ReportTagTest(PartImageTestMixin, InvenTreeTestCase):
"""Unit tests for the report template tags."""
+100
View File
@@ -19,6 +19,7 @@ from django.test import TestCase
from django.urls import reverse
from django.utils.timezone import now
from error_report.models import Error
from pypdf import PdfReader
import report.models as report_models
@@ -814,6 +815,105 @@ class LabelTest(InvenTreeAPITestCase):
self.assertEqual(result['part__active'], 'False')
class LabelValidationTest(InvenTreeAPITestCase):
"""Exercise template validation through label rendering and worker tasks."""
fixtures = ['category', 'part', 'location', 'stock']
superuser = True
def setUp(self):
"""Create a label which requires a serial number."""
super().setUp()
cache.clear()
self.message = 'Serial number is required for this label'
self.template = LabelTemplate.objects.create(
name='Serial validation label',
model_type='stockitem',
template=ContentFile(
'{% load report %}'
'{% if not stock_item.serial %}'
'{% raise_error "' + self.message + '" %}'
'{% endif %}Serial: {{ stock_item.serial }}',
name='SerialValidationLabel.html',
),
)
self.valid = StockItem.objects.get(pk=105)
self.invalid = StockItem.objects.get(pk=100)
self.later = StockItem.objects.get(pk=501)
def test_render_validation(self):
"""Both wrappers log and preserve failures from an actual template."""
plugin = registry.get_plugin('inventreelabel')
for wrapper in ['render_to_pdf', 'render_to_html']:
with (
self.subTest(wrapper=wrapper),
patch('plugin.base.label.mixins.log_error') as log_error,
self.assertRaises(ValidationError) as raised,
):
getattr(plugin, wrapper)(self.template, self.invalid, None)
self.assertEqual(raised.exception.messages, [self.message])
log_error.assert_called_once_with(wrapper, plugin=plugin.slug)
def test_worker_validation(self):
"""Real template failures stop rendering and persist their diagnostics."""
plugin = registry.get_plugin('inventreelabel')
output = report_models.DataOutput.objects.create(
output_type=report_models.DataOutput.DataOutputTypes.LABEL,
template_name=self.template.name,
plugin=plugin.slug,
total=3,
)
diagnostics = Error.objects.filter(
path=f'plugin.{plugin.slug}.render_to_pdf', info__contains=self.message
)
error_count = diagnostics.count()
item_ids = [self.valid.pk, self.invalid.pk, self.later.pk]
with (
patch.object(
LabelTemplate,
'render_as_string',
side_effect=self.template.render_as_string,
) as render,
self.assertRaises(ValidationError) as raised,
):
print_labels(
self.template.pk,
item_ids,
output.pk,
self.user.pk,
plugin.slug,
options={},
)
self.assertEqual(raised.exception.messages, [self.message])
self.assertEqual(
[call.args[0].pk for call in render.call_args_list], item_ids[:2]
)
self.assertEqual(diagnostics.count(), error_count + 1)
output.refresh_from_db()
self.assertEqual(output.errors, {'error': self.message})
self.assertFalse(output.complete)
self.assertFalse(output.output)
def test_partial_batch(self):
"""A later invalid label cannot undo an earlier print."""
plugin = registry.get_plugin('inventreelabel')
with (
patch.object(plugin, 'print_label') as print_label,
patch.object(plugin, 'get_generated_file') as get_generated_file,
self.assertRaises(ValidationError),
):
self.template.print([self.valid, self.invalid, self.later], plugin)
print_label.assert_called_once()
self.assertEqual(print_label.call_args.kwargs['item_instance'], self.valid)
get_generated_file.assert_not_called()
class PrintTestMixins:
"""Mixin that enables e2e printing tests."""