Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 18 additions & 5 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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::

Expand All @@ -34,18 +34,30 @@ 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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

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
~~~~~~~~~~~~~~~~~~~~~

Expand Down Expand Up @@ -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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
12 changes: 12 additions & 0 deletions django_fastdev/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@
IfNode,
ForNode
)
from django.template.defaultfilters import (
default,
default_if_none,
)
from django.template.loader_tags import (
BlockNode,
ExtendsNode,
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{ nonexistent_var|default:"fallback" }}
67 changes: 67 additions & 0 deletions tests/test_template_variable_resolution.py
Original file line number Diff line number Diff line change
@@ -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"