mirror of
https://github.com/inventree/InvenTree.git
synced 2026-09-27 14:16:02 +00:00
[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:
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user