diff --git a/CHANGELOG.md b/CHANGELOG.md index f73535f..ecca012 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,12 @@ so make sure you follow the template --> +## 4.x.x unreleased + +### Fixed + +* `is_static_expression` now handles expressions with filters correctly + ## 4.0.0 2024-03-18 ### Breaking Changes diff --git a/src/allianceutils/template.py b/src/allianceutils/template.py index d4798d9..a188ea3 100644 --- a/src/allianceutils/template.py +++ b/src/allianceutils/template.py @@ -173,12 +173,20 @@ def build_html_attrs(html_kwargs: Dict[str, str], prohibited_attrs: Optional[Lis return output -def is_static_expression(expr: Optional[FilterExpression]) -> bool: - """Check if a given expression is static""" +def is_static_expression(expr: Optional[Union[FilterExpression,str]]) -> bool: + """Check if a given expression is static + This can be used when writing custom template tags to determine if the value passed in is a static value, and can + be resolved without ``context``. + """ + if expr is None or isinstance(expr, str): + return True + if not isinstance(expr, FilterExpression): + return False # type: ignore[unreachable] # unreachable because of type, but we want to return False rather than crash if not a FilterExpression # the arg.var.lookups is how Variable internally determines if value is a literal. See its # implementation of ``resolve``. - if not expr or not isinstance(expr.var, Variable) or expr.var.lookups is None: - return True + if not isinstance(expr.var, Variable) or expr.var.lookups is None: + # If it has filters then we assume it's not static + return not expr.filters # There are 3 built-ins that look like vars but get resolved from a static list (see ``BaseContext``) return expr.var.var in ["None", "True", "False"] diff --git a/src/test_allianceutils/tests/test_template_utils.py b/src/test_allianceutils/tests/test_template_utils.py new file mode 100644 index 0000000..c6dd719 --- /dev/null +++ b/src/test_allianceutils/tests/test_template_utils.py @@ -0,0 +1,33 @@ +from typing import cast + +from django.template.base import Template +from django.test import SimpleTestCase + +from allianceutils.template import is_static_expression +from allianceutils.templatetags.default_value import DefaultValueNode + + +class TestTemplateUtils(SimpleTestCase): + + def _get_as_var(self, contents: str): + # Bit hacky, but use the default_value tag to parse the variable, which when then access from + # ``DefaultValueNode.assignments``. + template = Template(f'{{% load default_value %}}{{% default_value test_var={contents} %}}') + node: DefaultValueNode = cast(DefaultValueNode, template.compile_nodelist()[-1]) + return node.assignments['test_var'] + + def test_is_static_expression_string(self): + self.assertTrue(is_static_expression(self._get_as_var('"foo"'))) + + def test_is_static_expression_bool(self): + self.assertTrue(is_static_expression(self._get_as_var("True"))) + self.assertTrue(is_static_expression(self._get_as_var("False"))) + + def test_is_static_expression_with_filter(self): + self.assertFalse(is_static_expression(self._get_as_var("foo|add:''"))) + + def test_is_static_expression_raw_string(self): + self.assertTrue(is_static_expression("foo")) + + def test_is_static_expression_other_value(self): + self.assertFalse(is_static_expression({})) # type: ignore[arg-type] # just testing runtime behaviour