Repository: netbox-community/netbox

Issue:
v2.9.6 Exception KeyError on 'debug' for objects with Custom Links
### Environment
* Python version: 3.7.7
* NetBox version: 2.9.6

### Steps to Reproduce
1.  Install Netbox 2.9.6
2.  In the web UI, visit any object's page that has a custom link
2b. In my environment only Devices, VMs, and IP Addresses have custom links, and those are the only ones experiencing the following error.
3.  Receive error:
```
<class 'KeyError'>
'debug'
Python version: 3.7.7
NetBox version: 2.9.6
```

<!-- What did you expect to happen? -->
### Expected Behavior
The page should load properly

<!-- What happened instead? -->
### Observed Behavior
Error message received.  Full error stack provided by rodvand on Slack:
```
Traceback (most recent call last):
 File "/opt/netbox-2.9.6/venv/lib/python3.7/site-packages/django/core/handlers/exception.py", line 47, in inner
   response = get_response(request)
 File "/opt/netbox-2.9.6/venv/lib/python3.7/site-packages/django/core/handlers/base.py", line 179, in _get_response
   response = wrapped_callback(request, *callback_args, **callback_kwargs)
 File "/opt/netbox-2.9.6/venv/lib/python3.7/site-packages/django/views/generic/base.py", line 73, in view
   return self.dispatch(request, *args, **kwargs)
 File "/opt/netbox/netbox/utilities/views.py", line 123, in dispatch
   return super().dispatch(request, *args, **kwargs)
 File "/opt/netbox-2.9.6/venv/lib/python3.7/site-packages/django/views/generic/base.py", line 101, in dispatch
   return handler(request, *args, **kwargs)
 File "/opt/netbox/netbox/dcim/views.py", line 1094, in get
   'show_interface_graphs': Graph.objects.filter(type__model='interface').exists(),
 File "/opt/netbox-2.9.6/venv/lib/python3.7/site-packages/django/shortcuts.py", line 19, in render
   content = loader.render_to_string(template_name, context, request, using=using)
 File "/opt/netbox-2.9.6/venv/lib/python3.7/site-packages/django/template/loader.py", line 62, in render_to_string
   return template.render(context, request)
 File "/opt/netbox-2.9.6/venv/lib/python3.7/site-packages/django/template/backends/django.py", line 61, in render
   return self.template.render(context)
 File "/opt/netbox-2.9.6/venv/lib/python3.7/site-packages/django/template/base.py", line 170, in render
   return self._render(context)
 File "/opt/netbox-2.9.6/venv/lib/python3.7/site-packages/django/test/utils.py", line 96, in instrumented_test_render
   return self.nodelist.render(context)
 File "/opt/netbox-2.9.6/venv/lib/python3.7/site-packages/django/template/base.py", line 938, in render
   bit = node.render_annotated(context)
 File "/opt/netbox-2.9.6/venv/lib/python3.7/site-packages/django/template/base.py", line 905, in render_annotated
   return self.render(context)
 File "/opt/netbox-2.9.6/venv/lib/python3.7/site-packages/django/template/loader_tags.py", line 150, in render
   return compiled_parent._render(context)
 File "/opt/netbox-2.9.6/venv/lib/python3.7/site-packages/django/test/utils.py", line 96, in instrumented_test_render
   return self.nodelist.render(context)
 File "/opt/netbox-2.9.6/venv/lib/python3.7/site-packages/django/template/base.py", line 938, in render
   bit = node.render_annotated(context)
 File "/opt/netbox-2.9.6/venv/lib/python3.7/site-packages/django/template/base.py", line 905, in render_annotated
   return self.render(context)
 File "/opt/netbox-2.9.6/venv/lib/python3.7/site-packages/django/template/loader_tags.py", line 62, in render
   result = block.nodelist.render(context)
 File "/opt/netbox-2.9.6/venv/lib/python3.7/site-packages/django/template/base.py", line 938, in render
   bit = node.render_annotated(context)
 File "/opt/netbox-2.9.6/venv/lib/python3.7/site-packages/django/template/base.py", line 905, in render_annotated
   return self.render(context)
 File "/opt/netbox-2.9.6/venv/lib/python3.7/site-packages/django/template/library.py", line 192, in render
   output = self.func(*resolved_args, **resolved_kwargs)
 File "/opt/netbox/netbox/extras/templatetags/custom_links.py", line 36, in custom_links
   'debug': context['debug'],  # django.template.context_processors.debug
 File "/opt/netbox-2.9.6/venv/lib/python3.7/site-packages/django/template/context.py", line 83, in __getitem__
   raise KeyError(key)
Exception Type: KeyError at /dcim/devices/28/
Exception Value: 'debug'
```


Discussion:

Resolving patch:
diff --git a/netbox/extras/templatetags/custom_links.py b/netbox/extras/templatetags/custom_links.py
--- a/netbox/extras/templatetags/custom_links.py
+++ b/netbox/extras/templatetags/custom_links.py
@@ -33,7 +33,7 @@ def custom_links(context, obj):
     # Pass select context data when rendering the CustomLink
     link_context = {
         'obj': obj,
-        'debug': context['debug'],  # django.template.context_processors.debug
+        'debug': context.get('debug', False),  # django.template.context_processors.debug
         'request': context['request'],  # django.template.context_processors.request
         'user': context['user'],  # django.contrib.auth.context_processors.auth
         'perms': context['perms'],  # django.contrib.auth.context_processors.auth
diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py
--- a/netbox/netbox/settings.py
+++ b/netbox/netbox/settings.py
@@ -16,7 +16,7 @@
 # Environment setup
 #
 
-VERSION = '2.9.6'
+VERSION = '2.9.7'
 
 # Hostname
 HOSTNAME = platform.node()


Test patch:
diff --git a/netbox/extras/tests/test_views.py b/netbox/extras/tests/test_views.py
--- a/netbox/extras/tests/test_views.py
+++ b/netbox/extras/tests/test_views.py
@@ -2,11 +2,13 @@
 import uuid
 
 from django.contrib.auth.models import User
+from django.contrib.contenttypes.models import ContentType
+from django.test import override_settings
 from django.urls import reverse
 
 from dcim.models import Site
 from extras.choices import ObjectChangeActionChoices
-from extras.models import ConfigContext, ObjectChange, Tag
+from extras.models import ConfigContext, CustomLink, ObjectChange, Tag
 from utilities.testing import ViewTestCases, TestCase
 
 
@@ -124,3 +126,24 @@ def test_objectchange(self):
         objectchange = ObjectChange.objects.first()
         response = self.client.get(objectchange.get_absolute_url())
         self.assertHttpStatus(response, 200)
+
+
+class CustomLinkTest(TestCase):
+    user_permissions = ['dcim.view_site']
+
+    def test_view_object_with_custom_link(self):
+        customlink = CustomLink(
+            content_type=ContentType.objects.get_for_model(Site),
+            name='Test',
+            text='FOO {{ obj.name }} BAR',
+            url='http://example.com/?site={{ obj.slug }}',
+            new_window=False
+        )
+        customlink.save()
+
+        site = Site(name='Test Site', slug='test-site')
+        site.save()
+
+        response = self.client.get(site.get_absolute_url(), follow=True)
+        self.assertEqual(response.status_code, 200)
+        self.assertIn(f'FOO {site.name} BAR', str(response.content))