Add raise_error template tag (#12940)

This commit is contained in:
Petr Ledvina authored and GitHub committed 2026-09-28 07:04:49 +10:00
1 parent 5d0c4d9646
commit 967f7a55d1
5 files changed
+183 -1

No files matched your search

@@ -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."""