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 %}
|
{% 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
|
## Database Helpers
|
||||||
|
|
||||||
A number of helper functions are available for accessing database objects:
|
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_name: Name of the report template
|
||||||
template_revision: Revision of the report template
|
template_revision: Revision of the report template
|
||||||
user: User who is creating the report (if available)
|
user: User who is creating the report (if available)
|
||||||
|
report_vars: Private dict for the {% get_var %} / {% set_var %} template tags
|
||||||
"""
|
"""
|
||||||
|
|
||||||
base_url: str
|
base_url: str
|
||||||
@@ -162,6 +163,7 @@ class BaseContextExtension(TypedDict):
|
|||||||
template_name: str
|
template_name: str
|
||||||
template_revision: int
|
template_revision: int
|
||||||
user: Optional[AbstractUser]
|
user: Optional[AbstractUser]
|
||||||
|
report_vars: dict
|
||||||
|
|
||||||
|
|
||||||
class LabelContextExtension(TypedDict):
|
class LabelContextExtension(TypedDict):
|
||||||
@@ -341,6 +343,10 @@ class ReportTemplateBase(
|
|||||||
'template_name': self.name,
|
'template_name': self.name,
|
||||||
'template_revision': self.revision,
|
'template_revision': self.revision,
|
||||||
'user': kwargs.get('user'),
|
'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):
|
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)
|
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:
|
def media_file_exists(path: Path | str) -> bool:
|
||||||
"""Check if a media file exists at the specified path.
|
"""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')
|
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):
|
def test_asset(self):
|
||||||
"""Tests for asset files."""
|
"""Tests for asset files."""
|
||||||
# Test that an error is raised if the file does not exist
|
# Test that an error is raised if the file does not exist
|
||||||
|
|||||||
Reference in New Issue
Block a user