Repository: jazzband/django-debug-toolbar

Issue:
TypeError at /login/
Django Debug Toolbar version: 2.2

With the following view:

```python
from django.contrib.auth.views import LoginView

class PublicLoginView(LoginView):
    redirect_field_name = None
```

Getting the following error:

```bash
Environment:

Request Method: GET
Request URL: http://(removed)/login/

Django Version: 2.2.2
Python Version: 3.6.9
Installed Applications:
['django.contrib.admin',
 'django.contrib.auth',
 'django.contrib.contenttypes',
 'django.contrib.humanize',
 'django.contrib.sessions',
 'django.contrib.messages',
 'django.contrib.staticfiles',
 'django.forms',
 'capture_tag',
 'django_celery_beat',
 'django_celery_results',
 'debug_toolbar']
Installed Middleware:
['debug_toolbar.middleware.DebugToolbarMiddleware',
 'django.middleware.security.SecurityMiddleware',
 'django.contrib.sessions.middleware.SessionMiddleware',
 'django.middleware.common.CommonMiddleware',
 'django.middleware.csrf.CsrfViewMiddleware',
 'django.contrib.messages.middleware.MessageMiddleware',
 'django.middleware.clickjacking.XFrameOptionsMiddleware',
 'django.contrib.auth.middleware.AuthenticationMiddleware',
 'global_login_required.GlobalLoginRequiredMiddleware']

Traceback:

File "/home/django/srv/current/venv/lib/python3.6/site-packages/django/core/handlers/exception.py" in inner
  34.             response = get_response(request)

File "/home/django/srv/current/venv/lib/python3.6/site-packages/django/core/handlers/base.py" in _get_response
  145.                 response = self.process_exception_by_middleware(e, request)

File "/home/django/srv/current/venv/lib/python3.6/site-packages/django/core/handlers/base.py" in _get_response
  143.                 response = response.render()

File "/home/django/srv/current/venv/lib/python3.6/site-packages/django/template/response.py" in render
  106.             self.content = self.rendered_content

File "/home/django/srv/current/venv/lib/python3.6/site-packages/django/template/response.py" in rendered_content
  83.         content = template.render(context, self._request)

File "/home/django/srv/current/venv/lib/python3.6/site-packages/django/template/backends/django.py" in render
  61.             return self.template.render(context)

File "/home/django/srv/current/venv/lib/python3.6/site-packages/django/template/base.py" in render
  171.                     return self._render(context)

File "/home/django/srv/current/venv/lib/python3.6/site-packages/django/test/utils.py" in instrumented_test_render
  95.     template_rendered.send(sender=self, template=self, context=context)

File "/home/django/srv/current/venv/lib/python3.6/site-packages/django/dispatch/dispatcher.py" in send
  175.             for receiver in self._live_receivers(sender)

File "/home/django/srv/current/venv/lib/python3.6/site-packages/django/dispatch/dispatcher.py" in <listcomp>
  175.             for receiver in self._live_receivers(sender)

File "/home/django/srv/current/venv/lib/python3.6/site-packages/debug_toolbar/panels/templates/panel.py" in _store_template_info
  91.                 key_values = sorted(context_layer.items())

Exception Type: TypeError at /login/
Exception Value: '<' not supported between instances of 'NoneType' and 'str'
```

This only happens when `redirect_field_name` is set to `None`. If I remove it or change it to a string things work as expected and there aren't any errors.


Discussion:
The issue here is that `LoginView` has a key-value pair in it's context with the `redirect_field_name` as a key. You can override the `get_context_data` method in `PublicLoginView` or find an alternate way to achieve your goal.

```
class PublicLoginView(LoginView):
    redirect_field_name = None

        def get_context_data(self, **kwargs):
            context = super().get_context_data(**kwargs)
            if self.redirect_field_name is None:
                context.pop(self.redirect_field_name, None)
            return context
```

Why are you setting `redirect_field_name` to `None`? Maybe we can find a different way to solve that for you.
@tim-schilling thanks, we'll look at modifying the context. What we've done for now is set redirect_field_name to an unused/absurd string to keep DDT from throwing errors due to the `sorted()` use and setting `redirect_field_name` to `None`.

The use case for removing the redirect field is to help curb some auth-related redirect loops for certain views by disabling the "next" feature of the login view.
IMO, this should be considered incorrect use of `LoginView` and therefore closed. Template variable names are expected to be strings, so using `None` would be API misuse. 

@merit-mthompson one option would be to override get_redirect_url to always return `''`.

https://github.com/django/django/blob/f1f24539d8c86f60d1e2951a19eb3178e15d6399/django/contrib/auth/views.py#L69-L80
Django has a test specifically for `None` which leads me to believe it's at least allowed: https://github.com/django/django/blob/cc7c16af98e4c9a7146c248c8567aac7a50e0fbb/tests/auth_tests/test_mixins.py#L156

Feel free to close, we're working around it. The fix would be to change how things are being looped/sorted in DDT at https://github.com/jazzband/django-debug-toolbar/blob/master/debug_toolbar/panels/templates/panel.py#L91. Maybe pop None items out before sorting.
Thanks for linking to that test. I agree we should support this if Django does.

I think we could sort by `repr()` as a workaround. Are you interested in submitting a PR with a test?

Resolving patch:
diff --git a/debug_toolbar/panels/templates/panel.py b/debug_toolbar/panels/templates/panel.py
--- a/debug_toolbar/panels/templates/panel.py
+++ b/debug_toolbar/panels/templates/panel.py
@@ -88,10 +88,9 @@ def _store_template_info(self, sender, **kwargs):
         for context_layer in context.dicts:
             if hasattr(context_layer, "items") and context_layer:
                 # Check if the layer is in the cache.
-                key_values = sorted(context_layer.items())
                 pformatted = None
-                for _key_values, _pformatted in self.pformat_layers:
-                    if _key_values == key_values:
+                for key_values, _pformatted in self.pformat_layers:
+                    if key_values == context_layer:
                         pformatted = _pformatted
                         break
 
@@ -133,7 +132,7 @@ def _store_template_info(self, sender, **kwargs):
                             finally:
                                 recording(True)
                     pformatted = pformat(temp_layer)
-                    self.pformat_layers.append((key_values, pformatted))
+                    self.pformat_layers.append((context_layer, pformatted))
                 context_list.append(pformatted)
 
         kwargs["context"] = context_list


Test patch:
diff --git a/tests/templates/registration/login.html b/tests/templates/registration/login.html
new file mode 100644
--- /dev/null
+++ b/tests/templates/registration/login.html
@@ -0,0 +1 @@
+{% extends 'base.html' %}
diff --git a/tests/test_integration.py b/tests/test_integration.py
--- a/tests/test_integration.py
+++ b/tests/test_integration.py
@@ -2,6 +2,7 @@
 import re
 import unittest
 
+import django
 import html5lib
 from django.contrib.staticfiles.testing import StaticLiveServerTestCase
 from django.core import signing
@@ -319,6 +320,25 @@ def test_server_timing_headers(self):
         for expected in expected_partials:
             self.assertTrue(re.compile(expected).search(server_timing))
 
+    def test_auth_login_view_without_redirect(self):
+        response = self.client.get("/login_without_redirect/")
+        self.assertEqual(response.status_code, 200)
+        parser = html5lib.HTMLParser()
+        doc = parser.parse(response.content)
+        el = doc.find(".//*[@id='djDebug']")
+        store_id = el.attrib["data-store-id"]
+        response = self.client.get(
+            "/__debug__/render_panel/",
+            {"store_id": store_id, "panel_id": "TemplatesPanel"},
+        )
+        self.assertEqual(response.status_code, 200)
+        # The key None (without quotes) exists in the list of template
+        # variables.
+        if django.VERSION < (3, 0):
+            self.assertIn("None: &#39;&#39;", response.json()["content"])
+        else:
+            self.assertIn("None: &#x27;&#x27;", response.json()["content"])
+
 
 @unittest.skipIf(webdriver is None, "selenium isn't installed")
 @unittest.skipUnless(
diff --git a/tests/urls.py b/tests/urls.py
--- a/tests/urls.py
+++ b/tests/urls.py
@@ -1,3 +1,4 @@
+from django.contrib.auth.views import LoginView
 from django.urls import include, path, re_path
 
 import debug_toolbar
@@ -20,5 +21,6 @@
     path("cached_view/", views.cached_view),
     path("json_view/", views.json_view),
     path("redirect/", views.redirect_view),
+    path("login_without_redirect/", LoginView.as_view(redirect_field_name=None)),
     path("__debug__/", include(debug_toolbar.urls)),
 ]