Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Fixed #15619 – Added CSRF protection in logout view. #1934

Closed
wants to merge 1 commit into from

5 participants

@KrzysiekJ

Logout is now performed only for POST requests.
GET requests return confirmation page.

Based on patches from ashchristopher and vzima.

https://code.djangoproject.com/ticket/15619

@ashchristopher

I originally wrote this patch, but was lost in the transition to GitHub (my bad) - one thing that I think we should have is to do POST via javascript so that currently functionality is maintained - if JS is not enabled, or the link is opened in a new tab, then the confirmation page is shown.

Just my thoughts.

@KrzysiekJ

I don’t think that punishing users for not enabling JavaScript is a good thing, so we should probably leave the plain HTML form by default and replace it with a link when JavaScript is enabled. However the only advantage we would gain is that users would be able to log out in a new tab, which seems to be a rather uncommon use case. In most cases, when user logs out, he doesn’t want to leave open tabs showing what he was doing before.

Also the inability to submit a form to a new tab is a general issue which maybe, if at all, should be solved at the browser level. I don’t see any reason (except some sort of backwards compatibility) why we should make a workaround only in this particular place. For example, why not allow submitting search form in admin to a new tab? So, unless some of the core devs express their support, I’m probably not going to implement this.

@ziima

I finally finished my rebase, but you were a bit faster #1963

It differs in a few things we should merge, on of which is missed to fix the logout link in password_change_* templates.

@ziima

As for JS, I'm also for logout form. I only have one comment which is that logout button shouldn't look like a link. I hate elements which looks like a link but can't be opened in a new tab.

@KrzysiekJ

I’ve updated the patch and incorporated your changes to the password_change_* templates. There are still a few differences – for example, I’ve removed dummy “post” input from the form and have just got rid of XHTML-style backslashes in input tags. I’ve also changed “logout” (noun) in the submit button to “log out” (verb).

As for the logout button look, styling it differently will probably require some design decisions, so maybe it would be better to do it via a separate ticket.

@ziima

It looks like a logout button style will be an issue. I propose to split this patch into two. This one which changes the logout function and second which changes the logout link. It would be troubling if this patch got stuck because of style of logout link/button.

For the patch: You should check the diff in docs. I'm not very experienced in writing docs for django, but the changes in AdminSite should be mentioned as well. You haven't also fixed the arguments of logout function in docs. At last, I'm not sure how the information about changes between versions works, but I tried to note them.

@KrzysiekJ KrzysiekJ Fixed #15619 – Added CSRF protection in the logout view.
Logout is now performed only for POST requests.
GET requests return confirmation page.

Based on patches from ashchristopher and vzima.
2e78c4f
@KrzysiekJ

Thanks for the review, I’ve added the missing documentation. About the version notifications: I’m not really sure whether they add more value or noise in this case, so I did not add them. However I may do it if they get more support.

@timgraham
Owner

Please send a new PR if someone can update this and include the concerns mentioned in the mailing list thread that Collin linked above.

@timgraham timgraham closed this
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Dec 2, 2013
  1. @KrzysiekJ

    Fixed #15619 – Added CSRF protection in the logout view.

    KrzysiekJ authored
    Logout is now performed only for POST requests.
    GET requests return confirmation page.
    
    Based on patches from ashchristopher and vzima.
This page is out of date. Refresh to see the latest.
View
4 django/contrib/admin/sites.py
@@ -49,6 +49,7 @@ class AdminSite(object):
app_index_template = None
login_template = None
logout_template = None
+ logout_confirmation_template = None
password_change_template = None
password_change_done_template = None
@@ -322,6 +323,9 @@ def logout(self, request, extra_context=None):
}
if self.logout_template is not None:
defaults['template_name'] = self.logout_template
+ if self.logout_confirmation_template is not None:
+ defaults['confirmation_template_name'] = self.logout_confirmation_template
+
return logout(request, **defaults)
@never_cache
View
17 django/contrib/admin/static/admin/css/base.css
@@ -788,12 +788,25 @@ table#change-history tbody th {
overflow: hidden;
}
-#header a:link, #header a:visited {
+#header form{
+ display: inline;
+}
+
+#header a:link, #header a:visited, #header input[type="submit"] {
color: #fff;
}
-#header a:hover {
+#header a:hover, #header input[type="submit"]:hover{
text-decoration: underline;
+ cursor: pointer;
+}
+
+#header input[type="submit"] {
+ background: none;
+ border: none;
+ font-family: inherit;
+ margin: 0;
+ padding: 0;
}
#branding h1 {
View
5 django/contrib/admin/templates/admin/base.html
@@ -38,7 +38,10 @@
{% if user.has_usable_password %}
<a href="{% url 'admin:password_change' %}">{% trans 'Change password' %}</a> /
{% endif %}
- <a href="{% url 'admin:logout' %}">{% trans 'Log out' %}</a>
+ <form action="{% url 'admin:logout' %}" method="post">
+ {% csrf_token %}
+ <input type="submit" value="{% trans 'Log out' %}">
+ </form>
{% endblock %}
</div>
{% endif %}
View
18 django/contrib/admin/templates/registration/logout_confirmation.html
@@ -0,0 +1,18 @@
+{% extends "admin/base_site.html" %}
+{% load i18n %}
+
+{% block breadcrumbs %}
+ <div class="breadcrumbs">
+ <a href="../">{% trans "Home" %}</a> &rsaquo;
+ </div>
+{% endblock %}
+
+{% block content %}
+ <p>{% trans "Are you sure you want to log out?" %}</p>
+ <form action="" method="post">
+ {% csrf_token %}
+ <div>
+ <input type="submit" value="{% trans 'Yes, I’m sure' %}">
+ </div>
+ </form>
+{% endblock %}
View
11 django/contrib/admin/templates/registration/password_change_done.html
@@ -1,6 +1,15 @@
{% extends "admin/base_site.html" %}
{% load i18n %}
-{% block userlinks %}{% url 'django-admindocs-docroot' as docsroot %}{% if docsroot %}<a href="{{ docsroot }}">{% trans 'Documentation' %}</a> / {% endif %}{% trans 'Change password' %} / <a href="{% url 'admin:logout' %}">{% trans 'Log out' %}</a>{% endblock %}
+{% block userlinks %}
+ {% url 'django-admindocs-docroot' as docsroot %}
+ {% if docsroot %}
+ <a href="{{ docsroot }}">{% trans 'Documentation' %}</a> /
+ {% endif %}
+ {% trans 'Change password' %} /
+ <form action="{% url 'admin:logout' %}" method="post">
+ <input type="submit" value="{% trans 'Log out' %}">
+ </form>
+{% endblock %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a>
View
11 django/contrib/admin/templates/registration/password_change_form.html
@@ -1,7 +1,16 @@
{% extends "admin/base_site.html" %}
{% load i18n static %}
{% block extrastyle %}{{ block.super }}<link rel="stylesheet" type="text/css" href="{% static "admin/css/forms.css" %}" />{% endblock %}
-{% block userlinks %}{% url 'django-admindocs-docroot' as docsroot %}{% if docsroot %}<a href="{{ docsroot }}">{% trans 'Documentation' %}</a> / {% endif %} {% trans 'Change password' %} / <a href="{% url 'admin:logout' %}">{% trans 'Log out' %}</a>{% endblock %}
+{% block userlinks %}
+ {% url 'django-admindocs-docroot' as docsroot %}
+ {% if docsroot %}
+ <a href="{{ docsroot }}">{% trans 'Documentation' %}</a> /
+ {% endif %}
+ {% trans 'Change password' %} /
+ <form action="{% url 'admin:logout' %}" method="post">
+ <input type="submit" value="{% trans 'Log out' %}">
+ </form>
+{% endblock %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a>
View
7 django/contrib/auth/tests/templates/registration/logout_confirmation.html
@@ -0,0 +1,7 @@
+<p>Are you sure you want to log out?</p>
+<form action="" method="post">
+ {% csrf_token %}
+ <div>
+ <input type="submit" value="Yes, I’m sure">
+ </div>
+</form>
View
4 django/contrib/auth/tests/test_signals.py
@@ -56,13 +56,13 @@ def test_login(self):
def test_logout_anonymous(self):
# The log_out function will still trigger the signal for anonymous
# users.
- self.client.get('/logout/next_page/')
+ self.client.post('/logout/next_page/')
self.assertEqual(len(self.logged_out), 1)
self.assertEqual(self.logged_out[0], None)
def test_logout(self):
self.client.login(username='testclient', password='password')
- self.client.get('/logout/next_page/')
+ self.client.post('/logout/next_page/')
self.assertEqual(len(self.logged_out), 1)
self.assertEqual(self.logged_out[0].username, 'testclient')
View
53 django/contrib/auth/tests/test_views.py
@@ -53,7 +53,7 @@ def login(self, password='password'):
return response
def logout(self):
- response = self.client.get('/admin/logout/')
+ response = self.client.post('/admin/logout/')
self.assertEqual(response.status_code, 200)
self.assertTrue(SESSION_KEY not in self.client.session)
@@ -361,7 +361,7 @@ def fail_login(self, password='password'):
})
def logout(self):
- self.client.get('/logout/')
+ self.client.post('/logout/')
def test_password_change_fails_with_invalid_old_password(self):
self.login()
@@ -606,67 +606,74 @@ def test_remote(self):
@skipIfCustomUser
class LogoutTest(AuthViewsTestCase):
+ def client_is_logged_in(self):
+ """Tell whether some user is logged in test client."""
+ return SESSION_KEY in self.client.session
- def confirm_logged_out(self):
- self.assertTrue(SESSION_KEY not in self.client.session)
+ def test_logout_via_get(self):
+ """Logout attempt via GET renders confirmation template."""
+ self.login()
+ response = self.client.get('/logout/')
+ self.assertContains(response, 'Are you sure you want to logout?')
+ self.assertTrue(self.client_is_logged_in())
def test_logout_default(self):
- "Logout without next_page option renders the default template"
+ """"Logout without next_page option renders the default template"""
self.login()
- response = self.client.get('/logout/')
+ response = self.client.post('/logout/')
self.assertContains(response, 'Logged out')
- self.confirm_logged_out()
+ self.assertFalse(self.client_is_logged_in())
def test_14377(self):
# Bug 14377
self.login()
- response = self.client.get('/logout/')
+ response = self.client.post('/logout/')
self.assertTrue('site' in response.context)
def test_logout_with_overridden_redirect_url(self):
# Bug 11223
self.login()
- response = self.client.get('/logout/next_page/')
+ response = self.client.post('/logout/next_page/')
self.assertEqual(response.status_code, 302)
self.assertURLEqual(response.url, '/somewhere/')
- response = self.client.get('/logout/next_page/?next=/login/')
+ response = self.client.post('/logout/next_page/?next=/login/')
self.assertEqual(response.status_code, 302)
self.assertURLEqual(response.url, '/login/')
- self.confirm_logged_out()
+ self.assertFalse(self.client_is_logged_in())
def test_logout_with_next_page_specified(self):
"Logout with next_page option given redirects to specified resource"
self.login()
- response = self.client.get('/logout/next_page/')
+ response = self.client.post('/logout/next_page/')
self.assertEqual(response.status_code, 302)
self.assertURLEqual(response.url, '/somewhere/')
- self.confirm_logged_out()
+ self.assertFalse(self.client_is_logged_in())
def test_logout_with_redirect_argument(self):
"Logout with query string redirects to specified resource"
self.login()
- response = self.client.get('/logout/?next=/login/')
+ response = self.client.post('/logout/?next=/login/')
self.assertEqual(response.status_code, 302)
self.assertURLEqual(response.url, '/login/')
- self.confirm_logged_out()
+ self.assertFalse(self.client_is_logged_in())
def test_logout_with_custom_redirect_argument(self):
"Logout with custom query string redirects to specified resource"
self.login()
- response = self.client.get('/logout/custom_query/?follow=/somewhere/')
+ response = self.client.post('/logout/custom_query/?follow=/somewhere/')
self.assertEqual(response.status_code, 302)
self.assertURLEqual(response.url, '/somewhere/')
- self.confirm_logged_out()
+ self.assertFalse(self.client_is_logged_in())
def test_logout_with_named_redirect(self):
"Logout resolves names or URLs passed as next_page."
self.login()
- response = self.client.get('/logout/next_page/named/')
+ response = self.client.post('/logout/next_page/named/')
self.assertEqual(response.status_code, 302)
self.assertURLEqual(response.url, '/password_reset/')
- self.confirm_logged_out()
+ self.assertFalse(self.client_is_logged_in())
def test_security_check(self, password='password'):
logout_url = reverse('logout')
@@ -683,11 +690,11 @@ def test_security_check(self, password='password'):
'bad_url': urlquote(bad_url),
}
self.login()
- response = self.client.get(nasty_url)
+ response = self.client.post(nasty_url)
self.assertEqual(response.status_code, 302)
self.assertFalse(bad_url in response.url,
"%s should be blocked" % bad_url)
- self.confirm_logged_out()
+ self.assertFalse(self.client_is_logged_in())
# These URLs *should* still pass the security check
for good_url in ('/view/?param=http://example.com',
@@ -704,11 +711,11 @@ def test_security_check(self, password='password'):
'good_url': urlquote(good_url),
}
self.login()
- response = self.client.get(safe_url)
+ response = self.client.post(safe_url)
self.assertEqual(response.status_code, 302)
self.assertTrue(good_url in response.url,
"%s should be allowed" % good_url)
- self.confirm_logged_out()
+ self.assertFalse(self.client_is_logged_in())
@skipIfCustomUser
View
6 django/contrib/auth/views.py
@@ -60,13 +60,19 @@ def login(request, template_name='registration/login.html',
current_app=current_app)
+@csrf_protect
def logout(request, next_page=None,
template_name='registration/logged_out.html',
+ confirmation_template_name='registration/logout_confirmation.html',
redirect_field_name=REDIRECT_FIELD_NAME,
current_app=None, extra_context=None):
"""
Logs out the user and displays 'You are logged out' message.
"""
+ if request.method != 'POST':
+ return TemplateResponse(request, confirmation_template_name, extra_context,
+ current_app=current_app)
+
auth_logout(request)
if next_page is not None:
View
10 docs/ref/contrib/admin/index.txt
@@ -2278,8 +2278,9 @@ Root and login templates
If you wish to change the index, login or logout templates, you are better off
creating your own ``AdminSite`` instance (see below), and changing the
-:attr:`AdminSite.index_template` , :attr:`AdminSite.login_template` or
-:attr:`AdminSite.logout_template` properties.
+:attr:`AdminSite.index_template` , :attr:`AdminSite.login_template`,
+:attr:`AdminSite.logout_template` or
+:attr:`AdminSite.logout_confirmation_template` properties.
``AdminSite`` objects
=====================
@@ -2353,6 +2354,11 @@ Templates can override or extend base admin templates as described in
Path to a custom template that will be used by the admin site logout view.
+.. attribute:: AdminSite.logout_confirmation_template
+
+ Path to a custom confirmation template that will be used by the admin site
+ logout view when it is called via ``GET``.
+
.. attribute:: AdminSite.password_change_template
Path to a custom template that will be used by the admin site password
View
4 docs/releases/1.7.txt
@@ -710,6 +710,10 @@ Miscellaneous
``select_related('foo').select_related('bar')``. Previously the latter would
have been equivalent to ``select_related('bar')``.
+* :func:`~django.contrib.auth.views.logout` is now CSRF protected and should be
+ called via ``POST``. If called via ``GET``, a confirmation page containing
+ a logout form is displayed.
+
Features deprecated in 1.7
==========================
View
16 docs/topics/auth/default.txt
@@ -602,7 +602,7 @@ patterns.
defaults to ``/accounts/profile/``). If login isn't successful, it
redisplays the login form.
- It's your responsibility to provide the html for the login template
+ It's your responsibility to provide the HTML for the login template
, called ``registration/login.html`` by default. This template gets passed
four template context variables:
@@ -679,9 +679,9 @@ patterns.
.. _site framework docs: ../sites/
-.. function:: logout(request, [next_page, template_name, redirect_field_name, current_app, extra_context])
+.. function:: logout(request, [next_page, template_name, confirmation_template_name, redirect_field_name, current_app, extra_context])
- Logs a user out.
+ Logs a user out when called via ``POST``, displays a logout form otherwise.
**URL name:** ``logout``
@@ -693,9 +693,13 @@ patterns.
logging the user out. Defaults to
:file:`registration/logged_out.html` if no argument is supplied.
- * ``redirect_field_name``: The name of a ``GET`` field containing the
- URL to redirect to after log out. Defaults to ``next``. Overrides the
- ``next_page`` URL if the given ``GET`` parameter is passed.
+ * ``confirmation_template_name``: The full name of a template to display
+ when the view is called via ``GET``. Defaults to
+ :file:`registration/logout_confirmation.html` if no argument is supplied.
+
+ * ``redirect_field_name``: The name of a ``GET`` or ``POST`` field
+ containing the URL to redirect to after log out. Defaults to ``next``.
+ Overrides the ``next_page`` URL if the given parameter is passed.
* ``current_app``: A hint indicating which application contains the current
view. See the :ref:`namespaced URL resolution strategy
View
1  tests/admin_views/customadmin.py
@@ -17,6 +17,7 @@ class Admin2(admin.AdminSite):
login_form = forms.CustomAdminAuthenticationForm
login_template = 'custom_admin/login.html'
logout_template = 'custom_admin/logout.html'
+ logout_confirmation_template = 'custom_admin/logout_confirmation.html'
index_template = ['custom_admin/index.html'] # a list, to test fix for #18697
password_change_template = 'custom_admin/password_change_form.html'
password_change_done_template = 'custom_admin/password_change_done.html'
View
64 tests/admin_views/tests.py
@@ -471,7 +471,7 @@ def testIsNullLookups(self):
def testLogoutAndPasswordChangeURLs(self):
response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit)
- self.assertContains(response, '<a href="/test_admin/%s/logout/">' % self.urlbit)
+ self.assertContains(response, '<form action="/test_admin/%s/logout/" method="post">' % self.urlbit)
self.assertContains(response, '<a href="/test_admin/%s/password_change/">' % self.urlbit)
def testNamedGroupFieldChoicesChangeList(self):
@@ -783,9 +783,15 @@ def testCustomAdminSiteLoginTemplate(self):
self.assertTemplateUsed(response, 'custom_admin/login.html')
self.assertContains(response, 'Hello from a custom login template')
- def testCustomAdminSiteLogoutTemplate(self):
+ def testCustomAdminSiteLogoutConfirmationTemplate(self):
response = self.client.get('/test_admin/admin2/logout/')
self.assertIsInstance(response, TemplateResponse)
+ self.assertTemplateUsed(response, 'custom_admin/logout_confirmation.html')
+ self.assertContains(response, 'Hello from a custom logout confirmation template')
+
+ def testCustomAdminSiteLogoutTemplate(self):
+ response = self.client.post('/test_admin/admin2/logout/')
+ self.assertIsInstance(response, TemplateResponse)
self.assertTemplateUsed(response, 'custom_admin/logout.html')
self.assertContains(response, 'Hello from a custom logout template')
@@ -933,7 +939,7 @@ def testLogin(self):
login = self.client.post('/test_admin/admin/', self.super_login)
self.assertRedirects(login, '/test_admin/admin/')
self.assertFalse(login.context)
- self.client.get('/test_admin/admin/logout/')
+ self.client.post('/test_admin/admin/logout/')
# Test if user enters email address
response = self.client.get('/test_admin/admin/')
@@ -955,7 +961,7 @@ def testLogin(self):
login = self.client.post('/test_admin/admin/', self.adduser_login)
self.assertRedirects(login, '/test_admin/admin/')
self.assertFalse(login.context)
- self.client.get('/test_admin/admin/logout/')
+ self.client.post('/test_admin/admin/logout/')
# Change User
response = self.client.get('/test_admin/admin/')
@@ -963,7 +969,7 @@ def testLogin(self):
login = self.client.post('/test_admin/admin/', self.changeuser_login)
self.assertRedirects(login, '/test_admin/admin/')
self.assertFalse(login.context)
- self.client.get('/test_admin/admin/logout/')
+ self.client.post('/test_admin/admin/logout/')
# Delete User
response = self.client.get('/test_admin/admin/')
@@ -971,7 +977,7 @@ def testLogin(self):
login = self.client.post('/test_admin/admin/', self.deleteuser_login)
self.assertRedirects(login, '/test_admin/admin/')
self.assertFalse(login.context)
- self.client.get('/test_admin/admin/logout/')
+ self.client.post('/test_admin/admin/logout/')
# Regular User should not be able to login.
response = self.client.get('/test_admin/admin/')
@@ -1042,7 +1048,7 @@ def testAddView(self):
post = self.client.post('/test_admin/admin/admin_views/article/add/', add_dict)
self.assertEqual(post.status_code, 403)
self.assertEqual(Article.objects.all().count(), 3)
- self.client.get('/test_admin/admin/logout/')
+ self.client.post('/test_admin/admin/logout/')
# Add user may login and POST to add view, then redirect to admin root
self.client.get('/test_admin/admin/')
@@ -1056,7 +1062,7 @@ def testAddView(self):
self.assertEqual(Article.objects.all().count(), 4)
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].subject, 'Greetings from a created object')
- self.client.get('/test_admin/admin/logout/')
+ self.client.post('/test_admin/admin/logout/')
# Super can add too, but is redirected to the change list view
self.client.get('/test_admin/admin/')
@@ -1067,7 +1073,7 @@ def testAddView(self):
post = self.client.post('/test_admin/admin/admin_views/article/add/', add_dict)
self.assertRedirects(post, '/test_admin/admin/admin_views/article/')
self.assertEqual(Article.objects.all().count(), 5)
- self.client.get('/test_admin/admin/logout/')
+ self.client.post('/test_admin/admin/logout/')
# 8509 - if a normal user is already logged in, it is possible
# to change user into the superuser without error
@@ -1095,7 +1101,7 @@ def testChangeView(self):
self.assertEqual(response.status_code, 403)
post = self.client.post('/test_admin/admin/admin_views/article/1/', change_dict)
self.assertEqual(post.status_code, 403)
- self.client.get('/test_admin/admin/logout/')
+ self.client.post('/test_admin/admin/logout/')
# change user can view all items and edit them
self.client.get('/test_admin/admin/')
@@ -1118,7 +1124,7 @@ def testChangeView(self):
post = self.client.post('/test_admin/admin/admin_views/article/1/', change_dict)
self.assertContains(post, 'Please correct the errors below.',
msg_prefix='Plural error message not found in response to post with multiple errors')
- self.client.get('/test_admin/admin/logout/')
+ self.client.post('/test_admin/admin/logout/')
# Test redirection when using row-level change permissions. Refs #11513.
RowLevelChangePermissionModel.objects.create(id=1, name="odd id")
@@ -1135,7 +1141,7 @@ def testChangeView(self):
response = self.client.post('/test_admin/admin/admin_views/rowlevelchangepermissionmodel/2/', {'name': 'changed'})
self.assertEqual(RowLevelChangePermissionModel.objects.get(id=2).name, 'changed')
self.assertRedirects(response, '/test_admin/admin/')
- self.client.get('/test_admin/admin/logout/')
+ self.client.post('/test_admin/admin/logout/')
for login_dict in [self.joepublic_login, self.no_username_login]:
self.client.post('/test_admin/admin/', login_dict)
response = self.client.get('/test_admin/admin/admin_views/rowlevelchangepermissionmodel/1/')
@@ -1152,7 +1158,7 @@ def testChangeView(self):
self.assertEqual(RowLevelChangePermissionModel.objects.get(id=2).name, 'changed')
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'login-form')
- self.client.get('/test_admin/admin/logout/')
+ self.client.post('/test_admin/admin/logout/')
def testHistoryView(self):
"""History view should restrict access."""
@@ -1162,7 +1168,7 @@ def testHistoryView(self):
self.client.post('/test_admin/admin/', self.adduser_login)
response = self.client.get('/test_admin/admin/admin_views/article/1/history/')
self.assertEqual(response.status_code, 403)
- self.client.get('/test_admin/admin/logout/')
+ self.client.post('/test_admin/admin/logout/')
# change user can view all items and edit them
self.client.get('/test_admin/admin/')
@@ -1181,7 +1187,7 @@ def testHistoryView(self):
response = self.client.get('/test_admin/admin/admin_views/rowlevelchangepermissionmodel/2/history/')
self.assertEqual(response.status_code, 200)
- self.client.get('/test_admin/admin/logout/')
+ self.client.post('/test_admin/admin/logout/')
for login_dict in [self.joepublic_login, self.no_username_login]:
self.client.post('/test_admin/admin/', login_dict)
@@ -1192,7 +1198,7 @@ def testHistoryView(self):
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'login-form')
- self.client.get('/test_admin/admin/logout/')
+ self.client.post('/test_admin/admin/logout/')
def testConditionallyShowAddSectionLink(self):
"""
@@ -1254,7 +1260,7 @@ def testCustomModelAdminTemplates(self):
response = self.client.get('/test_admin/admin/admin_views/customarticle/%d/history/' % article_pk)
self.assertTemplateUsed(response, 'custom_admin/object_history.html')
- self.client.get('/test_admin/admin/logout/')
+ self.client.post('/test_admin/admin/logout/')
def testDeleteView(self):
"""Delete view should restrict access and actually delete items."""
@@ -1269,7 +1275,7 @@ def testDeleteView(self):
post = self.client.post('/test_admin/admin/admin_views/article/1/delete/', delete_dict)
self.assertEqual(post.status_code, 403)
self.assertEqual(Article.objects.all().count(), 3)
- self.client.get('/test_admin/admin/logout/')
+ self.client.post('/test_admin/admin/logout/')
# Delete user can delete
self.client.get('/test_admin/admin/')
@@ -1288,7 +1294,7 @@ def testDeleteView(self):
article_ct = ContentType.objects.get_for_model(Article)
logged = LogEntry.objects.get(content_type=article_ct, action_flag=DELETION)
self.assertEqual(logged.object_id, '1')
- self.client.get('/test_admin/admin/logout/')
+ self.client.post('/test_admin/admin/logout/')
def testDisabledPermissionsWhenLoggedIn(self):
self.client.login(username='super', password='secret')
@@ -1367,7 +1373,7 @@ def test_no_standard_modeladmin_urls(self):
r = self.client.get('/test_admin/admin/')
# we shouldn' get an 500 error caused by a NoReverseMatch
self.assertEqual(r.status_code, 200)
- self.client.get('/test_admin/admin/logout/')
+ self.client.post('/test_admin/admin/logout/')
@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
@@ -1703,7 +1709,7 @@ def test_staff_member_required_decorator_works_as_per_admin_login(self):
login = self.client.post('/test_admin/admin/secure-view/', self.super_login)
self.assertRedirects(login, '/test_admin/admin/secure-view/')
self.assertFalse(login.context)
- self.client.get('/test_admin/admin/logout/')
+ self.client.post('/test_admin/admin/logout/')
# make sure the view removes test cookie
self.assertEqual(self.client.session.test_cookie_worked(), False)
@@ -1727,7 +1733,7 @@ def test_staff_member_required_decorator_works_as_per_admin_login(self):
login = self.client.post('/test_admin/admin/secure-view/', self.adduser_login)
self.assertRedirects(login, '/test_admin/admin/secure-view/')
self.assertFalse(login.context)
- self.client.get('/test_admin/admin/logout/')
+ self.client.post('/test_admin/admin/logout/')
# Change User
response = self.client.get('/test_admin/admin/secure-view/')
@@ -1735,7 +1741,7 @@ def test_staff_member_required_decorator_works_as_per_admin_login(self):
login = self.client.post('/test_admin/admin/secure-view/', self.changeuser_login)
self.assertRedirects(login, '/test_admin/admin/secure-view/')
self.assertFalse(login.context)
- self.client.get('/test_admin/admin/logout/')
+ self.client.post('/test_admin/admin/logout/')
# Delete User
response = self.client.get('/test_admin/admin/secure-view/')
@@ -1743,7 +1749,7 @@ def test_staff_member_required_decorator_works_as_per_admin_login(self):
login = self.client.post('/test_admin/admin/secure-view/', self.deleteuser_login)
self.assertRedirects(login, '/test_admin/admin/secure-view/')
self.assertFalse(login.context)
- self.client.get('/test_admin/admin/logout/')
+ self.client.post('/test_admin/admin/logout/')
# Regular User should not be able to login.
response = self.client.get('/test_admin/admin/secure-view/')
@@ -1873,8 +1879,9 @@ def test_changelist_input_html(self):
# search field and search submit button = 2
# CSRF field = 1
# field to track 'select all' across paginated views = 1
- # 6 + 4 + 4 + 1 + 2 + 1 + 1 = 19 inputs
- self.assertContains(response, "<input", count=19)
+ # Logout form = 2
+ # 6 + 4 + 4 + 1 + 2 + 1 + 1 + 2 = 21 inputs
+ self.assertContains(response, "<input", count=21)
# 1 select per object = 3 selects
self.assertContains(response, "<select", count=4)
@@ -3617,7 +3624,8 @@ def test_readonly_get(self):
self.assertNotContains(response, 'name="posted"')
# 3 fields + 2 submit buttons + 5 inline management form fields, + 2
# hidden fields for inlines + 1 field for the inline + 2 empty form
- self.assertContains(response, "<input", count=15)
+ # + 2 inputs for logout form
+ self.assertContains(response, "<input", count=17)
self.assertContains(response, formats.localize(datetime.date.today()))
self.assertContains(response,
"<label>Awesomeness level:</label>")
@@ -4318,7 +4326,7 @@ def tearDown(self):
self.client.logout()
def test_client_logout_url_can_be_used_to_login(self):
- response = self.client.get('/test_admin/admin/logout/')
+ response = self.client.post('/test_admin/admin/logout/')
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'registration/logged_out.html')
self.assertEqual(response.request['PATH_INFO'], '/test_admin/admin/logout/')
View
6 tests/templates/custom_admin/logout_confirmation.html
@@ -0,0 +1,6 @@
+{% extends "registration/logout_confirmation.html" %}
+
+{% block content %}
+ Hello from a custom logout confirmation template.
+ {{ block.super }}
+{% endblock %}
Something went wrong with that request. Please try again.