[report] Add set_var and get_var helpers (#12901)

* [report] Add set_var and get_var helpers

* Add type hints

* Adjust helpers
This commit is contained in:
Oliver
2026-09-21 16:38:03 +10:00
committed by GitHub
parent fb0cc89fc3
commit 2f91f9a068
4 changed files with 164 additions and 0 deletions
+6
View File
@@ -152,6 +152,7 @@ class BaseContextExtension(TypedDict):
template_name: Name of the report template
template_revision: Revision of the report template
user: User who is creating the report (if available)
report_vars: Private dict for the {% get_var %} / {% set_var %} template tags
"""
base_url: str
@@ -162,6 +163,7 @@ class BaseContextExtension(TypedDict):
template_name: str
template_revision: int
user: Optional[AbstractUser]
report_vars: dict
class LabelContextExtension(TypedDict):
@@ -341,6 +343,10 @@ class ReportTemplateBase(
'template_name': self.name,
'template_revision': self.revision,
'user': kwargs.get('user'),
# A private, per-render dict for the {% get_var %} / {% set_var %} tags.
# This is discarded once rendering of *this* instance completes,
# and is never shared between instances, reports, or requests.
'report_vars': {},
}
def get_context(self, instance: models.Model, **kwargs):
@@ -173,6 +173,53 @@ def getkey(container: dict, key: str, backup_value: Optional[Any] = None) -> Any
return container.get(key, backup_value)
@register.simple_tag(takes_context=True)
def set_var(context: dict, name: str, value: Any) -> str:
"""Store a named variable, for later retrieval with get_var.
Arguments:
context: The template context, which should contain a 'report_vars' dictionary.
name: The name to store the variable against (must be a string)
value: The value to store
Returns:
An empty string - this tag does not render any output
"""
if not isinstance(name, str):
logger.warning('set_var() called with non-string name')
return ''
store = context.get('report_vars')
if isinstance(store, dict):
store[name] = value
else:
logger.warning('set_var() called outside of a valid report context')
return ''
@register.simple_tag(takes_context=True)
def get_var(context: dict, name: str, backup_value: Optional[Any] = None) -> Any:
"""Retrieve a named variable previously stored with set_var.
Arguments:
context: The template context, which should contain a 'report_vars' dictionary.
name: The name of the variable to retrieve
backup_value: Value to return if the variable has not been set (default = None)
Returns:
The stored value, or backup_value if the variable has not been set
"""
store = context.get('report_vars')
if not isinstance(store, dict):
logger.warning('get_var() called outside of a valid report context')
return backup_value
return store.get(name, backup_value)
def media_file_exists(path: Path | str) -> bool:
"""Check if a media file exists at the specified path.
+65
View File
@@ -61,6 +61,71 @@ class ReportTagTest(PartImageTestMixin, InvenTreeTestCase):
None, report_tags.getkey('not a container', 'not-a-key', 'a value')
)
def test_get_set_var(self):
"""Tests for the 'get_var' and 'set_var' template tags."""
# Directly exercise the tag functions against a report-shaped context
context = Context({'report_vars': {}})
# Not yet set - should return the backup value
self.assertIsNone(report_tags.get_var(context, 'foo'))
self.assertEqual(report_tags.get_var(context, 'foo', 'backup'), 'backup')
# set_var renders no output, and stores the value for later retrieval
self.assertEqual(report_tags.set_var(context, 'foo', 'bar'), '')
self.assertEqual(report_tags.get_var(context, 'foo'), 'bar')
# Overwrite the value
report_tags.set_var(context, 'foo', 'baz')
self.assertEqual(report_tags.get_var(context, 'foo'), 'baz')
# A non-string name is rejected
report_tags.set_var(context, 123, 'nope')
self.assertNotIn(123, context['report_vars'])
# If the report context is missing (or malformed), fail safe rather than crash
broken_context = Context({'report_vars': 'not-a-dict'})
self.assertEqual(report_tags.set_var(broken_context, 'foo', 'bar'), '')
self.assertEqual(report_tags.get_var(broken_context, 'foo', 'backup'), 'backup')
missing_context = Context({})
report_tags.set_var(missing_context, 'foo', 'bar')
self.assertEqual(
report_tags.get_var(missing_context, 'foo', 'backup'), 'backup'
)
# set_var / get_var must not expose or mutate other context variables
full_context = Context({'report_vars': {}, 'user': 'sensitive-user-object'})
report_tags.set_var(full_context, 'user', 'hijacked')
self.assertEqual(full_context['user'], 'sensitive-user-object')
self.assertEqual(full_context['report_vars']['user'], 'hijacked')
# Exercise the tags via full template rendering, to confirm that a variable
# set inside a {% for %} loop remains visible outside of the loop
# (unlike Django's built-in scoping rules for block-local context changes),
# which is what makes these tags useful for accumulating totals.
template = Template(
'{% load report %}'
'{% set_var "total" 0 %}'
'{% for value in values %}'
'{% get_var "total" as total %}'
'{% add total value as running_total %}'
'{% set_var "total" running_total %}'
'{% endfor %}'
'{% get_var "total" as final_total %}'
'Total: {{ final_total }}'
)
rendered = template.render(Context({'values': [1, 2, 3, 4], 'report_vars': {}}))
self.assertIn('Total: 10', rendered)
# Two separate renders must not share state
context_a = Context({'report_vars': {}})
context_b = Context({'report_vars': {}})
report_tags.set_var(context_a, 'shared_name', 'value-a')
report_tags.set_var(context_b, 'shared_name', 'value-b')
self.assertEqual(report_tags.get_var(context_a, 'shared_name'), 'value-a')
self.assertEqual(report_tags.get_var(context_b, 'shared_name'), 'value-b')
def test_asset(self):
"""Tests for asset files."""
# Test that an error is raised if the file does not exist