diff --git a/README.rst b/README.rst index 90c981d..06649bb 100644 --- a/README.rst +++ b/README.rst @@ -10,13 +10,13 @@ Features Error on non-existent template variables ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Django templates by default hide errors, and when it does show an error it's often not very helpful. This app will change this so that if you do: +Django templates by default hide errors, and when it does show an error it's often not very helpful. This app changes this behavior to provide more informative error messages. For example, if you use: .. code:: html {{ does_not_exist }} -instead of rendering that as an empty string, this app will give you an error message: +instead of rendering that as an empty string, this app will raise a :code:`FastDevVariableDoesNotExist` error with a detailed message: .. code:: @@ -34,11 +34,22 @@ instead of rendering that as an empty string, this app will give you an error me request user -There are more specialized error messages for when you try to access the contents of a :code:`dict`, and attributes of an object a few levels deep like :code:`foo.bar.baz` (where baz doesn't exist). +There are more specialized error messages for accessing non-existent keys in a :code:`dict` or attributes of an object several levels deep, such as :code:`foo.bar.baz` (where :code:`baz` doesn't exist). -By default, :code:`django-fastdev` only checks templates that exist within your project directory. If you want it to check ALL templates, including stock django templates and templates from third party libraries, add :code:`FASTDEV_STRICT_TEMPLATE_CHECKING = True` to your project :code:`settings.py`. +**Handling `default` and `default_if_none` Filters** -Variable access inside :code:`{% if %}` will not crash unless the setting :code:`FASTDEV_STRICT_IF` is set to :code:`True`. If you use this setting and want to check for existence of a variable, use :code:`{% ifexists %}` from the fastdev template tag library. +When using the :code:`default` or :code:`default_if_none` filters, :code:`django-fastdev` will not raise an exception for non-existent variables. Instead, it behaves as one might intuitively expect by populating the context variable with the result of the filter operation. For example: + +.. code:: html + + {{ does_not_exist|default:"N/A" }} + {{ does_not_exist|default_if_none:"" }} + +In these cases: + * If :code:`does_not_exist` is undefined, :code:`default:"N/A"` will render as :code:`N/A`, and :code:`default_if_none:""` will render as an empty string (:code:`""`). + * This ensures that templates using these filters handle missing variables gracefully, aligning with Django's built-in behavior while maintaining :code:`django-fastdev`'s strict checking for other cases. + +By default, :code:`django-fastdev` only checks templates that exist within your project directory. To check ALL templates, including stock Django templates and templates from third-party libraries, add :code:`FASTDEV_STRICT_TEMPLATE_CHECKING = True` to your project :code:`settings.py`. Improved TemplateDoesNotExist errors @@ -46,6 +57,7 @@ Improved TemplateDoesNotExist errors Good suggestions for what you wanted to do, and a complete list of all valid values makes it very easy to fix `TemplateDoesNotExist` errors. + NoReverseMatch errors ~~~~~~~~~~~~~~~~~~~~~ @@ -105,6 +117,7 @@ Django will silently throw away `hello!` because you wrote :code:`contents` inst of :code:`content`. :code:`django-fastdev` will turn this into an error which lists the invalid and valid block names in alphabetical order. + Better error messages for reverse ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/django_fastdev/apps.py b/django_fastdev/apps.py index 7db9a68..3cf32fd 100644 --- a/django_fastdev/apps.py +++ b/django_fastdev/apps.py @@ -35,6 +35,10 @@ IfNode, ForNode ) +from django.template.defaultfilters import ( + default, + default_if_none, +) from django.template.loader_tags import ( BlockNode, ExtendsNode, @@ -354,6 +358,14 @@ def resolve_override(self, context, ignore_failures=False, ignore_failures_for_r except FastDevVariableDoesNotExist: raise except VariableDoesNotExist as e: + # If the filter includes default or default_if_none, suppress + # the exception and return None + if any( + filter == default + for filter, args in self.filters + ): + return orig_resolve(self, context) + if not strict_template_checking(): # worry only about templates inside our project dir; if they # exist elsewhere, then go to standard django behavior diff --git a/tests/templates/ignored/test_ignored_template_with_filter.html b/tests/templates/ignored/test_ignored_template_with_filter.html new file mode 100644 index 0000000..b759dec --- /dev/null +++ b/tests/templates/ignored/test_ignored_template_with_filter.html @@ -0,0 +1 @@ +{{ nonexistent_var|default:"fallback" }} diff --git a/tests/test_template_variable_resolution.py b/tests/test_template_variable_resolution.py new file mode 100644 index 0000000..b53b120 --- /dev/null +++ b/tests/test_template_variable_resolution.py @@ -0,0 +1,67 @@ +import pytest +from django.template import Context, Template +from django.template.loader import get_template +from django.test import TestCase +from django_fastdev.apps import FastDevVariableDoesNotExist +from unittest.mock import patch + + +context = Context({"existing_var": "test_value"}) + + +def test_nonexistent_variable_with_default_filter(): + template = Template('{{ nonexistent_var|default:"fallback" }}') + result = template.render(context) + assert result == "fallback", "Expected fallback value for None with default filter" + + +def test_nonexistent_variable_with_default_if_none_filter(): + template = Template('{{ nonexistent_var|default_if_none:"fallback" }}') + with pytest.raises(FastDevVariableDoesNotExist) as cm: + result = template.render(context) + assert "nonexistent_var does not exist in context" in str(cm.value) + + +def test_nonexistent_variable_without_filters(): + template = Template("{{ nonexistent_var }}") + with pytest.raises(FastDevVariableDoesNotExist) as cm: + template.render(context) + assert "nonexistent_var does not exist in context" in str(cm.value) + + +def test_existing_variable_with_default_filter(): + template = Template('{{ existing_var|default:"fallback" }}') + result = template.render(context) + assert result == "test_value", "Expected existing variable value with default filter" + + +def test_existing_variable_with_default_if_none_filter(): + """Test that an existing variable with |default_if_none filter returns its value.""" + template = Template('{{ existing_var|default_if_none:"fallback" }}') + result = template.render(context) + assert result == "test_value", "Expected existing variable value with default_if_none filter" + + +def test_nested_nonexistent_variable_with_default_filter(): + template = Template('{{ obj.nonexistent_field|default:"fallback" }}') + context = Context({"obj": {"existing_field": "value"}}) + result = template.render(context) + assert result == "fallback", "Expected fallback value for None with nested default filter" + + +def test_ignored_template_with_filter(): + template = get_template('ignored/test_ignored_template_with_filter.html') + result = template.render().strip() + assert result == "fallback", "Expected Django default behavior for ignored template" + + +def test_nonexistent_variable_with_multiple_filters(): + template = Template('{{ nonexistent_var|upper|default:"fallback" }}') + result = template.render(context) + assert result == "fallback", "Expected fallback value for None with multiple filters including default" + + +def test_nonexistent_variable_with_multiple_filters2(): + template = Template('{{ nonexistent_var|default:"fallback"|upper }}') + result = template.render(context) + assert result == "FALLBACK", "Expected fallback value for None with multiple filters including default"