Repository: encode/django-rest-framework

Issue:
If login view is not registered the browsable API displays an empty dropdown control.
Using with Django 1.7c2

![screenshot from 2014-08-07 21 33 59](https://cloud.githubusercontent.com/assets/5354184/3842534/2a4814f6-1e38-11e4-8ca1-e049eb03ff54.png)



Discussion:
Assume you've not included the login and logout views.
Have you included this in your URLconf?...

```
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
```

From the installation instructions [here](http://www.django-rest-framework.org/#installation)
I'll go ahead and reopen the ticket if that's not the issue.

I have a feeling this is coming from following the pre-release "Lightweight Django". We use the browsable API but rely on the browser to hold the Basic Auth credentials rather than use the `SessionAuthentication`. Unfortunately the UI doesn't degrade well for this case. It looks like there is/should be a dropdown but there isn't.

To be clear I'm not saying this needs to be fixed in DRF. We'll add a note that this dropdown shouldn't be expected.

Actually yes, it should degrade more nicely.

@mlavin: Looking forward to taking a look at "Lightweight Django" btw - just holding off ATM as I'm tempted to wait a purchase the hardcopy when it's ready :) (And one of these days we might wanna catch up on your DEP for request parsing)

:smile: yes the DEP is waiting on the book but one day I'll get back to it.

Thanks @mlavin for pointing that out, yes indeed it's from your book. It confuse me why would there be a dropdown if it doesn't have any content. As I'm new to DRF, I tried to debug but I can't find any solution. 

BTW, excited for the next content release...

If the login views are not registered, we shouldn't style the username as a dropdown with empty login link.

DjangoCon guidance - ensure that the broswable API displays sensibly if the login/logout links are not present. The username should still display if authenticated, but should not present the dropdown control. Include a screenshot with your pull request.


Resolving patch:
diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py
--- a/rest_framework/templatetags/rest_framework.py
+++ b/rest_framework/templatetags/rest_framework.py
@@ -41,22 +41,31 @@ def optional_login(request):
     except NoReverseMatch:
         return ''
 
-    snippet = "<a href='%s?next=%s'>Log in</a>" % (login_url, escape(request.path))
+    snippet = "<li><a href='{href}?next={next}'>Log in</a></li>".format(href=login_url, next=escape(request.path))
     return snippet
 
 
 @register.simple_tag
-def optional_logout(request):
+def optional_logout(request, user):
     """
     Include a logout snippet if REST framework's logout view is in the URLconf.
     """
     try:
         logout_url = reverse('rest_framework:logout')
     except NoReverseMatch:
-        return ''
-
-    snippet = "<a href='%s?next=%s'>Log out</a>" % (logout_url, escape(request.path))
-    return snippet
+        return '<li class="navbar-text">{user}</li>'.format(user=user)
+
+    snippet = """<li class="dropdown">
+        <a href="#" class="dropdown-toggle" data-toggle="dropdown">
+            {user}
+            <b class="caret"></b>
+        </a>
+        <ul class="dropdown-menu">
+            <li><a href='{href}?next={next}'>Log out</a></li>
+        </ul>
+    </li>"""
+
+    return snippet.format(user=user, href=logout_url, next=escape(request.path))
 
 
 @register.simple_tag


Test patch:
diff --git a/tests/browsable_api/__init__.py b/tests/browsable_api/__init__.py
new file mode 100644
diff --git a/tests/browsable_api/auth_urls.py b/tests/browsable_api/auth_urls.py
new file mode 100644
--- /dev/null
+++ b/tests/browsable_api/auth_urls.py
@@ -0,0 +1,10 @@
+from __future__ import unicode_literals
+from django.conf.urls import patterns, url, include
+
+from .views import MockView
+
+urlpatterns = patterns(
+    '',
+    (r'^$', MockView.as_view()),
+    url(r'^auth/', include('rest_framework.urls', namespace='rest_framework')),
+)
diff --git a/tests/browsable_api/no_auth_urls.py b/tests/browsable_api/no_auth_urls.py
new file mode 100644
--- /dev/null
+++ b/tests/browsable_api/no_auth_urls.py
@@ -0,0 +1,9 @@
+from __future__ import unicode_literals
+from django.conf.urls import patterns
+
+from .views import MockView
+
+urlpatterns = patterns(
+    '',
+    (r'^$', MockView.as_view()),
+)
diff --git a/tests/browsable_api/test_browsable_api.py b/tests/browsable_api/test_browsable_api.py
new file mode 100644
--- /dev/null
+++ b/tests/browsable_api/test_browsable_api.py
@@ -0,0 +1,65 @@
+from __future__ import unicode_literals
+from django.contrib.auth.models import User
+from django.test import TestCase
+
+from rest_framework.test import APIClient
+
+
+class DropdownWithAuthTests(TestCase):
+    """Tests correct dropdown behaviour with Auth views enabled."""
+
+    urls = 'tests.browsable_api.auth_urls'
+
+    def setUp(self):
+        self.client = APIClient(enforce_csrf_checks=True)
+        self.username = 'john'
+        self.email = 'lennon@thebeatles.com'
+        self.password = 'password'
+        self.user = User.objects.create_user(self.username, self.email, self.password)
+
+    def tearDown(self):
+        self.client.logout()
+
+    def test_name_shown_when_logged_in(self):
+        self.client.login(username=self.username, password=self.password)
+        response = self.client.get('/')
+        self.assertContains(response, 'john')
+
+    def test_logout_shown_when_logged_in(self):
+        self.client.login(username=self.username, password=self.password)
+        response = self.client.get('/')
+        self.assertContains(response, '>Log out<')
+
+    def test_login_shown_when_logged_out(self):
+        response = self.client.get('/')
+        self.assertContains(response, '>Log in<')
+
+
+class NoDropdownWithoutAuthTests(TestCase):
+    """Tests correct dropdown behaviour with Auth views NOT enabled."""
+
+    urls = 'tests.browsable_api.no_auth_urls'
+
+    def setUp(self):
+        self.client = APIClient(enforce_csrf_checks=True)
+        self.username = 'john'
+        self.email = 'lennon@thebeatles.com'
+        self.password = 'password'
+        self.user = User.objects.create_user(self.username, self.email, self.password)
+
+    def tearDown(self):
+        self.client.logout()
+
+    def test_name_shown_when_logged_in(self):
+        self.client.login(username=self.username, password=self.password)
+        response = self.client.get('/')
+        self.assertContains(response, 'john')
+
+    def test_dropdown_not_shown_when_logged_in(self):
+        self.client.login(username=self.username, password=self.password)
+        response = self.client.get('/')
+        self.assertNotContains(response, '<li class="dropdown">')
+
+    def test_dropdown_not_shown_when_logged_out(self):
+        response = self.client.get('/')
+        self.assertNotContains(response, '<li class="dropdown">')
diff --git a/tests/browsable_api/views.py b/tests/browsable_api/views.py
new file mode 100644
--- /dev/null
+++ b/tests/browsable_api/views.py
@@ -0,0 +1,15 @@
+from __future__ import unicode_literals
+
+from rest_framework.views import APIView
+from rest_framework import authentication
+from rest_framework import renderers
+from rest_framework.response import Response
+
+
+class MockView(APIView):
+
+    authentication_classes = (authentication.SessionAuthentication,)
+    renderer_classes = (renderers.BrowsableAPIRenderer,)
+
+    def get(self, request):
+        return Response({'a': 1, 'b': 2, 'c': 3})