Repository: nautobot/nautobot

Issue:
netutils filters are not available in Django templates, only Jinja2 templates
### Environment
* Nautobot version (Docker tag too if applicable): 1.5.10

### Steps to Reproduce
1. Embed a netutils filter in a Jinja2 template, such as a CustomField's content: `{{ 1000 | bits_to_name }}`
2. It works
3. Embed a netutils filter in a Django template, such as `site.html`: `{{ 1000 | bits_to_name }}`
4. Template rendering error due to unknown/unrecognized filter

### Expected Behavior

#1083 claimed that these filters are supported in both contexts, but only added testing covering their use in Jinja2 templates. 

### Observed Behavior

It looks like the Django case has been broken all this time.


Discussion:

Resolving patch:
diff --git a/nautobot/utilities/apps.py b/nautobot/utilities/apps.py
--- a/nautobot/utilities/apps.py
+++ b/nautobot/utilities/apps.py
@@ -7,17 +7,10 @@ class UtilitiesConfig(NautobotConfig):
     def ready(self):
         super().ready()
 
-        # Register netutils jinja2 filters in django_jinja and Django Template
+        # Register netutils jinja2 filters in django_jinja
         from netutils.utils import jinja2_convenience_function
         from django_jinja import library
 
-        from django import template
-
-        register = template.Library()
-
         for name, func in jinja2_convenience_function().items():
             # Register in django_jinja
             library.filter(name=name, fn=func)
-
-            # Register in Django Template
-            register.filter(name, func)
diff --git a/nautobot/utilities/templatetags/netutils.py b/nautobot/utilities/templatetags/netutils.py
new file mode 100644
--- /dev/null
+++ b/nautobot/utilities/templatetags/netutils.py
@@ -0,0 +1,9 @@
+from django import template
+from netutils.utils import jinja2_convenience_function
+
+register = template.Library()
+
+for name, func in jinja2_convenience_function().items():
+    # Register as a simple_tag in Django Template context.
+    # We use simple_tag() rather than filter() because many netutils functions take more than one arg.
+    register.simple_tag(func, name=name)


Test patch:
diff --git a/nautobot/utilities/tests/test_templatetags_netutils.py b/nautobot/utilities/tests/test_templatetags_netutils.py
new file mode 100644
--- /dev/null
+++ b/nautobot/utilities/tests/test_templatetags_netutils.py
@@ -0,0 +1,52 @@
+import inspect
+
+from django.template import Context, Engine
+from django.template.exceptions import TemplateSyntaxError
+from django.test import TestCase
+
+from netutils.utils import jinja2_convenience_function
+
+
+class NautobotTemplateTagsNetutilsTest(TestCase):
+    """Test the use of netutils functions as Django template filters."""
+
+    def test_netutils_filters_in_django(self):
+        """Verify that each function is at least available as a filter."""
+        engine = Engine.get_default()
+        context = Context({})
+        for filter_name, filter_func in jinja2_convenience_function().items():
+            with self.subTest(f'Testing filter "{filter_name}"'):
+                template_string = "{% load netutils %}{% " + filter_name
+                signature = inspect.signature(filter_func)
+                i = 1
+                for param_name, param in signature.parameters.items():
+                    template_string += f" {param_name}="
+                    if param.annotation == str:
+                        template_string += f'"{i}"'
+                    elif param.annotation == bool:
+                        template_string += "True"
+                    elif param.annotation in (int, float):
+                        template_string += str(i)
+                    elif param.annotation in (list, tuple):
+                        template_string += "[]"
+                    elif param.annotation == dict:
+                        template_string += "{}"
+                    else:
+                        template_string += "None"
+                    i += 1
+                template_string += " %}"
+                template = engine.from_string(template_string)
+                try:
+                    template.render(context)
+                except TemplateSyntaxError as exc:
+                    # Django doesn't see this as a valid template-tag, or similar - shouldn't happen
+                    self.fail(str(exc))
+                except (AttributeError, KeyError, TypeError, ValueError):
+                    # We didn't pass "valid" params to the function, but at least Django saw it as a template tag.
+                    pass
+                except ConnectionError:
+                    # Yes, some netutils functions such as tcp_ping actually make network calls when invoked. Eeek.
+                    pass
+                except Exception:
+                    # Catch-all - at least it wasn't a TemplateSyntaxError, so good enough for now.
+                    pass