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:
@@ -80,6 +80,52 @@ To return an element corresponding to a certain key in a container which support
|
||||
{% endraw %}
|
||||
```
|
||||
|
||||
## Session Variables
|
||||
|
||||
Variables assigned with the `as` keyword (as described above) are only visible within the template block they were assigned in - for example, a variable assigned inside a `{% raw %}{% for %}{% endraw %}` loop is not visible once the loop ends. This makes it awkward to accumulate a value (such as a running total) across a loop.
|
||||
|
||||
To get around this, the `set_var` and `get_var` functions can be used to store and retrieve a named variable which remains visible for the remainder of the current report or label render, regardless of which template block it was set within:
|
||||
|
||||
### set_var
|
||||
|
||||
Assign a value to a named variable within the current rendering context. This variable will remain accessible for the remainder of the report or label render, regardless of which template block it was set within.
|
||||
|
||||
::: report.templatetags.report.set_var
|
||||
options:
|
||||
show_docstring_description: false
|
||||
show_source: False
|
||||
|
||||
### get_var
|
||||
|
||||
Retrieve the value of a named variable previously stored with `set_var`. If the variable has not been set, a backup value can be provided.
|
||||
|
||||
::: report.templatetags.report.get_var
|
||||
options:
|
||||
show_docstring_description: false
|
||||
show_source: False
|
||||
|
||||
#### Example
|
||||
|
||||
```html
|
||||
{% raw %}
|
||||
{% load report %}
|
||||
|
||||
{% set_var "total" 0 %}
|
||||
|
||||
{% for line in lines %}
|
||||
{% get_var "total" as total %}
|
||||
{% add total line.quantity as new_total %}
|
||||
{% set_var "total" new_total %}
|
||||
{% endfor %}
|
||||
|
||||
{% get_var "total" as final_total %}
|
||||
Total quantity: {{ final_total }}
|
||||
{% endraw %}
|
||||
```
|
||||
|
||||
!!! info "Isolated per Render"
|
||||
The variables stored with `set_var` are private to the report or label instance currently being rendered. They are reset for every instance, and are never shared between reports, requests, or users.
|
||||
|
||||
## Database Helpers
|
||||
|
||||
A number of helper functions are available for accessing database objects:
|
||||
|
||||
@@ -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